DEV Community

Cover image for TCJSGame Camera Class: Complete Reference Guide
Kehinde Owolabi
Kehinde Owolabi

Posted on

TCJSGame Camera Class: Complete Reference Guide

TCJSGame Camera Class: Complete Reference Guide

Game Camera Systems

Introduction to the Camera Class

TCJSGame's Camera Class provides powerful viewport control for creating scrolling worlds, following players, and managing large game environments. The camera system allows you to create games that extend beyond the visible screen area.

🎥 Basic Camera Setup

Camera Initialization

// Camera is automatically created with Display
const camera = display.camera;

// Access camera properties
console.log("Camera position:", camera.x, camera.y);
console.log("World size:", camera.worldWidth, camera.worldHeight);

// Manual camera creation (if needed)
const customCamera = new Camera(0, 0, 2000, 1500);
display.camera = customCamera;
Enter fullscreen mode Exit fullscreen mode

Constructor Parameters

class Camera {
    constructor(x = 0, y = 0, worldWidth = 1000, worldHeight = 1000) {
        this.x = x;                 // Current X position
        this.y = y;                 // Current Y position
        this.target = null;         // Component to follow
        this.speed = 5;             // Movement speed
        this.worldWidth = worldWidth;   // Total world width
        this.worldHeight = worldHeight; // Total world height
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 Camera Follow Systems

Basic Object Following

// Follow a component directly (no smoothing)
camera.follow(player, false);

// Follow with smooth movement (lerping)
camera.follow(player, true);

// Example: Different follow styles for different game types
function setupPlatformerCamera() {
    // Platformer: Smooth follow on X, direct follow on Y
    camera.follow(player, false);
}

function setupTopDownCamera() {
    // Top-down: Smooth follow in both directions
    camera.follow(player, true);
}

function setupStrategyCamera() {
    // Strategy: No automatic follow, manual control
    camera.target = null;
}
Enter fullscreen mode Exit fullscreen mode

Advanced Follow Configurations

// Custom follow function with offset
function customCameraFollow() {
    if (camera.target) {
        // Center on target with offset
        const targetX = camera.target.x - display.canvas.width / 2 + 100;
        const targetY = camera.target.y - display.canvas.height / 2 - 50;

        if (camera.smooth) {
            // Smooth interpolation
            camera.x += (targetX - camera.x) * 0.1;
            camera.y += (targetY - camera.y) * 0.1;
        } else {
            // Direct positioning
            camera.x = targetX;
            camera.y = targetY;
        }

        // Apply world bounds
        clampCameraToWorld();
    }
}

function clampCameraToWorld() {
    camera.x = Math.max(0, Math.min(camera.x, camera.worldWidth - display.canvas.width));
    camera.y = Math.max(0, Math.min(camera.y, camera.worldHeight - display.canvas.height));
}
Enter fullscreen mode Exit fullscreen mode

🌍 World Boundaries and Constraints

Setting World Size

// Define your game world boundaries
camera.worldWidth = 3000;   // 3000 pixels wide
camera.worldHeight = 2000;  // 2000 pixels tall

// Example: Progressive world expansion
function expandWorld(newWidth, newHeight) {
    camera.worldWidth = newWidth;
    camera.worldHeight = newHeight;
    console.log(`World expanded to ${newWidth}x${newHeight}`);
}

// Dynamic world sizing based on level
function setupLevelCamera(level) {
    const levelSizes = {
        1: { width: 800, height: 600 },
        2: { width: 1200, height: 800 },
        3: { width: 2000, height: 1200 }
    };

    const size = levelSizes[level] || levelSizes[1];
    camera.worldWidth = size.width;
    camera.worldHeight = size.height;
}
Enter fullscreen mode Exit fullscreen mode

Boundary Enforcement

// Manual boundary checking
function enforceCameraBounds() {
    const maxX = camera.worldWidth - display.canvas.width;
    const maxY = camera.worldHeight - display.canvas.height;

    camera.x = Math.max(0, Math.min(camera.x, maxX));
    camera.y = Math.max(0, Math.min(camera.y, maxY));
}

// Example: Update in game loop
function update() {
    // Your game logic...
    enforceCameraBounds();
}
Enter fullscreen mode Exit fullscreen mode

🚀 Camera Movement and Control

Manual Camera Positioning

// Direct camera control
camera.x = 500;     // Move camera to X position 500
camera.y = 300;     // Move camera to Y position 300

// Relative movement
camera.x += 10;     // Pan right
camera.y -= 5;      // Pan up

// Example: Camera shake effect
function cameraShake(intensity = 10, duration = 500) {
    const originalX = camera.x;
    const originalY = camera.y;
    let elapsed = 0;

    const shakeInterval = setInterval(() => {
        elapsed += 50;
        camera.x = originalX + (Math.random() - 0.5) * intensity;
        camera.y = originalY + (Math.random() - 0.5) * intensity;

        if (elapsed >= duration) {
            clearInterval(shakeInterval);
            camera.x = originalX;
            camera.y = originalY;
        }
    }, 50);
}

// Trigger camera shake on events
function playerHit() {
    cameraShake(15, 300);
}
Enter fullscreen mode Exit fullscreen mode

Smooth Camera Transitions

// Smooth movement to target position
function smoothCameraTo(x, y, duration = 1000) {
    const startX = camera.x;
    const startY = camera.y;
    const startTime = Date.now();

    function animate() {
        const currentTime = Date.now();
        const elapsed = currentTime - startTime;
        const progress = Math.min(elapsed / duration, 1);

        // Easing function for smooth movement
        const ease = progress < 0.5 ? 2 * progress * progress : -1 + (4 - 2 * progress) * progress;

        camera.x = startX + (x - startX) * ease;
        camera.y = startY + (y - startY) * ease;

        if (progress < 1) {
            requestAnimationFrame(animate);
        }
    }

    animate();
}

// Example: Transition between areas
function moveToBossArena() {
    smoothCameraTo(1500, 800, 2000);
}
Enter fullscreen mode Exit fullscreen mode

🎮 Practical Camera Systems

Platformer Camera Behavior

class PlatformerCamera {
    constructor() {
        this.camera = display.camera;
        this.leadDistance = 100; // How far ahead to look
        this.deadZone = 50;      // Area where camera doesn't move
    }

    update(player) {
        const targetX = player.x - display.canvas.width / 2;
        const targetY = player.y - display.canvas.height / 2;

        // Horizontal leading (look ahead in movement direction)
        if (player.speedX > 0) {
            targetX += this.leadDistance;
        } else if (player.speedX < 0) {
            targetX -= this.leadDistance;
        }

        // Vertical adjustment (only follow when falling or jumping high)
        if (player.y < this.camera.y + this.deadZone) {
            targetY = player.y - this.deadZone;
        } else if (player.y > this.camera.y + display.canvas.height - this.deadZone - player.height) {
            targetY = player.y - display.canvas.height + this.deadZone + player.height;
        }

        // Smooth camera movement
        this.camera.x += (targetX - this.camera.x) * 0.1;
        this.camera.y += (targetY - this.camera.y) * 0.08;

        // Enforce bounds
        this.enforceBounds();
    }

    enforceBounds() {
        this.camera.x = Math.max(0, Math.min(this.camera.x, this.camera.worldWidth - display.canvas.width));
        this.camera.y = Math.max(0, Math.min(this.camera.y, this.camera.worldHeight - display.canvas.height));
    }
}

// Usage
const platformerCam = new PlatformerCamera();

function update() {
    platformerCam.update(player);
}
Enter fullscreen mode Exit fullscreen mode

Top-Down Adventure Camera

class TopDownCamera {
    constructor() {
        this.camera = display.camera;
        this.smoothness = 0.1;
        this.zoom = 1.0;
    }

    update(player) {
        // Center camera on player
        const targetX = player.x - display.canvas.width / 2;
        const targetY = player.y - display.canvas.height / 2;

        // Smooth interpolation
        this.camera.x += (targetX - this.camera.x) * this.smoothness;
        this.camera.y += (targetY - this.camera.y) * this.smoothness;

        this.enforceBounds();
    }

    setZoom(newZoom, duration = 1000) {
        // Smooth zoom transition
        const startZoom = this.zoom;
        const startTime = Date.now();

        function animateZoom() {
            const progress = (Date.now() - startTime) / duration;
            const currentZoom = startZoom + (newZoom - startZoom) * Math.min(progress, 1);

            // Apply zoom by scaling canvas (simplified)
            display.canvas.style.transform = `scale(${currentZoom})`;
            this.zoom = currentZoom;

            if (progress < 1) {
                requestAnimationFrame(animateZoom);
            }
        }

        animateZoom();
    }

    enforceBounds() {
        const maxX = this.camera.worldWidth - display.canvas.width;
        const maxY = this.camera.worldHeight - display.canvas.height;

        this.camera.x = Math.max(0, Math.min(this.camera.x, maxX));
        this.camera.y = Math.max(0, Math.min(this.camera.y, maxY));
    }
}
Enter fullscreen mode Exit fullscreen mode

🔧 Advanced Camera Techniques

Multiple Camera Targets

class MultiTargetCamera {
    constructor() {
        this.camera = display.camera;
        this.targets = [];
        this.mode = 'average'; // 'average', 'focus', 'dynamic'
    }

    addTarget(target) {
        this.targets.push(target);
    }

    removeTarget(target) {
        const index = this.targets.indexOf(target);
        if (index > -1) {
            this.targets.splice(index, 1);
        }
    }

    update() {
        if (this.targets.length === 0) return;

        let targetX, targetY;

        switch(this.mode) {
            case 'average':
                // Average position of all targets
                const avgX = this.targets.reduce((sum, t) => sum + t.x, 0) / this.targets.length;
                const avgY = this.targets.reduce((sum, t) => sum + t.y, 0) / this.targets.length;
                targetX = avgX - display.canvas.width / 2;
                targetY = avgY - display.canvas.height / 2;
                break;

            case 'focus':
                // Focus on primary target
                const primary = this.targets[0];
                targetX = primary.x - display.canvas.width / 2;
                targetY = primary.y - display.canvas.height / 2;
                break;

            case 'dynamic':
                // Dynamic zoom to fit all targets
                const bounds = this.calculateTargetBounds();
                targetX = bounds.centerX - display.canvas.width / 2;
                targetY = bounds.centerY - display.canvas.height / 2;
                this.adjustZoom(bounds);
                break;
        }

        // Smooth movement
        this.camera.x += (targetX - this.camera.x) * 0.05;
        this.camera.y += (targetY - this.camera.y) * 0.05;

        this.enforceBounds();
    }

    calculateTargetBounds() {
        let minX = Infinity, maxX = -Infinity;
        let minY = Infinity, maxY = -Infinity;

        this.targets.forEach(target => {
            minX = Math.min(minX, target.x);
            maxX = Math.max(maxX, target.x);
            minY = Math.min(minY, target.y);
            maxY = Math.max(maxY, target.y);
        });

        return {
            minX, maxX, minY, maxY,
            width: maxX - minX,
            height: maxY - minY,
            centerX: (minX + maxX) / 2,
            centerY: (minY + maxY) / 2
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Camera Effects and Transitions

class CameraEffects {
    constructor() {
        this.camera = display.camera;
        this.effects = new Map();
    }

    // Screen shake effect
    shake(intensity = 10, duration = 500) {
        const effectId = 'shake_' + Date.now();
        const originalX = this.camera.x;
        const originalY = this.camera.y;
        let elapsed = 0;

        const effect = setInterval(() => {
            elapsed += 50;
            this.camera.x = originalX + (Math.random() - 0.5) * intensity;
            this.camera.y = originalY + (Math.random() - 0.5) * intensity;

            if (elapsed >= duration) {
                clearInterval(effect);
                this.camera.x = originalX;
                this.camera.y = originalY;
                this.effects.delete(effectId);
            }
        }, 50);

        this.effects.set(effectId, effect);
    }

    // Zoom effect
    zoomTo(scale, duration = 1000) {
        const effectId = 'zoom_' + Date.now();
        const startScale = 1.0;
        const startTime = Date.now();

        function animateZoom() {
            const progress = (Date.now() - startTime) / duration;
            const currentScale = startScale + (scale - startScale) * Math.min(progress, 1);

            // Apply zoom (you'd need to implement actual zoom functionality)
            console.log("Zoom scale:", currentScale);

            if (progress < 1) {
                requestAnimationFrame(animateZoom);
            } else {
                this.effects.delete(effectId);
            }
        }

        animateZoom();
    }

    // Fade to color effect
    fadeToColor(color, duration = 1000) {
        const overlay = new Component(display.canvas.width, display.canvas.height, color, 0, 0, "rect");
        overlay.alpha = 0;
        display.add(overlay);

        let alpha = 0;
        const fadeInterval = setInterval(() => {
            alpha += 0.02;
            overlay.alpha = Math.min(alpha, 1);

            if (alpha >= 1) {
                clearInterval(fadeInterval);
                setTimeout(() => {
                    display.add(overlay, 1); // Remove from current scene
                }, 500);
            }
        }, duration / 50);
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 Camera Utility Functions

Screen-to-World Coordinate Conversion

// Convert screen coordinates to world coordinates
function screenToWorld(screenX, screenY) {
    return {
        worldX: screenX + display.camera.x,
        worldY: screenY + display.camera.y
    };
}

// Convert world coordinates to screen coordinates
function worldToScreen(worldX, worldY) {
    return {
        screenX: worldX - display.camera.x,
        screenY: worldY - display.camera.y
    };
}

// Example: Click on world objects
function handleClick(screenX, screenY) {
    const worldPos = screenToWorld(screenX, screenY);

    // Check if click hit any objects in world space
    gameObjects.forEach(obj => {
        if (worldPos.worldX >= obj.x && worldPos.worldX <= obj.x + obj.width &&
            worldPos.worldY >= obj.y && worldPos.worldY <= obj.y + obj.height) {
            obj.onClick();
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Viewport Culling for Performance

// Only render objects within camera viewport
function isInViewport(component) {
    return component.x + component.width >= display.camera.x &&
           component.x <= display.camera.x + display.canvas.width &&
           component.y + component.height >= display.camera.y &&
           component.y <= display.camera.y + display.canvas.height;
}

// Optimized rendering system
class OptimizedRenderer {
    constructor() {
        this.visibleObjects = [];
    }

    updateVisibleObjects(allObjects) {
        this.visibleObjects = allObjects.filter(obj => isInViewport(obj));
    }

    render() {
        this.visibleObjects.forEach(obj => {
            obj.update(display.context);
        });
    }
}

// Usage
const renderer = new OptimizedRenderer();

function update() {
    renderer.updateVisibleObjects(allGameObjects);
    // Only visible objects will be rendered
}
Enter fullscreen mode Exit fullscreen mode

🎮 Complete Game Integration

Platformer Game with Advanced Camera

class PlatformerGame {
    constructor() {
        this.display = new Display();
        this.display.start(800, 600);

        this.player = new Component(30, 30, "blue", 100, 100, "rect");
        this.player.physics = true;
        this.player.gravity = 0.5;
        this.display.add(this.player);

        this.camera = this.display.camera;
        this.camera.worldWidth = 2000;
        this.camera.worldHeight = 1200;

        this.platforms = this.createPlatforms();
        this.setupCameraSystem();
    }

    setupCameraSystem() {
        // Custom camera behavior for platformer
        this.camera.follow(this.player, true);

        // Camera constraints for platformer
        this.cameraConstraints = {
            minY: 0,
            maxY: this.camera.worldHeight - this.display.canvas.height
        };
    }

    update() {
        // Player movement
        if (this.display.keys[37]) this.player.speedX = -5;
        if (this.display.keys[39]) this.player.speedX = 5;
        if (this.display.keys[38] && this.player.gravitySpeed === 0) {
            this.player.speedY = -12;
        }

        // Custom camera update
        this.updateCamera();
    }

    updateCamera() {
        // Platformer-specific camera logic
        const targetX = this.player.x - this.display.canvas.width / 2;
        const targetY = this.player.y - this.display.canvas.height / 2;

        // Only follow player vertically when falling or jumping high
        const verticalThreshold = 100;
        if (this.player.y < this.camera.y + verticalThreshold || 
            this.player.y > this.camera.y + this.display.canvas.height - verticalThreshold) {
            this.camera.y += (targetY - this.camera.y) * 0.05;
        }

        // Always follow horizontally
        this.camera.x += (targetX - this.camera.x) * 0.1;

        // Enforce camera bounds
        this.enforceCameraBounds();
    }

    enforceCameraBounds() {
        this.camera.x = Math.max(0, Math.min(this.camera.x, 
            this.camera.worldWidth - this.display.canvas.width));
        this.camera.y = Math.max(this.cameraConstraints.minY, 
            Math.min(this.camera.y, this.cameraConstraints.maxY));
    }
}
Enter fullscreen mode Exit fullscreen mode

⚡ Performance Optimization

Camera Update Optimization

class OptimizedCamera {
    constructor() {
        this.camera = display.camera;
        this.lastUpdate = 0;
        this.updateInterval = 2; // Update every 2 frames
    }

    update() {
        // Skip update if not enough frames have passed
        if (display.frameNo - this.lastUpdate < this.updateInterval) {
            return;
        }

        this.lastUpdate = display.frameNo;

        // Perform camera calculations
        if (this.camera.target) {
            const targetX = this.camera.target.x - display.canvas.width / 2;
            const targetY = this.camera.target.y - display.canvas.height / 2;

            this.camera.x += (targetX - this.camera.x) * 0.1;
            this.camera.y += (targetY - this.camera.y) * 0.1;

            this.enforceBounds();
        }
    }

    enforceBounds() {
        this.camera.x = Math.max(0, Math.min(this.camera.x, 
            this.camera.worldWidth - display.canvas.width));
        this.camera.y = Math.max(0, Math.min(this.camera.y, 
            this.camera.worldHeight - display.canvas.height));
    }
}
Enter fullscreen mode Exit fullscreen mode

📚 Conclusion

TCJSGame's Camera Class provides:

  • Smooth object following with lerping support
  • World boundary management for large game environments
  • Flexible camera control for various game types
  • Performance optimization through viewport culling
  • Advanced effects like shaking and transitions

Key camera patterns for different game genres:

  • Platformers: Horizontal following with vertical constraints
  • Top-down games: Centered following with smooth movement
  • Strategy games: Manual camera control with edge scrolling
  • Multiplayer games: Dynamic zoom to fit all players

The camera system works seamlessly with TCJSGame's rendering pipeline, automatically applying transformations to create immersive scrolling worlds. Combine camera control with TCJSGame's other systems to build professional-quality 2D games with engaging camera behaviors!

Top comments (0)