self and Methods
Learning Objectives
- Understand the difference between dot (
.) and colon (:) syntax - Master how
selfis passed to methods - Know when to use each syntax
- Avoid common pitfalls with method binding
AE/JS Syntax Comparison
| Concept | After Effects | JavaScript | Luau (Rive) |
|---|---|---|---|
| Object reference | this | this | self |
| Method call | N/A | obj.method() | obj:method() (colon!) |
| Static call | N/A | Class.method() | Class.method() (dot) |
| Define method | N/A | method() { ... } | function Class:method() ... end |
| Pass different context | N/A | method.call(other) | Class.method(other) |
This is the #1 source of bugs for people learning Luau!
obj.method() -- WRONG: self is nil inside method!
obj:method() -- CORRECT: self is obj
The colon (:) is syntactic sugar that automatically passes the object as the first argument (self). Forget it, and you'll get "attempt to index nil" errors.
JavaScript always passes this automatically.
Luau requires you to use : to get self passed automatically.
Rive Context
In Rive Node Scripts, you constantly work with methods:
- Renderer methods:
renderer:drawPath(),renderer:save(),renderer:restore() - Path methods:
path:moveTo(),path:lineTo(),path:close() - Your own class methods:
sprite:update(),particle:draw()
Understanding the colon syntax is essential for writing correct Rive code. Get it wrong, and your self will be nil or the wrong object entirely.
The Colon Syntax: Syntactic Sugar
The colon (:) is syntactic sugar that automatically passes the object as the first argument:
-- These are EXACTLY equivalent:
obj:method(arg1, arg2)
obj.method(obj, arg1, arg2)
Definition time:
-- These are EXACTLY equivalent:
function MyClass:doSomething(x)
print(self.value + x)
end
function MyClass.doSomething(self, x)
print(self.value + x)
end
The colon is just shorthand - nothing magical happens.
Exercise 1: Dot vs Colon Basics ⭐
Premise
After increment (11) + increment (12) + add 5 (17) + add 3 (20), the final value is 20. Both colon and dot syntax work - colon just automatically passes self.
By the end of this exercise, you will be able to Complete the TODO sections to call counter methods using both dot and colon syntax, then print the final value.
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_Exercise1DotVsColonBasics
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
local Counter = {}
Counter.__index = Counter
export type CounterType = {
value: number,
}
function Counter.new(initial: number): CounterType
local self = setmetatable({}, Counter)
self.value = initial
return (self :: any) :: CounterType
end
-- Method defined with colon (receives self automatically)
function Counter:increment()
self.value += 1
end
-- Method defined with dot (self is explicit first parameter)
function Counter.add(self: CounterType, amount: number)
self.value += amount
end
export type DotColonDemo = {
counter: CounterType,
}
function init(self: DotColonDemo): boolean
self.counter = Counter.new(10)
print(`Initial: {self.counter.value}`)
-- TODO 1: Call increment using COLON syntax
-- (colon automatically passes self.counter as self)
print(`After :increment(): {self.counter.value}`)
-- TODO 2: Call increment using DOT syntax
-- (dot requires you to pass self.counter manually)
print(`After .increment(counter): {self.counter.value}`)
-- TODO 3: Call add(5) using COLON syntax
print(`After :add(5): {self.counter.value}`)
-- TODO 4: Call add(3) using DOT syntax
print(`After .add(counter, 3): {self.counter.value}`)
-- Print the ANSWER line for validation
print(`ANSWER: {self.counter.value}`)
return true
end
function draw(self: DotColonDemo, renderer: Renderer)
end
return function(): Node<DotColonDemo>
return {
init = init,
draw = draw,
counter = Counter.new(0),
}
end
Assignment
Complete these tasks:
- Complete the TODO sections to call counter methods using both dot and colon syntax, then print the final value.
- 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
Exercise 2: Why self Matters - Multiple Instances ⭐
Premise
Each sprite reports twice (once with colon, once with dot syntax), totaling 4 reports. self allows each instance to know its own identity.
By the end of this exercise, you will be able to Make each sprite report itself using both colon syntax and the stored function with dot syntax.
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
Exercise2_Exercise2WhySelfMattersMultipleInstances
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
local Sprite = {}
Sprite.__index = Sprite
export type SpriteType = {
id: string,
x: number,
y: number,
}
function Sprite.new(id: string, x: number, y: number): SpriteType
local self = setmetatable({}, Sprite)
self.id = id
self.x = x
self.y = y
return (self :: any) :: SpriteType
end
function Sprite:report()
print(`I am {self.id} at ({self.x}, {self.y})`)
end
export type SelfIdentityDemo = {
spriteA: SpriteType,
spriteB: SpriteType,
reportCount: number,
}
function init(self: SelfIdentityDemo): boolean
self.spriteA = Sprite.new("A", -60, 0)
self.spriteB = Sprite.new("B", 60, 0)
self.reportCount = 0
-- TODO 1: Call report on spriteA using COLON syntax
-- Then increment reportCount
-- TODO 2: Call report on spriteB using COLON syntax
-- Then increment reportCount
-- Store the report function (loses binding to any specific sprite)
local reportFunc = Sprite.report
-- TODO 3: Use reportFunc with DOT syntax to report spriteA
-- (pass spriteA as the first argument)
-- Then increment reportCount
-- TODO 4: Use reportFunc with DOT syntax to report spriteB
-- Then increment reportCount
print(`ANSWER: {self.reportCount}`)
return true
end
function draw(self: SelfIdentityDemo, renderer: Renderer)
end
return function(): Node<SelfIdentityDemo>
return {
init = init,
draw = draw,
spriteA = Sprite.new("", 0, 0),
spriteB = Sprite.new("", 0, 0),
reportCount = 0,
}
end
Assignment
Complete these tasks:
- Make each sprite report itself using both colon syntax and the stored function with dot syntax.
- 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
Exercise 3: Debugging self Errors ⭐⭐
Premise
After resizing to 200x100 and calling through the bound wrapper, the output shows the new dimensions. The bound wrapper captures self in a closure.
By the end of this exercise, you will be able to Complete the TODOs to call methods correctly and create a bound wrapper that preserves self.
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
Exercise3_Exercise3DebuggingSelfErrors
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
local Widget = {}
Widget.__index = Widget
export type WidgetType = {
name: string,
width: number,
height: number,
}
function Widget.new(name: string, width: number, height: number): WidgetType
local self = setmetatable({}, Widget)
self.name = name
self.width = width
self.height = height
return (self :: any) :: WidgetType
end
function Widget:describe(): string
return `{self.name}: {self.width}x{self.height}`
end
function Widget:resize(w: number, h: number)
self.width = w
self.height = h
end
export type DebugSelfDemo = {
widget: WidgetType,
}
function init(self: DebugSelfDemo): boolean
self.widget = Widget.new("MyWidget", 100, 50)
-- TODO 1: Call describe using COLON syntax
print("Colon call: " .. "REPLACE_WITH_COLON_CALL")
-- TODO 2: Call describe using DOT syntax (pass widget as first arg)
print("Dot call: " .. "REPLACE_WITH_DOT_CALL")
-- Store the method (loses binding)
local describeMethod = Widget.describe
-- TODO 3: Call describeMethod with the widget as argument
print("Stored method: " .. "REPLACE_WITH_STORED_CALL")
-- TODO 4: Create a bound wrapper function that calls describe correctly
local boundDescribe = function()
-- Return the result of calling describe on self.widget
return "REPLACE_WITH_BOUND_CALL"
end
-- Resize the widget
self.widget:resize(200, 100)
-- The bound wrapper still works after resize!
print("Bound wrapper after resize: " .. boundDescribe())
print("ANSWER: " .. boundDescribe())
return true
end
function draw(self: DebugSelfDemo, renderer: Renderer)
end
return function(): Node<DebugSelfDemo>
return {
init = init,
draw = draw,
widget = Widget.new("", 0, 0),
}
end
Assignment
Complete these tasks:
- Complete the TODOs to call methods correctly and create a bound wrapper that preserves self.
- 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
When to Use Dot vs Colon
Use Colon (:) for:
- Instance methods that need
self - Any method that accesses instance properties
- Most method calls in practice
self.sprite:draw(renderer)
self.spring:step(dt)
path:lineTo(point)
Use Dot (.) for:
- Static functions (constructors, utilities)
- When you need to pass a different
self - Storing method references
local sprite = Sprite.new(0, 0) -- Constructor
local dist = Vector.distance(a, b) -- Instance method
MyClass.method(differentObj, args) -- Explicit self
Exercise 4: Method Patterns in Rive ⭐⭐
Premise
After spawning 5 particles and running one update cycle, 5 particles remain alive (life < maxLife after one frame). The pattern shows static utils use dot, instance methods use colon.
By the end of this exercise, you will be able to Complete the Particle class methods using the correct dot/colon syntax patterns.
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
Exercise4_Exercise4MethodPatternsInRive
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Util-style module with static functions (use DOT)
local MathUtil = {}
function MathUtil.lerp(a: number, b: number, t: number): number
return a + (b - a) * t
end
-- Class with instance methods (use COLON)
local Particle = {}
Particle.__index = Particle
export type ParticleType = {
x: number,
y: number,
life: number,
maxLife: number,
}
function Particle.new(x: number, y: number): ParticleType
local self = setmetatable({}, Particle)
self.x = x
self.y = y
self.life = 0
self.maxLife = 2.0
return (self :: any) :: ParticleType
end
-- TODO 1: Define update as an INSTANCE method (use colon syntax)
-- It should: increment life by dt, return true if life < maxLife
function Particle.update(self: ParticleType, dt: number): boolean
-- Replace with correct implementation
return false
end
-- TODO 2: Define getAlpha as an INSTANCE method (use colon syntax)
-- It should: calculate progress (life/maxLife), use MathUtil.lerp to fade from 1 to 0
function Particle.getAlpha(self: ParticleType): number
-- Replace with correct implementation using MathUtil.lerp
return 1.0
end
export type MethodPatternsDemo = {
particles: {ParticleType},
}
function init(self: MethodPatternsDemo): boolean
self.particles = {}
-- Spawn particles using static constructor (DOT syntax)
for i = 1, 5 do
table.insert(self.particles, Particle.new(0, 0))
end
print(`Spawned {#self.particles} particles`)
-- Simulate one update cycle
local alive = {}
for _, p in ipairs(self.particles) do
-- TODO 3: Call update on particle p using COLON syntax
-- If update returns true, add to alive table
if true then -- Replace with correct method call
table.insert(alive, p)
end
end
print(`After update: {#alive} alive`)
print(`ANSWER: {#alive}`)
return true
end
function draw(self: MethodPatternsDemo, renderer: Renderer)
end
return function(): Node<MethodPatternsDemo>
return {
init = init,
draw = draw,
particles = {},
}
end
Assignment
Complete these tasks:
- Complete the Particle class methods using the correct dot/colon syntax patterns.
- 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
Exercise 5: Method Chaining ⭐⭐
Premise
The chain builds a pentagon with 5 commands: moveTo + 4 lineTo calls. Returning self from each method enables the fluent API pattern.
By the end of this exercise, you will be able to Complete the Builder methods so they can be chained together to build a path.
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
Exercise5_Exercise5MethodChaining
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
local Builder = {}
Builder.__index = Builder
export type BuilderType = {
commands: number,
}
function Builder.new(): BuilderType
local self = setmetatable({}, Builder)
self.commands = 0
return (self :: any) :: BuilderType
end
-- TODO 1: Complete moveTo to track command and enable chaining
function Builder:moveTo(x: number, y: number): BuilderType
self.commands += 1
-- What should you return to enable chaining?
return nil :: any -- FIX THIS
end
-- TODO 2: Complete lineTo to track command and enable chaining
function Builder:lineTo(x: number, y: number): BuilderType
self.commands += 1
-- What should you return to enable chaining?
return nil :: any -- FIX THIS
end
-- TODO 3: Complete close to track command and enable chaining
function Builder:close(): BuilderType
self.commands += 1
-- What should you return to enable chaining?
return nil :: any -- FIX THIS
end
function Builder:getCommandCount(): number
return self.commands
end
export type ChainingDemo = {
commandCount: number,
}
function init(self: ChainingDemo): boolean
print("Building path with chaining...")
-- This chain should work after you fix the TODOs
local count = Builder.new()
:moveTo(-50, -50)
:lineTo(50, -50)
:lineTo(50, 50)
:lineTo(-50, 50)
:lineTo(-50, -50) -- Back to start
:getCommandCount()
self.commandCount = count
print(`Path commands: {count}`)
print(`ANSWER: {count}`)
return true
end
function draw(self: ChainingDemo, renderer: Renderer)
end
return function(): Node<ChainingDemo>
return {
init = init,
draw = draw,
commandCount = 0,
}
end
Assignment
Complete these tasks:
- Complete the Builder methods so they can be chained together to build a path.
- 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
Exercise 6: Callbacks and self (Binding Problem) ⭐⭐
Premise
All 3 buttons fire their callbacks correctly because each callback is wrapped in a closure that captures the specific button reference and uses colon syntax to call handleClick.
By the end of this exercise, you will be able to Create properly bound callbacks for each button so they correctly identify themselves when clicked.
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
Exercise6_Exercise6CallbacksAndSelfBindingProblem
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
local Button = {}
Button.__index = Button
export type ButtonType = {
label: string,
onClick: (() -> ())?,
}
function Button.new(label: string): ButtonType
local self = setmetatable({}, Button)
self.label = label
self.onClick = nil
return (self :: any) :: ButtonType
end
function Button:handleClick()
print(`{self.label} was clicked!`)
end
export type CallbackDemo = {
buttons: {ButtonType},
clickCount: number,
}
function init(self: CallbackDemo): boolean
-- Create three buttons
local submit = Button.new("Submit")
local cancel = Button.new("Cancel")
local reset = Button.new("Reset")
self.buttons = { submit, cancel, reset }
self.clickCount = 0
-- TODO 1: Set submit.onClick to a wrapper function that calls handleClick
-- WRONG approach: submit.onClick = submit.handleClick (loses self!)
-- CORRECT: Wrap in a closure that calls submit:handleClick()
submit.onClick = nil -- FIX THIS
-- TODO 2: Set cancel.onClick with proper binding
cancel.onClick = nil -- FIX THIS
-- TODO 3: Set reset.onClick with proper binding
reset.onClick = nil -- FIX THIS
-- Simulate clicking all buttons
for _, btn in ipairs(self.buttons) do
if btn.onClick then
btn.onClick()
self.clickCount += 1
end
end
print(`ANSWER: {self.clickCount}`)
return true
end
function draw(self: CallbackDemo, renderer: Renderer)
end
return function(): Node<CallbackDemo>
return {
init = init,
draw = draw,
buttons = {},
clickCount = 0,
}
end
Assignment
Complete these tasks:
- Create properly bound callbacks for each button so they correctly identify themselves when clicked.
- 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
Quick Reference: Dot vs Colon
| Scenario | Syntax | Example |
|---|---|---|
| Call instance method | : | obj:draw(renderer) |
| Call static function | . | Sprite.new(0, 0) |
| Define instance method | : | function Sprite:draw() |
| Define static function | . | function Sprite.new() |
| Pass different self | . | Sprite.draw(other, r) |
| Store method reference | . | local fn = obj.method |
| Call stored method | . | fn(obj, args) |
Knowledge Check
Common Mistakes
1. Using Dot When Colon is Needed
-- WRONG: Dot doesn't pass self
obj.method() -- self is nil!
-- CORRECT: Colon passes self
obj:method() -- self is obj
2. Using Colon for Static Functions
-- WRONG: Constructor doesn't need self
local sprite = Sprite:new(0, 0) -- Passes Sprite as first arg!
-- CORRECT: Constructor uses dot
local sprite = Sprite.new(0, 0)
3. Forgetting self in Method Definitions
-- WRONG: Using dot but accessing self
function MyClass.method()
print(self.value) -- self doesn't exist!
end
-- CORRECT: Use colon for methods that need self
function MyClass:method()
print(self.value) -- self is passed automatically
end
4. Storing Methods Without Binding
-- WRONG: Loses self
local fn = obj.method
fn() -- self is nil!
-- CORRECT: Wrap in closure
local fn = function() obj:method() end
fn() -- Works!
Self-Assessment Checklist
- I understand that colon is syntactic sugar for passing self
- I know when to use dot vs colon for calls
- I know when to use dot vs colon for definitions
- I can debug "attempt to index nil" errors related to self
- I understand how to properly store methods as callbacks
- I can implement method chaining with return self
Next Steps
- Continue to 3.3 Inheritance & Patterns
- Need a refresher? Review Quick Reference