问题描述
我是iOS游戏开发者,我看到了一个有趣的物理&最近绘制游戏糖,糖".在游戏中,屏幕产生了很多像素粒子(成千上万个像素粒子),这些像素粒子可以自由落到地面上.播放器可以绘制任何形状的线条,从而可以将这些粒子引导到某些杯子中.来自Google的图片:
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:
我正在尝试将SpriteKit与Swift配合使用,以达到类似的效果.这是我得到的:
I'm trying to achieve similar effect using SpriteKit with Swift. Here's what I got:
然后我遇到了性能问题.一旦粒子数量>100.CPU和能源成本非常高. (我使用iPhone 6s).因此,我相信糖,糖"中的物理引擎比现实的SpriteKit简单得多.但是我不知道那里有什么物理引擎,如何在SpriteKit中实现呢?
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:我使用一个图像作为所有这些粒子的纹理,仅加载一次以节省性能.我只使用SKSpriteNode,出于性能原因也没有使用ShapeNode.
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.
推荐答案
我已经很长时间没有做过模拟沙子了,所以我想我会为您创建一个快速演示.
I have not done a sand sim for a long time so I thought I would create a quick demo for you.
这是用javascript完成的,左键添加沙子,右键绘制线条.根据机器的不同,它将处理数千粒沙子.
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.
它通过创建一个像素数组来工作,每个像素在x,y位置都有一个x,y增量,以及一个标志来指示它处于非活动状态(死).每帧我清除显示,然后添加墙壁.然后,对于每个像素,我检查侧面或下方是否有像素(取决于移动方向),并添加侧滑,壁反弹或重力.如果某个像素一段时间未移动,则将其设置为死像素,仅绘制该像素以节省计算时间.
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.
SIM卡非常简单,第一个像素(纹理)永远不会碰到另一个像素,因为它以清晰的显示进行绘制,像素只能看到在其之前创建的像素.但这很有效,因为它们可以自我组织,并且不会相互重叠.
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.
您可以在功能显示中找到逻辑,(从底部开始的第二个功能)有一些用于自动演示的代码,然后是用于绘制墙,显示墙,获取像素数据并为每个像素执行sim的代码
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>
这篇关于(用于Sugar,Sugar等游戏的物理引擎)针对许多物理精灵的SpriteKit性能优化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!