Backgrounds
Lesson 1: Background Settings Are Object Literals
A background’s settings are grouped in a JavaScript object literal: property names describe the settings, and values configure one background layer. The level examples build an image path from gameEnv.path, then place that path and other settings in a data object.
const image_src_reef = path + "/images/projects/gamebuilder/bg/reef.png";
const image_data_reef = {
name: 'reef',
greeting: "Welcome to the reef!",
src: image_src_reef,
pixels: { height: 360, width: 643 }
};
Here, src identifies the image to load. In the parallax example, velocity, opacity, and tiles configure movement, visibility, and repeated drawing. The pixels and greeting values are useful descriptive data in the level, but these background classes use the loaded image’s actual dimensions and do not display the greeting. Add a property only when the level or engine needs it.
Lesson 2: A Level Maps Data to Engine Classes
A level constructor creates data objects, then lists each background as a descriptor in this.classes:
this.classes = [
{ class: GameEnvBackground, data: image_data_reef },
{ class: BackgroundParallax, data: image_data_school_fish },
];
Each descriptor pairs a constructor (class) with its configuration (data). The game engine’s GameLevel.create() method loops through these descriptors and constructs each object with the equivalent of new descriptor.class(descriptor.data, gameEnv). It then adds the object to gameEnv.gameObjects, where the game loop can update it. This is the connection between a level’s object literals and the running game.
The first example maps image__data_atat to GameEnvBackground for a fixed full-canvas background. The reef example maps image_data_reef to the default GameEnvBackground class (imported under the local name GamEnvBackground in that snippet) and image_data_school_fish to BackgroundParallax for a second, moving layer.
Lesson 3: Constructors and Lifecycle Methods Create the Background
GameEnvBackground receives (data, gameEnv). Its constructor saves the data and creates an Image using data.src. Its update() calls draw(), which draws the image across the game canvas; resize() draws it again at the new canvas size. If there is no image source, this class fills the canvas with its default color.
BackgroundParallax also receives (data, gameEnv), then passes its settings to its parent Background constructor. The parent loads the image, creates and aligns a canvas after the image loads, and adds that canvas to the game container. BackgroundParallax adds its own position, velocity, tile settings, and opacity. Its image-load handler sets the canvas opacity and places that canvas behind the other game objects.
The engine repeatedly calls each object’s update() method. It also calls resize() when the game window changes size and destroy() when the level is removed. These lifecycle methods are why a background class behaves as part of the game rather than as a CSS page background.
Lesson 4: Settings Produce the Visual Effects
The AT-AT level uses GameEnvBackground without a velocity setting, so its image is redrawn to cover the game canvas and stays fixed behind the player, NPC, and projectiles.
The reef level uses BackgroundParallax for the school of fish. Each update() adds velocity.x and velocity.y to the layer’s position, wraps the position when it reaches an image boundary, and calls draw(). The draw method repeats the image tiles to cover the canvas. With velocity: { x: 0.5, y: 0 }, the layer scrolls horizontally; opacity: "0.8" makes it partly transparent; and tiles: { y: 1 } asks the engine to use one row vertically while it calculates the horizontal coverage.
To make a background change, start by changing one data value, such as src, velocity, opacity, or tiles, and trace how that value is used by the selected class. Keep the foreground game objects unchanged so you can identify the background effect.
Background Practice
Use one of the background data objects in the examples below. Change a background setting and explain which constructor or method uses it and what changes on screen.
Try one of these background-only changes:
- Change the static background image by updating its
srcvalue. - Adjust the fish layer’s
velocityoropacity. - Change
tilesand observe how the parallax layer fills the canvas.
Leave the player, NPC, and projectile data unchanged. Preserve the @assets/... import path in the first example; it is an intentional project path alias resolved by the site build.
Challenge
Play the Star Wars game level with snowspeeder and AT-AT background. Observe background is static but AT-AT's have projectiles to simuulate action.
Challenge
Observe the parallax effect with the reef background and the school of fish moving across the screen.
import GameObject from './GameObject.js';
/** Background class for primary background
*
*/
export class GameEnvBackground extends GameObject {
constructor(data = null, gameEnv = null) {
super(gameEnv);
this.data = data; // Store the data for identification
if (data.src) {
this.image = new Image();
this.image.src = data.src;
} else {
this.image = null;
}
}
update() {
this.draw();
}
draw() {
const ctx = this.gameEnv.ctx;
const width = this.gameEnv.innerWidth;
const height = this.gameEnv.innerHeight;
if (this.image) {
// Draw the background image scaled to the canvas size
ctx.drawImage(this.image, 0, 0, width, height);
} else {
// Fill the canvas with fillstyle color if no image is provided
ctx.fillStyle = '#063970';
ctx.fillRect(0, 0, width, height);
}
}
/** For primary background, resize is the same as draw
*
*/
resize() {
this.draw();
}
/** Destroy Game Object
* remove object from this.gameEnv.gameObjects array
*/
destroy() {
const index = this.gameEnv.gameObjects.indexOf(this);
if (index !== -1) {
this.gameEnv.gameObjects.splice(index, 1);
}
}
}
export default GameEnvBackground;
import Background from './Background.js';
/** Parallax Background GameObject
* - Tiling: draw multiple of the image to fill the gameCanvas extents
* - Scrolling: adds velocity or position updates to the update(), to scroll the background
*/
export class BackgroundParallax extends Background {
/**
* Constructor is called by GameLevel create() method
* @param {Object} data - The data object for the background
* @param {Object} gameEnv - The game environment object for convenient access to game properties
*/
constructor(data = null, gameEnv = null) {
// Set default ID and zIndex for parallax backgrounds
const parallaxData = {
...data,
id: data.id || "parallax-background",
zIndex: data.zIndex || "1"
};
// Call parent constructor which handles image loading and canvas creation
super(parallaxData, gameEnv);
// Parallax-specific properties
this.position = data.position || { x: 0, y: 0 };
this.velocity = data.velocity || { x: 0, y: 0 };
this.tiles = data.tiles || null; // Optional: { x: num, y: num } to control tiling
this.scaleToFit = data.scaleToFit || null; // Optional: 'width' or 'height' to scale image instead of tile
// Store reference to parent's onload to extend it
const parentOnload = this.image.onload;
// Extend the image.onload to add parallax-specific setup
this.image.onload = () => {
// Call parent onload first
if (parentOnload) {
parentOnload.call(this);
}
// Set opacity for parallax effect
this.canvas.style.opacity = data.opacity || "0.3";
// Reposition canvas to be first in container (behind everything)
const gameContainer = this.gameEnv.container;
if (gameContainer && this.canvas.parentNode) {
if (gameContainer.firstChild !== this.canvas) {
gameContainer.insertBefore(this.canvas, gameContainer.firstChild);
}
}
};
}
/**
* Update is called by GameLoop on all GameObjects
* Overrides parent to add parallax scrolling
*/
update() {
// Update the position for parallax scrolling
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Wrap the position to prevent overflow
// For rightward movement (positive velocity.x), wrap when completing a full width cycle
if (this.velocity.x > 0 && this.position.x >= this.width) {
this.position.x = 0;
}
// For leftward movement (negative velocity.x), wrap when going too far left
if (this.velocity.x < 0 && this.position.x <= -this.width) {
this.position.x = 0;
}
// For downward movement (positive velocity.y), wrap at bottom
if (this.velocity.y > 0 && this.position.y >= this.height) {
this.position.y = 0;
}
// For upward movement (negative velocity.y), wrap at top
if (this.velocity.y < 0 && this.position.y <= -this.height) {
this.position.y = 0;
}
// Draw the background image
this.draw();
}
/**
* Draws the background image with tiling and scrolling
* Overrides parent to add parallax tiling effect
*/
draw() {
if (!this.isInitialized) {
return; // Skip drawing if not initialized
}
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
// Calculate scaled dimensions if scaleToFit is enabled
let drawWidth = this.width;
let drawHeight = this.height;
if (this.scaleToFit === 'width') {
// Scale to fit canvas width, maintain aspect ratio
drawWidth = canvasWidth;
drawHeight = (this.height / this.width) * canvasWidth;
} else if (this.scaleToFit === 'height') {
// Scale to fit canvas height, maintain aspect ratio
drawHeight = canvasHeight;
drawWidth = (this.width / this.height) * canvasHeight;
}
// Calculate the wrapped position, Scrolling
let xWrapped = this.position.x % drawWidth;
let yWrapped = this.position.y % drawHeight;
if (xWrapped > 0) {
xWrapped -= drawWidth;
}
if (yWrapped > 0) {
yWrapped -= drawHeight;
}
// Calculate the number of draws needed to fill the canvas, Tiling
// Smart tiling: auto-calculate scrolling dimension, allow manual control of non-scrolling dimension
let numHorizontalDraws, numVerticalDraws;
if (this.scaleToFit === 'width') {
// Scaled to width: only need 1 horizontal, calculate vertical for scrolling
numHorizontalDraws = 1;
numVerticalDraws = Math.ceil(canvasHeight / drawHeight) + 1;
} else if (this.scaleToFit === 'height') {
// Scaled to height: only need 1 vertical, calculate horizontal for scrolling
numVerticalDraws = 1;
numHorizontalDraws = Math.ceil(canvasWidth / drawWidth) + 1;
} else if (this.velocity.x !== 0) {
// Horizontal scrolling: auto-calculate horizontal tiles to cover screen width
numHorizontalDraws = Math.ceil(canvasWidth / drawWidth) + 1;
// Use configured vertical tiles or auto-calculate
numVerticalDraws = this.tiles?.y ? (this.velocity.y !== 0 ? this.tiles.y + 1 : this.tiles.y) : (Math.ceil(canvasHeight / drawHeight) + 1);
} else if (this.velocity.y !== 0) {
// Vertical scrolling: auto-calculate vertical tiles to cover screen height
numVerticalDraws = Math.ceil(canvasHeight / drawHeight) + 1;
// Use configured horizontal tiles or auto-calculate
numHorizontalDraws = this.tiles?.x ? this.tiles.x : (Math.ceil(canvasWidth / drawWidth) + 1);
} else {
// No scrolling or both directions: use configured tiles or auto-calculate both
numHorizontalDraws = this.tiles?.x || (Math.ceil(canvasWidth / drawWidth) + 1);
numVerticalDraws = this.tiles?.y || (Math.ceil(canvasHeight / drawHeight) + 1);
}
// Clear the canvas
this.ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw the background image multiple times to fill the canvas, Tiling
for (let i = 0; i < numHorizontalDraws; i++) {
for (let j = 0; j < numVerticalDraws; j++) {
this.ctx.drawImage(
this.image, // Source image
0, 0, this.width, this.height, // Source rectangle
xWrapped + i * drawWidth, yWrapped + j * drawHeight, drawWidth, drawHeight); // Destination rectangle
}
}
}
}
export default BackgroundParallax;