2

This is a problem with my last question that I posted, but when I clear the canvas, the objects just glitch out. It disappears and then appears. But why does it glitch? I set the interval to 0 MS, but it's just pop and then appear.

The little glitch is going on here: https://cmt-1.hoogidyboogidy.repl.co/

Probably it's just this device. I use laptop and computer. A windows 7 computer is pretty old and a chromebook, eh just bad.

const c = document.getElementById('c')
const ctx = c.getContext('2d')

c.height = window.innerHeight
c.width = window.innerWidth

let blockInfo = {
    h: 15, // Defining height and width so they can be changed. (NOTE: The height and width doesn't have to be the same value)
    w: 15
}

let renderedObjects = false // Keep in case you're gonna make colission

let player = {
    speed: 0.125,
    x: 2,
    y: 2,
    height: 1,
    width: 1
}

function clearObjects() {
    // Clear all
    ctx.clearRect(0, 0, c.width, c.height)
}

function renderObjects() {
    // Render block
    ctx.fillStyle = '#00b02f'
    ctx.fillRect(5 * blockInfo.w, 5 * blockInfo.h, blockInfo.w, blockInfo.h)

    // Render player
    ctx.fillStyle = '#000000'
    ctx.fillRect(player.x * blockInfo.w, player.y * blockInfo.h, player.width * blockInfo.w, player.height * blockInfo.h)
}

function press(e) {
    let w = e.which
    
    if (renderedObjects == true) {
        clearObjects()
        
        if (w == 39) {
            player.x += player.speed
        } else if (w == 37) {
            player.x -= player.speed
        } else if (w == 38) {
            player.y -= player.speed
        } else if (w == 40) {
            player.y += player.speed
        }
    }
}

setInterval(function() {
    renderedObjects = false
    renderObjects()
    renderedObjects = true
}, 0)
body {
    margin: 0;
    overflow: hidden;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>repl.it</title>
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body onkeydown="press(event)">
        <canvas id="c"></canvas>

    <script src="script.js"></script>
  </body>
</html>

2 Answers2

2

While the accepted answer solves the problem and offers great advice to use requestAnimationFrame which automatically batches all re-renders as part of its optimized animation support, there are more fundamental design issues at hand. The glitchy animation is more of a symptom of this than anything else.

Although requestAnimationFrame is smart enough to (inadvertently) bail the design out, there's no reason you shouldn't be able to use setInterval just fine (but don't use setInterval for other reasons than this).

The typical flow for animating is:

 ______________________
|| :synchronous code: ||  :asynchronous code:
||                    ||
||   [update state]<---------+
||          |         ||     |
||          v         ||     | (keypresses are collected, etc)
||   [clear screen]   ||     |
||          |         ||     |
||          v         ||     |
||   [redraw screen]---------+
`|____________________|`

However, your approach pulls screen clears out of the synchronous update block to fire at random points. There can be a delay before another run of the atomic update/render code has a chance to redraw the screen. If animation is like a flip book, this is like opening up the possibility of blank pages being shown.

A better way to approach this, regardless of how naive the rendering loop is (including setInterval), is to record keypresses and user input as it occurs but do nothing else (including screen clearing, position updates, etc). Then, once your update/render function gets another chance to run, you can update entity positions as a batch and perform all re-renders as a batch.

It may seem inefficient to clear and redraw the screen on every frame, but that's just how animation works. If you only want to clear and redraw when a key is pressed, that's fine, but then leave out the animation loop entirely and re-render the screen only in the key handler. This is fine for many applications, but if you're making a game, it's likely that you'll want enemies to move and things to happen in real-time, even if the player isn't pushing a button, so it probably won't help much in this case. The key is not to mix the two approaches.

All of this leads to decoupling the keyboard/UI from the game logic (player movement, etc). You can use a specific UI-to-game logic coupling module to handle translation of user input into player position changes and other state updates. This is the same idea as decoupling rendering logic from game/state update logic.

Entities are anything that's renderable (responds to a render(ctx) function). Thinking in terms of OOP, the box and player share the same base class--they're renderable rectangles, so we can build them with a constructor function of sorts and toss on the speed property for player.

Now, of course, some of this can be premature optimization for a small app like this, but you'll see it's more or less the same amount of code.

Putting together all of the points above, we get something like:

const canvas = document.createElement("canvas");
document.body.append(canvas);
canvas.width = innerWidth;
canvas.height = innerHeight;
const ctx = canvas.getContext("2d");

const makeRectangleEntity = props => ({
  ...props,
  render: function (ctx) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.w, this.h);
  }
});

const block = makeRectangleEntity({
  x: 75, y: 75, w: 15, h: 15, color: "#00b02f"
});
const player = makeRectangleEntity({
  x: 20, y: 20, w: 15, h: 15, 
  color: "#000", speed: 0.125
});
const entities = [block, player];

const keyCodeActions = {
  37: () => (player.x -= player.speed),
  38: () => (player.y -= player.speed),
  39: () => (player.x += player.speed),
  40: () => (player.y += player.speed),
};

const keysPressed = new Set();
document.addEventListener("keydown", e => {
  keysPressed.add(e.keyCode);
});
document.addEventListener("keyup", e => {
  keysPressed.delete(e.keyCode);
});

// Show that setInterval wasn't the problem, but do
// replace this with requestAnimationFrame anyway.
setInterval(() => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // update entity state
  for (const keyCode of keysPressed) {
    if (keyCode in keyCodeActions) {
      keyCodeActions[keyCode]();
    }
  }
  
  // redraw screen
  entities.forEach(e => e.render(ctx));
}, 0);
body {
  margin: 0;
  overflow: hidden;
}
ggorlen
  • 26,337
  • 5
  • 34
  • 50
  • Thank you for telling me the details. It is very packed with details. Mine as well let your details go hiking since it's packed. – Unknown Bobby Dec 23 '20 at 02:06
  • No problem. Keep in mind Stack Overflow is a community Q&A website so answers are here to help future readers in addition to you. If you need a TL;DR here, the answer is that you should synchronously run all of your update and rendering code at the same time in the update loop--don't clear the screen outside of the rendering loop in a keypress handler like this. – ggorlen Dec 23 '20 at 02:22
1

Instead of setInterval, use requestAnimationFrame. Even though you specified 0 for setInterval, it will actually still wait some minimum delay before calling the callback again. This is unrelated to your device. requestAnimationFrame is specifically useful for... animations.

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

c.height = window.innerHeight;
c.width = window.innerWidth;

let blockInfo = {
    h: 15, // Defining height and width so they can be changed. (NOTE: The height and width doesn't have to be the same value)
    w: 15
};

let renderedObjects = false; // Keep in case you're gonna make colission

let player = {
    speed: 0.125,
    x: 2,
    y: 2,
    height: 1,
    width: 1
};

function clearObjects() {
    // Clear all
    ctx.clearRect(0, 0, c.width, c.height);
}

function renderObjects() {
    // Render block
    ctx.fillStyle = '#00b02f';
    ctx.fillRect(5 * blockInfo.w, 5 * blockInfo.h, blockInfo.w, blockInfo.h);

    // Render player
    ctx.fillStyle = '#000000';
    ctx.fillRect(player.x * blockInfo.w, player.y * blockInfo.h, player.width * blockInfo.w, player.height * blockInfo.h);
}

function press(e) {
    let w = e.which;
    
    if (renderedObjects == true) {
        clearObjects(); 
       
        if (w == 39) {
            player.x += player.speed;
        } else if (w == 37) {
            player.x -= player.speed;
        } else if (w == 38) {
            player.y -= player.speed;
        } else if (w == 40) {
            player.y += player.speed;
        }
    }
}

(function loop() {
    renderedObjects = false;
    renderObjects();
    renderedObjects = true;
    requestAnimationFrame(loop);
})();
body {
    margin: 0;
    overflow: hidden;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>repl.it</title>
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body onkeydown="press(event)">
        <canvas id="c"></canvas>

    <script src="script.js"></script>
  </body>
</html>

Unrelated: please terminate your statements with an explicit semi-colon. To rely on the automatic semi colon insertion is just taking an extra risk of unintended errors.

trincot
  • 211,288
  • 25
  • 175
  • 211