Search code examples
javascriptcanvasgame-engineviewport

How do I make the canvas context follow the player?


I am building a game using JavaScript and canvases, and I want to implement a viewport to display a larger game world. I have already created a canvas element and loaded some stationary objects and a player object that can be moved around using keyboard input.

However, I am unsure how to implement a viewport to show only a portion of the game world and allow the player to move around it. Can someone provide an example of how to implement a viewport using JavaScript and canvases, based on the provided code?

Specifically, I am looking for code that will allow me to define a viewport size and position, and to update the game world position based on the player's movement, so that the player remains centered in the viewport as they move around.

Here is my current code. Note, I am very new to js:

    // Get the canvas element and its context
    const canvas = document.createElement("canvas");
    document.body.append(canvas);
    const ctx = canvas.getContext("2d");

    // Player coords and update method that moves the player based on input
    const player = {
      x: 0,
      y: 0,
      vel: 1,
      update(){
        if(key.left) this.x -= this.vel;
        if(key.right) this.x += this.vel;
        if(key.up) this.y -= this.vel;
        if(key.down) this.y += this.vel;
      }
    };





    // Keys pressed (Used for movement)
    const key = {
      left: false,
      right: false,
      up: false,
      down: false
    }
    
    
    // Get Input
    window.addEventListener('keydown', (e) =>
    {
      switch(e.code){
        case "KeyW":
          key["up"] = true;
          break;
        case "KeyS":
          key["down"] = true;
          break;
        case "KeyA":
          key["left"] = true;
          break;
        case "KeyD":
          key["right"] = true;
          break;
      }
    });

    // Relieve Input
    window.addEventListener('keyup', (e) =>
    {
      switch(e.code){
        case "KeyW":
          key["up"] = false;
          break;
        case "KeyS":
          key["down"] = false;
          break;
        case "KeyA":
          key["left"] = false;
          break;
        case "KeyD":
          key["right"] = false;
          break;
      }
    });

    function update(){
      ctx.clearRect(0,0,canvas.width, canvas.height);
      // Draw stationary objects
      ctx.fillRect(100, 100, 10, 10);
      ctx.fillRect(100, 120, 10, 10);

      // Draw Player and update position
      player.update();
      ctx.fillRect(player.x, player.y, 10, 10);
      requestAnimationFrame(update);
    }
    update();

I have tried saving the the context and translating then restoring. That didn't not work however, I was expecting the player to stay centered in the screen. Here is the code I tried:

    function update(){
      ctx.clearRect(0,0,canvas.width, canvas.height);
      // Draw stationary objects
      ctx.fillRect(100, 100, 10, 10);
      ctx.fillRect(100, 120, 10, 10);

      // Draw Player and update position
      ctx.save();
      ctx.translate(player.x, player.y);
      player.update();
      ctx.fillRect(canvas.width/2 - 5, canvas.height/2 - 5, 10, 10);
      ctx.restore();
      requestAnimationFrame(update);
    }

Solution

  • Most 2D scenes implementations in games work with a parent/child tree where the scene is the top parent (parenting an object position-wise is simply making them dependent of another object's position.)

    Once you have that, you can easily move the viewport by adding every object to the same parent (for exemple, the scene) containing all your objects, and move that parent to simulate a camera.

    Here's some pseudo code to get the ideas behind such a system.

    //Pseudo code, don't simply copy-paste it
    // Make a class from which all the objects in the game will inherit; it'll take care of managing the position.
    
    class GameObject {
        position: {x: 0, y: 0}
        parent: null;
        children: [];
        function setPosition(pX, pY) { //Sets the position within the parent
            position.x = pX;
            position.y = pY;
        }
        //This is the core of our parent/child tree
        function addChild(pGameObject) {
            children.push(pGameObject);
            pGameObject.parent = this;
        }
        function removeChild() //take care of updating the parent/children vars
    
        function toGlobalCoordinates() { //Allows us to get the global coordinates (= coordinates on the canvas)
           let lParent = this.parent;
           let globalPos = {x: 0, y: 0}
           //Goes up the parents tree, breaks when there's no more parent thanks to the default: parent = null in the class
           while(lParent) {
               globalPos.x += lParent.position.x;
               globalPos.y += lParent.position.y;
               lParent = lParent.parent;
           }
           globalPos.x += this.position.x;
           globalPos.y += this.position.y;
           return globalPos;
        }
     
        function draw(pCtx) { //We pass the canvas ctx to be able to draw on it
            //Use the global position to draw your object
            let drawingPos = this.toGlobalCoordinates();
            pCtx.fillRect(drawingPos.x, drawingPos.y, 100, 100);
        }
    }
    

    Then, we have the other classes like the Player and we have to take care of the rendering

    //extends from GameObject to benefit from the position and its methods
    class Player extends GameObject {
        methods() ... //Implement methods specific to the player
    }
    
    //Use the scene as the base gameobject (our top parent)
    let scene  = new GameObject();
    let player = new Player();
    let stationnaryObject1 = new GameObject()
    
    scene.addChild(player)
    scene.addChild(stationnaryObject1)
    
    //The function called on requestAnimationFrame
    update() {
       //clearRect to clean the canvas then
    
       //On key press, move the player, move the scene in the opposite direction to keep the keep focus on the player
       if(keypress) {
         player.setPosition(player.position.x + newX, player.position.x + newY)
         scene.setPosition(scene.position.x - newX, scene.position.x - newY)
       }
       
       //Draw the objects with their new position
       scene.draw(ctx);
       player.draw(ctx);
    }
    

    That's more or less a basic way to handle being able to make a camera (aka move the viewport in the gameworld).

    You should take a look at PixiJS if you're looking for a very robust way to handle that, and a lot more. It's a very powerful and easy to learn JS library to handle 2D display on WebGL & canvas (no more drawing yourself !)

    To learn more about the Scene graph/tree I talked about : https://pixijs.io/guides/basics/scene-graph.html the same kind of tree was used in Flash games for example.

    TLDR : Making a camera is changing a position all objects' positions are attached to.

    I hope you'll have fun developping games !