# Gem Collector your first 3D game. ```{raw} html ▶ Run in browser ``` **Tags:** `tutorial` `3d` `signals` # Gem Collector Your first step into 3D. The good news: almost nothing you learned changes. Nodes, input actions, `on_update`, and signals all work exactly the same. Three things are new, and they are what this tutorial is about: a **camera** you place in space, **meshes with materials** to make objects, and a **light** so you can see them. Drive the cube around with WASD or the arrow keys and roll over the spinning gems. ## Step 1: Place a camera and a light In 2D the view was just the window. In 3D you put a `Camera3D` somewhere in space and aim it with `look_at`. And a 3D surface is black until something lights it, so add a `DirectionalLight3D` (a sun: parallel rays from one direction): ```python self.add_child(Camera3D(position=Vec3(0, 15, 15), look_at=Vec3(0, 0, 0), fov=55)) sun = self.add_child(DirectionalLight3D(position=Vec3(6, 12, 6))) sun.intensity = 1.3 ``` ## Step 2: Build objects from meshes A visible 3D object is a `MeshInstance3D`: a `Mesh` (the shape) plus a `Material` (the look). The ground is a unit cube scaled wide and flat; the player is a cube; the gems are spheres: ```python ground = self.add_child(MeshInstance3D(mesh=Mesh.cube(1.0), material=Material(colour=(0.24, 0.27, 0.32, 1)))) ground.scale = Vec3(2 * FIELD + 2, 1.0, 2 * FIELD + 2) ``` ## Step 3: Move on the ground plane The player reads the same input actions from *Input and Movement*, but now moves on the **X/Z plane** (Y is up in 3D), leaving height alone: ```python def on_update(self, dt): dx = Input.get_strength("move_right") - Input.get_strength("move_left") dz = Input.get_strength("move_down") - Input.get_strength("move_up") self.position += Vec3(dx, 0, dz) * self.SPEED * dt ``` ## Step 4: Spin and collect with signals Each `Gem` spins in place with `rotate_y()`, and when the player gets close it emits its `collected` signal, passing itself along: ```python def on_update(self, dt): self.rotate_y(2.5 * dt) if near(self.position, self._player.position): self.collected.emit(self) ``` The root connects that signal to score and respawn. The gem does not know about the score; the score does not poll the gems. That is the same decoupling you used in *Nodes and Signals*, now in 3D: ```python gem.collected.connect(self._on_collected) def _on_collected(self, gem): self.score += 1 self.hud.text = f"Score: {self.score}" gem.respawn() ``` ## Run it ```bash uv run python examples/tutorials/gem_collector/main.py ``` ## What's next - **Monolith to Composed** -- the architecture pattern behind clean node design. - The **3D feature references** -- shadows, particles, physics bodies, glTF models, and more. ## Source ```{literalinclude} ../../examples/tutorials/gem_collector/main.py :language: python :linenos: ```