Josh Bruce Online

Dino Runner Game Documentation

Overview

A classic endless runner game inspired by Chrome’s offline dinosaur game, featuring sprite-based animations, procedural obstacle generation, and responsive controls. Built with HTML5 Canvas and vanilla JavaScript for optimal performance.

Technical Architecture

Core Technologies

File Structure

dino/
├── index.html              # Main game application
├── index.css               # Game styling and responsive layout
├── index.js                # Core game logic and mechanics
├── real.html               # Alternative game version
└── assets/                 # Sprite assets and media
    ├── default_100_percent/
    │   ├── 100-offline-sprite.png
    │   ├── 100-error-offline.png
    │   └── 100-disabled.png
    ├── default_200_percent/
    │   ├── 200-offline-sprite.png
    │   ├── 200-error-offline.png
    │   └── 200-disabled.png
    ├── offline-sprite-1x.png
    ├── offline-sprite-2x.png
    └── [various demo GIFs]

Game Architecture

Core Game Classes

// Main game controller
class Runner {
    constructor(containerElement, opt_config) {
        this.containerEl = containerElement;
        this.config = opt_config || {};
        this.dimensions = DIMENSIONS;
        this.canvas = null;
        this.canvasCtx = null;
        
        this.distanceRan = 0;
        this.highestScore = 0;
        this.time = 0;
        this.runningTime = 0;
        this.msPerFrame = 1000 / FPS;
        this.currentSpeed = this.config.SPEED;
        
        this.obstacles = [];
        this.activated = false;
        this.playing = false;
        this.crashed = false;
        
        this.init();
    }
}

// Dinosaur player class
class Trex {
    constructor(canvas, spritePos) {
        this.canvas = canvas;
        this.canvasCtx = canvas.getContext('2d');
        this.spritePos = spritePos;
        this.xPos = 0;
        this.yPos = 0;
        this.groundYPos = 0;
        this.currentFrame = 0;
        this.currentAnimFrames = [];
        this.blinkDelay = 0;
        this.animStartTime = 0;
        this.timer = 0;
        this.msPerFrame = 1000 / FPS;
        this.config = Trex.config;
        this.status = Trex.status.WAITING;
        
        this.jumping = false;
        this.ducking = false;
        this.jumpVelocity = 0;
        this.reachedMinHeight = false;
        this.speedDrop = false;
        this.jumpCount = 0;
        this.jumpspotX = 0;
        
        this.init();
    }
}

// Obstacle management
class Obstacle {
    constructor(canvasCtx, type, spriteImgPos, dimensions, gapCoefficient, speed, opt_xOffset) {
        this.canvasCtx = canvasCtx;
        this.spritePos = spriteImgPos;
        this.typeConfig = type;
        this.gapCoefficient = gapCoefficient;
        this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
        this.dimensions = dimensions;
        this.remove = false;
        this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
        this.yPos = 0;
        this.width = 0;
        this.collisionBoxes = [];
        this.gap = 0;
        this.speedOffset = 0;
        
        this.init(speed);
    }
}

Game Configuration

// Core game constants
const DEFAULT_WIDTH = 600;
const FPS = 60;
const MAX_CLOUDS = 6;
const RUNNER_MAX_OBSTACLES = 3;
const RUNNER_CLEAR_TIME = 3000;
const RUNNER_BOTTOM_PAD = 10;
const RUNNER_TOP_PAD = 10;

// Game dimensions
const DIMENSIONS = {
    WIDTH: DEFAULT_WIDTH,
    HEIGHT: 150
};

// Speed configuration
const SPEED = {
    DROP_VELOCITY: -5,
    GRAVITY: 0.6,
    INITIAL_JUMP_VELOCITY: -10,
    JUMP_ACCEL: -0.3,
    MAX_JUMP_HEIGHT: 30,
    MIN_JUMP_HEIGHT: 35,
    SPEED: 6,
    SPEED_DROP_COEFFICIENT: 3
};

Sprite Animation System

Sprite Sheet Management

// Sprite positions and configurations
const TREX_CONFIG = {
    DROP_VELOCITY: -5,
    GRAVITY: 0.6,
    HEIGHT: 47,
    HEIGHT_DUCK: 25,
    INIITAL_JUMP_VELOCITY: -10,
    INTRO_DURATION: 1500,
    MAX_JUMP_HEIGHT: 30,
    MIN_JUMP_HEIGHT: 35,
    SPEED_DROP_COEFFICIENT: 3,
    SPRITE_WIDTH: 262,
    START_X_POS: 50,
    WIDTH: 44,
    WIDTH_DUCK: 59
};

// Animation frame sequences
const ANIMATION_FRAMES = {
    WAITING: [44, 0],  // Standing position
    RUNNING: [
        [76, 6],       // Running frame 1
        [118, 6]       // Running frame 2
    ],
    CRASHED: [220, 2], // Death animation
    JUMPING: [0, 2],   // Jump frame
    DUCKING: [
        [264, 6],      // Duck frame 1
        [323, 6]       // Duck frame 2
    ]
};

Animation Control

// Frame animation system
updateFrame: function(deltaTime) {
    this.timer += deltaTime;
    
    if (this.timer >= this.msPerFrame) {
        this.currentFrame = this.currentFrame == this.currentAnimFrames.length - 1 ? 
            0 : this.currentFrame + 1;
        this.timer = 0;
    }
},

// Dynamic animation switching
setAnimationFrame: function(frames) {
    this.currentAnimFrames = frames;
    this.currentFrame = 0;
},

// Sprite rendering
draw: function(x, y) {
    var sourceX = this.currentAnimFrames[this.currentFrame][0];
    var sourceY = this.currentAnimFrames[this.currentFrame][1];
    var sourceWidth = this.typeConfig.width;
    var sourceHeight = this.typeConfig.height;
    
    this.canvasCtx.drawImage(
        Runner.imageSprite,
        sourceX, sourceY,
        sourceWidth, sourceHeight,
        x, y,
        sourceWidth, sourceHeight
    );
}

Physics System

Jump Mechanics

// Dinosaur jumping physics
updateJump: function(deltaTime, speed) {
    var msPerFrame = Trex.config.INIITAL_JUMP_VELOCITY / 2;
    
    // Adjust jump velocity for game speed
    if (this.speedDrop) {
        this.jumpVelocity += this.config.GRAVITY * deltaTime * speed / 2;
    } else {
        this.jumpVelocity += this.config.GRAVITY * deltaTime;
    }
    
    // Update vertical position
    this.yPos += this.jumpVelocity;
    
    // Ground collision detection
    if (this.yPos < this.minJumpHeight || this.yPos < this.config.MIN_JUMP_HEIGHT) {
        this.reachedMinHeight = true;
    }
    
    // Landing detection
    if (this.yPos > this.groundYPos) {
        this.yPos = this.groundYPos;
        this.jumpDown = false;
        this.jumping = false;
        this.jumpCount = 0;
        this.setAnimationFrame(this.config.RUNNING_ANIM_FRAMES);
    }
}

Collision Detection

// Precise collision detection system
checkForCollision: function(obstacle, tRex) {
    var obstacleBoxes = obstacle.collisionBoxes;
    var tRexBox = tRex.getBoundingBox();
    
    for (var t = 0; t < obstacleBoxes.length; t++) {
        for (var i = 0; i < tRexBox.length; i++) {
            // Box intersection test
            if (boxCompare(tRexBox[i], obstacleBoxes[t])) {
                return [tRexBox[i], obstacleBoxes[t]];
            }
        }
    }
    return false;
},

// Box intersection algorithm
function boxCompare(tRexBox, obstacleBox) {
    var crashed = false;
    var tRexBoxX = tRexBox.x;
    var tRexBoxY = tRexBox.y;
    var tRexBoxW = tRexBox.width;
    var tRexBoxH = tRexBox.height;
    
    var obstacleBoxX = obstacleBox.x;
    var obstacleBoxY = obstacleBox.y;
    var obstacleBoxW = obstacleBox.width;
    var obstacleBoxH = obstacleBox.height;
    
    // AABB collision detection
    if (tRexBoxX < obstacleBoxX + obstacleBoxW &&
        tRexBoxX + tRexBoxW > obstacleBoxX &&
        tRexBoxY < obstacleBoxY + obstacleBoxH &&
        tRexBoxY + tRexBoxH > obstacleBoxY) {
        crashed = true;
    }
    
    return crashed;
}

Obstacle System

Procedural Generation

// Dynamic obstacle placement
addNewObstacle: function() {
    var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
    var obstacleType = Obstacle.types[obstacleTypeIndex];
    
    // Calculate spacing based on game speed
    var obstacleGap = this.getObstacleGap(this.currentSpeed, obstacleType.gapCoefficient);
    
    var obstacle = new Obstacle(
        this.canvasCtx,
        obstacleType,
        obstacleType.spriteImgPos,
        this.dimensions,
        obstacleGap,
        this.currentSpeed
    );
    
    this.obstacles.push(obstacle);
    
    // Schedule next obstacle
    this.scheduleNextObstacle();
},

// Intelligent gap calculation
getObstacleGap: function(speed, gapCoefficient) {
    var minGap = Math.round(this.canvas.width / speed + RUNNER_MAX_OBSTACLES * Obstacle.config.MIN_GAP_COEFFICIENT);
    var maxGap = minGap * 2;
    var gap = getRandomNum(minGap, maxGap) * gapCoefficient;
    return gap;
}

Obstacle Types

// Obstacle configurations
Obstacle.types = [
    {
        type: 'CACTUS_SMALL',
        width: 17,
        height: 35,
        yPos: 105,
        multipleSpeed: 4,
        minGap: 120,
        minSpeed: 0,
        collisionBoxes: [
            new CollisionBox(0, 7, 5, 27),
            new CollisionBox(4, 0, 6, 34),
            new CollisionBox(10, 4, 7, 14)
        ]
    },
    {
        type: 'CACTUS_LARGE',
        width: 25,
        height: 50,
        yPos: 90,
        multipleSpeed: 7,
        minGap: 120,
        minSpeed: 0,
        collisionBoxes: [
            new CollisionBox(0, 12, 7, 38),
            new CollisionBox(8, 0, 7, 49),
            new CollisionBox(13, 10, 10, 38)
        ]
    },
    {
        type: 'PTERODACTYL',
        width: 46,
        height: 40,
        yPos: [100, 75, 50], // Multiple flight heights
        multipleSpeed: 999,
        minSpeed: 8.5,
        minGap: 150,
        collisionBoxes: [
            new CollisionBox(15, 15, 16, 5),
            new CollisionBox(18, 21, 24, 6),
            new CollisionBox(2, 14, 4, 3),
            new CollisionBox(6, 10, 4, 7),
            new CollisionBox(10, 8, 6, 9)
        ],
        animationFrames: [
            {x: 134, y: 0},
            {x: 180, y: 0}
        ]
    }
];

Control Systems

Input Handling

// Multi-input support (keyboard and touch)
onKeyDown: function(e) {
    if (!this.crashed && !this.paused) {
        switch (e.keyCode) {
            case KEYCODES.SPACE:
            case KEYCODES.UP:
                this.tRex.startJump(this.currentSpeed);
                this.setPlayStatus(true);
                break;
            case KEYCODES.DOWN:
                if (this.tRex.jumping) {
                    this.tRex.setSpeedDrop();
                } else if (!this.tRex.jumping && !this.tRex.ducking) {
                    this.tRex.setDuck(true);
                }
                break;
        }
    } else if (this.crashed && e.type == EVENTS.KEYDOWN) {
        this.restart();
    }
},

// Touch controls for mobile
onTouchStart: function(e) {
    if (!this.touchController.classList.contains(TOUCH_CONTROLLER)) {
        e.preventDefault();
        if (this.playing) {
            this.tRex.startJump(this.currentSpeed);
        } else {
            this.play();
        }
    }
}

Mobile Optimization

// Touch event optimization
enableTouchControls: function() {
    document.addEventListener(EVENTS.TOUCHSTART, this);
    document.addEventListener(EVENTS.TOUCHEND, this);
},

// Gesture detection
handleTouchGestures: function(e) {
    var touch = e.touches[0];
    var startY = this.touchStartY;
    var currentY = touch.clientY;
    var deltaY = currentY - startY;
    
    // Swipe down for duck
    if (deltaY > SWIPE_THRESHOLD) {
        if (this.tRex.jumping) {
            this.tRex.setSpeedDrop();
        } else {
            this.tRex.setDuck(true);
        }
    }
}

Scoring System

Distance Tracking

// Real-time score calculation
updateScore: function() {
    var score = (this.distanceRan * 0.01) | 0;
    
    if (score > this.highestScore) {
        this.highestScore = score;
        this.distanceMeter.setHighScore(this.highestScore);
    }
    
    this.distanceMeter.update(this.deltaTime, score);
},

// Achievement milestones
checkForMilestone: function(score) {
    if (score > 0 && score % 100 === 0) {
        this.playSound(this.soundFx.SCORE);
        this.spawnNightMode();
    }
}

High Score Persistence

// Local storage integration
saveHighScore: function(score) {
    try {
        localStorage.setItem('dino-high-score', score.toString());
    } catch (e) {
        // Handle storage errors
        console.warn('Unable to save high score');
    }
},

loadHighScore: function() {
    try {
        var saved = localStorage.getItem('dino-high-score');
        return saved ? parseInt(saved, 10) : 0;
    } catch (e) {
        return 0;
    }
}

Visual Effects

Day/Night Cycle

// Dynamic environment changes
spawnNightMode: function() {
    if (!this.nightMode.isNight) {
        this.nightMode.isNight = true;
        this.invertColors();
        this.scheduleNightMode();
    }
},

// Color inversion effect
invertColors: function() {
    var ctx = this.canvasCtx;
    var imageData = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    var data = imageData.data;
    
    for (var i = 0; i < data.length; i += 4) {
        data[i] = 255 - data[i];     // Red
        data[i + 1] = 255 - data[i + 1]; // Green
        data[i + 2] = 255 - data[i + 2]; // Blue
    }
    
    ctx.putImageData(imageData, 0, 0);
}

Cloud Animation

// Background cloud system
updateClouds: function(speed, deltaTime) {
    var cloudSpeed = this.config.BG_CLOUD_SPEED / 1000 * deltaTime * speed;
    var numClouds = this.clouds.length;
    
    if (numClouds) {
        for (var i = numClouds - 1; i >= 0; i--) {
            this.clouds[i].update(cloudSpeed);
        }
        
        var lastCloud = this.clouds[numClouds - 1];
        
        // Add new clouds as needed
        if (numClouds < this.config.MAX_CLOUDS &&
            (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
            this.cloudFrequency > getRandomNum(1, this.config.CLOUD_FREQUENCY)) {
            this.addCloud();
        }
        
        // Remove off-screen clouds
        this.clouds = this.clouds.filter(function(obj) {
            return !obj.remove;
        });
    }
}

Performance Optimizations

Efficient Game Loop

// Optimized update cycle
scheduleNextUpdate: function() {
    if (!this.updatePending) {
        this.updatePending = true;
        this.raqId = requestAnimationFrame(this.update.bind(this));
    }
},

update: function() {
    this.updatePending = false;
    
    var now = getTimeStamp();
    var deltaTime = now - (this.time || now);
    this.time = now;
    
    if (this.playing) {
        this.clearCanvas();
        
        if (this.tRex.jumping) {
            this.tRex.updateJump(deltaTime);
        }
        
        this.runningTime += deltaTime;
        var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
        
        if (this.tRex.jumpCount == 1 && !this.playingIntro) {
            this.playIntro();
        }
        
        if (this.playingIntro) {
            this.horizon.update(0, this.currentSpeed, hasObstacles);
        } else {
            deltaTime = !this.started ? 0 : deltaTime;
            this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, this.inverted);
        }
        
        var collision = hasObstacles && checkForCollision(this.horizon.obstacles[0], this.tRex);
        
        if (!collision) {
            this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
            
            if (this.currentSpeed < this.config.MAX_SPEED) {
                this.currentSpeed += this.config.ACCELERATION;
            }
        } else {
            this.gameOver();
        }
        
        var playAchievementSound = this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan * 0.01));
        
        if (playAchievementSound) {
            this.playSound(this.soundFx.SCORE);
        }
        
        if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
            this.invertTimer = 0;
            this.invertTrigger = false;
            this.invert(false);
        } else if (this.invertTimer) {
            this.invertTimer += deltaTime;
        }
    }
    
    if (this.playing || (!this.activated && this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
        this.tRex.update(deltaTime);
        this.scheduleNextUpdate();
    }
}

Memory Management

// Efficient object pooling
clearObstacles: function() {
    this.obstacles = [];
},

// Canvas optimization
clearCanvas: function() {
    this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH, this.dimensions.HEIGHT);
},

// Resource cleanup
destroy: function() {
    this.stop();
    cancelAnimationFrame(this.raqId);
    this.canvas = null;
    this.canvasCtx = null;
}

Responsive Design

Canvas Scaling

/* Responsive canvas styling */
.runner-canvas {
    height: 150px;
    position: absolute;
    top: 0;
    z-index: 2;
    image-rendering: -moz-crisp-edges;
    image-rendering: -webkit-crisp-edges;
    image-rendering: pixelated;
    image-rendering: crisp-edges;
}

/* Mobile optimizations */
@media (max-width: 600px) {
    .runner-container {
        height: 120px;
        transform: scale(0.8);
        transform-origin: top left;
    }
}

/* High DPI display support */
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) {
    .runner-canvas {
        image-rendering: -webkit-optimize-contrast;
        image-rendering: pixelated;
    }
}

Viewport Adaptation

// Dynamic canvas sizing
adjustDimensions: function() {
    var devicePixelRatio = window.devicePixelRatio || 1;
    var actualWidth = this.dimensions.WIDTH * devicePixelRatio;
    var actualHeight = this.dimensions.HEIGHT * devicePixelRatio;
    
    this.canvas.width = actualWidth;
    this.canvas.height = actualHeight;
    this.canvas.style.width = this.dimensions.WIDTH + 'px';
    this.canvas.style.height = this.dimensions.HEIGHT + 'px';
    
    this.canvasCtx.scale(devicePixelRatio, devicePixelRatio);
}

Code Quality Assessment

Strengths

Areas for Enhancement

Conclusion

The Dino Runner game represents an excellent implementation of the classic endless runner genre, featuring sophisticated sprite animation, precise physics, and optimal performance. The combination of nostalgic pixel art aesthetics with modern web technologies creates an engaging and accessible gaming experience.

Technical Rating: 8.6/10