Game Object Patterns
Reference catalog for all game objects in Super Mario 3D Web Edition. Use this skill when creating new game objects or modifying existing ones.
Base Class: GameObject
All game objects extend GameObject from src/engine/GameObject.ts.
export abstract class GameObject {
protected engine: GameEngine;
mesh!: THREE.Object3D;
body!: CANNON.Body;
isActive = true;
constructor(engine: GameEngine) { this.engine = engine; }
abstract create(): void;
abstract update(deltaTime: number): void;
protected syncMeshToBody(): void { /* copies body pos/rot to mesh */ }
destroy(): void { /* removes mesh + body, sets isActive = false */ }
}
Key contract:
- •
create()— Build visuals (Three.js) and physics (cannon-es), add to engine - •
update(deltaTime)— Called every frame; always guard withif (!this.isActive) return; - •
destroy()— Clean removal from scene and physics world
Existing Objects
Mario (Player Character)
- •File:
src/game/objects/Mario.ts - •Config:
InputManager(no config object — takes input directly) - •Visual: Collada 3D model loaded from
/assets/mario/mario.daeviaColladaLoader. Native model ~90 units tall, scaled 0.02 to ~1.8 game units. Includes shadow decal plane beneath. Model wrapped in container group to preserve Z_UP rotation from loader. - •Assets:
/public/assets/mario/—mario.dae(Collada model),mario.fbx(FBX alternative), ~30 texture PNGs (eyes variants: center/closed/dead/half_closed/left/right/up/down; colors: blue/red/white/skin/shoe/hair; overalls_button, mustache, sideburn, logo, metal, wing/wing_tip). Textures with_editsuffix are editor variants. - •Physics:
CANNON.Box(0.3, 0.5, 0.3), mass=1, fixedRotation=true - •States: Idle, Running, Jumping, DoubleJump, TripleJump, GroundPound, WallSlide, Falling, Dead
- •Game state: coins, stars, lives (100 coins = 1 extra life), isGameOver, isDead
- •Public methods:
die(),respawn(),resetGame(),collectCoin(),collectStar() - •Pattern: State machine enum + switch-based animation
- •Movement speeds: walk=14, run=22 (with gravity=-25)
- •Jump forces: single=13, double=15, triple=19
Platform (Static Surface)
- •File:
src/game/objects/Platform.ts - •Config:
PlatformConfig { position: {x,y,z}, size: {x,y,z}, color: number } - •Visual:
THREE.BoxGeometrywithMeshStandardMaterial - •Physics:
CANNON.Box(half-extents of size), mass=0 (static) - •Pattern: Simplest possible game object — good template for new static objects
Coin (Collectible)
- •File:
src/game/objects/Coin.ts - •Config:
position: { x: number, y: number, z: number } - •Visual:
THREE.CylinderGeometry(flat disc) rotated upright + glow sphere overlay - •Physics:
CANNON.Sphere(0.5), mass=0,isTrigger: true,collisionResponse: false - •Behavior: Spins on Y-axis, bobs up/down sinusoidally, random phase offset
- •Pattern: Trigger-based collectible with visual feedback
Goomba (Patrolling Enemy)
- •File:
src/game/objects/Goomba.ts - •Config:
GoombaConfig { x, y, z: number, patrolRadius: number } - •Visual: Multi-part mushroom character (body sphere, head/cap, eyes, pupils, eyebrows, feet)
- •Physics:
CANNON.Sphere(0.4), mass=0 (kinematic), collisionResponse=true - •Behavior: Circular patrol path using sin/cos, walk bob animation
- •Pattern: Kinematic enemy with config-driven patrol behavior
Common Patterns
Pattern: Config-Driven Object
interface MyConfig {
x: number; y: number; z: number;
// ... specific properties
}
export class MyObject extends GameObject {
private config: MyConfig;
constructor(engine: GameEngine, config: MyConfig) {
super(engine);
this.config = config;
this.create();
}
}
Pattern: Multi-Part Visual
create(): void {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: 0xFF0000 });
const part = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), mat);
part.castShadow = true;
group.add(part);
// ... more parts ...
group.position.set(this.config.x, this.config.y, this.config.z);
this.mesh = group;
this.engine.addToScene(this.mesh);
}
Pattern: Trigger Collectible
this.body = new CANNON.Body({
mass: 0,
shape: new CANNON.Sphere(0.5),
position: new CANNON.Vec3(x, y, z),
isTrigger: true,
collisionResponse: false,
});
Pattern: Kinematic Movement (Patrol)
update(deltaTime: number): void {
this.angle += this.speed * deltaTime;
const newX = this.startX + Math.cos(this.angle) * this.radius;
const newZ = this.startZ + Math.sin(this.angle) * this.radius;
this.mesh.position.set(newX, this.config.y, newZ);
this.body.position.set(newX, this.config.y, newZ);
}
Pattern: Sinusoidal Animation
update(deltaTime: number): void {
this.time += deltaTime;
this.mesh.position.y = this.startY + Math.sin(this.time * speed) * amplitude;
}
World Integration
Objects are added in World.buildLevel():
// Array-driven placement
const positions = [{ x: 5, y: 1, z: 5 }, { x: -5, y: 1, z: 5 }];
for (const pos of positions) {
this.addEntity(new MyObject(this.engine, pos));
}
Decorative objects (trees, pipes) are added directly to the scene without being tracked as entities.
World Collision Detection
World.ts manages typed arrays of game objects for collision checking:
private mario: Mario | null = null; private coins: Coin[] = []; private goombas: Goomba[] = [];
Collisions are checked each frame in update() after entity updates:
if (this.mario && !this.mario.isDead && !this.mario.isGameOver) {
this.checkCoinCollisions();
this.checkGoombaCollisions();
}
Patterns Added: 2026-02-11
Pattern: Death Animation
Disable collisionResponse so the body ignores platforms, apply upward velocity for a "pop" effect, spin the mesh, then handle respawn or game-over after a timer:
die(): void {
if (this.isDead) return;
this.isDead = true;
this.state = MarioState.Dead;
this.deathTimer = 0;
this.body.collisionResponse = false;
this.body.velocity.set(0, 12, 0); // Pop up
}
// In update(), when isDead:
this.mesh.rotation.z = this.deathTimer * 3; // Spin
if (this.deathTimer > 2) this.handleDeathComplete();
Pattern: Distance-Based Collision (World-Level)
Check distance between bodies in the World update loop instead of relying on physics events for game logic:
private checkCollisions(): void {
const marioPos = this.mario.body.position;
for (const obj of this.targetObjects) {
if (!obj.isActive) continue;
const dx = marioPos.x - obj.body.position.x;
const dy = marioPos.y - obj.body.position.y;
const dz = marioPos.z - obj.body.position.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < hitRadius) {
// Handle collision
}
}
}
When to use: For game-logic collisions (collecting items, enemy hits) where cannon-es trigger events are unreliable or hard to wire up. Keep radius values small: 1.0-1.2 for contact, 0.5-0.8 for precision.
Pattern: Game State Reset
Separate respawn() (position reset) from resetGame() (full state reset) for clean restart flow:
respawn(): void {
this.body.position.set(0, 5, 0);
this.body.velocity.set(0, 0, 0);
this.isGrounded = false;
}
resetGame(): void {
this.lives = 3;
this.coins = 0;
this.stars = 0;
this.isGameOver = false;
this.isDead = false;
this.respawn();
}
Pattern: Loading External 3D Models (Collada)
Use ColladaLoader to load .dae models. Wrap the loaded scene in a container group to isolate the loader's Z_UP rotation correction from your own animation transforms. Guard animation code with a modelLoaded flag since loading is async.
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
private modelLoaded = false;
private loadModel(): void {
const loader = new ColladaLoader();
loader.load('/assets/mario/mario.dae', (collada) => {
const model = collada.scene;
const s = 0.02; // Scale native units to game units
model.scale.set(s, s, s);
// Enable shadows on all meshes
model.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
(child as THREE.Mesh).castShadow = true;
(child as THREE.Mesh).receiveShadow = true;
}
});
// Wrap to preserve loader's rotation
const container = new THREE.Group();
container.add(model);
this.marioGroup.add(container);
this.modelLoaded = true;
});
}
Key points:
- •The loader may apply a Z_UP → Y_UP rotation on the scene root; wrapping in a container prevents animation code from overwriting it
- •Always enable
castShadow/receiveShadowviatraverse()on loaded models - •Use
modelLoadedflag to skip animation until the model is ready
Pattern: Game-Over UI Overlay
Use a CSS overlay (display: none toggled to display: flex via .visible class) controlled from main.ts:
// In game loop:
if (mario.isGameOver && !gameOverShown) {
gameOverShown = true;
gameOverEl.classList.add('visible');
document.exitPointerLock();
}
// Restart handler:
restartBtn.addEventListener('click', () => {
mario.resetGame();
gameOverEl.classList.remove('visible');
gameOverShown = false;
});