# Your First 3D Game Build an asteroid dodger from scratch in 7 steps. Each section adds to the same project. ## 1. A Cube in Space ```python from simvx.core import Camera3D, Material, MeshInstance3D, Node, ResourceCache, Vec3 from simvx.graphics import App class Game(Node): def ready(self): cam = self.add_child(Camera3D(name="Camera", position=Vec3(0, 12, 15))) cam.look_at(Vec3(0, 0, -5)) cam.fov = 60.0 self.player = self.add_child(MeshInstance3D(name="Player")) self.player.mesh = ResourceCache.get().resolve_mesh("mesh://cube?size=0.5") self.player.material = Material(colour=(0, 1, 0, 1)) App(title="Asteroid Dodger", width=1024, height=768).run(Game()) ``` `Camera3D` defines the viewpoint -- `look_at()` orients it toward a target. `MeshInstance3D` renders geometry; use `ResourceCache` to get built-in meshes (`mesh://cube`, `mesh://sphere`). `Material(colour=)` sets the surface colour as RGBA. ## 2. Move the Player ```python from simvx.core import Camera3D, Input, InputMap, Key, Material, MeshInstance3D, Node, ResourceCache, Vec3 from simvx.graphics import App class Game(Node): def ready(self): cam = self.add_child(Camera3D(name="Camera", position=Vec3(0, 12, 15))) cam.look_at(Vec3(0, 0, -5)) cam.fov = 60.0 self.player = self.add_child(MeshInstance3D(name="Player", position=Vec3(0, 1, 0))) self.player.mesh = ResourceCache.get().resolve_mesh("mesh://cube?size=0.5") self.player.material = Material(colour=(0, 1, 0, 1)) def process(self, dt: float): move = Input.get_vector("left", "right", "forward", "back") self.player.position += Vec3(move.x, 0, move.y) * 15.0 * dt self.player.position.x = max(-20, min(20, self.player.position.x)) self.player.position.z = max(-20, min(20, self.player.position.z)) InputMap.add_action("left", [Key.A, Key.LEFT]) InputMap.add_action("right", [Key.D, Key.RIGHT]) InputMap.add_action("forward", [Key.W, Key.UP]) InputMap.add_action("back", [Key.S, Key.DOWN]) App(title="Asteroid Dodger", width=1024, height=768).run(Game()) ``` `Input.get_vector()` returns a normalised `Vec2` from four action names (left, right, up, down). We map it onto the XZ plane for top-down movement and clamp to a play area. ## 3. Spawn Asteroids Add a timer that spawns falling cubes: ```python import random from simvx.core import Timer, Vec3 class Asteroid(MeshInstance3D): fall_speed = 8.0 def ready(self): self.mesh = ResourceCache.get().resolve_mesh("mesh://cube?size=1.0") self.material = Material(colour=(1, 0.2, 0.2, 1)) def process(self, dt: float): self.position.y -= self.fall_speed * dt self.rotate_x(180.0 * dt) if self.position.y < -15: self.destroy() ``` In the `Game.ready()` method, add a spawn timer: ```python timer = Timer(duration=2.0, one_shot=False, autostart=True) timer.timeout.connect(self._spawn_asteroid) self.add_child(timer) def _spawn_asteroid(self): x = random.uniform(-20, 20) z = random.uniform(-20, 20) self.add_child(Asteroid(name="Asteroid", position=Vec3(x, 20, z))) ``` `Timer` fires its `timeout` signal at regular intervals. `destroy()` removes a node and all its children from the tree. ## 4. Collision Upgrade the player and asteroids to physics bodies with collision shapes: ```python from simvx.core import CharacterBody3D, CollisionShape3D class Player(CharacterBody3D): speed = 15.0 def ready(self): self.collision = self.add_child(CollisionShape3D(name="Collision", radius=0.5)) mesh = self.add_child(MeshInstance3D(name="Mesh")) mesh.mesh = ResourceCache.get().resolve_mesh("mesh://cube?size=0.5") mesh.material = Material(colour=(0, 1, 0, 1)) def process(self, dt: float): move = Input.get_vector("left", "right", "forward", "back") self.velocity = Vec3(move.x, 0, move.y) * self.speed self.move_and_slide(dt) ``` Check collisions in the game's `process()`: ```python for asteroid in self.find_all(Asteroid): if self.player.collision.overlaps(asteroid.collision): self.game_over = True ``` `CharacterBody3D` provides `move_and_slide(dt)` for physics-based movement. `CollisionShape3D(radius=)` creates a sphere collider. `overlaps()` checks intersection between two shapes. ## 5. Score and HUD Use `Text2D` for screen-space text: ```python from simvx.core import Text2D class Game(Node): def ready(self): # ... camera, player, timer ... self.score = 0 self.score_text = self.add_child( Text2D(name="Score", text="Score: 0", x=20, y=20, font_scale=2.0, font_colour=(255, 255, 255, 255)) ) self.game_over_text = self.add_child( Text2D(name="GameOver", text="", x=400, y=350, font_scale=3.0, font_colour=(255, 0, 0, 255)) ) def process(self, dt: float): if self.game_over: self.game_over_text.text = f"GAME OVER! Score: {self.score}" return self.score = int(self.elapsed_time) self.score_text.text = f"Score: {self.score}" ``` `Text2D` renders text as a 2D overlay. Set `x`, `y` for screen position, `font_scale` for size, and `font_colour` for colour. ## 6. Polish Add post-processing with `WorldEnvironment`: ```python from simvx.core import WorldEnvironment class Game(Node): def ready(self): # ... game setup ... env = self.add_child(WorldEnvironment()) env.bloom_enabled = True env.bloom_threshold = 0.8 env.ssao_enabled = True env.fog_enabled = True env.fog_density = 0.02 env.fog_colour = (0.05, 0.05, 0.15) ``` `WorldEnvironment` is the canonical way to configure rendering effects. The renderer reads its properties each frame -- no direct renderer access needed. ## 7. Next Steps More 3D examples to explore: - `game_asteroids3d.py` -- Top-down arcade game with 3D objects - `game_spaceinvaders3d.py` -- Classic arcade game with 3D meshes - `3d_lighting.py` -- Directional, point, and spot lights - `3d_shadows.py` -- Cascade shadow maps with debug visualisation - `3d_ssao.py` -- Screen-space ambient occlusion - `3d_particles.py` -- Sub-emitters, collision, trails - `3d_ibl.py` -- Image-based lighting with metallic spheres - `3d_model_viewer.py` -- Load glTF models with orbit camera See {doc}`examples` for the full list, or {doc}`editor_tutorial` to build games visually in the editor.