简体   繁体   中英

How can I add delays in between each iteration of a While Loop for a Canvas Animation?

Im writing a Tomb of the Mask game clone in Canvas for an assignment, and I wanted to create a small animation so that when my character "Snaps" to the next wall, it doesnt just "teleport" like it does now.

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

Currently I have a big keydown event and a switch case that detects for any of the 4 arrow keys, this is an example of one of the case statements:

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

Ill split up those map statements for a better understanding:

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.

Anyways, I have an animationFrame running on my player location at ALL TIMES so that if any adjustments are made, it will show them.

The while loops I use to send the player to the opposite wall (rather than just moving 1 block to the left or right, it needs to work JUST like TotM) are just immediately sending the results. I want it to quickly move all those blocks 1 by 1 but barely noticable...

Is it possible I can add some kind of "delay" inside the while loops so it can move 1 block, then wait 10 milliseconds, and then the next, so on so fourth?

Full Game and Code:

View in FULL PAGE or it wont work correctly..

 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>

View in FULL PAGE or it wont work correctly..

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.

This is BIG TOPIC

The normal thing to do would be for each thing (player, monster, etc..) to give them some kind of update function

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.

Thanks to ViktorW, gman and VLAZ for the help and answers, I came up with a solution though:

so if you put a "run object" into the project, and have 4 sub variables with true/false, and change them depending on which key you press, you can make it work perfectly.

I followed ViktorW's answer and applied this idea, plus I just made a simple interval instead of the logic Viktor used:

so outside of my movement function, I have the run variable

let run = {
 l: true,
 r: true,
 u: true,
 d: true
}

theres variables for the 4 directions (Left Right Up Down)

Then just add the logic into the movement function + the interval and ITS logic:

case "ArrowLeft":
if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
  if (run.l == true) { // Detect for direction
    var lInterval = setInterval(() => {
      if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
        playerCoords[0]--;
        run.r = run.u = run.d = false; // Set all other directions false
      } else {
        clearInterval(lInterval);
        run.r = run.u = run.d = true; // Set all other directions true when done moving
      }
    }, 10);
  }
}
break;

This PERFECTLY prevents the opposing movement of of the block if you use an interval to achieve the animation.

Check it out live here, use arrow keys to control: Tomb of the Mask Clone

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM