Loading images before rendering JS canvas
Asked Answered
D

1

1

I'm writing one of those simple games to learn JS and I'm learning HTML5 in the process so I need to draw things on canvas.

Here's the code:

 let paddle = new Paddle(GAME_WIDTH,GAME_HEIGHT);

 new InputHandler(paddle);

 let lastTime = 0;

 const ball = new Image();
 ball.src = 'assets/ball.png';

 function gameLoop(timeStamp){
   let dt = timeStamp - lastTime;
   lastTime = timeStamp;

   ctx.clearRect(0,0,600,600);
   paddle.update(dt);
   paddle.draw(ctx);

   ball.onload = () => {
    ctx.drawImage(ball,20,20);
  }

  window.requestAnimationFrame(gameLoop);
 }

gameLoop();

screenshot: no ball before comment

now I comment out the clearRect():

after comment

hello ball.

There's also a paddle at the bottom of the canvas that doesn't seem to be affected by the clearRect() method. It works just fine. What am I missing here?

Defend answered 21/4, 2020 at 5:56 Comment(2)
"What am I missing here?" at least one } to begin with. Also, if your indentation was correct that would help to read your code. gameLoop is never called. Depending on the two first points, ball.onload may never fire if it is indeed inside gameLoop, because it's an event listener, and will fire only once ; You'd be better having ball.onload = galeLoop;Circulation
@Circulation I've included the rest of the code. Thanks for taking the time to look into this. I'll study the event listenersDefend
S
1

It doesn't make much sense to put the image's onload handler inside the game loop. This means the game has to begin running before the image's onload function is set, leading to a pretty confusing situation.

The correct sequence is to set the onload handlers, then the image sources, then await all of the image onloads firing before running the game loop. Setting the main loop to an onload directly is pretty easy when you only have one image, but for a game with multiple assets, this can get awkward quickly.

Here's a minimal example of how you might load many game assets using Promise.all. Very likely, you'll want to unpack the loaded images into more descriptive objects rather than an array, but this is a start.

const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = 400;
canvas.height = 250;
const ctx = canvas.getContext("2d");

const assets = [
  "https://picsum.photos/120/100",
  "https://picsum.photos/120/120",
  "https://picsum.photos/120/140",
];
const assetsLoaded = assets.map(url =>
  new Promise((resolve, reject) => {
    const img = new Image();
    img.onerror = e => reject(`${url} failed to load`);
    img.onload = e => resolve(img);
    img.src = url;
  })
);

Promise
  .all(assetsLoaded)
  .then(images => {
    (function gameLoop() {
      requestAnimationFrame(gameLoop);
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      images.forEach((e, i) =>
        ctx.drawImage(
          e, 
          i * 120, // x
          Math.sin(Date.now() * 0.005) * 20 + 40 // y
        )
      );
    })();
  })
  .catch(err => console.error(err));
Spoilsport answered 21/4, 2020 at 6:31 Comment(2)
this worked perfectly! I'm not quite there yet but I'm starting to get the hang of js events. it can get quite confusingDefend
Yeah there is a learning curve to promises and callbacks. Keep at it.Spoilsport

© 2022 - 2024 — McMap. All rights reserved.