(Physics engine for games like Sugar, Sugar) Sprit

2019-09-10 14:10发布

I'm a iOS game developer and I saw an interesting physics & draw game "Sugar, Sugar" recently. In the game, there are lots of pixel particles (thousands of them) generated from the screen and free falling to the ground. Player can draw any shape of lines, which can guide those particles to certain cups. A image from google: enter image description here

I'm trying to achieve similar effect using SpriteKit with Swift. Here's what I got: enter image description here

Then I encounter a performance problem. Once the number of particles > 100. The CPU and energy costs are very high. (I use iPhone 6s). So I believe the Physics Engine in "Sugar, Sugar" is much simpler than the realistic SpriteKit. But I don't know what's the physics engine there and how can I achieve this in SpriteKit?

PS: I use one single image as texture for all those particles, only loaded once to save performance. I only use SKSpriteNode, no ShapeNode is used for performance reason too.

1条回答
Summer. ? 凉城
2楼-- · 2019-09-10 14:39

I have not done a sand sim for a long time so I thought I would create a quick demo for you.

It is done in javascript, left mouse adds sand, right mouse draws lines. Depending on the machine it will handle thousands of grains of sand.

It works by creating an array of pixels, each pixel has a x,y position a delta x,y and a flag to indicate it is inactive (dead). Every frame I clear the display and then add the walls. Then for each pixel I check if there are pixels to the sides or below (depending on the direction of movement) and add sideways slippage, bounce of wall, or gravity. If a pixel has not moved for some time I set it as dead and only draw it to save time on the calculations.

The sim is very simple, the first pixel (grain) will never bump into another because it is drawn with a clear display, pixels can only see pixels created before them. But this works well as they self organize and will not overlap each other.

You can find the logic in the function display, (second function from bottom) there is some code for auto demo, then code for drawing the walls, displaying the walls, getting the pixel data and then doing the sim for each pixel.

Its not perfect (like the game you have mentioned) but it is just a quick hack to show how it is done. Also I made it to big for the inset window so best viewed full page.

/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars 
var canvas, ctx, mouse;
var globalTime = 0; 
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
    var c,cs;
    cs = (c = document.createElement("canvas")).style; 
    c.id = CANVAS_ELEMENT_ID;    
    cs.position = "absolute";
    cs.top = cs.left = "0px";
    cs.width = cs.height = "100%";
    cs.zIndex = 1000;
    document.body.appendChild(c); 
    return c;
}
resizeCanvas = function () {
    if (canvas === U) { canvas = createCanvas(); }
    canvas.width = Math.floor(window.innerWidth/4);
    canvas.height = Math.floor(window.innerHeight/4); 
    ctx = canvas.getContext("2d"); 
    if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; }
mouse = (function(){
    function preventDefault(e) { e.preventDefault(); }
    var mouse = {
        x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
        over : false,  // mouse is over the element
        bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
        mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
    };
    var m = mouse;
    function mouseMove(e) {
        var t = e.type;
        m.x = e.offsetX; m.y = e.offsetY;
        if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
        m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
        if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }  
        else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
        else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
        else if (t === "mouseover") { m.over = true; }
        else if (t === "mousewheel") { m.w = e.wheelDelta; }
        else if (t === "DOMMouseScroll") { m.w = -e.detail; }
        if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
        e.preventDefault();
    }
    m.addCallback = function (callback) {
        if (typeof callback === "function") {
            if (m.callbacks === U) { m.callbacks = [callback]; }
            else { m.callbacks.push(callback); }
        } else { throw new TypeError("mouse.addCallback argument must be a function"); }
    }
    m.start = function (element, blockContextMenu) {
        if (m.element !== U) { m.removeMouse(); }        
        m.element = element === U ? document : element;
        m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
        m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
        if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
    }
    m.remove = function () {
        if (m.element !== U) {
            m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
            if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
            m.element = m.callbacks = m.contextMenuBlocked = U;
        }
    }
    return mouse;
})();
var done = function(){
    window.removeEventListener("resize",resizeCanvas)
    mouse.remove();
    document.body.removeChild(canvas);    
    canvas = ctx = mouse = U;
    L("All done!")
}

resizeCanvas(); // create and size canvas
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event
var simW = 200;
var simH = 200;
var wallCanvas = document.createElement("canvas"); 
wallCanvas.width = simW;
wallCanvas.height = simH;
var wallCtx = wallCanvas.getContext("2d"); 
var bounceDecay = 0.7;
var grav = 0.5;
var slip = 0.5;
var sandPerFrame = 5;
var idleTime  = 50;
var pixels = [];
var inactiveCounter = 0;
var demoStarted;
var lastMouse;
var wallX;
var wallY;
function display(){  // Sim code is in this function
    var blocked;
    var obstructed;
    w = canvas.width;
    h = canvas.height;
    var startX = Math.floor(w / 2) - Math.floor(simW / 2);
    var startY = Math.floor(h / 2) - Math.floor(simH / 2);    
    if(lastMouse === undefined){
        lastMouse = mouse.x + mouse.y;
    }
    if(lastMouse === mouse.x + mouse.y){
        inactiveCounter += 1;
        
    }else{
        inactiveCounter = 0;
    }
    if(inactiveCounter > 10 * 60){
        if(demoStarted === undefined){
            wallCtx.beginPath();
            var sy = simH / 6;
            for(var i = 0; i < 4; i ++){
                wallCtx.moveTo(simW * (1/6) - 10,sy * i + sy * 1);
                wallCtx.lineTo(simW * (3/ 6) - 10,sy * i + sy  * 2);
                wallCtx.moveTo(simW * (5/6) + 10,sy * i + sy  * 0.5);
                wallCtx.lineTo(simW * (3/6) +10,sy * i + sy  * 1.5);
            }
            wallCtx.stroke();            
          
            
        }
        mouse.x = startX * 4 + (simW * 2);
        mouse.y = startY * 4  +  (simH * 2 )/5;
        lastMouse = mouse.x + mouse.y;
        mouse.buttonRaw = 1;
        
    }

    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h);
    ctx.strokeRect(startX+1,startY+1,simW-2,simH-2)
    ctx.drawImage(wallCanvas,startX,startY); // draws the walls


    if(mouse.buttonRaw & 4){  // if right button draw walls
        if(mouse.x/4 > startX && mouse.x/4 < startX + simW && mouse.y/4 > startY && mouse.y/4 < startY + simH){
            if(wallX === undefined){
                wallX = mouse.x/4 - startX
                wallY = mouse.y/4 - startY
            }else{
                wallCtx.beginPath();
                wallCtx.moveTo(wallX,wallY);
                wallX = mouse.x/4 - startX
                wallY = mouse.y/4 - startY
                wallCtx.lineTo(wallX,wallY);
                wallCtx.stroke();
            }
        }
        
        
    }else{
        wallX = undefined;
    }
    if(mouse.buttonRaw & 1){ // if left button add sand
        for(var i = 0; i < sandPerFrame; i ++){
            var dir = Math.random() * Math.PI;
            var speed = Math.random() * 2;
            var dx = Math.cos(dir) * 2;
            var dy = Math.sin(dir) * 2;
            pixels.push({
                x : (Math.floor(mouse.x/4) - startX) + dx,
                y : (Math.floor(mouse.y/4) - startY) + dy,
                dy : dx * speed,
                dx : dy * speed,
                dead : false,
                inactive : 0,
                r : Math.floor((Math.sin(globalTime / 1000) + 1) * 127),
                g : Math.floor((Math.sin(globalTime / 5000) + 1) * 127),
                b : Math.floor((Math.sin(globalTime / 15000) + 1) * 127),
            });
        }
        if(pixels.length > 10000){ // if over 10000 pixels reset
            pixels = [];
        }
   
    }
    // get the canvas pixel data
    var data = ctx.getImageData(startX, startY,simW,simH);
    var d = data.data;
    
    // handle each pixel;
    for(var i = 0; i < pixels.length; i += 1){
        var p = pixels[i];
        if(!p.dead){
            var ind = Math.floor(p.x) * 4 + Math.floor(p.y) * 4 * simW;
            d[ind + 3] = 0;
            obstructed = false;
            p.dy += grav;
            var dist = Math.floor(p.y + p.dy) - Math.floor(p.y);
            if(Math.floor(p.y + p.dy) - Math.floor(p.y) >= 1){
                if(dist >= 1){
                    bocked = d[ind + simW * 4 + 3];
                }
                if(dist >= 2){
                    bocked += d[ind + simW * 4 * 2 + 3];
                }
                if(dist >= 3){
                    bocked += d[ind + simW * 4 * 3 + 3];
                }
                if(dist >= 4){
                    bocked += d[ind + simW * 4 * 4 + 3];
                }
                
                if( bocked > 0 || p.y + 1 > simH){
                    p.dy = - p.dy * bounceDecay;
                    obstructed = true;
                }else{
                    p.y += p.dy;
                }
            }else{
                p.y += p.dy;
            }
            if(d[ind + simW * 4 + 3] > 0){
                if(d[ind + simW * 4 - 1] === 0 && d[ind + simW * 4 + 4 + 3] === 0 ){
                    p.dx += Math.random() < 0.5 ? -slip/2 : slip/2;
                }else
                if(d[ind + 4 + 3] > 0  && d[ind + simW * 4 - 1] === 0 ){
                    p.dx -= slip;
                    
                }else
                if(d[ind - 1] + d[ind - 1 - 4] > 0  ){
                    p.dx += slip/2;
                    
                }else
                if(d[ind +3] + d[ind + 3 + 4] > 0  ){
                    p.dx -= slip/2;
                    
                }else
                if(d[ind + 1] + d[ind + 1] > 0 && d[ind + simW * 4 + 3] > 0 && d[ind + simW * 4 + 4 + 3] === 0 ){
                    p.dx += slip;
                    
                }else
                if(d[ind + simW * 4 - 1] === 0 ){
                    p.dx +=  -slip/2;
                    
                    
                }else
                if(d[ind + simW * 4 + 4 + 3] === 0 ){
                    p.dx +=  -slip/2;
                }
            }
            if(p.dx < 0){
                if(Math.floor(p.x + p.dx) - Math.floor(p.x) <= -1){
                    if(d[ind - 1] > 0){
                        p.dx = -p.dx * bounceDecay;
                    }else{
                        p.x += p.dx;
                    }
                }else{
                    p.x += p.dx;
                }
            }else
            if(p.dx > 0){
                if(Math.floor(p.x + p.dx) - Math.floor(p.x) >= 1){
                    if(d[ind + 4 + 3] > 0){
                        p.dx = -p.dx * bounceDecay;
                    }else{
                        p.x += p.dx;
                    }
                }else{
                    p.x += p.dx;
                }
                
            }
            var ind = Math.floor(p.x) * 4 + Math.floor(p.y) * 4 * simW;
            d[ind ] = p.r;
            d[ind + 1] = p.g;
            d[ind + 2] = p.b;
            d[ind + 3] = 255;
            if(obstructed && p.dx * p.dx + p.dy * p.dy < 1){
                p.inactive += 1;
                if(p.inactive > idleTime){
                    p.dead = true;
                }
            }
        }else{
            var ind = Math.floor(p.x) * 4 + Math.floor(p.y) * 4 * simW;
            d[ind ] = p.r;
            d[ind + 1] = p.g;
            d[ind + 2] = p.b;
            d[ind + 3] = 255;

        }
    }
    ctx.putImageData(data,startX, startY);
    
    
}
function update(timer){ // Main update loop
    globalTime = timer;
    display();  // call demo code
    // continue until mouse right down
    if (!(mouse.buttonRaw & 2)) { requestAnimationFrame(update); } else { done(); }
}
requestAnimationFrame(update);

/** SimpleFullCanvasMouse.js end **/
<p>Right click drag to draw walls</p>
<p>Left click hold to drop sand</p>
<p>Demo auto starts in 10 seconds is no input</p>
<p>Sim resets when sand count reaches 10,000 grains</p>
<p>Middle button quits sim</p>

查看更多
登录 后发表回答