3D World is a playable starter app on Gipity - a multiplayer rocket-launcher demo built on the 3d-engine template. Use it when you want a working reference or a fun playground. All 3D World games share the same visual style, physics, and multiplayer backend.

When to use this: When the user asks for a 3D World game, a playable 3D reference, or a multiplayer shooter. For a fresh build without rocket-launcher / demo-scene content to strip out, add 3d-engine instead and build your own features. For 2D games (platformer, puzzle, arcade) add 2d-game. For non-game web apps (wordle, quiz, card games), use web-simple or web-fullstack.

Quick Start - Start Here

STRONGLY RECOMMENDED: Begin every 3D World game by adding the 3d-world template with add. It sets up Three.js, Rapier physics, Colyseus multiplayer, player controls, and the full engine layer for you. Only hand-roll files if the user explicitly tells you to skip the template.

add name=3d-world title="<Game Name>"

Starting over in an existing project: If src/ already exists and the user wants a clean rebuild, call file_delete on src first, then run add normally. Or pass force=true to add to overwrite in one step - destructive, so confirm with the user first. Unrelated content (media, data, notes) is preserved either way.

Naming: Use the user's name verbatim if given. If they didn't specify, blend "Gip" or "Gipity" into the name (e.g. "Gipity World", "GipCraft") - be creative but don't force it.

This creates a playable game immediately - ground, player character, physics, camera, mobile controls. Then edit config.js and game.js to build your game.

Project Structure

After installing the template, list all files with file_list and read them to understand the project. All files are in src/ and fully editable. Key files:

Read the files before making changes - the comments explain what each one does.

Engine API

All engine modules are available via a single import:

import { world, assets, physics, player, network, ui, THREE, onInit, onUpdate, setConfig, primitives, constraints, workspace, features } from './core.js';

Lifecycle

// settings.js - tunable values
export const settings = {
  colors: { player: 0xf26522, ground: 0x4CAF50, objects: 0x2196F3 },
  world: { groundSize: 30 },
  gameplay: { objectCount: 5, spawnRange: 20, messageDuration: 3000 },
};

// strings.js - user-facing text
export const strings = { welcome: 'My Game' };

// objects.js - entity factories
import { world, assets, physics } from './core.js';
import { settings } from './settings.js';
export function createBlock(x, y, z, color = settings.colors.objects) { ... }

// game.js - orchestrator
import { setConfig, onInit, onUpdate, world, assets, physics, player, ui, THREE } from './core.js';
import { config } from './config.js';
import { settings } from './settings.js';
import { strings } from './strings.js';
import { createBlock } from './objects.js';

setConfig(config);

onInit(async () => {
  player.initPlayer({ color: settings.colors.player });

  const { groundSize } = settings.world;
  const ground = assets.createVoxelGround(groundSize, groundSize, settings.colors.ground);
  world.scene.add(ground);
  physics.addStaticBox({ x: 0, y: -0.5, z: 0 }, { x: groundSize / 2, y: 0.5, z: groundSize / 2 });

  const { objectCount, spawnRange } = settings.gameplay;
  for (let i = 0; i < objectCount; i++) {
    createBlock(Math.random() * spawnRange - spawnRange / 2, 0.5, Math.random() * spawnRange - spawnRange / 2);
  }

  ui.showMessage(strings.welcome, settings.gameplay.messageDuration);
});

onUpdate((dt) => {
  // Game update loop - runs every frame
});

World (world)

Assets (assets)

Physics (physics)

World Primitives (primitives) - v13+

Parts are the universal 3D building block. Dynamic (gravity-on) by default. Each Part is a 3x3x3 sub-voxel grid for detailed shapes.

// Create parts - they fall with gravity by default
const crate = primitives.createPart({ position: {x:0, y:10, z:0}, color: 0x8B4513, material: 'wood' });

// Anchored = no gravity, stays in place
const floor = primitives.createPart({ position: {x:0, y:0, z:0}, size: {x:20, y:1, z:20}, anchored: true, material: 'metal' });

// Sub-voxel shapes: FULL, SLAB, HALF, STAIR, SLOPE, CORNER, PILLAR, ARCH
const stair = primitives.createPart({ position: {x:3, y:1, z:0}, shape: primitives.SHAPES.STAIR, color: 0x4CAF50 });

// Runtime property changes
primitives.setProperty(crate, 'anchored', true);   // freeze in place
primitives.setProperty(crate, 'material', 'ice');   // change material (updates physics + visual)
primitives.setProperty(crate, 'color', 0xff0000);

// Query and remove
const redParts = primitives.queryParts({ color: 0xff0000 });
primitives.removePart(crate);

Part properties: position, rotation (quaternion), size, anchored, canCollide, mass, friction, elasticity, linearDamping, angularDamping, color, material, transparency, shape, castShadow, receiveShadow

Materials: plastic (default), metal, wood, glass, neon, ice, grass, sand - each sets visual + physics defaults

Spawn points:

primitives.createSpawnPoint({ position: {x:0, y:2, z:0}, teamColor: 0xff0000 });

Compound Blocks (primitives.createCompoundBlock)

Destructible blocks - a grid of welded sub-blocks that shatter on impact:

// 3x3x3 destructible block (27 welded 1x1x1 parts)
const block = primitives.createCompoundBlock({
  position: { x: 5, y: 1.5, z: 0 },
  color: 0xff0000,
  breakForce: 8,         // velocity delta threshold (higher = harder to break)
  gridSize: 3,           // blocks per axis (default 3 → 27 blocks)
  blockSize: 1,          // size of each sub-block (default 1)
  material: 'wood',      // material preset
  colorVariation: true,  // slight brightness variation per block (default true)
});

block.break(block.parts[0]);  // free a specific sub-block
block.breakAll();              // shatter everything
block.isIntact();              // any welds remaining?
block.parts;                   // array of all sub-block Parts

Constraints (constraints) - v13+

Connect Parts with physical joints:

// Weld - rigid lock (structures, attached parts)
constraints.weld(partA, partB);

// Hinge - rotation on one axis (doors, wheels, levers)
constraints.hinge(frame, door, { axis: {x:0,y:1,z:0}, limits: [-90, 90] });

// Spring - elastic (suspension, ropes, bouncy platforms)
constraints.spring(partA, partB, { stiffness: 100, damping: 10 });

// Management
constraints.getAll(part);     // all constraints on a part
constraints.remove(c);        // remove one
constraints.removeAll(part);  // remove all

Workspace (workspace) - v13+

World-level settings:

workspace.gravity = {x: 0, y: -30, z: 0};
workspace.snapEnabled = true;        // auto-snap nearby parts (creates Weld)
workspace.snapDistance = 0.15;
workspace.lighting.timeOfDay = 18;   // sunset (0-24)
workspace.lighting.fogEnabled = true;
workspace.lighting.fogNear = 40;
workspace.onSnap((a, b) => console.log('snapped'));

Camera Modes - v13+

player.cameraControl.mode = 'orbit';       // default third-person
player.cameraControl.mode = 'firstPerson'; // FPS
player.cameraControl.mode = 'topDown';     // birds-eye
player.cameraControl.mode = 'fixed';       // scriptable
player.cameraControl.setFixedPosition({x:10, y:8, z:10});
player.cameraControl.setFixedLookAt({x:0, y:0, z:0});

Player (player)

Network (network)

Multiplayer rides on the engine-agnostic @gipity/realtime kit (packages/realtime/). The network module is a thin 3D facade over it:

The room is already declared in the template's gipity.yaml (a realtime deploy phase), so gipity deploy provisions it automatically - no separate step. To add or manage rooms: gipity realtime room create|list|info|delete, or the realtime_room tool. See the app-realtime skill.

UI (ui)

InfoPanel (ui.InfoPanel) - v13+

Reusable 3D World-styled info display. Use for stats, inventories, leaderboards, dialogs, etc.

// Create a panel
const stats = new ui.InfoPanel({ title: 'Player Stats', position: 'top-right' });
stats.addRow('Health', '100', { color: '#0f0' });
stats.addRow('Score', '0', { bold: true });
stats.addRow('Ammo', '30');

// Update values (call in onUpdate or on events)
stats.setRow('Health', '75', { color: '#ff0' });
stats.setRow('Score', '1500');

// Remove a row
stats.removeRow('Ammo');

// Toggle with a key
const inv = new ui.InfoPanel({ title: 'Inventory', position: 'bottom-right', toggleKey: 'KeyI', visible: false });

// Custom position
const custom = new ui.InfoPanel({ position: 'custom', top: 100, right: 20, width: 200 });

// Raw HTML content
stats.setContent('<table>...</table>');

// Cleanup
stats.destroy();

Options: title, position (top-left/top-right/bottom-left/bottom-right/custom), width, visible, compact, toggleKey

Built-in debug panel: F3 toggles version info + FPS + console logs (ON by default)

Debug Panel

Press ` (backtick) to toggle the built-in debug panel. Shows:

Asset Catalog

Models, sounds, and textures are loaded by name from the shared CDN. Use assets.spawn('name') for models, assets.playSound('name') for sounds, assets.getTexture('name') for textures.

Note: The asset pack is being built. For now, use the built-in helpers:

Genre Recipes

Obby / Parkour

Tycoon

Simulator (Collect & Sell)

PvP Combat

Shooter (FPS/TPS)

Tower Defense

Horror

Racing

Multiplayer Patterns

Custom game events

Open a messages channel and send/receive typed events. Each channel namespaces its own wire types, so use as many as you like:

import { network } from './core.js';

const events = network.channel('events', { sync: 'messages' });
events.send('item_collected', { itemId: 'coin-3', points: 10 });
events.on('item_collected', (data) => {
  removeItem(data.itemId);
  updateScore(data.points);
});

Remote players

The multiplayer feature already renders remote-player avatars for you, driven by the network.avatars presence channel. To react to joins/leaves yourself:

network.avatars.onJoin((sid) => console.log('joined', sid));
network.avatars.onLeave((sid) => console.log('left', sid));
for (const [sid, peer] of network.avatars.peers()) {
  // peer.position {x,y,z}, peer.rotation (y radians)
}

Persistence

Use App API functions to save/load player data:

// functions/save-progress.js
export default async function (ctx, { db }) {
  const { data } = ctx.body;
  await db.execute(
    'INSERT INTO saves (user_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2',
    [ctx.auth.userId, data]
  );
  return { ok: true };
}

// functions/load-progress.js
export default async function (ctx, { db }) {
  const row = await db.findOne('saves', { user_id: ctx.auth.userId });
  return { data: row?.data ?? null };
}

Declare in gipity.yaml:

functions:
  save-progress:
    auth_level: user
    tables: [saves]
  load-progress:
    auth_level: user
    tables: [saves]

Mobile Support

Touch controls are automatic:

Features (Opt-in Gameplay Modules)

Enable built-in gameplay features via config.features. Features are template-level modules that auto-initialize during boot.

Enabling a Feature

In config.js:

export const config = {
  title: 'My Game',
  features: {
    'rocket-launcher': true,  // enable with defaults
  },
};

With custom settings:

features: {
  'rocket-launcher': {
    speed: 200,       // projectile speed (default: 120)
    cooldown: 1.0,    // seconds between shots (default: 0.15)
    blastRadius: 5,   // explosion radius (default: 10)
    blastForce: 60,   // knockback strength (default: 40)
    maxDistance: 300,  // max range (default: 150)
    size: 3.0,        // rocket model scale (default: 2.0)
  },
}

Interacting with Features in game.js

import { features } from './core.js';

onInit(() => {
  const rl = features.get('rocket-launcher');
  if (rl) {
    rl.onHit((pos) => { /* rocket hit something at pos */ });
    rl.onExplode((pos) => { /* explosion at pos */ });
    rl.onFire((origin, dir) => { /* rocket fired */ });
  }
});

Available Features

Feature Key Description
Multiplayer multiplayer @gipity/realtime transport + remote-player avatars rendered automatically. Optional host-authoritative world-state sync via sync.worldState: true. Disable with 'multiplayer': false for solo games.
Rocket Launcher rocket-launcher Projectile weapon with physics explosions. Left-click to fire, B for debug traces.

Multiplayer in Depth

Multiplayer is a feature like rocket-launcher - flip it on or off. It runs on the engine-agnostic @gipity/realtime kit in packages/realtime/ (that package's README.md and examples/ show non-game uses too). By default it connects, broadcasts the local player at ~20 Hz on the avatars presence channel, and renders a mesh for every remote peer.

Solo / single-player game:

features: { 'multiplayer': false }

Basic multiplayer (default):

features: { 'multiplayer': { room: 'my-arena' } }

Authoritative world state (host-elected, drift-corrected). Use when every client must see the same blocks/objects in the same place - e.g. shared sandbox or destructible level:

features: { 'multiplayer': { room: 'lobby', sync: { worldState: true } } }

With sync.worldState on, the feature creates a host-authoritative world entities channel. The 3D adapter (js/network/adapter-3d.js) already knows how to serialize and apply Parts - no game-side registration needed. The first client to join becomes host (3-phase claim election); if the host drops, another client takes over automatically (a client holding world data is promoted instantly, else alphabetical tiebreaker).

To sync your own non-Part state, open an entities channel directly and supply an adapter - see packages/realtime/contracts/adapter.contract.md and the worked examples/.

Performance Tips

Deploy Verification

Use the browser tool to verify deploys when it matters - first deploy, structural changes (new pages, new frameworks, changed imports), or when something might have broken. Skip verification for trivial changes (copy tweaks, style adjustments, config values).

To verify: browser action=open url=<deployed-url> - waits for async modules, captures console errors automatically. Check output for [Console errors captured after page load]. Use browser action=screenshot to confirm visual correctness.

Debugging in production: Add console.error() calls to app code for diagnostics, redeploy, then use browser action=console to read the output. Remove debug logging when done.

The version line [3D World] Game Title v1.0 (build 2026-...) appears in console on every boot - use it to confirm the correct build is deployed.

Related Skills