Overworld Lesson
Game in game Lesson
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
- Quick demo
- Architecture & module overview
- Key patterns & code highlights
- Live demo: Overworld → Platformer
- Performance
- 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.gameObjectsused 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,.loadFailedchecks andtry/catchin draw functions. -
Debounced collision checks:
lastCollisionCheckprevents thrashing. -
Modal mini-game lifecycle:
pauseRpg()+PlatformerMini.start()pattern.
Gameplay Mechanics
-
Player movement & bounds —
sprite_data_player.canMoveTo(...)andPlatformerMini.update()keyboard handling. -
Creeper — movement with
updatePosition(),playAnimation(),checkPlayerCollision()→explode(). -
NPC → mini-game —
sprite_data_villager.interact()opens dialogue and callsplatformerMini.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 fromwindow.innerHeightso 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 callif (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
lastCollisionCheckguard reduces redundant work and prevents repeated explosion triggers within a short timeframe. -
Collision margin: Subtracting/adding
40pixels (collisionMargin) tightens the effective hitbox to avoid unfair instant collisions when sprites are visually close but not logically touching. -
State flags:
hasExplodedprevents re-triggering the explosion routine;isCollidingtracks when the overlap is continuous so explosion only starts on the transition from non-overlap → overlap. -
Integration tip: Keep your
playerandcreepercoordinates 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) soremoveEventListenerworks 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-indexto ensure it sits above UI but below dialog overlays if needed. - requestAnimationFrame loop: preferred over
setIntervalfor 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.
- Bound handlers: store the bound function references (
- Reuse tip: Expose
onExitor 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 checkingimg.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
requestAnimationFramefor smoother motion:
Instead of usingsetInterval, userequestAnimationFramewhen 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 fromgetBoundingClientRect()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 (likegetBoundingClientRect()) for hit detection.
Instead, keep all your objects (player, enemies, walls) in the same shared coordinate system inside yourgameEnv.
This keeps collision math consistent and easier to manage. -
Game restarting too harshly:
Try not to usewindow.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.