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¶
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¶
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:
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:
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:
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():
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:
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:
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 objectsgame_spaceinvaders3d.py– Classic arcade game with 3D meshes3d_lighting.py– Directional, point, and spot lights3d_shadows.py– Cascade shadow maps with debug visualisation3d_ssao.py– Screen-space ambient occlusion3d_particles.py– Sub-emitters, collision, trails3d_ibl.py– Image-based lighting with metallic spheres3d_model_viewer.py– Load glTF models with orbit camera
See Examples Gallery for the full list, or Building a Simple Game with the SimVX Editor to build games visually in the editor.