Sidequest

Drawing the Ground

Ground Class

At this point let's render the ground to fill the gap at the bottom of the canvas. We know the ground is going to need to scroll over time, so there's going to be more logic than just rendering. We'll create a Ground class in preparation for this, then go over all the new information.

Go ahead and create the class:

1
import { SpriteData } from "#/components/sprite-data";
2
import { Vector2d } from "#/components/vector2d";
3
4
type GroundOptions = {
5
position: Vector2d;
6
spriteData: SpriteData;
7
spriteSheet: HTMLImageElement;
8
};
9
10
export class Ground {
11
position: Vector2d;
12
spriteData: SpriteData;
13
spriteSheet: HTMLImageElement;
14
15
constructor(options: GroundOptions) {
16
this.position = options.position;
17
this.spriteData = options.spriteData;
18
this.spriteSheet = options.spriteSheet;
19
}
20
21
public draw(context: CanvasRenderingContext2D) {
22
context.drawImage(
23
this.spriteSheet,
24
this.spriteData.sourceX,
25
this.spriteData.sourceY,
26
this.spriteData.width,
27
this.spriteData.height,
28
this.position.x,
29
this.position.y,
30
this.spriteData.width,
31
this.spriteData.height
32
);
33
}
34
}

Let's break this down.

SpriteData Component

We've discussed leveraging sprite sheet metadata when we rendered the background. We're going to need to start passing this metadata around so our classes can leverage this information to draw. We'll call this SpriteData to keep hammering the point home that this is sprite related.

Let's create the class to capture that information:

1
/**
2
* Represents rectangular sprite data from a source image.
3
*/
4
export class SpriteData {
5
constructor(
6
/**
7
* The x position of the sprite in the source image.
8
*/
9
public sourceX: number,
10
11
/**
12
* The y position of the sprite in the source image.
13
*/
14
public sourceY: number,
15
16
public width: number,
17
public height: number
18
) {}
19
}

What is a Vector2d?

In game development, we often need to represent x and y values for many things such as: position, velocity, and direction, to name a few. So rather than having variables such as: positionX, positionY, velocityX, and velocityY, we can use a vector as our common data structure. This also opens us up to vector math down the road, even if we don't use it in this project.

Vectors are so common in game development that you'll likely come across them when Googling for solutions to day-to-day problems. As an example, in a top down shooter you could use vector math to calculate the change needed to make the player look at (rotate to) an enemy.

We're adding the 2d suffix to make it clear that this is a vector of information in 2d space. You can imagine how this might scale for a 3d game and store x, y, and z values.

Let's create a new file:

1
export class Vector2d {
2
constructor(public x = 0, public y = 0) {}
3
}

Creating a Ground Instance

Add an instance of Ground to the bottom of the main.ts file, and call its draw() method.

1
import spriteSheetUrl from "#/assets/image/spritesheet.png";
+
import { Vector2d } from "#/components/vector2d";
3
import { config } from "#/config";
+
import { Ground } from "#/entities/ground";
5
import { loadImage } from "#/lib/asset-loader";
6
import { spriteMap } from "#/sprite-map";
7
8
// ...
9
+
const ground = new Ground({
+
position: new Vector2d(0, config.gameHeight - spriteMap.ground.height),
+
spriteData: spriteMap.ground,
+
spriteSheet,
+
});
+
+
ground.draw(context);

Notice to position the ground we're subtracting the height of the ground from the height of the canvas. This will snap it to the bottom.

Next, let's make the ground move.