javascriptcssanimationcanvasflicker

.png flickers while animating using drawImage in canvas javascript


I have created a simple game using Canvas in Javascript where the player has to avoid obstacles by jumping over or shrinking. The obstacle images, which are animated moving towards the left, flickers at some sort of interval. The interval increases in parallel with the gameSpeed variable, making me wonder if this might have something to do with either the spawnObstacle() or the update() functions. Another reason why I believe so is because neither the drawn playerImg.png nor the drawn Score/Highscore ever flickers. I have tried several different solutions found online, but none seem to solve my problem.

Here is my index.js and index.html below:

import {registerNewHighscore, makeList} from "./globalHs.js";

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

const newGameBtn = document.getElementById("newGame");
const seeInstrBtn = document.getElementById("instrBtn");
const seeHsBtn = document.getElementById("hsBtn");
const goBackBtn = document.getElementById("backBtn");

let score;
let highscore;
let scoreText;
let highscoreText;
let player;
let gravity;
let obstacles = [];
let gameSpeed;
let keyPressed;
let isKeyPressed = false
let active = true;
let rotation = 0;

ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;

function createImage(path){
    let image = new Image();
    image.src = path;
    return image;
}

const obsImg = createImage("img/chatbubble.png");
const rockImg = createImage("img/rock.png");
const roadblockImg = createImage("img/roadblock.png");
const playerImg = createImage("img/logoPlayer.png");

newGameBtn.addEventListener('click', function() {
    document.getElementById("newGame").style.display = "none";
    document.getElementById("header").style.display = "none";
    document.getElementById("instr").style.display = "none";       
    document.getElementById("main").style.display = "block";
    document.getElementById("instrBtn").style.display = "none";
    document.getElementById("hsBtn").style.display = "none";
    document.getElementById("hsBoard").style.display = "none";
    start();
});

seeInstrBtn.addEventListener('click', function(){
    document.getElementById("header").style.display = "none";
    document.getElementById("instrBtn").style.display = "none";
    document.getElementById("newGame").style.display = "none";
    document.getElementById("instr").style.display = "block";
    document.getElementById("instr").style.visibility = "visible";
    document.getElementById("backBtn").style.display = "block";
    document.getElementById("hsBtn").style.display = "none";
    document.getElementById("hsBoard").style.display = "none";
    document.getElementById("backBtn").style.top = "50%";
});

seeHsBtn.addEventListener('click', function(){
    document.getElementById("header").style.display = "none";
    document.getElementById("hsBtn").style.display = "none";
    document.getElementById("newGame").style.display = "none";
    document.getElementById("instrBtn").style.display = "none";
    document.getElementById("instr").style.display = "none";
    document.getElementById("hsBoard").style.display = "block";
    document.getElementById("backBtn").style.display = "block";
    document.getElementById("backBtn").style.top = "70%";
    makeList();
});

goBackBtn.addEventListener('click', function() {
    goBack();
});

function goBack() {
    document.getElementById("backBtn").style.display = "none";
    document.getElementById("instr").style.display = "none";
    document.getElementById("header").style.display = "block";
    document.getElementById("newGame").style.display = "block";
    document.getElementById("instrBtn").style.display = "block";
    document.getElementById("hsBtn").style.display = "block";
    document.getElementById("hsBoard").style.display = "none";
};

document.addEventListener('keydown', function(evt) {
  if (isKeyPressed) return;
  
  isKeyPressed = true;
  keyPressed = evt.code;

});
document.addEventListener('keyup', function(evt) {
    if (evt.code !== keyPressed) return; // only respond to the key already pressed
    
    isKeyPressed = false;
    keyPressed = null;
});

function randomIntInRange (min, max){
    return Math.round(Math.random() * (max - min) + min);
}

class Player{
    constructor (x, y, r, w, h, playerImg){
        this.playerImg = playerImg;
        this.x = x;
        this.y = y;
        this.r = r;
        this.w = r*2;
        this.h = r*2;

        this.dy = 0;
        this.jumpForce = 18;
        this.originalRad = r;
        this.grounded = false;
        this.jumpTimer = 0;
        /* this.newRotation = 0; */
    }

    animate () {
        if (['Space', 'KeyW'].includes(keyPressed)) {
             this.jump();
        } else{
            this.jumpTimer = 0;
        }

        if (['ShiftLeft', 'KeyS'].includes(keyPressed)){
            /* this.newRotation = rotation * 2; */
            this.r = this.originalRad / 2;
            this.w = this.originalRad;
            this.h = this.originalRad;
        } else {
            /* this.newRotation = rotation; */
            this.r = this.originalRad;
            this.w = this.r * 2;
            this.h = this.r * 2;
        }

        this.y += this.dy;

        if (this.y + this.r < canvas.height){
            this.dy += gravity;
            this.grounded = false;
        } else{
            this.dy = 0;
            this.grounded = true;
            this.y = canvas.height - this.r;
        }
    
        this.draw();
    }
    
    jump () {
        if (this.r != this.originalRad) return;

        if (this.grounded && this.jumpTimer == 0){
            this.jumpTimer = 1.5;
            this.dy = -this.jumpForce;
        } else if (this.jumpTimer > 0 && this.jumpTimer < 15){
            this.jumpTimer++;
            this.dy = -this.jumpForce - (this.jumpTimer / 50);
        } 
    }

    draw () {
        ctx.translate(this.x, this.y);
        ctx.rotate(rotation);
        
        ctx.translate(-(this.x), -(this.y));
        ctx.drawImage(this.playerImg, (this.x-this.r), (this.y-this.r), this.w, this.h);
        ctx.setTransform(1, 0, 0, 1, 0, 0);
    }
}

class Obstacle {
    constructor (x, y, w, h, obsImg){
        this.obsImg = obsImg;
        this.x = x,
        this.y = y,
        this.w = w;
        this.h = h;

        this.dx = -gameSpeed;
        obsImg.width = this.w;
        obsImg.height = this.h;
    }

    update (){
        this.x += this.dx;
        this.dx = -gameSpeed;
    }

    draw () {
        ctx.beginPath();
        ctx.fillStyle = "rgba(0, 0, 0, 0)";
        ctx.fillRect(this.x, this.y, this.w, this.h,);
        ctx.drawImage(this.obsImg, this.x, this.y, this.w*1.1, this.h);
        ctx.closePath();
    }

    /////// CIRCLE
    /*draw () {
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, (2 * Math.PI), false)
        ctx.fillStyle = this.c;
        ctx.fill();
        ctx.closePath();
    }*/

    /////// ELLIPSE
    /*draw () {
        ctx.beginPath();
        ctx.ellipse(this.x, this.y, this.radX, this.radY, 0, 0, 2 * Math.PI);
        ctx.fillStyle = this.c;
        ctx.fill();
        ctx.stroke();
    }*/
}

class Rock {
    constructor (x, y, w, h, rockImg){
        this.rockImg = rockImg;
        this.x = x,
        this.y = y,
        this.w = w;
        this.h = h;

        this.dx = -gameSpeed;
        rockImg.width = this.w;
        rockImg.height = this.h;
    }

    update (){
        this.x += this.dx;
        this.dx = -gameSpeed;
    }

    draw () {
        ctx.beginPath();
        ctx.fillStyle = "rgba(0, 0, 0, 0)";
        ctx.fillRect(this.x+20, this.y+40, this.w-20, this.h-40);
        ctx.drawImage(this.rockImg, this.x-20, this.y, this.w*1.5, this.h*1.5);
        ctx.closePath();
    }
}

class Roadblock {
    constructor (x, y, w, h, roadblockImg){
        this.roadblockImg = roadblockImg;
        this.x = x,
        this.y = y,
        this.w = w;
        this.h = h;

        this.dx = -gameSpeed;
        roadblockImg.width = this.w;
        roadblockImg.height = this.h;
    }

    update (){
        this.x += this.dx;
        this.dx = -gameSpeed;
    }

    draw () {
        ctx.beginPath();
        ctx.fillStyle = "rgba(0, 0, 0, 0)";
        ctx.fillRect(this.x, this.y+15, this.w, this.h,);
        ctx.drawImage(this.roadblockImg, this.x, this.y, this.w, this.h*1.15);
        ctx.closePath();
    }
}

class Text{
    constructor(t, x, y, a, c, s){
        this.t = t;
        this.x = x;
        this.y = y;
        this.a = a;
        this.c = c;
        this.s = s;
    }

    draw () {
        ctx.beginPath();
        ctx.fillStyle = this.c;
        ctx.font = this.s + "px";
        ctx.textAlign = this.a;
        ctx.fillText(this.t, this.x, this.y);
        ctx.closePath();
    }
}

function getDistance(player, obstacle) {
    var distX = Math.abs(player.x - (obstacle.x + obstacle.w / 2));
    var distY = Math.abs(player.y - (obstacle.y + obstacle.h / 2));

    if (distX > (obstacle.w / 2 + player.r)) { return false; }
    if (distY > (obstacle.h / 2 + player.r)) { return false; }

    if (distX <= (obstacle.w)) { return true; }
    if (distY <= (obstacle.h)) { return true; }

    var dx = distX - obstacle.w / 2;
    var dy = distY - obstacle.h / 2;
    return (dx * dx + dy * dy <= (player.r*player.r));
}

let initialSpawnTimer = 200; 
let spawnTimer = initialSpawnTimer;

function spawnObstacle (){
    let sizeX;
    let sizeY;
    let type = randomIntInRange(0, 2);
    let obstacle = new Obstacle(
        canvas.width + sizeX, 
        canvas.height - sizeX, 
        sizeX,
        sizeY,
        obsImg
        );
        
        if (type == 0){
            sizeX = randomIntInRange(100, 160);
            sizeY = sizeX / 2;
            obstacle = new Rock(
                canvas.width + sizeX, 
                canvas.height - sizeY, 
                sizeX,
                sizeY,
                rockImg  
                );  
            } else if (type == 1){
            sizeX = randomIntInRange(80, 160);
            sizeY = sizeX / 2;
            obstacle = new Obstacle(
                canvas.width + sizeX, 
                canvas.height - sizeX, 
                sizeX,
                sizeY,
                obsImg
                );
            obstacle.y -= player.originalRad  + randomIntInRange(-50, 150);
        } else if (type == 2){
            sizeX = 150;
            sizeY = sizeX / 2;
            obstacle = new Roadblock(
                canvas.width + sizeX, 
                canvas.height - sizeY, 
                sizeX,
                sizeY,
                roadblockImg  
            );
        }

    obstacles.push(obstacle);
}


function start () {
    ctx.canvas.width = window.innerWidth;
    ctx.canvas.height = window.innerHeight;
    
    ctx.font = "40px Courier New";

    active = true;

    gameSpeed = 6;
    gravity = 1;

    score = 0;
    highscore = 0;
    if (localStorage.getItem('highscore')){
        highscore = localStorage.getItem('highscore');
    }

    player = new Player(100, 0, 50, 100, 100, playerImg);

    scoreText = new Text("Score: " + score, 45, 45, "left", "#212121", "40");
    highscoreText = new Text("Highscore: " + highscore, 45,
    90, "left", "gold", "40");
    
    window.requestAnimationFrame(update);
}


let lastTime;

function update (time) {
    if (lastTime != null) {
        const delta = time - lastTime;
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    spawnTimer--;
    if (spawnTimer <= 0){
        spawnObstacle();
        spawnTimer = initialSpawnTimer - gameSpeed * 8;

        if (spawnTimer < 60){
            spawnTimer = randomIntInRange(40, 80);
        }
    }

    for (let i = 0; i < obstacles.length; i++){
        let o = obstacles[i];
        o.draw();
        o.update();

        if (o.x + o.y < 0){
            obstacles.splice(i, 1);
        }

        
        if (getDistance(player, o)) {
            active = false;
            obstacles = [];
            spawnTimer = initialSpawnTimer;
            gameSpeed = 6;    
            window.localStorage.setItem('highscore', highscore);
            score -= 1;
            highscore -= 1;
            if (score >= highscore){
                registerNewHighscore(highscore+1);
            }
            goBack();
        }
    }

    lastTime = time;
    if (active){
        window.requestAnimationFrame(update);
    }
    
    player.animate();
    
    score++;
    scoreText.t = "Score: " + score;
    scoreText.draw();


    if (score > highscore){
        highscore = score;
        highscoreText.t = "Highscore: " + highscore;
        
    }

    highscoreText.draw();
    
    
    rotation+=Math.PI/180 * 2 + gameSpeed * 0.01;
    gameSpeed += 0.002;
    
}
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kindly Waiting Game V2</title>
    <link rel="stylesheet" href="styles.css" />
</head>
<body>
    <div class="world">
        <div id="header">The Kindly Game</div>
        <div id="newGame" onmouseover="this.style.backgroundColor = 'goldenrod'"
        onmouseout="this.style.backgroundColor= 'gold'">New Game</div>
        <div id="instrBtn" onmouseover="this.style.backgroundColor = 'goldenrod'" onmouseout="this.style.backgroundColor 
        = 'gold'">Instructions</div>
        <div id="hsBtn" onmouseover="this.style.backgroundColor = 'goldenrod'" onmouseout="this.style.backgroundColor 
        = 'gold'">Highscores</div>

        <div id="instr" style="display: none">Avoid the <br>chatbubbles, <br>roadblocks, <br>and rocks!<br><br>
            Instructions: <br>Space/W: Jump <br>ShiftLeft/S: Shrink</div>
        <div id="hsBoard" style="display: none">Highscores:</div>
        <div id="backBtn" onmouseover="this.style.backgroundColor = 'goldenrod'" onmouseout="this.style.backgroundColor
        = 'gold'">Back</div>

        <div id="main"></div>
        <div id="score"></div>

        <div id="cloudLarge"></div>
        <div id="cloudMedium"></div>
        <div id="cloudSmall"></div>
        <div id="cloudSmall2"></div>
        <div id="ufo"></div>
        <div id="airplane"></div>
        <div id="ye"></div>
    </div>

    <canvas id="game" width="640" height="400"></canvas>
    <script src="globalHs.js" type="module"></script>
    <script src="index.js" type="module"></script>
</body>
</html>

Solution

  • Your issue is caused by this block of code inside your update() function:

    if (o.x + o.y < 0){
        obstacles.splice(i, 1);
    }
    

    Now while I don't know the exact logic why you're checking the sum of the obstacles' horizontal and vertical position, you're actually removing something from an array with the .splice() method. As this is happening inside a for-loop you're actually modifying the length of the array while it might still loop over it.

    You can fix this by looping over the array from the last to the first element:

    for (let i = obstacles.length-1; i>=0; i--) {
        let o = obstacles[i];
        o.draw();
        o.update();
    
        if (o.x + o.y < 0) {
            obstacles.splice(i, 1);
        }
    
    
        if (getDistance(player, o)) {
            active = false;
            obstacles = [];
            spawnTimer = initialSpawnTimer;
            gameSpeed = 6;
            window.localStorage.setItem('highscore', highscore);
            score -= 1;
            highscore -= 1;
            if (score >= highscore) {
                registerNewHighscore(highscore + 1);
            }
            goBack();
        }
    }