Skip to main content

self and Methods

Learning Objectives

  • Understand the difference between dot (.) and colon (:) syntax
  • Master how self is passed to methods
  • Know when to use each syntax
  • Avoid common pitfalls with method binding

AE/JS Syntax Comparison

ConceptAfter EffectsJavaScriptLuau (Rive)
Object referencethisthisself
Method callN/Aobj.method()obj:method() (colon!)
Static callN/AClass.method()Class.method() (dot)
Define methodN/Amethod() { ... }function Class:method() ... end
Pass different contextN/Amethod.call(other)Class.method(other)
Critical Difference: Colon vs Dot

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_Exercise1DotVsColonBasics
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Complete the TODO sections to call counter methods using both dot and colon syntax, then print the final value.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_Exercise2WhySelfMattersMultipleInstances
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Make each sprite report itself using both colon syntax and the stored function with dot syntax.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_Exercise3DebuggingSelfErrors
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Complete the TODOs to call methods correctly and create a bound wrapper that preserves self.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_Exercise4MethodPatternsInRive
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Complete the Particle class methods using the correct dot/colon syntax patterns.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_Exercise5MethodChaining
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Complete the Builder methods so they can be chained together to build a path.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise6_Exercise6CallbacksAndSelfBindingProblem
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Create properly bound callbacks for each button so they correctly identify themselves when clicked.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is at the top
  • All TODOs are replaced with working code
  • Console output includes the ANSWER: line


Quick Reference: Dot vs Colon

ScenarioSyntaxExample
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

Q:What does the colon syntax (obj:method()) do?
Q:When defining function MyClass:method(), what is self?
Q:What happens if you call obj.method() instead of obj:method()?
Q:How do you safely store a method for later use as a callback?

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