Search code examples
javascriptecmascript-6game-physics

Making movement smoother - JS Canvas Game development


Im experimenting with ES6 & Canvas for game development. In this example I have an image of a spaceship and I rotate it with right and left arrow keys, and move it forward with upper key. If you look at the rotation and the moving forward you see that it's laggy. Is there a way to make it smoother?

Plunker

The code for 'animation' and movement: (found in plunker as well)

from engine.js:

let mainLoop = function() {
        clrscr();
        draw();
        requestAnimationFrame(mainLoop);
    }

let draw = function() {
        spaceship.draw(ctx);    
    }

    let keyDownListener = function(e) {

    if(e.keyCode == 37)
        spaceship.rotateLeft();

        if(e.keyCode == 38)
        spaceship.moveForward(ctx);

    if(e.keyCode == 39)
        spaceship.rotateRight();

    if(e.keyCode == 32)
        createExplosion();
    };

    let clrscr = function() {
        ctx.fillStyle="#415575";
        ctx.fillRect(0,0,w,h);
    }

from spaceship.js:

let width = image.width * resizeMultiplier;
  let height = image.height * resizeMultiplier;

  const rotateDelta = 0.37; 
  const forwardDelta = 0.77;

  let draw = function(context) {
    //Save context
    context.save();
    //Translate before rotate
    context.translate(x,y);
    //Rotate on translated 0,0
    context.rotate((angle) * Math.PI/180);
    //Draw rotated image
    context.drawImage(image, -(width/2), -(height/2), width, height);
    //Restore the translated & rotated coords to where we began
    context.restore(); 
  }

  let rotateRight = function() {
    console.log(angle);
    angle = (angle === 360) ? 0 : angle + (rotateDelta *(1000/60));
  }

  let rotateLeft = function() {
    console.log(angle);
    angle = (angle === -360) ? 0 : angle - (rotateDelta *(1000/60));
  }

  let moveForward = function() {
    let dx = Math.sin((angle) * Math.PI/180);
    let dy = - Math.cos((angle) * Math.PI/180);
    x += dx * forwardDelta * (1000/60);
    y += dy * forwardDelta * (1000/60);
    console.log('dx: ',dx,' dy: ',dy);
    //x += forwardDelta * (1000/20);
  }

Thank you for your time.


Solution

  • The main reason for that is that you use dedicated functions for each input:
    spaceship.rotateLeft() , spaceship.moveForward() , spaceship.rotateRight()

    While this looks very good OOP wise, the result is that every time the window.onkeydown handler is invoked, it will interrupt/reset the current execution because the key-handler is not in sync with the frame-function.
    The frame-function runs (in theory) at a fixed interval. But the key-handler doesn't follow the same pattern, because that get fired when you press a key. So the two kinda work against each other.
    (Anyone please correct me if I'm wrong, not 100% sure about this.)

    In any case, you can resolve it by moving the key-handler to spaceship.js, and in the key-handler set a boolean for every key to true when you press it, and also add a onkeyup handler to set them back to false again.
    And in your draw() function you call the function that calculates all the movement, right before you draw the new values:

      //move
      const rotateDelta = 7; //degrees
      const forwardDelta = 10;
      let key = {up:false, left:false, right:false, fire:false};
    
    //DRAW--------------------
      let draw = function(ctx) {
        move();
        ctx.save();
        ...
      };
    
    //MOVE--------------------
      let move = function() {
        if (key.left) {angle = (angle <= -360)?0: angle-rotateDelta;}
        if (key.right) {angle = (angle >= 360)?0: angle+rotateDelta;}
        if (key.up) {
          x += Math.sin(angle*Math.PI/180)*forwardDelta;
          y += -Math.cos(angle*Math.PI/180)*forwardDelta;
        }
        if (key.fire) {}
      };
    
    //KEY-HANDLER--------------------
      window.onkeydown = function(e) {
        if (e.keyCode == 37) {key.left=true;} //LEFT
        if (e.keyCode == 38) {key.up=true;} //UP
        if (e.keyCode == 39) {key.right=true;} //RIGHT
        if (e.keyCode == 32) {key.fire=true;} //SPACEBAR
      };
      window.onkeyup = function(e) {
        if (e.keyCode == 37) {key.left=false;} //LEFT
        if (e.keyCode == 38) {key.up=false;} //UP
        if (e.keyCode == 39) {key.right=false;} //RIGHT
        if (e.keyCode == 32) {key.fire=false;} //SPACEBAR
      };
    
    • As you can see, I also removed what I thought were unnecessary complications for both distance and rotation (*(1000/60) and unnecessary brackets etc). If you do need them you can use them of course, but I wanted to keep the calculations as light as possible to remove any cause for stutter. And rotateDelta and forwardDelta now have nice round values.
      In Plunker they are 7 and 10, in the SO Code Snippet I had to lower them a bit because the ship had to be smaller due to the available space.
    • In the left- and right-key calculations I changed angle === -360 and angle === 360 to respectively angle <= -360 and angle >= 360, because your angle could skip over 360 and then the value wouldn't be reset. This way is more secure.
    • And the last major thing I changed is adding width:100%; and height:100% to the <html>, so the <canvas> properly covers the whole page.
    • I made some other changes, mainly for myself to get a good overview of your code. If you like the style, use it, otherwise ignore it.

    And the result is a smooth-sailing space machine:

    Engine = window.Engine || {};
    
    Engine = function() {
      let canvas,ctx, w,h;
      
      //player
      let ssImgPath = "http://i68.tinypic.com/2q87s0i.png";
      let ssSizeRatio = 0.1; //multiplier for original image dimensions
      let spaceship; //player object
      
      let ssImage = new Image();
      ssImage.src = ssImgPath;
      
    //INIT--------------------
      let initModule = function() {
        canvas = document.getElementById("canvas");
        canvas.width = document.body.clientWidth;
        canvas.height = document.body.clientHeight;
        ctx = canvas.getContext("2d");
        w=canvas.width, h=canvas.height;
        
        spaceship = new Spaceship({x:w/2, y:h*0.7, angle:0, canvasW:w, canvasH:h, resizeMultiplier:ssSizeRatio, image:ssImage});
        mainLoop();
      };
      
    //FRAME-LOOP--------------------
      let mainLoop = function() {
        clrscr();
        draw();
        requestAnimationFrame(mainLoop);
      };
      let clrscr = function() {
        ctx.fillStyle = "rgb(65,85,117)";
        ctx.fillRect(0,0,w,h);
      };
      let draw = function() {
        spaceship.draw(ctx);
      };
      
    //RETURN--------------------
      return {initModule};
    }(); window.onload=Engine.initModule;
    
    
    /*==============================================================*/
    /****************************************************************/
    /*==============================================================*/
    
    
    Spaceship = window.Spaceship || {};
    
    Spaceship = function(options) {
      let {x,y,angle, canvasW,canvasH, resizeMultiplier, image} = options;
      let width = image.width*resizeMultiplier;
      let height = image.height*resizeMultiplier;
      
      //move
      const rotateDelta = 7; //degrees
      const forwardDelta = 5;
      let key = {up:false, left:false, right:false, fire:false};
      
    //DRAW--------------------
      let draw = function(ctx) {
        move();
        
        ctx.save();
        ctx.translate(x,y);
        ctx.rotate((angle) * Math.PI / 180); //player rotation
        ctx.drawImage(image, -width/2, -height/2, width,height);
        ctx.restore();
      };
      
    //MOVE--------------------
      let move = function() {
        if (key.left) {angle = (angle <= -360)?0: angle-rotateDelta;}
        if (key.right) {angle = (angle >= 360)?0: angle+rotateDelta;}
        if (key.up) {
          x += Math.sin(angle*Math.PI/180)*forwardDelta;
          y += -Math.cos(angle*Math.PI/180)*forwardDelta;
        }
        if (key.fire) {console.log("pew");}
      };
      
    //KEY-HANDLER--------------------
      window.onkeydown = function(e) {
        if (e.keyCode == 37) {key.left=true;} //LEFT
        if (e.keyCode == 38) {key.up=true;} //UP
        if (e.keyCode == 39) {key.right=true;} //RIGHT
        if (e.keyCode == 32) {key.fire=true;} //SPACEBAR
      };
      window.onkeyup = function(e) {
        if (e.keyCode == 37) {key.left=false;} //LEFT
        if (e.keyCode == 38) {key.up=false;} //UP
        if (e.keyCode == 39) {key.right=false;} //RIGHT
        if (e.keyCode == 32) {key.fire=false;} //SPACEBAR
      };
      
    //RETURN--------------------
      return {draw};
    };
    html, body {width:100%; height:99%; margin:0; padding:0;}
    <!DOCTYPE html>
    <html>
      <head>
        <title>Asteroids</title>
        <link rel=stylesheet href="asteroids.css">
        <script src="engine.js"></script>
        <script src="spaceship.js"></script>
      </head>
      
      <body>
        <canvas id="canvas"></canvas>
      </body>
    </html>
    plunker: https://plnkr.co/edit/nKAyweLV4d0hmmIRzxG4?p=preview