粒子系統的基本概念很簡單,只要使用類別或結構來包裝粒子的動作操作與狀態即可,然而其困難處在於,如何擬真的模擬出粒子的動作,這就與物理、化學有所關聯了,然而有時候是可以用一些小技倆來簡化真實世界中的粒子動作,而在螢幕上又有相當的效果。
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>

