0
\$\begingroup\$

I'm trying to animate a sprite sheet. I have a single image, and multiple objects of the same type but drawn at different locations on the canvas.

What I'm trying to do is have each object move with an animation speed of its own, and an overall game framerate using requestAnimationFrame.

Be I find that it clears the whole canvas, instead of redrawing.
I've previously tried the setInterval approach, but it causes flickering (I can't really tell why, even after googling it).

const objectsArray = []
const gameFrameRate = 1000/30;
let allowedSoldiersQuantity = 30;
let globalImageObjects = [];

let newObject = new genericObject('robot.png', 200, 200);
objectsArray.push(newObject)
let otherObject = new genericObject('2.png', 300, 300);
objectsArray.push(otherObject)

canvas = document.getElementById('game_canvas');
context = canvas.getContext('2d');
canvas.width = 1200;
canvas.height = 720;
canvas.style.borderColor = 'red'
canvas.style.borderWidth = '5px'

setup = function() {

    // setInterval Approach
    //setInterval(function (){
    //    context.clearRect(0, 0, canvas.width, canvas.height)
    //    objectsArray.map((element) => {
    //        element.animateObjectImage(element.objectImage, 4)
    //    })
    //},30)

    // RequestAnimationFrame approach
    objectsArray.map((element) => {
        element.animateObjectImage(element.objectImage, 9)
    })
    context.clearRect(0, 0, canvas.width, canvas.height)
    window.requestAnimationFrame(setup)
}
window.requestAnimationFrame(setup)


document.addEventListener('click', (event) => {
    console.log(event.x);
    console.log(event.y);
    if (allowedSoldiersQuantity){
        let object = new genericObject('robot.png', event.x, event.y)
        objectsArray.push(object)
        object.animateObjectImage(object.objectImage, 5)
        allowedSoldiersQuantity -= 1;
    }
}, false)

function genericObject(objectSpriteSheet, xPosition, yPosition){
    this.objectImage = (function (){
        let newImageObject = new Image()
        newImageObject.src = objectSpriteSheet
        return (newImageObject)
    })()
    this.animateObjectImage = function(objectImage, frameRate){
        let setIntervalTime = 1000 / frameRate;
        let currentFrame = 1;
        let totalSprites = 8;
        let spriteHeight = objectImage.height;
        let spriteWidth = objectImage.width / totalSprites;
        setInterval(function animateObjectSprites(){
            currentFrame = (currentFrame + 1) % totalSprites;
            let screenX = currentFrame * spriteWidth;
            context.drawImage(objectImage, screenX, 0, spriteWidth, spriteHeight, xPosition, yPosition, spriteWidth, spriteHeight);
            currentFrame++;
        }, setIntervalTime)
    }
}
//setup(); // This function should be run with the setInterval approach
```
\$\endgroup\$
2
  • \$\begingroup\$ You seem to still be using functions as pseudo-classes. Note that the JavaScript ES6 class keyword is supported by all relevant web browsers for quite a while now. So there is a much cleaner way to do OOP in JavaScript now. \$\endgroup\$
    – Philipp
    Sep 16, 2022 at 15:27
  • \$\begingroup\$ I was indeed planning on learning it, then refactor the code by then. \$\endgroup\$
    – xquilt
    Sep 16, 2022 at 17:01

1 Answer 1

0
\$\begingroup\$

The point of a function passed to requestAnimationFrame which then passes itself back to requestAnimationFrame at its end is not to be a setup-function. It's a drawing-function which gets called over and over again by the browser's rendering engine.

I think what it is supposed to do in your code example is to prepare the canvas for first rendering and schedule the animations for all the objects by calling their animateObjectImage functions which then all create an interval. This is something you only need to do once, not over and over again. And you don't need to call it via requestAnimationFrame. So both instances of the line window.requestAnimationFrame(setup) make no sense. Just call the setup method once when you start the game.

Then there is the problem that you have your calls to context.drawImage not during the requestAnimationFrame function but as part of the setInterval function. That's not how you are supposed to do it! All drawing should happen during requestAnimationFrame, so the web browser can properly integrate the rendering into its own rendering loop. Functions scheduled via setInterval get executed whenever, so they should not contain any canvas drawing.

Now you might wonder: "But I don't know how often the browser calls requestAnimationFrame. How am I supposed to properly time my animations with that function?"

The answer is to separate logic from drawing! A principle that should be obeyed in any well-designed game architecture.

  • Have the setInterval function set which frame is the currently displayed frame for that object.
  • Have the requestAnimationFrame draw whatever is the currently displayed frame for the object.

You can do that by breaking the animateObjectImage method of your genericObject function pseudo-class into two separate methods setup and draw. The setup method does initialization and schedules the logic (but not the drawing!) and the draw method draws whatever state the logic has set.

function genericObject(objectSpriteSheet, xPosition, yPosition){
    this.objectImage = (function (){
        let newImageObject = new Image()
        newImageObject.src = objectSpriteSheet
        return (newImageObject)
    })()
    // setup function
    this.setup = function(objectImage, frameRate){
        let setIntervalTime = 1000 / frameRate;
        let currentFrame = 1;
        let totalSprites = 8;
        let spriteHeight = objectImage.height;
        let spriteWidth = objectImage.width / totalSprites;
        setInterval(function(){
            currentFrame = (currentFrame + 1) % totalSprites;
            currentFrame++;
        }, setIntervalTime)
    }
    // draw function
    this.draw = function() {
            let screenX = currentFrame * spriteWidth;
            context.drawImage(objectImage, screenX, 0, spriteWidth, spriteHeight, xPosition, yPosition, spriteWidth, spriteHeight);
    }
}

Then in the main program, you have both a setup method and a draw method. The setup method starts the animation loop of the draw method:

function setup() {
    objectsArray.forEach((element) => {
        element.setup(element.objectImage, 9)
    })
    window.requestAnimationFrame(draw)
}

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height)
    objectsArray.forEach((element) => {
        element.draw()
    })
    window.requestAnimationFrame(draw)
}

And then you initialize your game by calling the setup method when the page has loaded:

window.onload = function(){  
    /* ...all the other initializations... */
    setup();
}  

By the way, there is another problem here that you don't wait with drawing until all the sprites have loaded. But that's another problem for another question.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the thorough explanation. I was missing many points. Just want to add that, when I've separated the last object method, I additionally had to refactor both of their local variable to be instance variables (will look to refine even further, because the suffix this is now sprawling everywhere), and subsequently refactor the setInterval callback to arrow function to preserve this context. Also, I can't get the last part concerning the 'the wait of the sprites being loaded' to address it in a question, in a first place. \$\endgroup\$
    – xquilt
    Sep 16, 2022 at 16:54
  • \$\begingroup\$ I've also noticed that you've mentioned couple concepts related to game design. Is there any resources you'd recommend to systematically learn it, as i'm just watching Youtube videos. \$\endgroup\$
    – xquilt
    Sep 16, 2022 at 16:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .