如何在Canvas动画的While循环的每次迭代之间添加延迟?

我在Canvas中为任务写了一个面具游戏克隆墓,我想创建一个小动画,以便当我的角色“捕捉”到下一堵墙时,它不再像现在那样“传送”。

See here for a live review: https://codepen.io/SkylerSpark/pen/GRpdzBZ

当前,我有一个很大的keydown事件和一个开关案例,它可以检测4个箭头键中的任何一个,这是case语句之一的示例:

case "ArrowLeft":
    if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
        while (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
            playerCoords[0]--;
        }
    }

为了更好的理解,我将这些map语句拆分开了:

map[playerCoords[1]][playerCoords[0] - 1] != 1

map[] - Main Map Data (1s and 0s that determine the game layout)
playerCoords[0 / 1] - Location of the Player

map[ pc[1] ] (going to get the sub array of map[playerCoords[1]]) > [pc[0] - 1] (-1 to look for the block to the left of the player)

then Im selecting all of that into one statement and detecting if its equal to 1 (1 is a brick block) to see if the player should MOVE or NOT MOVE.

无论如何,我始终在播放器位置上运行一个animationFrame,这样,如果进行了任何调整,它将显示它们。

我用来将玩家发送到对面墙的while循环(而不仅仅是向左或向右移动1个方块,它需要像TotM一样工作)只是立即发送结果。我希望它快速将所有这些块一一移动,但几乎没有引起注意...

是否可以在while循环中添加某种“延迟”,使其可以移动1个块,然后等待10毫秒,然后再等待下一个,依此类推?

完整游戏和代码:

在全页中查看,否则将无法正常工作。

const cvs = document.querySelector(".bastione"),
	ctx = cvs.getContext("2d");

const cvs2 = document.querySelector(".basPlayer"),
	ctx2 = cvs2.getContext("2d");

 ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = false;
 ctx2.imageSmoothingEnabled = ctx2.mozImageSmoothingEnabled = ctx2.webkitImageSmoothingEnabled = false;

function loadImage(src, callback) {
	var img = new Image();
	img.onload = callback;
	img.setAttribute("crossorigin", "anonymous");
	img.src = src;
	return img;
}

function ran(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

const map = [
	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
	[1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1],
	[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
	[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
	[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
	[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
	[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
	[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
	[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1],
	[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
	[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
	[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1],
	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
];

const drawEnvironment = {
	init: () => {
		drawEnvironment.renderBack();
	},
	renderBack: () => {
		let cx = 0, cy = 0;
		map.forEach(e => {
			for (var i = 0; i < e.length; i++) {
				if (e[i] == 1) {
					let v = ran(0, 10);
					if (v > 0 && v < 8) {
		   	ctx.drawImage(spriteImage, 0, 0, 32, 32, cx, cy, 32, 32);
					} else {
		   	ctx.drawImage(spriteImage, 32, 0, 32, 32, cx, cy, 32, 32);
					}
					cx += 32;
				} else if (e[i] == 2 || e[i] == 3) {
		   ctx.drawImage(spriteImage, 64, 64, 32, 32, cx, cy, 32, 32);
					cx += 32;
				} else {
					let v = ran(0, 10);
					if (v > 0 && v < 5) {
		   	 ctx.drawImage(spriteImage, 128, 64, 32, 32, cx, cy, 32, 32);
						if (v == 10) {
		   	 ctx.drawImage(spriteImage, 128, 32, 32, 32, cx, cy, 32, 32);
						}
					} else {
		   	ctx.drawImage(spriteImage, 128, 32, 32, 32, cx, cy, 32, 32);
					}
					cx += 32;
				}
			}
			cx = 0;
			cy += 32;
		});
		ctx.drawImage(spriteImage, 0, 0, 32, 32, 0, 0, 32, 32);
	}
};

let playerCoords = [1, 1];

const drawPlayer = {
	init: () => {
		drawPlayer.playerLoc();
		drawPlayer.playerMove();
	},
	playerLoc: () => {
		ctx2.drawImage(spriteImage, 0, 64, 32, 32, playerCoords[0] * 32, playerCoords[1] * 32, 32, 32);
		window.requestAnimationFrame(drawPlayer.playerLoc);
	},
	playerMove: () => {
		document.addEventListener("keydown", function(event) {
			ctx2.clearRect(0, 0, cvs2.width, cvs2.height);
  	event.preventDefault();
  	const key = event.key;
  	switch (key) {
    case "ArrowLeft":
					if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
						while (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
							playerCoords[0]--;
						}
					}
     break;
    case "ArrowRight":
					if (map[playerCoords[1]][playerCoords[0] + 1] != 1) {
						while (map[playerCoords[1]][playerCoords[0] + 1] != 1) {
							playerCoords[0]++;
						}
					}
     break;
    case "ArrowUp":
					if (map[playerCoords[1] - 1][playerCoords[0]] != 1) {
						while (map[playerCoords[1] - 1][playerCoords[0]] != 1) {
							playerCoords[1]--;
						}
					}
     break;
    case "ArrowDown":
					if (map[playerCoords[1] + 1][playerCoords[0]] != 1) {
						while (map[playerCoords[1] + 1][playerCoords[0]] != 1) {
							playerCoords[1]++;
						}
					}
     break;
  	}
		});
	}
}

const spriteImage = loadImage(
	"https://cdn.jsdelivr.net/gh/FunctFlow/Bastione-Game@1d0514c968a737061916ae5e160b20eaf3a6b8b4/Sprites/Bastione_Sprites.png",
	() => {
		drawEnvironment.init();
		drawPlayer.init();
 }
);
* {
	box-sizing: border-box;
	overflow: hidden;
}

body {
	text-align: center;
	background: black;
}

canvas {
	display: inline-block;
}

.basPlayer {
	position: absolute;
	margin-left: -1024px;
}
<canvas class=bastione width=1024 height=512></canvas>
<canvas class=basPlayer width=1024 height=512></canvas>

在全页中查看,否则将无法正常工作。

评论
  • overo
    overo 回复

    这是大话题

    通常要做的是为每件事(玩家,怪物等)赋予它们某种更新功能

    const allTheThings = [];
    
    function loop() {
      for (const thing of things) {
        thing.update();
      }
      requestAnimationFrame(loop);
    }
    

    In each of those update functions you would do whatever is appropriate for that thing doing only what is need at this moment. So for example the player might have a update function like this

    class Player() {
      constructor() { 
        this.coords = [1, 1];
        this.delta = [0, 0];
      }
      update() {
        if (this.waiting) {
          this.waiting -= 1;
        } else if (this.moving) {
          this.coords[0] = this.delta[0];
          this.coords[1] = this.delta[1];
          this.waiting = 10;
        } else {
          // check the keys
          // and set this.delta and moving appropriately
        }
      }
    }
    

    Then you can make a player and add it to this array of allTheThings

    const player = new Player();
    allTheThings.push(player);
    

    Note that is way over simplified. Most games don't directly update in an object like that. Instead, just like allTheThings calls update for each thing, a thing itself might have a list of subthings (components) each of which also has an update function. A thing, or a GameThing, is a collection of these components.

    Further, there are all kinds of ways to help organize what those update functions do. In the example above there were 2 flags moving and waiting but as the game gets more and more complicated there get to be too many flags so people have come up with things like Finite State Machines and Coroutines and may other techniques to help make that stuff simpler.

    Maybe not useful but here is an article that uses some of these techniques.

  • xiste
    xiste 回复

    You can use setInterval to create a loop which runs at a specific frequency.

    case "ArrowLeft":
        if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
            let interval = setInterval(() => {
                if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
                    // Stop the interval, and do nothing.
                    clearInterval(interval)
                    return
                }
                playerCoords[0]--;
            }, 100) // <- 100 here is delay in milliseconds
        }
    

    This code is now running asynchronously. This means that while we are waiting for the delay, other code can run. If we are not careful, this can introduce bugs. For example: You need to think about what would happen if the player presses arrowRight while the arrowLeft is still being processed. If you do not fix that case, the character will move both left and right at the same time, which would mean that he would never hit a wall and you would be stuck in an endless loop.