Sprite Animation

I am working on a bare bones game to familiarize myself with Javascript again after about a 15 year hiatus.

I have established a strong footing in creating all of the necessary components that I need to implement the Player creation, the Sprite and it’s Animation…

However, for the life of me, I can not figure out why I can’t swap the animation state without creating a “new Sprite” with the state that I want to use…

The first iteration of this project used a sprite sheet, and I could get that to work without any hiccups… but this one where the animation state is a collection of individual images is really throwing me for a loop…

Any suggestions would be greatly appreciated !



const LIB = {
    STICK: {
        idle: {
            imgSrc: ['stick/idle1.png','stick/idle2.png','stick/idle3.png','stick/idle4.png','stick/idle5.png','stick/idle6.png','stick/idle7.png','stick/idle8.png'],
            frameRate: 8,
            bufferRate: 6
        },
        walk: {
            imgSrc: ['stick/walk1.png','stick/walk2.png','stick/walk3.png','stick/walk4.png','stick/walk5.png','stick/walk6.png','stick/walk7.png','stick/walk8.png'],
            frameRate: 8,
            bufferRate: 5
        },
        jump: {
            imgSrc: ['stick/jump1.png','stick/jump2.png','stick/jump3.png','stick/jump4.png','stick/jump5.png'],
            frameRate: 5,
            bufferRate: 5
        }
    }
};

window.addEventListener('DOMContentLoaded', function(){
 
const canvas = document.getElementById('stick-fighter');
const c = canvas.getContext('2d');

canvas.width  = 800;
canvas.height = 478;


const GLOBAL = {
    CANVAS: {
        WIDTH: 800,
        HEIGHT: 478
    },
    SCALE: { x: 0.5, y: 0.5 }
};

const GAME = {
    GRAVITY: 0.5,
    FRICTION: 0.5
};

const PLAYER = {
    CONTROLS: {
        a: { pressed: false },
        d: { pressed: false },
    },
    WALKSPEED: 5,
    RUNSPEED: 5,
    DASH_DISTANCE: 20
};

function Player () {

    EM.Add( this );

    this.name = 'P1';
    this.character = null;
    this.sprite    = null;
    this.movement = new Movement( this );
    
    this.setupStates   = (character, state) => {
        this.character = LIB[character];
        this.sprite    = new Sprite(this.character[state]);
        this.height    = this.sprite.height;
        this.width     = this.sprite.width;    
        this.position  = this.sprite.position;
        this.velocity  = this.sprite.velocity;
        this.sprite.animation = new Animation( this.sprite );
    };

    this.setState = (state) => {
        this.sprite.animationState = this.character[state];
    };

    this.Update = () => {
        this.velocity.x *= 1 - GAME.FRICTION;
        this.sprite.animation.Update();
        this.sprite.Draw();
    }
}

function Movement( entity ) {

    this.Left = () => {
        // entity.setCharacter('STICK','walk');
        entity.setState('walk');
        if (PLAYER.CONTROLS.a.pressed){
            entity.sprite.position.x -= PLAYER.WALKSPEED;
        }
    };

    this.Right = () => {
        entity.setState('walk');
        if (PLAYER.CONTROLS.d.pressed){
            entity.sprite.position.x += PLAYER.WALKSPEED;
        }
    };
}

function Sprite ( state ) {
    this.position = { x: 0, y: 0 };
    this.velocity = { x: 0, y: 0 };

    this.imageSource = [];
    this.animationState = state;

    this.animationState.imgSrc.forEach( (src, index) => {
        const img = new Image();
        img.src = src;
        img.onload = () => {
            if (index === 0) {
                this.width = img.width;
                this.height = img.height;
            }
        };
        this.imageSource.push(img)
    });

    this.animation = new Animation( this );

    this.Draw = () => {
        c.drawImage(
            this.imageSource[this.animation.currentFrame],
            this.position.x,
            this.position.y,
            this.width,
            this.height
        );
    };
}

function Animation( sprite ) {
    this.currentFrame = 0;
    this.frameCounter = 0;
    this.totalFrames  = sprite.animationState.imgSrc.length;
    this.frameRate    = sprite.animationState.frameRate;
    this.bufferRate   = sprite.animationState.bufferRate;

    this.Update = () => {
        this.frameCounter++;
        if (this.frameCounter >= this.frameRate) {
            if (
                this.bufferRate === 0 ||
                this.frameCounter % this.bufferRate === 0
                ) {

                this.currentFrame = (this.currentFrame + 1) % this.totalFrames;
            }
        }
    }
}

const entities = [];
    
function EntityManager() {
    this.Add = function(entity) { entities.push(entity); };
    this.Remove = function(entity){
        const index = entities.indexOf(entity);
        if (index !== -1){
            entities.splice(index, 1);
        }
    };
    this.applyGravity = () => {
        entities.forEach(entity => {
            const bottom = entity.position.y + entity.velocity.y + entity.height;
            if (bottom < GLOBAL.CANVAS.HEIGHT){
                entity.velocity.y += GAME.GRAVITY;
                entity.position.y += entity.velocity.y;
            }else{
                entity.isJumping = false;
                entity.velocity.y = 0;
            }
        });
    }
}

const EM = new EntityManager();
const player1 = new Player();
player1.setupStates('STICK','idle');

function animate () {
        window.requestAnimationFrame (animate);
        c.clearRect(0,0,canvas.width,canvas.height);
        c.fillStyle= 'white';
        c.fillRect(0,0,canvas.width, canvas.height);
        c.save();
            c.scale(GLOBAL.SCALE.x, GLOBAL.SCALE.y)
            player1.Update();
        c.restore();

        EM.applyGravity();
}


animate();

    document.addEventListener('keydown', (event)=> {
        switch( event.key ) {
            case 'a':
            PLAYER.CONTROLS.a.pressed = true;
            player1.movement.Left();
            break;
        case 'd':
            PLAYER.CONTROLS.d.pressed = true;
            player1.movement.Right();
            break;
        }
    });
    document.addEventListener('keyup', (event)=> {
        switch( event.key ) {
            case 'a':
            case 'd':
                PLAYER.CONTROLS[event.key].pressed = false;
                break;
        }
    });

});

I have tried iterating through the LIB.STICK and creating a new Animation for each state, but so far… not matter how I approached accessing the behavior, it doesn’t swap as expected.

Thank you for taking the time to read this.

To go more in deep could you please share more about the LIB.STICK object? and how you are using it in your coding?

At what point do you call animate() again? I’m following your call chain backwards, and i see you call animate once, but only once…

(animate() calls player1.Update(), which calls this.sprite.Draw(), which draws the sprite’s image. But if you dont call animate or player1.Update again, it’ll never update…)

Yes, of course.

const LIB = {
    STICK: {
        idle: {
            imgSrc: ['stick/idle1.png','stick/idle2.png','stick/idle3.png','stick/idle4.png','stick/idle5.png','stick/idle6.png','stick/idle7.png','stick/idle8.png'],
            frameRate: 8,
            bufferRate: 6
        },
        walk: {
            imgSrc: ['stick/walk1.png','stick/walk2.png','stick/walk3.png','stick/walk4.png','stick/walk5.png','stick/walk6.png','stick/walk7.png','stick/walk8.png'],
            frameRate: 8,
            bufferRate: 5
        },
        jump: {
            imgSrc: ['stick/jump1.png','stick/jump2.png','stick/jump3.png','stick/jump4.png','stick/jump5.png'],
            frameRate: 5,
            bufferRate: 5
        }
    }
};

I’ve create LIB to be used as an animation library for a variety of characters and their respective animation related data.

For example, the character STICK – is the only one at this time – but will have a variety of animation states such as the default animation state ‘idle’, where upon player input will have a state change to walk, run, jump, dash, etc…

I’ve created it this way to assist in making the code more dynamic, so that I can select a character

LIB[selectedCharacter]

where selectedCharacter is STICK
and selectedAnimation is representative of the animation behavior or state

LIB[selectedCharacter][selectedAnimation]
LIB.STICK.idle
LIB[selectedCharacter][selectedAnimation]

I hope this helps in explaining what I’m trying to accomplish.

From my understanding, this snippet here will allow for recursive calls to animate.

function **animate** () {
        window.requestAnimationFrame (**animate**);
        c.clearRect(0,0,canvas.width,canvas.height);
        c.fillStyle= 'white';
        c.fillRect(0,0,canvas.width, canvas.height);
        c.save();
            c.scale(GLOBAL.SCALE.x, GLOBAL.SCALE.y)
            player1.Update();
        c.restore();

        EM.applyGravity();
}


**animate**();

As far as i understand by changing animation state you could make it ready for other changes you wnant.

    // ... existing code ...

    this.changeState = function(newState) {
        this.animationState = newState;
        this.imageSource = [];
        this.animationState.imgSrc.forEach( (src, index) => {
            const img = new Image();
            img.src = src;
            img.onload = () => {
                if (index === 0) {
                    this.width = img.width;
                    this.height = img.height;
                }
            };
            this.imageSource.push(img)
        });
        this.animation = new Animation( this );
    };

    // ... existing code ...
}

Once you done with it try this type of coding to change the state.

    // ... existing code ...

    this.changeState = function(newState) {
        this.sprite.changeState(this.character[newState]);
    };

    // ... existing code ...
}

If it works fine so now you can change it without creating new one.

Like :slight_smile:

player1.changeState('walk');

It’s been quite the effort, but I found a solution eventually.

Initially I had to secure a way to load all of the images that I was going to use for the animations and still have access to them later

    const images = {};
    
    function loadImages() {
        Object.keys(LIB.STICK).forEach(state => {
            const stateData = LIB.STICK[state];
            images[state] = [];
    
            stateData.imgSrc.forEach(imageSrc => {
                const img = new Image();
                img.src = imageSrc;
                img.onload = () => {
                    images[state].push({
                        image: img,
                        width: img.naturalWidth,
                        height: img.naturalHeight
                    });
                };
            });
        });
    }

Something interesting that I found, was that for some reason the image width and height was no registering correctly and I had to use naturalWidth and naturalHeight to get the correct dimensions.

I took a step back and genuinely considered the scope of the various functions and how I was accessing the data to pass to the next step in the process.

function Player () {

    this.character = getCharacter('STICK');
        this.currentAnimation = 'idle';

    this.sprite = new Sprite(this.character);
        this.position = this.sprite.position;
        this.velocity = this.sprite.velocity;

    this.use = function(state) {
        this.currentAnimation = state;
    }

    this.Draw = () => {
        // This was a little difficult to realize how to actually access the image.
        // There may be a simpler way. I'll have to revisit it later.
        const frame = images[this.currentAnimation][this.sprite.animations[this.currentAnimation].currentFrame];

        c.drawImage(
            frame.image,
            this.position.x,
            this.position.y,
            frame.width,
            frame.height
        );
    };

    this.Update = () => {
        this.sprite.animations[this.currentAnimation].Update();
        this.Draw();
    }
}

Creating, essentially, a clone of the LIB object to reference “pre assembled” animations for each animation behavior for the sprite

function Sprite (character) {
    this.position = { x:0, y:0 };
    this.velocity = { x:0, y:0 };
    this.animations = {};
    Object.keys(character).forEach(state => {
        this.animations[state] = new Animation(character[state]);
    });
}

Nothing in this function was changed

function Animation (state) {
    this.currentFrame = 0;
    this.frameCounter = 0;
    this.totalFrames  = state.imgSrc.length;
    this.frameRate    = state.frameRate;
    this.bufferRate   = state.bufferRate;

    this.Update = () => {
        this.frameCounter++;
        if (this.frameCounter >= this.frameRate) {
            if (
                this.bufferRate === 0 ||
                this.frameCounter % this.bufferRate === 0
                ) {

                this.currentFrame = (this.currentFrame + 1) % this.totalFrames;
            }
        }
    }
}

Thank you all for providing some assistance in resolving this issue. I was stumped for several days and couldn’t figure it out.

But now I have a working version!

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.