煙火


粒子系統的基本概念很簡單,只要使用類別或結構來包裝粒子的動作操作與狀態即可,然而其困難處在於,如何擬真的模擬出粒子的動作,這就與物理、化學有所關聯了,然而有時候是可以用一些小技倆來簡化真實世界中的粒子動作,而在螢幕上又有相當的效果。
 
Java物件導向的特性特別適合用來包裝粒子系統,將所有與粒子有關的操作也一併包裝進去;如果是VB或C,就使用結構來包裝粒子的屬性。

在模擬的部份,首先是爆炸的時候,粒子所得到的水平與垂直速度,可以遵守動量守恒來計算出每一個粒子所獲得的速度,但這邊實際上並沒有這麼作,簡單的使用 亂數來模擬也可以達到不錯的效果;煙火粒子在下落的時候,是受重力的影響,vy = vt + 9.8 * t;而粒子的顏色部份,初始時使用亂數來決定RGB值,而在粒子生命進入倒數時,改變為紅色,最後再改變為藍色,以配合夜空的顏色,讓粒子有燃燒殆盡的感 覺。

在移動的方面,由於螢幕大小有限,所以讓一個像素代表實際移動一公尺,以免粒子因重力加速度的影響,一下子就跑出螢幕外了。

大部份的模擬是如此,其它的請自己看看 實例,目前還沒有加上煙的效果。
  • Firework.java
package cc.openhome.particle;

import java.awt.*;
import javax.swing.JApplet;

import static java.lang.Math.*;

public class Firework extends JApplet implements Runnable {
private final int MAX = 200;
private FireworkParticle[] particles; // 煙火粒子
private int xCenter, yCenter;
private Image offScreen;
private Graphics gOffScreen;

public void init() {
setSize(640, 480);
setBackground(Color.black); // 背景為黑色
particles = new FireworkParticle[MAX]; // 建立粒子

// 煙火初始位置
xCenter = (int) (getWidth() / 2 + random() * 150 - 150);
yCenter = (int) (getHeight() / 2 + random() * 150 - 150);
for (int i = 0; i < MAX; i++) {
particles[i] = new FireworkParticle();
}

// 建立次畫面
offScreen = createImage(getWidth(), getHeight());
gOffScreen = offScreen.getGraphics();
}

public void start() {
(new Thread(this)).start();
}

public void update(Graphics g) {
paint(g);
}

public void paint(Graphics g) {
g.drawImage(offScreen, 0, 0, this);
}

public void run() {
while (true) {
boolean replay = true;
for (FireworkParticle particle : particles) {
if (particle.isAlive()) {
replay = false;
break;
}
}
// 是否重新施放
if (replay) {
for (FireworkParticle particle : particles) {
particle.resume(xCenter, yCenter, MAX);
particle.setLife((int) (random() * 20));
}
}
gOffScreen.clearRect(0, 0, getWidth(), getHeight());
for (FireworkParticle particle : particles) {
if (particle.isAlive()) {
double x = particle.getPoint().getX();
double y = particle.getPoint().getY();
gOffScreen.setColor(particle.getColor());
gOffScreen.fillOval((int) x, (int) y, 3, 3);
particle.nextState();
}
}

// 重繪畫面
repaint();

// 暫停執行緒 150 毫秒
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();context.stroke();
}
}
}
}

class FireworkParticle {
private final static Color LIFE_LESS_5 = new Color(255, 0, 0);
private final static Color LIFE_LESS_2 = new Color(0, 0, 255);
private Point position = new Point(); // 粒子的位置
private double vx, vy; // 粒子的速度
private int life; // 粒子的生命值
private Color color; // 粒子的顏色
private int time; // 粒子存活至今的時間

void resume(int x, int y, int max) {
position.setLocation(x, y);
vx = random() * max - random() * max;
vy = random() * max - random() * max;
color = new Color((int) (random() * 255),
(int) (random() * 255),
(int) (random() * 255));
time = 0;
}

void setLife(int life) {
this.life = life;
}

boolean isAlive() {
return life != 0;
}

Point getPoint() {
return position;
}

Color getColor() {
return color;
}

void nextState() {
vy += 9.8 * time;
position.setLocation(
position.getX() + vx * 0.1,
position.getY() + vy * 0.1);
life--;
time++;
if (life < 2) {
color = LIFE_LESS_2;
} else if (life < 5) {
color = LIFE_LESS_5;
}
}
}

以下是使用HTML5 Canvas的方式(如果瀏覽器支援HTML5 Canvas,例如最新版的Firexfox、Chrome、IE9等,可以直接將下面的內容存為HTML或按下檔名連結,直接載入瀏覽器執行觀看結果:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=Big5" http-equiv="content-type">
<script type="text/javascript">
window.onload = function() {
function Color(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
this.toString = function() {
return 'rgb(' +
[this.r, this.g, this.b].join() + ')';
};
}

function Point(x, y) {
this.x = x || 0;
this.y = y || 0;
this.setLocation = function(x, y) {
this.x = x;
this.y = y;
};
}

function FireworkParticle() {
var LIFE_LESS_5 = new Color(255, 0, 0);
var LIFE_LESS_2 = new Color(0, 0, 255);
var position = new Point(); // 粒子的位置
var vx = vy = 0; // 粒子的速度
var color = null; // 粒子的顏色
var time = 0; // 粒子存活至今的時間
this.life = 0; // 粒子的生命值

this.resume = function(x, y, max) {
position.setLocation(x, y);
vx = Math.random() * max - Math.random() * max;
vy = Math.random() * max - Math.random() * max;
color = new Color(parseInt(Math.random() * 255),
parseInt(Math.random() * 255),
parseInt(Math.random() * 255));
time = 0;
};

this.isAlive = function() {
return this.life > 0;
};

this.getPoint = function() {
return position;
};

this.getColor = function() {
return color;
};

this.nextState = function() {
vy += 9.8 * time;
position.setLocation(
position.x + vx * 0.1,
position.y + vy * 0.01);
this.life--;
time++;
if (this.life < 2) {
color = LIFE_LESS_2;
} else if(this.life < 5) {
color = LIFE_LESS_5;
}
};
}


var canvas1 = document.getElementById('canvas1');
var canvas2 = document.getElementById('canvas2');
var context1 = canvas1.getContext('2d');
var context2 = canvas2.getContext('2d');


var MAX = 200;

var particles = []; // 建立粒子

for(var i = 0; i < MAX; i++) {
particles[i] = new FireworkParticle();
}

var context = context2;
setTimeout(function() {
var replay = true;
for(var i in particles) {
if(particles[i].isAlive()) {
replay = false;
break;
}
}

// 是否重新施放
if(replay) {
for(var i in particles) {
// 煙火初始位置
var xCenter = canvas1.width / 2 +
Math.random() * 150 - 150;
var yCenter = canvas1.height / 2.5 +
Math.random() * 150 - 150;
particles[i].resume(xCenter, yCenter, MAX);
particles[i].life =
parseInt(Math.random() * 20);
}
}
context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(0, 0, canvas1.width, canvas1.height);
for(var i in particles) {
if(particles[i].isAlive()) {
var x = particles[i].getPoint().x;
var y = particles[i].getPoint().y;
context.fillStyle =
particles[i].getColor().toString();
context.beginPath();
context.arc(x, y, 2, 0, 2 * Math.PI, true);
context.closePath();
context.fill();
particles[i].nextState();
}
}


if(context === context2) {
document.body.replaceChild(canvas2, canvas1);
context = context1;
}
else {
document.body.replaceChild(canvas1, canvas2);
context = context2;
}

setTimeout(arguments.callee, 200);
}, 200);
};
</script>
</head>
<body>
<canvas id="canvas1" width="640" height="480"></canvas>
<canvas id="canvas2" width="640" height="480"></canvas>
</body>
</html>