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);
}
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 !