0

Now I have a simple code without a main game loop. I'm rendering the sprites on the canvas using the Loader constructor and its image.onload function (because without image.onload I won't see any sprites) Now I want to animate on of my sprites and for that I need to create a draw loop. Unfortunately, this is where my knowledge ends. I tried creating render function and just copy pasting my ship.drawimage(boat, boatPosX, boatPosY, 50, 50); methods what so ever, but it's not working because I need image.onload function which is inside Loader. And I can't put Loader constructor to my render() function because then var background = new Loader("ground.png"); var boat = new Loader("ship.png"); can't access the constructor variable to init new object.

So at this point I'm pretty lost how I should refactor my code better?

Here is the full code:

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");

var mapArray = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 2, 2, 0],
  [0, 0, 1, 1, 1, 0, 0, 2, 0, 0],
  [0, 0, 1, 1, 1, 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, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]


var StyleSheet = function(image, width, height) {
  this.image = image;
  this.width = width;
  this.height = height;


  this.draw = function(image, sx, sy, swidth, sheight, x, y, width, height) {
      context.drawImage(image, sx, sy, swidth, sheight,x, y, width, height);
  }
  this.drawimage = function(image, x, y, width, height) {
    context.drawImage(image, x, y, width, height);
  }
}

/* Initial Sprite Position */

var boatPosX = 230;
var boatPosY = 200;

var Loader = function(src) {
  this.image = new Image();
  this.image.src = src;
  this.image.onload = function() {
  var sprite = new StyleSheet(background, 36, 36);
  var ship = new StyleSheet(boat, 90, 100);
  for (let i = 0; i < mapArray.length; i++) {
    for (let j = 0; j < mapArray[i].length; j++) {
      if (mapArray[i][j] == 0) {
        sprite.draw(background, 190, 230, 26, 26, i * sprite.width, j * sprite.height, sprite.width, sprite.height);
      }
      if (mapArray[i][j] == 1) {
        sprite.draw(background, 30, 30, 26, 26, i * sprite.width, j * sprite.height, sprite.width, sprite.height);
      }
      if (mapArray[i][j] == 2) {
        sprite.draw(background, 200, 20, 26, 26, i * sprite.width, j * sprite.height, sprite.width, sprite.height);
      }
    }
  }
  ship.drawimage(boat, boatPosX, boatPosY, 50, 50);
}
  return this.image;
}

function render() {

}

setInterval(render, 10);

/* Sprite controls */

function move(e) {
  if (e.keyCode == 39) {
    boatPosX += 2;
    console.log("works");

  }
  if (e.keyCode == 37) {
    boatPosX -= 2;
  }
}

  document.onkeydown = move;

var background = new Loader("ground.png");
var boat = new Loader("ship.png");

console.log(background);

UPDATE:

So following my old questions, I decided to do some changes to my code so that I would be allowed to call requestAnimationFrame for my onload function and draw the sprite on the canvas constantly. For that I separated Loader constructor and my method onload by putting onload into a new function and assigning that function to Loader prototype. Then I do var background = new Loader("ground.png"); and background.render(); but I get Uncaught TypeError: background.render is not a function error. Not sure what I'm doing wrong?

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");

var mapArray = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 2, 2, 0],
  [0, 0, 1, 1, 1, 0, 0, 2, 0, 0],
  [0, 0, 1, 1, 1, 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, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]


var StyleSheet = function(image, width, height) {
  this.image = image;
  this.width = width;
  this.height = height;


  this.draw = function(image, sx, sy, swidth, sheight, x, y, width, height) {
      context.drawImage(image, sx, sy, swidth, sheight,x, y, width, height);
  }
  this.drawimage = function(image, x, y, width, height) {
    context.drawImage(image, x, y, width, height);

  }
}

/* Initial Sprite Position */

var boatPosX = 230;
var boatPosY = 200;

var Loader = function(src) {
  this.image = new Image();
  this.image.src = src;
  return this.image;
}

Loader.prototype.render = function() {
  this.image.onload = function() {
  var sprite = new StyleSheet(background, 36, 36);
  var ship = new StyleSheet(boat, 90, 100);
  for (let i = 0; i < mapArray.length; i++) {
    for (let j = 0; j < mapArray[i].length; j++) {
      if (mapArray[i][j] == 0) {
        sprite.draw(background, 190, 230, 26, 26, i * sprite.width, j * sprite.height, sprite.width, sprite.height);
      }
      if (mapArray[i][j] == 1) {
        sprite.draw(background, 30, 30, 26, 26, i * sprite.width, j * sprite.height, sprite.width, sprite.height);
      }
      if (mapArray[i][j] == 2) {
        sprite.draw(background, 200, 20, 26, 26, i * sprite.width, j * sprite.height, sprite.width, sprite.height);
      }
    }
  }
  ship.drawimage(boat, boatPosX, boatPosY, 50, 50);
}
}



/* Sprite controls */

function move(e) {
  if (e.keyCode == 39) {
    boatPosX += 2;
    console.log("works");

  }
  if (e.keyCode == 37) {
    boatPosX -= 2;
  }
}

  document.onkeydown = move;

var background = new Loader("ground.png");
var boat = new Loader("ship.png");
background.render();
console.log(background);

Codepen example: https://codepen.io/Limpuls/pen/dejVpR

Limpuls
  • 856
  • 1
  • 19
  • 37
  • 1
    Check out the `requestAnimationFrame` method ([MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)). – John Ellmore May 14 '18 at 18:31
  • @JohnEllmore that doesn't answer my question. I know what rAF is. I still need to refactor my code for setInterval or rAF to work – Limpuls May 14 '18 at 18:32
  • Since you need to load the image before the loop can start, you need to put the code that starts the loop (`setInterval` or the first call to `requestAnimationFrame`) inside the `onload` function of the image. Is that it? Honestly it's a little unclear what your exact question is. – Máté Safranka May 14 '18 at 18:33
  • Maybe this article can help: https://developer.mozilla.org/en-US/docs/Games/Anatomy – Máté Safranka May 14 '18 at 18:35
  • Also, just a really minor note on terminology: what you're calling a `StyleSheet` is actually a *sprite* sheet. – Máté Safranka May 14 '18 at 18:36
  • @MátéSafranka I put rAF inside this.image.onload() but without any callback of course I get an error. When I put callback function I want to call on repeat (in this case it's `this.image.onload()` I get `script.js:58 Uncaught TypeError: Cannot read property 'onload' of undefined` – Limpuls May 14 '18 at 18:43
  • Sorry for the unclear question. The question is - how I take my current code and refactor it to a game loop? I need to make sprite animation and draw every loop. Not just on image.onload which happens only once – Limpuls May 14 '18 at 18:46
  • @MátéSafranka So I putted rAF inside the `onload` function but I'm not sure what the callback function for it should be? – Limpuls May 14 '18 at 18:57
  • The purpose of the main loop is to update the game state (e.g. the player's position, based on keyboard input), and present the view to the user (i.e. draws the background and all the sprites). Write a function that does that, and then calls `requestAnimationFrame()` and passes itself as the callback. Again, refer to the article I linked, it's explained much better there. – Máté Safranka May 14 '18 at 19:07
  • So by referencing to the article I see that I need to delete all my code and start all over again, because I'm using the constructors all over the place and the article isn't. And because I'm using constructor I can't create such a function as it is now. Cool. – Limpuls May 14 '18 at 19:11
  • @MátéSafranka I updated my OP, maybe you could take a look at it? – Limpuls May 14 '18 at 19:12
  • If you could publish a working example of your code I could have a look. On some place like CodePen. – Johan Karlsson May 14 '18 at 19:14
  • @JohanKarlsson Updated OP with working example. Appreciate your help – Limpuls May 14 '18 at 19:21

1 Answers1

1

I made a fork of your Pen and refactored your code. Now you have a game loop using requestAnimationFrame: https://codepen.io/DonKarlssonSan/pen/rvrGvL/

Edit, adding what I changed:

  • I removed the Loader and instead just use plain Image and img.src = url which I then pass in as parameters to the constructor to StyleSheet.

  • I extracted the loop over mapArray into a render method which also functions as the main ("game") loop.

Code:

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");

var mapArray = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 2, 2, 0],
  [0, 0, 1, 1, 1, 0, 0, 2, 0, 0],
  [0, 0, 1, 1, 1, 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, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];

var StyleSheet = function(image, width, height) {
  this.image = image;
  this.width = width;
  this.height = height;

  this.draw = function(image, sx, sy, swidth, sheight, x, y, width, height) {
    context.drawImage(image, sx, sy, swidth, sheight, x, y, width, height);
  };
  this.drawimage = function(image, x, y, width, height) {
    context.drawImage(image, x, y, width, height);
  };
};

/* Initial Sprite Position */

var boatPosX = 230;
var boatPosY = 200;

function render() {
  requestAnimationFrame(render);

  for (let i = 0; i < mapArray.length; i++) {
    for (let j = 0; j < mapArray[i].length; j++) {
      if (mapArray[i][j] == 0) {
        this.sprite.draw(
          background,
          190,
          230,
          26,
          26,
          i * this.sprite.width,
          j * this.sprite.height,
          this.sprite.width,
          this.sprite.height
        );
      }
      if (mapArray[i][j] == 1) {
        this.sprite.draw(
          background,
          30,
          30,
          26,
          26,
          i * this.sprite.width,
          j * this.sprite.height,
          this.sprite.width,
          this.sprite.height
        );
      }
      if (mapArray[i][j] == 2) {
        this.sprite.draw(
          background,
          200,
          20,
          26,
          26,
          i * this.sprite.width,
          j * this.sprite.height,
          this.sprite.width,
          this.sprite.height
        );
      }
    }
  }
  this.ship.drawimage(boat, boatPosX, boatPosY, 50, 50);
};

function move(e) {
  if (e.keyCode == 39) {
    boatPosX += 2;
    console.log("right");
  }
  if (e.keyCode == 37) {
    boatPosX -= 2;
    console.log("left");
  }
}

document.onkeydown = move;

var background = new Image();
background.src = "http://i67.tinypic.com/35lx8y0.png";
var sprite = new StyleSheet(background, 36, 36);

var boat = new Image();
boat.src = "http://i66.tinypic.com/b7b9tc.png";
var ship = new StyleSheet(boat, 90, 100);

render();
Johan Karlsson
  • 494
  • 5
  • 7
  • Thanks a lot for the help! Things are much more clear right now. I just wonder how does it work for you without calling image.onload? In the past I tried to draw something on the canvas and it didn't work without the `onload`? Is it no more necessary after having the game loop? I also wonder why my Updated code didn't work and I was getting `Uncaught TypeError: background.render is not a function` error. – Limpuls May 14 '18 at 20:22
  • 1
    1. Yes, I think it's thanks to the working game loop that you don't need onload any more. I guess it could happen that the game loop starts before there are any images loaded and it would cause some errors at first but when the images are loaded they will be rendered. 2. The reason for `Uncaught TypeError: background.render is not a function` was that you returned `this.image` in the constructor function which caused `background` to be of type `Image()` which does not have a `render()` method. – Johan Karlsson May 14 '18 at 20:30
  • Okay and one last question. Now that we have a game loop and we are consistently drawing on the canvas, don't we need to clear the canvas before every loop? I mean it keeps redrawing, right? Then how come it's not stacking, for example the boat sprite when it's moving? – Limpuls May 14 '18 at 23:29
  • Since you redraw every frame from scratch, it is not "stacking". Since you probably would like some sprites to freely move around you will need to redraw the whole background to "erase" the sprite in the old position. In other words: redrawing the complete background is in a way clearing the canvas. – Johan Karlsson May 16 '18 at 14:46