(Sugar, Sugar 等游戏的物理引擎)SpriteKit 对许多物理精灵的性能优化

(Physics engine for games like Sugar, Sugar) SpriteKit performance optimization for many physics sprites

我是一名 iOS 游戏开发者,最近我看到了一款有趣的物理和绘画游戏 "Sugar, Sugar"。在游戏中,有许多像素粒子(数千个)从屏幕上生成并自由落到地上。玩家可以绘制任何形状的线条,这些线条可以将这些粒子引导到特定的杯子。图片来自 google:

我正在尝试通过 Swift 使用 SpriteKit 来实现类似的效果。这是我得到的:

然后我遇到了性能问题。一旦粒子数量 > 100。 CPU 和能源成本非常高。 (我使用 iPhone 6s)。所以我相信 "Sugar, Sugar" 中的物理引擎比现实的 SpriteKit 简单得多。但是我不知道那里的物理引擎是什么,我怎样才能在 SpriteKit 中实现它?

PS: 我使用一个图像作为所有这些粒子的纹理,只加载一次以节省性能。我只用了SKSpriteNode,出于性能原因也没有使用ShapeNode。

我已经很长时间没有玩沙子模拟了,所以我想我会为你制作一个快速演示。

在javascript完成,鼠标左键加沙子,右键画线。根据机器的不同,它可以处理数千粒沙子。

它通过创建一个像素数组来工作,每个像素都有一个 x,y 位置和一个 delta x,y 以及一个标志来指示它是不活动的(死的)。每一帧我都清除显示然后添加墙壁。然后对于每个像素,我检查两侧或下方是否有像素(取决于移动方向)并添加侧滑、墙壁反弹或重力。如果某个像素有一段时间没有移动,我将其设置为死像素并仅绘制它以节省计算时间。

sim 非常简单,第一个像素(颗粒)永远不会碰到另一个,因为它是用清晰的显示绘制的,像素只能看到在它们之前创建的像素。但这很有效,因为它们可以自我组织并且不会相互重叠。

你可以在功能展示中找到逻辑,(倒数第二个功能)有一些自动演示代码,然后是绘制墙壁,显示墙壁,获取像素数据然后模拟的代码每个像素。

它并不完美(就像您提到的游戏一样),但它只是一个快速的 hack 来展示它是如何完成的。我还为插图做了很大的 window 所以整页浏览效果最好。

/** 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 **/
* { font-family: arial; }
canvas { image-rendering: pixelated; }
<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>