(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>
我是一名 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>