粒子系統


一路進行簡單的物理模擬,到了〈彈性碰撞〉的範例時,幾個球互相碰撞,感覺自成一套系統?是的!可以進一步地把這些球收集起來,成為碰撞系統,這只要將〈彈性碰撞〉範例中的 setup 流程重構一下,成為 CollisionSystem 建構式,然後 draw 的部份也抽取出來,成為 update

class CollisionSystem {
    constructor(number, width, height, minR = 5, maxR = 20, maxVx = 5, maxVy = 5) {

        const minX = 1.5 * maxR;
        const maxX = width - 1.5 * maxR;

        const minY = 1.5 * maxR;
        const maxY = height - 1.5 * maxR;

        this.circles = [];
        while(this.circles.length < number) {
            const r = random(5, 20);
            const coordinate = createVector(random(minX, maxX), random(minY, maxY));
            if(this.circles.every(c => p5.Vector.sub(c.body.coordinate, coordinate).mag() > (c.radius + r))) {
                this.circles.push(new Circle(
                   new Body(
                       coordinate, 
                       createVector(random(-maxVx, maxVx), random(-maxVy, maxVy)),
                       PI * r * r
                   ),
                   r
                ));
            }
        }
    }

    update() {
        this.circles.forEach(c => c.update());
        checkCollision(this.circles);
        this.circles.forEach(c => checkEdges(c));
    }
}

現在 CollisionSystem 負責這些圓的生成、移動,接著可以如下 setupdraw 繪圖,其餘程式碼不變:

let collisionSystem;
function setup() {
    createCanvas(300, 300);
    collisionSystem = new CollisionSystem(15, width, height);
}

function draw() {
    background(220);
    collisionSystem.update();
    collisionSystem.circles.forEach(c => c.draw());
}

... 其餘程式碼不變

若要更進一步地,可以讓這些圓具有生命值,若跟另一個圓碰撞後生命值就會減一,直到生命值耗盡為止後將之移除,這麼一來,就成為一個粒子系統了,CollisionSystem 管理的粒子就是圓,負責粒子的生成、移動、轉化、消亡。

在我們的設計中,圓只是外形,Body 本身才有碰撞時相關的質量、速度等資訊,為了能知道發生碰撞的時機,來為 Body 設計個通知機制:

class Body {
    constructor(coordinate, velocity, mass = 1) {
        this.coordinate = coordinate;
        this.velocity = velocity;
        this.mass = mass;
        // 使用 Set 來管理傾聽器
        this.collisionListeners = new Set();
    }

    ...

    addCollisionListener(listener) {
        this.collisionListeners.add(listener);
    }

    removeCollisionListener(listener) {
        this.collisionListeners.delete(listener);
    }

    collideWith(body) {
        const d = sub(this.coordinate, body.coordinate);
        const m = body.mass / (this.mass + body.mass);
        this.velocity = sub(
            this.velocity,
            d.mult(
                2 * m / pow(d.mag(), 2) * p5.Vector.dot(
                    sub(this.velocity, body.velocity),
                    sub(this.coordinate, body.coordinate)
                )
            )
        );
        // 發生碰撞時逐一呼叫傾聽器
        this.collisionListeners.forEach(listener => listener(this));
    }

    ...
}

對圓的生命週期管理部份,可以使用 Map,將 Circle 實例作為鍵(key),生命值作為值(value),對碰撞事件進行註冊,發生碰撞時減少生命值,而在 update 中,可以檢查粒子們的生命值,若小於等於 0 了,就將之移除:

class CollisionSystem {
    constructor(number, width, height, lifespan = 255, losing = 10, minR = 5, maxR = 20, maxVx = 5, maxVy = 5) {
        ...

        this.particles = new Map();  // 管理的粒子(也就是圓)

        while(this.particles.size < number) {
            const r = random(minR, maxR);
            const coordinate = createVector(random(minX, maxX), random(minY, maxY));
            if(Array.from(this.particles.keys()).every(c => p5.Vector.sub(c.body.coordinate, coordinate).mag() > (c.radius + r))) {
                const body = new Body(
                    coordinate, 
                    createVector(random(-maxVx, maxVx), random(-maxVy, maxVy)),
                    PI * r * r
                );

                // `Circle` 實例作為鍵(key),生命值作為值(value)
                const circle = new Circle(body, r);
                this.particles.set(circle, lifespan);
                // 發生碰撞時,減少生命值
                body.addCollisionListener(evt => {
                    this.particles.set(circle, this.particles.get(circle) - losing);
                });
            }
        }
    }

    update() {
        const circles = Array.from(this.particles.keys());
        circles.forEach(c => c.update());
        checkCollision(circles);
        circles.forEach(c => checkEdges(c));

        this.particles.forEach((lifespan, c) => {
            if(lifespan <= 0) {
                this.particles.delete(c);
            }
        });
    }
}

為了視覺化粒子生命值的變化,使用生命值作為圓的灰階填充:

function draw() {
    background(220);
    collisionSystem.update();
    collisionSystem.particles.forEach((lifespan, c) => {
        fill(lifespan);
        c.draw();
    });
}

現在用這個互相傷害系統來模擬一下吧!這些粒子哪個可以存活下來呢?