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.

Open In Jupyter K-12

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 B

The 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.

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()

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 collapses

In 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?