我尝试使用画布在HTML5和Javascript的帮助下创建一个小模拟。然而我的问题是,我无法想到一种控制像素行为的方法,而不会将每个像素都作为一个对象,这会导致我的模拟速度大幅下降。
到目前为止,这是代码:
var pixels = [];
class Pixel{
constructor(color){
this.color=color;
}
}
window.onload=function(){
canv = document.getElementById("canv");
ctx = canv.getContext("2d");
createMap();
setInterval(game,1000/60);
};
function createMap(){
pixels=[];
for(i = 0; i <= 800; i++){
pixels.push(sub_pixels = []);
for(j = 0; j <= 800; j++){
pixels[i].push(new Pixel("green"));
}
}
pixels[400][400].color="red";
}
function game(){
ctx.fillStyle = "white";
ctx.fillRect(0,0,canv.width,canv.height);
for(i = 0; i <= 800; i++){
for(j = 0; j <= 800; j++){
ctx.fillStyle=pixels[i][j].color;
ctx.fillRect(i,j,1,1);
}
}
for(i = 0; i <= 800; i++){
for(j = 0; j <= 800; j++){
if(pixels[i][j].color == "red"){
direction = Math.floor((Math.random() * 4) + 1);
switch(direction){
case 1:
pixels[i][j-1].color= "red";
break;
case 2:
pixels[i+1][j].color= "red";
break;
case 3:
pixels[i][j+1].color= "red";
break;
case 4:
pixels[i-1][j].color= "red";
break;
}
}
}
}
}
function retPos(){
return Math.floor((Math.random() * 800) + 1);
}
&#13;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script language="javascript" type="text/javascript" src="game.js"></script>
</head>
<body>
<canvas width="800px" height="800px" id="canv"></canvas>
</body>
</html>
&#13;
希望你能帮助我。
答案 0 :(得分:4)
有很多选项可以加快您的代码
以下内容将使大多数机器工作量太大。
// I removed fixed 800 and replaced with const size
for(i = 0; i <= size; i++){
for(j = 0; j <= size; j++){
ctx.fillStyle=pixels[i][j].color;
ctx.fillRect(i,j,1,1);
}
}
不要通过矩形写每个像素。使用您可以通过createImageData和相关函数从画布API获取的像素数据。它使用比数组快一点的类型化数组,并且可以在同一内容上拥有多个视图。
您可以在一次调用中将所有像素写入画布。不是那么快,但比你正在做的快几十倍。
const size = 800;
const imageData = ctx.createImageData(size,size);
// get a 32 bit view
const data32 = new Uint32Array(imageData.data.buffer);
// To set a single pixel
data32[x+y*size] = 0xFF0000FF; // set pixel to red
// to set all pixels
data32.fill(0xFF00FF00); // set all to green
获取像素坐标处的像素
const pixel = data32[x + y * imageData.width];
有关使用图像数据的详情,请参阅Accessing pixel data。
在将像素数据放到画布上之前,不会显示像素数据
ctx.putImageData(imageData,0,0);
这将为您带来重大改进。
当性能至关重要时,您会牺牲内存和简单性来获得更多的CPU周期,而不是做任何事情。
您有红色像素随机扩展到场景中,您读取每个像素并检查(通过慢速字符串比较)是否为红色。当你找到一个时,你会添加一个随机的红色像素。
检查绿色像素是一种浪费,可以避免。扩展完全被其他红色包围的红色像素也毫无意义。他们什么都不做。
您感兴趣的唯一像素是绿色像素旁边的红色像素。
因此,您可以创建一个缓冲区来保存所有活动红色像素的位置。活动红色至少有一个绿色。你检查所有活动红色的每一帧,如果可能的话,产生新的红色,如果它们被红色包围则杀死它们。
我们不需要存储每个红色的x,y坐标,只需存储内存地址,这样我们就可以使用平面阵列。
const reds = new Uint32Array(size * size); // max size way over kill but you may need it some time.
您不想在红色阵列中搜索红色,因此您需要跟踪有多少活动红色。您希望所有活动的红色都位于数组的底部。您需要每帧仅检查一次活动红色。如果红色已经死亡,那么它必须向下移动一个数组索引。但是你只希望每帧只移动一次红色。
我不知道这种类型的阵列被称为分离槽是什么,死的东西慢慢向上移动,现场的东西向下移动。或者未使用的物品冒泡使用的物品会沉淀到底部。
我会将其显示为功能性因为它更容易理解。但更好地实现为一个暴力函数
// data32 is the pixel data
const size = 800; // width and height
const red = 0xFF0000FF; // value of a red pixel
const green = 0xFF00FF00; // value of a green pixel
const reds = new Uint32Array(size * size); // max size way over kill but you var count = 0; // total active reds
var head = 0; // index of current red we are processing
var tail = 0; // after a red has been process it is move to the tail
var arrayOfSpawnS = [] // for each neighbor that is green you want
// to select randomly to spawn to. You dont want
// to spend time processing so this is a lookup
// that has all the possible neighbor combinations
for(let i = 0; i < 16; i ++){
let j = 0;
const combo = [];
i & 1 && (combo[j++] = 1); // right
i & 2 && (combo[j++] = -1); // left
i & 4 && (combo[j++] = -size); // top
i & 5 && (combo[j++] = size); // bottom
arrayOfSpawnS.push(combo);
}
function addARed(x,y){ // add a new red
const pixelIndex = x + y * size;
if(data32[pixelIndex] === green) { // check if the red can go there
reds[count++] = pixelIndex; // add the red with the pixel index
data32[pixelIndex] = red; // and set the pixel
}
}
function safeAddRed(pixelIndex) { // you know that some reds are safe at the new pos so a little bit faster
reds[count++] = pixelIndex; // add the red with the pixel index
data32[pixelIndex] = red; // and set the pixel
}
// a frame in the life of a red. Returns false if red is dead
function processARed(indexOfRed) {
// get the pixel index
var pixelIndex = reds[indexOfRed];
// check reds neighbors right left top and bottom
// we fill a bit value with each bit on if there is a green
var n = data32[pixelIndex + 1] === green ? 1 : 0;
n += data32[pixelIndex - 1] === green ? 2 : 0;
n += data32[pixelIndex - size] === green ? 4 : 0;
n += data32[pixelIndex + size] === green ? 8 : 0;
if(n === 0){ // no room to spawn so die
return false;
}
// has room to spawn so pick a random
var nCount = arrayOfSpawnS[n].length;
// if only one spawn point then rather than spawn we move
// this red to the new pos.
if(nCount === 1){
reds[indexOfRed] += arrayOfSpawnS[n][0]; // move to next pos
}else{ // there are several spawn points
safeAddRed(pixelIndex + arrayOfSpawnS[n][(Math.random() * nCount)|0]);
}
// reds frame is done so return still alive to spawn another frame
return true;
}
现在处理所有红色。
这是泡泡阵列的核心。 head
用于索引每个活动的红色。如果未遇到任何死亡,tail
等于head
,则tail
是移动当前head
的位置的索引。但是,如果遇到死项,则head
向上移动一个tail
仍然指向死项。这会将所有活动项目移至底部。
检查所有活动项目时head === count
。 tail
的值现在包含在迭代后设置的新count
。
如果您使用的是对象而不是整数,则不要将活动项目向下移动,而是交换head
和tail
项。这有效地创建了一个可用对象池,可在添加新项目时使用。这种类型的数组管理不会产生GC或分配开销,因此与堆栈和对象池相比非常快。
function doAllReds(){
head = tail = 0; // start at the bottom
while(head < count){
if(processARed(head)){ // is red not dead
reds[tail++] = reds[head++]; // move red down to the tail
}else{ // red is dead so this creates a gap in the array
// Move the head up but dont move the tail,
// The tail is only for alive reds
head++;
}
}
// All reads done. The tail is now the new count
count = tail;
}
该演示将向您展示速度的提升。我使用了功能版本,可能还有其他一些调整。
您还可以考虑使用webWorkers来提高事件的速度。 Web worker在单独的javascript上下文中运行,并提供真正的并发处理。
为了最终的速度使用WebGL。所有逻辑都可以通过GPU上的片段着色器完成。这种类型的任务非常适合GPU设计的并行处理。
稍后会回来清理这个答案(有点太长了)
我还为像素阵列添加了边界,因为红色是从像素阵列产生的。
const size = canvas.width;
canvas.height = canvas.width;
const ctx = canvas.getContext("2d");
const red = 0xFF0000FF;
const green = 0xFF00FF00;
const reds = new Uint32Array(size * size);
const wall = 0xFF000000;
var count = 0;
var head = 0;
var tail = 0;
var arrayOfSpawnS = []
for(let i = 0; i < 16; i ++){
let j = 0;
const combo = [];
i & 1 && (combo[j++] = 1); // right
i & 2 && (combo[j++] = -1); // left
i & 4 && (combo[j++] = -size); // top
i & 5 && (combo[j++] = size); // bottom
arrayOfSpawnS.push(combo);
}
const imageData = ctx.createImageData(size,size);
const data32 = new Uint32Array(imageData.data.buffer);
function createWall(){//need to keep the reds walled up so they dont run free
for(let j = 0; j < size; j ++){
data32[j] = wall;
data32[j * size] = wall;
data32[j * size + size - 1] = wall;
data32[size * (size - 1) +j] = wall;
}
}
function addARed(x,y){
const pixelIndex = x + y * size;
if (data32[pixelIndex] === green) {
reds[count++] = pixelIndex;
data32[pixelIndex] = red;
}
}
function safeAddRed(pixelIndex) {
reds[count++] = pixelIndex;
data32[pixelIndex] = red;
}
function processARed(indexOfRed) {
const pixelIndex = reds[indexOfRed];
var n = data32[pixelIndex + 1] === green ? 1 : 0;
n += data32[pixelIndex - 1] === green ? 2 : 0;
n += data32[pixelIndex - size] === green ? 4 : 0;
n += data32[pixelIndex + size] === green ? 8 : 0;
if(n === 0) { return false }
var nCount = arrayOfSpawnS[n].length;
if (nCount === 1) { reds[indexOfRed] += arrayOfSpawnS[n][0] }
else { safeAddRed(pixelIndex + arrayOfSpawnS[n][(Math.random() * nCount)|0]) }
return true;
}
function doAllReds(){
head = tail = 0;
while(head < count) {
if(processARed(head)) { reds[tail++] = reds[head++] }
else { head++ }
}
count = tail;
}
function start(){
data32.fill(green);
createWall();
var startRedCount = (Math.random() * 5 + 1) | 0;
for(let i = 0; i < startRedCount; i ++) { addARed((Math.random() * size-2+1) | 0, (Math.random() * size-2+1) | 0) }
ctx.putImageData(imageData,0,0);
setTimeout(doItTillAllDead,1000);
countSameCount = 0;
}
var countSameCount;
var lastCount;
function doItTillAllDead(){
doAllReds();
ctx.putImageData(imageData,0,0);
if(count === 0 || countSameCount === 100){ // all dead
setTimeout(start,1000);
}else{
countSameCount += count === lastCount ? 1 : 0;
lastCount = count; //
requestAnimationFrame(doItTillAllDead);
}
}
start();
<canvas width="800" height="800" id="canvas"></canvas>
答案 1 :(得分:2)
减速的主要原因是您假设需要为每个操作循环每个像素。您不这样做,因为对于您需要执行的每个操作,这将是640,000次迭代。
你也不应该在渲染循环中做任何操作逻辑。唯一应该存在的是绘图代码。所以这应该移到最好是一个单独的线程(Web Workers)。如果无法使用那些setTimeout / Interval调用。
所以先做几个小改动:
Make Pixel类包含像素的坐标以及颜色:
class Pixel{
constructor(color,x,y){
this.color=color;
this.x = x;
this.y = y;
}
}
保留最终会创建新红色像素的像素数组。还有一个用于跟踪哪些像素已更新,因此我们知道需要绘制哪些像素。
var pixels = [];
var infectedPixesl = [];
var updatedPixels = [];
现在要更改的最简单的部分是渲染循环。因为它唯一需要做的就是绘制像素,它只有几行。
function render(){
var numUpdatedPixels = updatedPixels.length;
for(let i=0; i<numUpdatedPixels; i++){
let pixel = updatedPixels[i];
ctx.fillStyle = pixel.color;
ctx.fillRect(pixel.x,pixel.y,1,1);
}
//clear out the updatedPixels as they should no longer be considered updated.
updatedPixels = [];
//better method than setTimeout/Interval for drawing
requestAnimationFrame(render);
}
从那里我们可以继续逻辑。我们将遍历infectedPixels
数组,并且每个像素我们决定一个随机方向并获得该像素。如果此选定像素为红色,则我们不执行任何操作并继续。否则我们改变它的颜色并将其添加到临时数组affectedPixels
。之后我们测试原始像素周围的所有像素是否都是红色,如果是,我们可以将其从infectedPixels
中删除,因为不需要再次检查它。然后将affectedPixels
中的所有像素添加到infectedPixels
,因为这些像素现在是需要检查的新像素。最后一步是将affectedPixels
添加到updatedPixels
,以便渲染循环绘制更改。
function update(){
var affectedPixels = [];
//needed as we shouldn't change an array while looping over it
var stillInfectedPixels = [];
var numInfected = infectedPixels.length;
for(let i=0; i<numInfected; i++){
let pixel = infectedPixels[i];
let x = pixel.x;
let y = pixel.y;
//instead of using a switch statement, use the random number as the index
//into a surroundingPixels array
let surroundingPixels = [
(pixels[x] ? pixels[x][y - 1] : null),
(pixels[x + 1] ? pixels[x + 1][y] : null),
(pixels[x] ? pixels[x][y + 1] : null),
(pixels[x - 1] ? pixels[x - 1][y] : null)
].filter(p => p);
//filter used above to remove nulls, in the cases of edge pixels
var rand = Math.floor((Math.random() * surroundingPixels.length));
let selectedPixel = surroundingPixels[rand];
if(selectedPixel.color == "green"){
selectedPixel.color = "red";
affectedPixels.push(selectedPixel);
}
if(!surroundingPixels.every(p=>p.color=="red")){
stillInfectedPixels.push(pixel);
}
}
infectedPixels = stillInfectedPixel.concat( affectedPixels );
updatedPixels.push(...affectedPixels);
}
演示
var pixels = [],
infectedPixels = [],
updatedPixels = [],
canv, ctx;
window.onload = function() {
canv = document.getElementById("canv");
ctx = canv.getContext("2d");
createMap();
render();
setInterval(() => {
update();
}, 16);
};
function createMap() {
for (let y = 0; y < 800; y++) {
pixels.push([]);
for (x = 0; x < 800; x++) {
pixels[y].push(new Pixel("green",x,y));
}
}
pixels[400][400].color = "red";
updatedPixels = [].concat(...pixels);
infectedPixels.push(pixels[400][400]);
}
class Pixel {
constructor(color, x, y) {
this.color = color;
this.x = x;
this.y = y;
}
}
function update() {
var affectedPixels = [];
var stillInfectedPixels = [];
var numInfected = infectedPixels.length;
for (let i = 0; i < numInfected; i++) {
let pixel = infectedPixels[i];
let x = pixel.x;
let y = pixel.y;
let surroundingPixels = [
(pixels[x] ? pixels[x][y - 1] : null),
(pixels[x + 1] ? pixels[x + 1][y] : null),
(pixels[x] ? pixels[x][y + 1] : null),
(pixels[x - 1] ? pixels[x - 1][y] : null)
].filter(p => p);
var rand = Math.floor((Math.random() * surroundingPixels.length));
let selectedPixel = surroundingPixels[rand];
if (selectedPixel.color == "green") {
selectedPixel.color = "red";
affectedPixels.push(selectedPixel);
}
if (!surroundingPixels.every(p => p.color == "red")) {
stillInfectedPixels.push(pixel);
}
}
infectedPixels = stillInfectedPixels.concat(affectedPixels);
updatedPixels.push(...affectedPixels);
}
function render() {
var numUpdatedPixels = updatedPixels.length;
for (let i = 0; i < numUpdatedPixels; i++) {
let pixel = updatedPixels[i];
ctx.fillStyle = pixel.color;
ctx.fillRect(pixel.x, pixel.y, 1, 1);
}
updatedPixels = [];
requestAnimationFrame(render);
}
<canvas id="canv" width="800" height="800"></canvas>