Backgrounds

9 min read

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 src value.
  • Adjust the fish layer’s velocity or opacity.
  • Change tiles and 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.

Lines: 1 Characters: 0
Game Status: Not Started

Challenge

Observe the parallax effect with the reef background and the school of fish moving across the screen.

Lines: 1 Characters: 0
Game Status: Not Started
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;

Computer Science & Software Engineering