Search code examples
javascripthtmlarrayscanvasonload-event

Images onload in one function


I'm trying to learn JavaScript, making my first game. How I can make all images onload in one function and later draw it in the canvas making my code shorter?

How can I put a lot of images in an array and later us it in a function.

This is my third day of learning JavaScript.

Thanks in advance.

var cvs = document.getElementById('canvas');
var ctx = cvs.getContext('2d');

//load images

var bird = new Image();
var bg = new Image();
var fg = new Image();
var pipeNorth = new Image();
var pipeSouth = new Image();

//images directions 
bg.src = "assets/bg.png";
bird.src = "assets/bird.png";
fg.src = "assets/fg.png";
pipeNorth.src = "assets/pipeNorth.png";
pipeSouth.src = "assets/pipeSouth.png";

var heightnum = 80;
var myHeight = pipeSouth.height+heightnum;
var bX = 10;
var bY = 150;
var gravity = 0.5;

// Key Control :D
 document.addEventListener("keydown",moveUP)
function moveUP(){
bY -= 20;
}
//pipe coordinates

var pipe = [];

pipe[0] = {
  x : cvs.width,
  y : 0
}
//draw images 


//Background img
  bg.onload = function back(){
    ctx.drawImage(bg,0,0);

  }
  //pipe north
  pipeNorth.onload = function tubo(){

    for(var i = 0; i < pipe.length; i++){

    ctx.drawImage(pipeNorth,pipe[i].x,pipe[i].y);
    pipe[i].x--;
    }
  }

  pipeSouth.onload = function tuba(){
    ctx.drawImage(pipeSouth,pipe[i].x,pipe[i].y+myHeight);

  }



  bird.onload = function pajaro(){
    ctx.drawImage(bird,bX,bY);
    bY += gravity;

    requestAnimationFrame(pajaro);  
  } 


  fg.onload = function flor(){
    ctx.drawImage(fg,0,cvs.height - fg.height);

  }




moveUP();
   back();
   tuba();
   pajaro();
   flor();

Solution

  • This can be done with Promise.all and image.decode(). Once Promise.all resolves, we can call the initialize function and continue on with the rest of the logic. This avoids race conditions where the main game loop's requestAnimationFrame is called from bird.onload, but it's possible that pipe entities and so forth haven't loaded yet.

    Here's a minimal, complete example:

    const initialize = images => {
    
      // images are loaded here and we can go about our business
      
      const canvas = document.createElement("canvas");
      document.body.appendChild(canvas);
      canvas.width = 400;
      canvas.height = 200;
      const ctx = canvas.getContext("2d");
    
      Object.values(images).forEach((e, i) =>
        ctx.drawImage(e, i * 100, 0)
      );
    };
    
    const imageUrls = [
      "https://picsum.photos/90/100",
      "https://picsum.photos/90/130",
      "https://picsum.photos/90/160",      
      "https://picsum.photos/90/190",
    ];
    
    Promise.all(imageUrls.map(async e => {
      const img = new Image();
      img.src = e;
      await img.decode();
      return img;
    })).then(initialize);

    Notice that I used an array in the above example to store the images. The problem this solves is that the

    var foo = ...
    var bar = ...
    var baz = ...
    var qux = ...
    foo.src = ...
    bar.src = ...
    baz.src = ...
    qux.src = ...
    foo.onload = ...
    bar.onload = ...
    baz.onload = ...
    qux.onload = ...
    

    pattern is extremely difficult to manage and scale. If you decide to add another thing into the game, then the code needs to be re-written to account for it and game logic becomes very wet. Bugs become difficult to spot and eliminate. Also, if we want a specific image, we'd prefer to access it like images.bird rather than images[1], preserving the semantics of the individual variables, but giving us the power to loop through the object and call each entity's render function, for example.

    All of this motivates an object to aggregate game entities. Some information we'd like to have per entity might include, for example, the entity's current position, dead/alive status, functions for moving and rendering it, etc.

    It's also a nice idea to have some kind of separate raw data object that contains all of the initial game state (this would typically be an external JSON file).

    Clearly, this can turn into a significant refactor, but it's a necessary step when the game grows beyond small (and we can incrementally adopt these design ideas). It's generally a good idea to bite the bullet up front.

    Here's a proof-of-concept illustrating some of the the musings above. Hopefully this offers some ideas for how you might manage game state and logic.

    const entityData = [
      {
        name: "foo", 
        path: "https://picsum.photos/80/80",
        x: 0,
        y: 0
      },
      {
        name: "baz", 
        path: "https://picsum.photos/80/150",
        x: 0,
        y: 90
      },
      {
        name: "quux", 
        path: "https://picsum.photos/100/130",
        x: 90,
        y: 110
      },
      {
        name: "corge", 
        path: "https://picsum.photos/200/240",
        x: 200,
        y: 0
      },
      {
        name: "bar",
        path: "https://picsum.photos/100/100",
        x: 90,
        y: 0
      }
      /* you can add more properties and functions 
         (movement, etc) to each entity
         ... try adding more entities ...
      */
    ];
    
    const entities = entityData.reduce((a, e) => {
      a[e.name] = {...e, image: new Image(), path: e.path};
      return a;
    }, {});
    
    const initialize = () => {
      const canvas = document.createElement("canvas");
      document.body.appendChild(canvas);
      canvas.width = innerWidth;
      canvas.height = innerHeight;
      const ctx = canvas.getContext("2d");
    
      for (const key of Object.keys(entities)) {
        entities[key].alpha = Math.random();
      }
      
      (function render () {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
      
        Object.values(entities).forEach(e => {
          ctx.globalAlpha = Math.abs(Math.sin(e.alpha += 0.005));
          ctx.drawImage(e.image, e.x, e.y);
          ctx.globalAlpha = 1;
        });
        
        requestAnimationFrame(render);
      })();
    };
    
    Promise.all(Object.values(entities).map(e => {
        e.image.src = e.path;
        return e.image.decode();
      }))
      .then(initialize)
      .catch(err => console.error(err));