Game Builder Tutorial 3: Build a First-Person 3D Dungeon
DEV Community

Game Builder Tutorial 3: Build a First-Person 3D Dungeon

Step 1 - A 3D scene

Pick New scene โ†’ 3D Map. Placement defaults to an accurate top-down grid (toggle View โ†’ 3D: Perspective / Top-down for an angled overview). The 3D Kit pack supplies blocks, pillars, crates, a spawn and scenery.

3D assets are meshes

This is the big difference from Tutorials 1 and 2: in 2D an asset's art is an image or a sprite sheet, but in 3D it's a mesh - geometry, not pixels. That's the asset model's third format, TYPE_MESH: source points at a glTF / .glb file, and at runtime GameSceneView.buildModels loads it with GltfLoader and realizes a Model under the perspective camera and lighting.

{
  "id": "statue",
  "name": "Statue",
  "kind": "actor",
  "type": "mesh",
  "source": "statue.glb"
}

The starter 3D Kit stays dependency-free by building its blocks, pillars and crates from primitive meshes (Primitives.cube(...)) shaded with each asset's colour, so you can lay out a level immediately without modeling anything. To use real modeled art, export a .glb from Blender (or drop in a CC0 model), put it in games/assets/, and set the asset's type to mesh with source pointing at the file - buildModels loads any element whose asset is a mesh and falls back to the primitive cube otherwise.

Unlike sprites, meshes are resolution-independent: there's no DPI or frame bookkeeping, the GPU just scales the geometry.

Step 2 - Pick the dungeon play style

With nothing selected, set the Inspector's 3D play style to dungeon. That switches the preview to a first-person walker with wall collision - the genre we're building. (Switch it to open for a free arena or flight for an aerial flyby of the same layout; the level data doesn't change, only how it plays.)

Step 3 - Build the maze walls

A maze wants continuous walls, not a row of separate posts - so build them with the Terrain tool's Wall brush rather than placing individual pillar objects. Select Terrain โ†’ Wall and paint the maze: a solid perimeter plus a few inner walls, leaving a corridor for Duke. Each painted cell becomes a full-width stone block, so adjacent cells join into a seamless wall (and the radar shows them as solid, not dotted). In dungeon play style those walls stop the walker, and the floor is laid as continuous stone - no tile-grid gaps.

(You can still drop standalone Pillar or Rock objects from the 3D Kit as scenery; they're great as freestanding columns, just not as a maze wall.)

Step 4 - Add the player spawn

Place a Spawn where the player starts and tick This is the player in its Behavior section - that marks which element the camera and controls drive. Its Elevation (Z) field raises it for multi-level layouts.

Step 5 - Walk it

Press Live. In dungeon style you walk in first person - Left/Right turn, Up/Down walk - and the maze walls stop you like real walls. A radar in the corner shows where everything is, which is essential once a level grows beyond one room. Live, you walk the corridor in first person and look around - the walls stop you, and the sunlit faces brighten or fall into shadow as you turn.

Coffee versus tea: fighting back

A maze to walk is a start, but Duke came down here to clear out the tea cups. From the 3D Kit, drop Tea Cup actors along the corridor - they're the enemy - and give Duke a ranged attack: Fire (or the space bar) launches a coffee bean straight ahead. In the preview the beans fly down the corridor, and a hit pops the cup and scores it.

In a shipped game that's an ordinary projectile loop in onUpdate - spawn a bean on the fire key, advance the live ones, and test each against the enemies:

private final List<float[]> beans = new ArrayList<>(); // {x, z, dirX, dirZ}

@Override
protected void onUpdate(double dt) {
    GameInput in = getInput();
    if (in.wasGameKeyPressed(Display.GAME_FIRE)) {
        beans.add(new float[]{
            playerX(), playerZ(),
            (float) Math.sin(yaw()),
            (float) -Math.cos(yaw())
        });
    }
    for (Iterator<float[]> it = beans.iterator(); it.hasNext();) {
        float[] b = it.next();
        b[0] += b[2] * 7 * dt;
        b[1] += b[3] * 7 * dt;
        GameElement cup = enemyNear(b[0], b[1], 0.5); // your spatial test
        if (cup != null) {
            scene().remove(spriteFor(cup)); // smash it
            addScore(cup.getInt("value", 25));
            it.remove();
        } else if (hitWall(b[0], b[1])) {
            it.remove(); // bean spent
        }
    }
}

The cups are just elements whose assetId your code treats as an enemy - exactly the "your rules read the data" idea from Tutorial 1, now in three dimensions. Want them to fight back? Move each cup toward Duke a little every frame and cost him a life on contact.

Light and shade

Look up and you'll see the sun. A 3D level carries one or more lights (the lights array in the .game), and the renderer shades every surface by how directly it faces the light: wall faces turned toward the sun are bright, faces turned away fall into shadow - which is exactly what gives the maze its depth as you move. That's the whole point of having a visible light source rather than flat-lit boxes.

Change the mood from code via getLight():

getLight()
    .setDirection(0.3f, -1f, 0.2f) // a lower sun casts longer, moodier shadows
    .setColor(0xfff2c14e)          // warm sunlight
    .setAmbientColor(0xff404858);  // fill light so shadows aren't pure black

On device this is real GPU lighting; the editor preview approximates the same sun angle so you can judge it while authoring.

Sculpting terrain (floors, hills, holes, ramps)

3D levels aren't just objects on a flat plane. Select the Terrain tool and you can paint the ground itself: Raise/Lower elevation, carve holes (open sky a flight level can fall through), stamp walls, and Paint a surface material - grass, road, stone, sand, water. Painted elevation changes render as smooth slopes, not stairs, so a road can ramp uphill. The walker rides the terrain height and is stopped by walls and holes.

What got saved, and how it renders

Save writes src/main/resources/games/CryptWalk.game (loaded at runtime as /CryptWalk.game - the resource namespace is flat). A 3D level stores the play style, the placed elements (with elevation and per-object scale), and any terrain you sculpted:

{
  "mode": "3d",
  "cols": 16,
  "rows": 16,
  "tileSize": 1,
  "props": {
    "view3d": "dungeon"
  },
  "camera": {
    "eye": [0, 8, 14],
    "target": [0, 0, 0],
    "fov": 60
  },
  "lights": [
    {
      "dir": [0.4, -1, 0.3],
      "color": "fff2e0",
      "ambient": "2a2f3a"
    }
  ],
  "elements": [
    {
      "id": "sp",
      "assetId": "spawn",
      "layer": "Models",
      "x": 8,
      "y": 14,
      "props": {
        "player": true
      }
    }
  ],
  "terrain": {
    "cols": 16,
    "rows": 16,
    "heights": [],
    "materials": [],
    "walls": ["...a height per maze-wall cell, 0 elsewhere..."]
  }
}

The companion is the same GameSceneView pattern as the other tutorials. The difference is realization: for a 3D level it builds one Model per element inside onSetup(GraphicsDevice) under a perspective camera with the level's lighting - on the GPU.

Your code still lives in onUpdate:

@Override
protected void onUpdate(double deltaSeconds) {
    GameInput in = getInput();
    // first-person controls (or let the built-in dungeon walker handle them)
    if (in.isGameKeyDown(Display.GAME_LEFT)) {
        turn(-1.4 * deltaSeconds);
    }
    if (in.isGameKeyDown(Display.GAME_RIGHT)) {
        turn(1.4 * deltaSeconds);
    }
    if (in.isGameKeyDown(Display.GAME_UP)) {
        walkForward(3.0 * deltaSeconds);
    }
    // win when the player reaches the exit element, lose on a trap, etc.
}

The in-editor preview is a fast software approximation for iteration; on device the scene is rendered by the GPU-accelerated GameSceneView.

Physics, effects and overriding defaults

Collision is handled for you - dungeon style stops the walker at walls and holes. For richer physics (projectiles, doors, movable crates) step a PhysicsWorld from onUpdate exactly as in Tutorial 1's physics section. Effects hang off the same loop.

A concrete example - a footstep sound and a torch-lit light fade, both reading the level's own data:

private final SoundPool sound = SoundPool.create(4);
private SoundEffect step; // step = sound.load("/footstep.wav");

// in onUpdate, when the player advances a tile:
sound.play(step, 0.6f, 0f, 1f, 0); // volume, pan, rate, no loop
getLight().setColor(torchLit ? 0xfff2e0 : 0x404858); // brighten when a torch is lit

Overriding - the play styles are presets, not constraints. Want a hybrid (walk like a dungeon but with no wall collision)? Set open and add your own collision in onUpdate.

Menus and HUD in 3D

A 3D game has its own interface to manage too - a map toggle, an inventory, a "you died" screen, a pause overlay - and because GameSceneView is a Codename One Component, all of it is the ordinary UI toolkit, not a 3D-specific layer. Drop a Dialog for the death screen, a Toolbar command for the map toggle, a Container of item buttons for the inventory. Tutorial 1's menu section applies unchanged - the game underneath happens to be 3D, but the menus are pure Codename One.

Scaling up: streaming worlds

A single 16ร—16 room fits in memory, but an open-world RPG does not. The gaming runtime supports a streaming, region-based world: terrain is paged in and out as chunks, the map is split into linked regions that load and unload around the player for seamless area transitions, and surfaces use pluggable materials. In the editor, New scene โ†’ Large World creates a region graph you grow with Add region (north/south/east/west); the active region's streaming terrain is what you edit and preview. The same .game / companion pattern loads it.

The finished game and next steps

You have Crypt Walk: a navigable first-person dungeon with sculpted terrain and wall collision. From here:

  • Enemies - place actors and, in onUpdate, move them toward the player and cost a life on contact (the slime logic from Tutorial 1 ports directly).
  • A goal - mark an exit element and end the level when the player reaches it.
  • Items and doors - collectibles open locked corridors; read a key / locked property.
  • Bigger maps - graduate to a Large World of streaming regions for a full crawler.

That's the trilogy - mvn cn1:create-game-scene then mvn cn1:gamebuilder, and you're building games by drawing them.

Comments

No comments yet. Start the discussion.