import scene3d
scene = scene3d.Scene()
scene.set_sky('#0f3460')
ground = scene.set_ground(length=18, width=10)
ground.set_material(scene3d.Material.Gravel.DarkGray)
ground.set_tiling(5)
ball = scene3d.Shapes.Sphere(diameter=1, segments=16)
ball.set_color('#e94560')
ball.set_position(-7, 0.5, 0)
scene.add(ball)
box = scene3d.Shapes.Box(width=1.5, height=1.5, depth=1.5)
box.set_color('#4488ff')
box.set_position(4, 0.75, 0)
scene.add(box)
ctx = scene.get_context('2d')
def on_hit():
box.set_color('#44cc88')
ctx.fill_style = '#ffffff'
ctx.font = '22px sans-serif'
ctx.fill_text('Hit!', 10, 30)
ball.on_collide(box, on_hit) # registered after add(), before run()
@scene.on_frame
def animate(dt):
x, y, z = ball.get_position()
ball.set_position(x + 3 * dt, y, z)
scene.run()Collisions
Games and simulations come alive when objects react to each other. A ball that bounces off a wall, a character that picks up a coin, a projectile that hits a target — all of these depend on detecting when two objects touch.
In scene3d, on_collide lets you register a function that fires the moment two meshes overlap. You already know this pattern from on_click — it’s the same idea, just triggered by proximity instead of a mouse click.
How Collision Detection Works
scene3d uses bounding box collision — each mesh is wrapped in an invisible box that exactly fits around it. When two of these boxes overlap, a collision is detected.
ball.on_collide(box, lambda: box.set_color('#44cc88'))
# | | |
# mesh A | handler — runs on first overlap
# mesh BThe handler fires once the first time the bounding boxes overlap. You register on_collide the same way as on_click: after both meshes are added to the scene, before scene.run().
The scene below has a ball rolling toward a box. When they touch, the box turns green.
The Rules for on_collide
scene.add(ball) # 1. add mesh A
scene.add(box) # 2. add mesh B
ball.on_collide(box, fn) # 3. register — AFTER both adds, BEFORE scene.run()
scene.run() # 4. start| Rule | Why |
|---|---|
| Register after both meshes are added | The collision system needs both objects to exist in the scene |
Register before scene.run() |
Handlers must be in place before the simulation starts |
| Fires once on first overlap | Gives you a clean single trigger — not a stream of events every frame |
The handler can be a lambda or a named function — exactly like on_click.
{ “question_type”: “multiple_choice”, “question”: “When must you call on_collide to register a collision handler?”, “options”: [ { “key”: “a”, “text”: “Before adding either mesh to the scene” }, { “key”: “b”, “text”: “After adding both meshes to the scene, but before scene.run()” }, { “key”: “c”, “text”: “Inside the @scene.on_frame callback” }, { “key”: “d”, “text”: “After scene.run() is called” } ], “answer”: “b”, “submitted_answer”: “” }
Multiple Collision Targets
One mesh can collide with many others — just call on_collide once for each pair. Each target gets its own independent handler.
ball.on_collide(target_a, lambda: target_a.set_color('#ffffff'))
ball.on_collide(target_b, lambda: target_b.set_color('#ffffff'))
ball.on_collide(target_c, lambda: target_c.set_color('#ffffff'))The scene below has a ball rolling through a row of colored boxes. Each box independently turns white when the ball reaches it.
import scene3d
scene = scene3d.Scene()
scene.set_sky('#0f3460')
ground = scene.set_ground(length=22, width=10)
ground.set_material(scene3d.Material.Gravel.DarkGray)
ground.set_tiling(5)
ball = scene3d.Shapes.Sphere(diameter=1, segments=16)
ball.set_color('#f5a623')
ball.set_position(-8, 0.5, 0)
scene.add(ball)
colors = ['#e94560', '#4488ff', '#44cc88', '#cc44ff']
targets = []
for i, color in enumerate(colors):
b = scene3d.Shapes.Box(width=1.2, height=1.2, depth=1.2)
b.set_color(color)
b.set_position(-3 + i * 3, 0.6, 0)
scene.add(b)
targets.append(b)
ctx = scene.get_context('2d')
hit_count = 0
def make_handler(idx):
def handler():
global hit_count
hit_count += 1
targets[idx].set_color('#ffffff')
ctx.clear()
ctx.fill_style = '#ffffff'
ctx.font = '20px sans-serif'
ctx.fill_text(f'Targets hit: {hit_count}', 10, 30)
return handler
for i in range(len(targets)):
ball.on_collide(targets[i], make_handler(i))
@scene.on_frame
def animate(dt):
x, y, z = ball.get_position()
ball.set_position(x + 3.5 * dt, y, z)
ctx.fill_style = '#ffffff'
ctx.font = '20px sans-serif'
ctx.fill_text('Targets hit: 0', 10, 30)
scene.run(){ “question_type”: “true_false”, “question”: “You can register multiple on_collide handlers on the same mesh — one for each object it might hit.”, “answer”: “True”, “submitted_answer”: “” }
Fires Once — A Feature, Not a Limitation
Because on_collide fires only once per overlap, it’s perfect for collectibles: a coin can only be picked up once, a power-up can only be claimed once. You don’t need any extra “already collected” flag — the handler simply won’t fire again.
The trick for hiding a collected item is to scale it to zero:
def collect():
coin.set_scale(0, 0, 0) # invisible — bounding box also collapsesIn the scene below, a player ball traces a looping path and collects gold coins. Each coin disappears the first time the player touches it, and the score updates.
import scene3d
import math
scene = scene3d.Scene()
scene.set_sky('#0f3460')
ground = scene.set_ground(length=20, width=16)
ground.set_material(scene3d.Material.Gravel.DarkGray)
ground.set_tiling(5)
player = scene3d.Shapes.Sphere(diameter=0.8, segments=16)
player.set_color('#e94560')
player.set_position(0, 0.4, 0)
scene.add(player)
coin_positions = [(-6, 0), (-3, 3), (0, -3), (3, 3), (6, 0)]
coins = []
for cx, cz in coin_positions:
c = scene3d.Shapes.Cylinder(diameter=0.7, height=0.2, tessellation=16)
c.set_color('#f5a623')
c.set_position(cx, 0.1, cz)
scene.add(c)
coins.append(c)
ctx = scene.get_context('2d')
score = 0
def draw_hud():
ctx.clear()
ctx.fill_style = '#ffffff'
ctx.font = '22px sans-serif'
ctx.fill_text(f'Score: {score} / {len(coins)}', 10, 30)
def make_collector(idx):
def collect():
global score
score += 1
coins[idx].set_scale(0, 0, 0) # hide the collected coin
draw_hud()
return collect
for i in range(len(coins)):
player.on_collide(coins[i], make_collector(i))
t = 0.0
@scene.on_frame
def animate(dt):
global t
t += dt
x = math.sin(t * 0.7) * 6
z = math.cos(t * 0.5) * 4
player.set_position(x, 0.4, z)
draw_hud()
scene.run(){ “question_type”: “multiple_choice”, “question”: “A ball collides with a box, triggering the handler. The ball stays touching the box. What happens next?”, “options”: [ { “key”: “a”, “text”: “The handler fires again every frame the objects stay touching” }, { “key”: “b”, “text”: “The handler fires once more when the ball moves away” }, { “key”: “c”, “text”: “The handler does not fire again — on_collide fires once per overlap” }, { “key”: “d”, “text”: “The scene stops until the ball moves away” } ], “answer”: “c”, “submitted_answer”: “” }
Try It Yourself
The player ball follows a looping path through a field of coins. Use the sliders to change the speed and the number of coins placed at random positions. When all coins are collected, a message appears on screen.
Can you find a speed where the ball collects all the coins quickly?
import scene3d
import math
import random
SPEED = 0.7 #@param {type:"slider", min:0.3, max:1.5, step:0.1}
NUM_COINS = 8 #@param {type:"slider", min:3, max:12, step:1}
scene = scene3d.Scene()
scene.set_sky('#0f3460')
ground = scene.set_ground(length=22, width=18)
ground.set_material(scene3d.Material.Gravel.DarkGray)
ground.set_tiling(5)
player = scene3d.Shapes.Sphere(diameter=0.8, segments=16)
player.set_color('#e94560')
player.set_position(0, 0.4, 0)
scene.add(player)
coins = []
random.seed(42)
for _ in range(NUM_COINS):
c = scene3d.Shapes.Cylinder(diameter=0.7, height=0.2, tessellation=16)
c.set_color('#f5a623')
c.set_position(random.uniform(-8, 8), 0.1, random.uniform(-6, 6))
scene.add(c)
coins.append(c)
ctx = scene.get_context('2d')
score = 0
def draw_hud():
ctx.clear()
ctx.fill_style = '#ffffff'
ctx.font = '22px sans-serif'
ctx.fill_text(f'Score: {score} / {NUM_COINS}', 10, 30)
if score == NUM_COINS:
ctx.fill_style = '#f5a623'
ctx.font = '26px sans-serif'
ctx.fill_text('All collected!', 10, 62)
def make_collector(idx):
def collect():
global score
score += 1
coins[idx].set_scale(0, 0, 0)
draw_hud()
return collect
for i in range(len(coins)):
player.on_collide(coins[i], make_collector(i))
t = 0.0
@scene.on_frame
def animate(dt):
global t
t += dt
x = math.sin(t * SPEED) * 8
z = math.cos(t * SPEED * 0.7) * 5
player.set_position(x, 0.4, z)
draw_hud()
scene.run()Think about a game mechanic that uses collisions — a ball breaking bricks, a character collecting power-ups, a spaceship dodging asteroids.
Describe how you’d set it up using on_collide. Which mesh would be the “actor” (the one calling on_collide)? What would happen in the handler? Would you use set_scale(0, 0, 0) to remove objects, or something else?