Dynamic Instantiation
Before this section, complete:
- Node Protocol — Factory function and lifecycle
- ViewModels — Data binding for component artboards
- Tables — Managing collections of instances
- Iteration — Looping through instance arrays
Spawn and manage Rive components at runtime to build particle systems, object pools, and procedural content.
Rive Context
Rive lets you instance Artboards from script inputs. This is how you build reusable components (particles, enemies, cards, etc.) with real Rive visuals that can be spawned and controlled from code.
Core steps:
- Add an
Input<Artboard<Data.YourVM>>to reference a component Artboard - Call
self.yourInput:instance()to create a new instance - Store instances and call
advance()on them in your advance callback, anddraw()in your draw callback
Exercise 1: Spawn Multiple Instances ⭐⭐⭐
Premise
Dynamic instantiation creates runtime copies of component artboards. You must call advance() on each instance in your advance callback, and draw() in your draw callback.
By the end of this exercise, you will be able to Complete the spawn function to create instances, and the advance/draw functions to update and render them.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_Exercise1SpawnMultipleInstances
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Spawn and animate multiple artboard instances
type Spawned = {
artboard: Artboard<Data.Star>,
position: Vector,
speed: number,
}
export type SpawnDemo = {
star: Input<Artboard<Data.Star>>,
count: Input<number>,
items: {Spawned},
}
local function spawn(self: SpawnDemo)
-- TODO 1: Create an instance from self.star using :instance()
local instance = nil -- Replace with self.star:instance()
-- TODO 2: Create a Spawned entry with random position and speed
local entry: Spawned = {
artboard = instance,
position = Vector.xy(math.random(-140, 140), math.random(-80, 80)),
speed = math.random(30, 90),
}
-- TODO 3: Add to self.items using table.insert
end
function init(self: SpawnDemo): boolean
self.items = {}
print(`Spawning {self.count} stars...`)
for _ = 1, self.count do
spawn(self)
end
print(`{#self.items} stars spawned!`)
print("ANSWER: spawned")
return true
end
function advance(self: SpawnDemo, seconds: number): boolean
-- TODO 4: Loop through self.items and for each:
-- a) Update position: item.position.x + item.speed * seconds
-- b) Wrap when x > 160 (reset to -160)
-- c) Call item.artboard:advance(seconds)
return true
end
function draw(self: SpawnDemo, renderer: Renderer)
-- TODO 5: Loop through self.items and for each:
-- a) save(), transform with position, draw artboard, restore()
end
return function(): Node<SpawnDemo>
return {
init = init,
advance = advance,
draw = draw,
star = late(),
count = 5,
items = {},
}
end
Assignment
Complete these tasks:
- Complete the spawn function to create instances, and the advance/draw functions to update and render them.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Editor setup:
- Create a component artboard named
Starwith aStarViewModel - Select the Node script and assign the
starInput to the component artboard
How it works:
initspawns the initial set of instances based oncountadvancemoves each instance and wraps it when it goes off-screendrawrenders each instance at its current position using transform save/restore
Common Patterns
Object Pooling
For frequently spawned/despawned objects, maintain a pool:
--!strict
export type ObjectPool = {
template: Input<Artboard<Data.Bullet>>,
active: {Artboard<Data.Bullet>},
inactive: {Artboard<Data.Bullet>},
}
local function acquire(self: ObjectPool): Artboard<Data.Bullet>
if #self.inactive > 0 then
local instance = table.remove(self.inactive)
table.insert(self.active, instance)
return instance
end
local instance = self.template:instance()
table.insert(self.active, instance)
return instance
end
local function release(self: ObjectPool, instance: Artboard<Data.Bullet>)
for i, active in ipairs(self.active) do
if active == instance then
table.remove(self.active, i)
table.insert(self.inactive, instance)
break
end
end
end
function init(self: ObjectPool): boolean
self.active = {}
self.inactive = {}
return true
end
function draw(self: ObjectPool, renderer: Renderer)
for _, instance in ipairs(self.active) do
instance:draw(renderer)
end
end
return function(): Node<ObjectPool>
return {
init = init,
draw = draw,
template = late(),
active = {},
inactive = {},
}
end
Key Takeaways
- Dynamic instantiation bridges designer-built visuals with procedural logic
- Use
Input<Artboard<Data.YourVM>>to reference component artboards - Call
:instance()to create new instances at runtime - Remember to call
advance()anddraw()on each instance in your respective callbacks - Use object pooling for frequently spawned/despawned objects
Next Steps
- Continue to Procedural Geometry
- Need a refresher? Review Quick Reference