Loading...
Loading...
Build 3D browser games with Three.js using event-driven modular architecture. Use when creating a new 3D game, adding 3D game features, setting up Three.js scenes, or working on any Three.js game project.
npx skill4agent add opusgamelabs/game-creator threejs-gameReference: See(quick guide) andreference/llms.txt(full API + TSL) for official Three.js LLM documentation. Prefer patterns from those files when they conflict with this skill.reference/llms-full.txt
three@0.183.0+mkdir <game-name> && cd <game-name>
npm init -y
npm install three@^0.183.0
npm install -D vitevite.config.jsimport { defineConfig } from 'vite';
export default defineConfig({
root: '.',
publicDir: 'public',
server: { port: 3000, open: true },
build: { outDir: 'dist' },
});package.json{
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
</script>src/
├── core/
│ ├── Game.js # Main orchestrator - init systems, render loop
│ ├── EventBus.js # Singleton pub/sub for all module communication
│ ├── GameState.js # Centralized state singleton
│ └── Constants.js # ALL config values, balance numbers, asset paths
├── systems/ # Low-level engine systems
│ ├── InputSystem.js # Keyboard/mouse/gamepad input
│ ├── PhysicsSystem.js # Collision detection
│ └── ... # Audio, particles, etc.
├── gameplay/ # Game mechanics
│ └── ... # Player, enemies, weapons, etc.
├── level/ # Level/world building
│ ├── LevelBuilder.js # Constructs the game world
│ └── AssetLoader.js # Loads models, textures, audio
├── ui/ # User interface
│ └── ... # Game over, overlays
└── main.js # Entry point - creates Game instanceGameState.reset()class EventBus {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event).add(callback);
return () => this.off(event, callback);
}
once(event, callback) {
const wrapper = (...args) => {
this.off(event, wrapper);
callback(...args);
};
this.on(event, wrapper);
}
off(event, callback) {
const cbs = this.listeners.get(event);
if (cbs) {
cbs.delete(callback);
if (cbs.size === 0) this.listeners.delete(event);
}
}
emit(event, data) {
const cbs = this.listeners.get(event);
if (cbs) cbs.forEach(cb => {
try { cb(data); } catch (e) { console.error(`EventBus error [${event}]:`, e); }
});
}
clear(event) {
event ? this.listeners.delete(event) : this.listeners.clear();
}
}
export const eventBus = new EventBus();
// Define ALL events as constants — use domain:action naming
export const Events = {
// Group by domain: player:*, enemy:*, game:*, ui:*, etc.
};import { PLAYER_CONFIG } from './Constants.js';
class GameState {
constructor() {
this.player = {
health: PLAYER_CONFIG.HEALTH,
score: 0,
};
this.game = {
started: false,
paused: false,
isPlaying: false,
};
}
reset() {
this.player.health = PLAYER_CONFIG.HEALTH;
this.player.score = 0;
this.game.started = false;
this.game.paused = false;
this.game.isPlaying = false;
}
}
export const gameState = new GameState();Constants.jsexport const PLAYER_CONFIG = {
HEALTH: 100,
SPEED: 5,
JUMP_FORCE: 8,
};
export const ENEMY_CONFIG = {
SPEED: 3,
HEALTH: 50,
SPAWN_RATE: 2000,
};
export const WORLD = {
WIDTH: 100,
HEIGHT: 50,
GRAVITY: 9.8,
FOG_DENSITY: 0.04,
};
export const CAMERA = {
FOV: 75,
NEAR: 0.01,
FAR: 100,
};
export const COLORS = {
AMBIENT: 0x404040,
DIRECTIONAL: 0xffffff,
FOG: 0x000000,
};
export const ASSET_PATHS = {
// model paths, texture paths, etc.
};renderer.setAnimationLoop()import * as THREE from 'three';
import { CAMERA, COLORS, WORLD } from './Constants.js';
class Game {
constructor() {
this.clock = new THREE.Clock();
this.init();
}
init() {
this.setupRenderer();
this.setupScene();
this.setupCamera();
this.setupSystems();
this.setupUI();
this.setupEventListeners();
this.renderer.setAnimationLoop(() => this.animate());
}
setupRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: false,
powerPreference: 'high-performance',
});
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('game-container').appendChild(this.renderer.domElement);
window.addEventListener('resize', () => this.onWindowResize());
}
setupScene() {
this.scene = new THREE.Scene();
this.scene.fog = new THREE.FogExp2(COLORS.FOG, WORLD.FOG_DENSITY);
this.scene.add(new THREE.AmbientLight(COLORS.AMBIENT, 0.5));
const dirLight = new THREE.DirectionalLight(COLORS.DIRECTIONAL, 1);
dirLight.position.set(5, 10, 5);
this.scene.add(dirLight);
}
setupCamera() {
this.camera = new THREE.PerspectiveCamera(
CAMERA.FOV,
window.innerWidth / window.innerHeight,
CAMERA.NEAR,
CAMERA.FAR,
);
}
setupSystems() {
// Initialize game systems
}
setupUI() {
// Initialize UI overlays
}
setupEventListeners() {
// Subscribe to EventBus events
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
animate() {
const delta = Math.min(this.clock.getDelta(), 0.1); // Cap delta to prevent spiral
// Update all systems with delta
this.renderer.render(this.scene, this.camera);
}
}
export default Game;import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });'three/webgpu'import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();import { texture, uv, color } from 'three/tsl';
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = texture(myTexture).mul(color(0xff0000));MeshBasicNodeMaterialMeshStandardNodeMaterialMeshPhysicalNodeMaterialLineBasicNodeMaterialSpriteNodeMaterialreference/llms-full.txtrenderer.setAnimationLoop()requestAnimationFrameMath.min(clock.getDelta(), 0.1)renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))Vector3Box3MeshBasicMaterialMeshStandardMaterialMeshPhysicalMaterialpowerPreference: 'high-performance'.dispose()/public/THREE.TextureLoaderGLTFLoaderthree/addonsimport { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
function loadModel(path) {
return new Promise((resolve, reject) => {
loader.load(
path,
(gltf) => resolve(gltf.scene),
undefined,
(error) => reject(error),
);
});
}| Game Type | Primary Mobile Input | Fallback |
|---|---|---|
| Marble/tilt/balance | Gyroscope (DeviceOrientation) | Virtual joystick |
| Runner/endless | Tap zones (left/right half) | Swipe gestures |
| Puzzle/turn-based | Tap targets (44px min) | Drag & drop |
| Shooter/aim | Virtual joystick + tap-to-fire | Dual joysticks |
| Platformer | Virtual D-pad + jump button | Tilt for movement |
moveXmoveZclass InputSystem {
constructor() {
this.keys = {};
this.moveX = 0; // -1..1
this.moveZ = 0; // -1..1
this.isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints > 1);
document.addEventListener('keydown', (e) => { this.keys[e.code] = true; });
document.addEventListener('keyup', (e) => { this.keys[e.code] = false; });
}
/** Call from a user gesture (e.g. PLAY button) to init gyro/joystick. */
async initMobile() {
// Request gyroscope permission (required on iOS 13+)
// If denied/unavailable, show virtual joystick fallback
}
/** Call once per frame. Merges all sources into moveX/moveZ. */
update() {
let mx = 0, mz = 0;
// Keyboard (always active, acts as override)
if (this.keys['ArrowLeft'] || this.keys['KeyA']) mx -= 1;
if (this.keys['ArrowRight'] || this.keys['KeyD']) mx += 1;
if (this.keys['ArrowUp'] || this.keys['KeyW']) mz -= 1;
if (this.keys['ArrowDown'] || this.keys['KeyS']) mz += 1;
const kbActive = mx !== 0 || mz !== 0;
if (!kbActive) {
// Read from gyro or joystick (whichever is active)
}
this.moveX = Math.max(-1, Math.min(1, mx));
this.moveZ = Math.max(-1, Math.min(1, mz));
}
}class GyroscopeInput {
constructor() {
this.available = false;
this.moveX = 0;
this.moveZ = 0;
this.calibBeta = null;
this.calibGamma = null;
}
async requestPermission() {
// iOS 13+: DeviceOrientationEvent.requestPermission()
// Must be called from a user gesture handler
}
recalibrate() {
// Capture current orientation as neutral position
}
update() {
// Apply deadzone, normalize to -1..1, smooth with EMA
}
}class VirtualJoystick {
constructor() {
this.active = false;
this.moveX = 0; // -1..1
this.moveZ = 0; // -1..1
}
show() {
// Create outer circle + inner knob DOM elements
// Track touch by identifier to handle multi-touch correctly
// Clamp knob movement to maxDistance from center
// Normalize displacement to -1..1
}
hide() { /* Remove DOM, reset values */ }
}input.moveXinput.moveZsrc/EventBus.jsdomain:actionConstants.jsGameState.jsGame.jsGameState.reset()Math.min(clock.getDelta(), 0.1)isMutednpm run build