Overworld + PlatformerMini

A browser 2D RPG prototype & embedded platformer

Presenter: Hope Fune
Audience: Developers learning to build their own version of a game similar to this


Run time: Demo + Important details + Q&A

Agenda

  1. Quick demo
  2. Architecture & module overview
  3. Key patterns & code highlights
  4. Live demo: Overworld → Platformer
  5. Performance
  6. Q&A / Resources

Quick demo

  • Interact with ‘portal’ NPC
  • Show enemy Creeper
  • Show: Walk to Villager, press E → start Platformer
  • Collect sword with C, defeat zombie (show red phase + particles)

Architecture & Modules

  • GameLevelOverworld: main level — constructs sprite_data_* objects and registers classes.

  • Sprite modules: Background, Player, Npc, Creeper, GameControl — small, focused classes.

  • PlatformerMini: self-contained mini-game with its own overlay canvas (start()/stop()/loop() lifecycle).

  • Communication: gameEnv + gameEnv.gameObjects used as the shared, global registry.

Why this matters: Small, focused modules make prototyping fast and let you reuse systems (dialogue UI, sprite configs, particle effects) across levels.

Core Design Patterns

  • Data-driven sprites: configuration objects (e.g., sprite_data_player) store assets + behavior flags.

  • Separation of concerns: Canvas for rendering, DOM overlays for UI/dialogue, and intervals/rAF for animation loops.

  • Defensive rendering: .complete, .loadFailed checks and try/catch in draw functions.

  • Debounced collision checks: lastCollisionCheck prevents thrashing.

  • Modal mini-game lifecycle: pauseRpg() + PlatformerMini.start() pattern.

Gameplay Mechanics

  • Player movement & boundssprite_data_player.canMoveTo(...) and PlatformerMini.update() keyboard handling.

  • Creeper — movement with updatePosition(), playAnimation(), checkPlayerCollision()explode().

  • NPC → mini-gamesprite_data_villager.interact() opens dialogue and calls platformerMini.start().

  • Platformer interactions — collectible (sword) gating enemy defeat and startZombieDeathAnimation().

Live Code Snippets


// sprite_data_player (JavaScript - abbreviated)
const sprite_data_player = {
  id: 'Player',
  greeting: 'I am Steve.',
  src: `${path}/images/gamify/steve.png`,
  SCALE_FACTOR: 5,
  ANIMATION_RATE: 50,
  pixels: { height: 1500, width: 600 },
  INIT_POSITION: { x: 0, y: window.innerHeight - (window.innerHeight / 5) - 40 },
  hitbox: { widthPercentage: 0.45, heightPercentage: 0.2 },
  keypress: { up: 87, left: 65, down: 83, right: 68 },
  velocity: { x: 5, y: 5 },

  // Bounds check: returns true if newX/newY keep the sprite inside canvas
  canMoveTo(newX, newY, canvasWidth, canvasHeight) {
    const leftBound = 0;
    const rightBound = canvasWidth - (this.pixels.width / this.SCALE_FACTOR);
    const topBound = 0;
    const bottomBound = canvasHeight - (this.pixels.height / this.SCALE_FACTOR);
    if (newX < leftBound || newX > rightBound) return false;
    if (newY < topBound || newY > bottomBound) return false;
    return true;
  }
};

Explanation — sprite_data_player (JavaScript)

  • Purpose: A data-driven configuration object that describes the player’s sprite, sizing, input mapping, and a small method for bounds checking.
  • Key fields:
    • src, pixels, SCALE_FACTOR — used to compute on-screen size from the spritesheet’s pixel dimensions.
    • INIT_POSITION — computed here from window.innerHeight so the sprite spawns near the bottom of the screen.
    • hitbox — percentages used later to compute collision boxes smaller than the full image for fairer collisions.
    • keypress — numeric key codes for movement; keeps input mapping in data rather than scattering it across code.
  • Method canMoveTo(...): centralizes boundary logic so movement code can call if (sprite_data_player.canMoveTo(x,y,w,h)) move(); — this reduces duplication and bugs.
  • Why JS object? Data-driven objects allow designers to tweak values (scale, speed, positions) without changing engine logic.
%%js 

// Simplified checkPlayerCollision (JavaScript - abbreviated)
function checkPlayerCollision(creeper, player) {
  // Skip if already exploded
  if (creeper.hasExploded) return false;

  // Throttle checks to once per 100ms to avoid heavy CPU use
  const now = Date.now();
  if (now - (creeper.lastCollisionCheck || 0) < 100) return false;
  creeper.lastCollisionCheck = now;

  // Compute rects (use whichever coordinate system your engine uses)
  const creeperRect = {
    left: creeper.x + 40,
    right: creeper.x + (creeper.width / creeper.scale) - 40,
    top: creeper.y + 40,
    bottom: creeper.y + (creeper.height / creeper.scale) - 40
  };

  const playerRect = {
    left: player.x,
    right: player.x + player.width,
    top: player.y,
    bottom: player.y + player.height
  };

  // Axis-Aligned Bounding Box overlap test
  const isOverlapping =
    creeperRect.left < playerRect.right &&
    creeperRect.right > playerRect.left &&
    creeperRect.top < playerRect.bottom &&
    creeperRect.bottom > playerRect.top;

  if (isOverlapping && !creeper.isColliding) {
    creeper.isColliding = true;
    creeper.explode(); // Trigger explosion routine
    return true;
  } else if (!isOverlapping && creeper.isColliding) {
    // Player moved away  reset flag so future collisions re-trigger
    creeper.isColliding = false;
  }

  return false;
}

Explanation — checkPlayerCollision (JavaScript)

  • Purpose: Detects collision between Creeper and Player with a simple AABB (axis-aligned bounding box) test and debounces repeated checks.

  • Why throttle? Collision checks can run many times per second; the lastCollisionCheck guard reduces redundant work and prevents repeated explosion triggers within a short timeframe.

  • Collision margin: Subtracting/adding 40 pixels (collisionMargin) tightens the effective hitbox to avoid unfair instant collisions when sprites are visually close but not logically touching.

  • State flags: hasExploded prevents re-triggering the explosion routine; isColliding tracks when the overlap is continuous so explosion only starts on the transition from non-overlap → overlap.

  • Integration tip: Keep your player and creeper coordinates in the same space (world vs screen). If you must mix DOM and canvas, centralize conversion functions (e.g., toCanvasCoords(element)).


// PlatformerMini lifecycle (JavaScript - abbreviated)
class PlatformerMini {
  constructor(gameEnv) {
    this.gameEnv = gameEnv;
    this.isRunning = false;
    this.canvas = null;
    this.ctx = null;
    this.animationFrameId = null;
  }

  start() {
    if (this.isRunning) return;
    this.isRunning = true;

    // Create overlay canvas
    this.canvas = document.createElement('canvas');
    this.canvas.id = 'platformerMiniCanvas';
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    Object.assign(this.canvas.style, {
      position: 'fixed',
      top: '0',
      left: '0',
      zIndex: '10000',
      backgroundColor: 'rgba(135, 206, 235, 1)'
    });
    document.body.appendChild(this.canvas);
    this.ctx = this.canvas.getContext('2d');

    // Setup input handlers (store bound refs so we can remove them later)
    this._keyDown = this.keyDownHandler.bind(this);
    this._keyUp = this.keyUpHandler.bind(this);
    window.addEventListener('keydown', this._keyDown);
    window.addEventListener('keyup', this._keyUp);

    // Start the main loop
    this.loop();
  }

  stop() {
    if (!this.isRunning) return;
    this.isRunning = false;

    // Remove listeners and canvas
    window.removeEventListener('keydown', this._keyDown);
    window.removeEventListener('keyup', this._keyUp);

    if (this.canvas && this.canvas.parentNode) {
      this.canvas.parentNode.removeChild(this.canvas);
      this.canvas = null;
      this.ctx = null;
    }

    if (this.animationFrameId) {
      cancelAnimationFrame(this.animationFrameId);
      this.animationFrameId = null;
    }
  }

  loop() {
    if (!this.isRunning) return;
    this.update(); // game logic
    this.draw();   // rendering
    this.animationFrameId = requestAnimationFrame(() => this.loop());
  }

  // Placeholders for handlers
  keyDownHandler(e) { /* handle inputs */ }
  keyUpHandler(e) { /* handle inputs */ }
  update() { /* update positions, physics */ }
  draw() { /* draw background, player, enemies */ }
}

Explanation — PlatformerMini lifecycle (JavaScript)

  • Purpose: Shows the standard modal mini-game pattern: create an overlay canvas, attach input handlers, run an rAF loop, and clean up on exit.

  • Key ideas:
    • Bound handlers: store the bound function references (this._keyDown) so removeEventListener works correctly — a common source of leaks/bugs is removing a different function than the one added.
    • Overlay canvas: using a fixed-position canvas isolates the mini-game visually and input-wise from the main overworld. Use z-index to ensure it sits above UI but below dialog overlays if needed.
    • requestAnimationFrame loop: preferred over setInterval for smooth frame pacing and to allow browsers to throttle when tabs are inactive.
    • Safe teardown: always cancel rAF and remove DOM nodes/listeners to avoid leaked references and stray updates after exit.
  • Reuse tip: Expose onExit or a promise to notify the caller when the mini-game finishes so the main game can resume cleanly.

Practical Tips

  • Keep your world consistent:
    Make sure everything in your game (players, enemies, and backgrounds), uses the same “map” or coordinate system.
    If you use both the canvas and regular web page elements (DOM), convert their positions carefully so things line up correctly.

  • Check if images are ready before using them:
    Before drawing pictures or sprites, make sure they’ve finished loading by checking img.complete.
    This prevents errors or missing graphics while your game runs.

  • Load everything first:
    Try to preload your images, sounds, and data before starting the level.
    This helps your game feel smoother and prevents lag when something new appears.

  • Start simple with visuals:
    When testing cool effects (like explosions or sparkles), you can first use regular HTML boxes (<div>s).
    Later, move those effects into the canvas or use object pooling if you need better performance.

  • Don’t Commit unless you know it is working: You can do this by using make, as you were taught. Add things one by one! For example, if you add an NPC and it works, thats when you commit it. If it doesn’t work, keep running it through make until it does!

Performance & Optimization Tips

  • Use requestAnimationFrame for smoother motion:
    Instead of using setInterval, use requestAnimationFrame when making things move or animate.
    It keeps your game running in sync with the screen’s refresh rate, which makes animations look smoother.

  • Make particles faster with canvas:
    If you have lots of small effects (like sparks or explosions), using too many HTML elements (<div>s) can slow things down.
    Drawing them on the canvas instead — and reusing (pooling) them — helps your game run faster and use less memory.

  • Keep your math organized:
    Use one system for positions and movement (like “world coordinates”) and stick to it.
    Don’t mix up screen positions from getBoundingClientRect() with your game’s own coordinates — it’ll make collisions and movement more accurate and easier to manage.

  • Use browser tools to check performance:
    Open your browser’s developer tools and look at things like frame rate (FPS), paint time, and memory use.
    This helps you spot what’s slowing your game down and fix it early.

Pitfalls & Quick Fixes

  • Event listeners not removing correctly:
    When you add event listeners (like key presses), make sure you save the exact function you used to add them.
    You have to use the same reference when removing them — otherwise, they stay active and can cause bugs or weird behavior.

  • Unreliable collision checks:
    Don’t rely on the browser’s element positions (like getBoundingClientRect()) for hit detection.
    Instead, keep all your objects (player, enemies, walls) in the same shared coordinate system inside your gameEnv.
    This keeps collision math consistent and easier to manage.

  • Game restarting too harshly:
    Try not to use window.location.reload() to restart the game.
    Instead, make a soft reset — clear the current level, reset positions and variables, and start again smoothly without reloading the whole page.

  • Images or sounds not loading:
    Sometimes assets fail to load, especially if the file path is wrong or loads too slowly.
    Always check if an image is ready before drawing it, and if it’s missing, show a simple backup or “loading” message instead of letting the game crash.

UX & Accessibility Tips

  • Show important info on-screen:
    Add a simple HUD (Heads-Up Display) that tells players what to do, what items they’ve collected, and how to play.
    This keeps players from getting confused or lost during the game.

  • Let players customize controls:
    Allow players to change which keys they use to move or interact.
    On phones or tablets, include easy-to-tap on-screen buttons so everyone can play comfortably.

  • Add sound and graphics options:
    Include buttons to turn music or sound effects on and off.
    Also, let players lower the number of visual effects (like particles or flashes) if their device is slower.

  • Make the game easy to understand and use:
    Show short hints like “Press E to interact” or “Use arrow keys to move.”
    Make buttons big enough so they’re easy to click or tap — this helps all players, especially on mobile or touchscreens.