Lesson 1.1: Variables, Types & Operators
Make sure you've completed one of the Getting Started pages first:
- Beginners: Your First Rive Script - teaches from scratch
- Experienced devs: How Rive Scripts Work - uses JS/AE comparisons
Both explain WHY Rive scripts have their specific structure.
Learning Objectives
By the end of this lesson, you will:
- Understand how to declare variables in Luau
- Know the difference between local and persistent variables
- Use all arithmetic, comparison, and logical operators
- Write your first working Rive scripts
Syntax Note: Coming from JavaScript or After Effects?
Throughout this course, you'll see syntax comparison boxes like this to help you translate concepts:
| Luau | JavaScript | After Effects |
|---|---|---|
local x = 5 | let x = 5; | var x = 5; |
-- comment | // comment | // comment |
function f() end | function f() {} | function f() {} |
~= (not equal) | !== | != |
and, or, not | &&, ` |
The Standard Script Template
Every script in this course uses this template. Copy this and keep it handy!
--!strict
export type MyScript = {
-- Your persistent data goes here
}
function init(self: MyScript): boolean
-- One-time setup code goes here
return true
end
function draw(self: MyScript, renderer: Renderer)
-- Drawing code goes here (can be empty)
end
return function(): Node<MyScript>
return {
init = init,
draw = draw,
}
end
Understanding Variables
What Is a Variable?
A variable is a named container for data. Think of it like a labeled box that holds a value.
local playerName = "Hero" -- A box labeled "playerName" containing "Hero"
local health = 100 -- A box labeled "health" containing 100
local isAlive = true -- A box labeled "isAlive" containing true
The local Keyword
In Luau, you must use local when declaring variables inside functions.
-- CORRECT: Use local for variables inside functions
function init(self: MyScript): boolean
local score = 0 -- Good!
local name = "Player" -- Good!
return true
end
-- WRONG: Without local, the variable becomes global (bad practice!)
function init(self: MyScript): boolean
score = 0 -- Creates a GLOBAL variable - avoid this!
return true
end
| Luau | JavaScript |
|---|---|
local x = 5 | let x = 5; or const x = 5; |
No local = global | var x = 5; (function-scoped) or global if outside function |
Key difference: In JavaScript, forgetting let/const/var in strict mode causes an error. In Luau, forgetting local silently creates a global variable, which can cause hard-to-find bugs.
Local Variables vs Persistent State
This is a critical concept that trips up many beginners.
Local Variables (Temporary)
Variables declared with local inside a function exist only while that function runs. They disappear when the function ends.
function init(self: MyScript): boolean
local score = 100 -- Created here
print(score) -- Works: 100
return true
end -- score is DESTROYED here
function advance(self: MyScript, elapsed: number): boolean
print(score) -- ERROR! score doesn't exist here!
return true
end
Persistent State (Using self)
To keep data across frames (between init, advance, and draw), store it on self:
export type MyScript = {
score: number, -- Declare the property in the type
}
function init(self: MyScript): boolean
self.score = 100 -- Store on self
print(self.score) -- Works: 100
return true
end
function advance(self: MyScript, elapsed: number): boolean
self.score += 1 -- Still works! Data persists
print(self.score) -- Works: 101, 102, 103...
return true
end
In After Effects expressions, you can't easily persist data between frames. The expression re-evaluates completely each frame.
After Effects workaround: Use layer markers, text layers, or the posterizeTime() function to approximate persistence.
Rive advantage: self.variableName persists naturally across frames.
Basic Data Types
Luau has these fundamental data types:
| Type | Example | Description |
|---|---|---|
nil | nil | Absence of value (like null in JS) |
boolean | true, false | Logical values |
number | 42, 3.14, 0xFF | All numbers (integers and decimals) |
string | "hello", 'world' | Text |
table | {}, {1,2,3} | Arrays and objects |
function | function() end | Callable code |
| Luau | JavaScript |
|---|---|
nil | null or undefined |
boolean | boolean |
number | number (no separate int/float) |
string | string |
table (array) | Array |
table (dict) | Object |
function | function |
Key difference: Luau has only nil for "no value", while JavaScript has both null and undefined.
Operators
Arithmetic Operators
These perform mathematical calculations.
local a = 10 + 5 -- Addition: 15
local b = 10 - 5 -- Subtraction: 5
local c = 10 * 5 -- Multiplication: 50
local d = 10 / 3 -- Division: 3.333...
local e = 10 // 3 -- Floor division: 3 (rounds down)
local f = 10 % 3 -- Modulo (remainder): 1
local g = 2 ^ 3 -- Exponentiation: 8
local h = -5 -- Negation: -5
| Operation | Luau | JavaScript |
|---|---|---|
| Floor division | 10 // 3 → 3 | Math.floor(10 / 3) → 3 |
| Exponentiation | 2 ^ 3 → 8 | 2 ** 3 → 8 |
| All others | Same | Same |
Compound Assignment Operators
These combine an operation with assignment for cleaner code.
local x = 10
x += 5 -- x = x + 5 → 15
x -= 3 -- x = x - 3 → 12
x *= 2 -- x = x * 2 → 24
x /= 4 -- x = x / 4 → 6
x //= 2 -- x = x // 2 → 3
x %= 2 -- x = x % 2 → 1
JavaScript has +=, -=, *=, /=, %= but not //= (floor division assignment).
Luau has all of these plus //= and ..= (string concatenation assignment).
Comparison Operators
These compare values and return true or false.
local a = 5 == 5 -- Equal: true
local b = 5 ~= 3 -- NOT equal: true (note: ~= not !=)
local c = 5 < 10 -- Less than: true
local d = 5 > 3 -- Greater than: true
local e = 5 <= 5 -- Less than or equal: true
local f = 5 >= 5 -- Greater than or equal: true
Luau uses ~= for "not equal", NOT !=!
| Luau | JavaScript / After Effects |
|---|---|
x ~= y | x !== y |
This is the #1 syntax mistake when coming from other languages!
Logical Operators
These work with boolean values.
local a = true and false -- Both must be true: false
local b = true or false -- At least one true: true
local c = not true -- Negation: false
| Luau | JavaScript |
|---|---|
and | && |
or | ` |
not | ! |
Example:
-- Luau
if health > 0 and hasWeapon then
// JavaScript
if (health > 0 && hasWeapon) {
String Operators
-- Concatenation (joining strings)
local full = "Hello" .. " " .. "World" -- "Hello World"
-- String length
local len = #"Hello" -- 5
-- String interpolation (backticks!)
local name = "Player"
local score = 100
local msg = `{name} scored {score}!` -- "Player scored 100!"
| Luau | JavaScript |
|---|---|
"a" .. "b" | "a" + "b" |
#str | str.length |
`{name}` | `${name}` |
Key difference: Luau uses .. for string concatenation, not +. Using + with strings in Luau will try to convert them to numbers!
Exercises
Exercise 1: Character Stats ⭐
Premise
Every Rive script starts by storing state. Variables are where you keep that state so you can show it in the UI, drive logic, or feed animations. If your types are wrong at the start, every later calculation or binding breaks.
By the end of this exercise, you will declare typed local variables and print a verification line from init.
Use Case
You are building a character selection card in Rive. The card needs a name label, a level badge, experience for a progress bar, and a boolean that controls whether an ultimate icon is visible. Those values are set once on init and then drive your UI. If you mix up a type (string vs number), you cannot bind it cleanly to the Artboard.
Example scenarios:
- Character cards with stats and unlocks
- HUD widgets with score + status flags
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_CharacterStats
- Assets panel →
-
Create required elements:
- Draw any shape on the artboard (this is just a node to attach to)
-
Attach the script:
- Drag the script onto the shape
-
Prepare the Console:
- View → Console
- Clear previous output
-
Run:
- Press Play in the State Machine
Starter Code
--!strict
export type Exercise1 = {}
function init(self: Exercise1): boolean
-- TODO 1: Declare a string variable 'characterName' set to "Warrior"
local characterName = "TODO"
-- TODO 2: Declare a number variable 'level' set to 5
local level = 0
-- TODO 3: Declare a number variable 'experience' set to 1250.5
local experience = 0
-- TODO 4: Declare a boolean variable 'hasUltimate' set to true
local hasUltimate = false
-- Validation (do not change):
print(`ANSWER: {characterName}, Lv.{level}, {experience}xp, ult={hasUltimate}`)
return true
end
function draw(self: Exercise1, renderer: Renderer)
end
return function(): Node<Exercise1>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
-
Declare
characterName- Type: string
- Value:
"Warrior"
-
Declare
level- Type: number
- Value:
5
-
Declare
experience- Type: number
- Value:
1250.5
-
Declare
hasUltimate- Type: boolean
- Value:
true
-
Keep the validation print unchanged
Expected Output
Your console output should display:
- The character name (a string)
- The level prefixed with "Lv."
- The experience with "xp" suffix
- The ultimate status as "ult=true" or "ult=false"
All values separated by commas in the format shown in the print statement.
Verify Your Answer
Checklist
- All four variables are declared with
local - Variable names match exactly (case-sensitive)
-
--!strictis near the top of the file - Console shows the expected
ANSWER:line
Exercise 2: Damage Breakdown ⭐
Premise
Most Rive interactions rely on math: positions, percentages, multipliers, and progress calculations. This exercise makes you compute a small damage breakdown that mirrors the calculations you will do for UI bars or hit effects.
By the end of this exercise, you will compute derived values and print a single deterministic result line.
Use Case
You are animating a combat HUD where the UI needs to display base damage, critical damage, and how much was blocked by armor. The UI labels update when a hit lands, and the damage numbers must be correct for the animation timing and visual effects to feel right. If you compute the percent wrong, your bar fill and text will disagree, which is a common bug in HUD scripts.
Example scenarios:
- Combat hit popups
- Health bar reduction with armor
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_DamageBreakdown
- Assets panel →
-
Create required elements:
- Draw any shape on the artboard
-
Attach the script and run:
- Drag the script onto the shape
- Press Play and check the Console
Starter Code
--!strict
export type Exercise2 = {}
function init(self: Exercise2): boolean
local baseDamage = 50
local critMultiplier = 2.5
local armorReduction = 15
-- TODO 1: Compute critDamage = baseDamage * critMultiplier
local critDamage = 0
-- TODO 2: Compute finalDamage = critDamage - armorReduction
local finalDamage = 0
-- TODO 3: Compute percentBlocked as an integer percent
-- Use math.floor(...) so the output is stable.
local percentBlocked = 0
print(`ANSWER: crit={critDamage},final={finalDamage},blocked={percentBlocked}%`)
return true
end
function draw(self: Exercise2, renderer: Renderer)
end
return function(): Node<Exercise2>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
-
Calculate critical damage
critDamage = baseDamage * critMultiplier
-
Calculate final damage
finalDamage = critDamage - armorReduction
-
Calculate blocked percent
percentBlocked = math.floor(armorReduction / critDamage * 100)
-
Keep the validation print unchanged
Expected Output
Your console output should show three computed values:
- Critical damage (base damage × crit multiplier)
- Final damage (critical damage − armor)
- Percentage blocked (armor as % of critical damage, floored to integer)
Verify Your Answer
Checklist
-
critDamageuses multiplication, not hard-coded values -
percentBlockeduses math.floor to avoid decimals - Console shows the exact
ANSWER:line
Exercise 3: Score Updates ⭐
Premise
Live UI elements update values constantly. Compound assignment operators make those updates readable and less error-prone, which matters when you are adjusting values every frame.
By the end of this exercise, you will update a value using +=, *=, -=, and //= and confirm the result.
Use Case
Imagine a score counter that increments on pickups, doubles during a combo, loses points on a penalty, and then snaps to a tiered value for ranking. Each of those updates is a small step that should be expressed clearly. Compound operators let you express those steps without repeating the variable name.
Example scenarios:
- Score counters with multipliers
- Resource totals that tick up and down
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_ScoreUpdates
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise3 = {}
function init(self: Exercise3): boolean
local score = 100
-- TODO 1: Add 50 using +=
-- TODO 2: Multiply by 2 using *=
-- TODO 3: Subtract 75 using -=
-- TODO 4: Floor-divide by 3 using //=
print(`ANSWER: {score}`)
return true
end
function draw(self: Exercise3, renderer: Renderer)
end
return function(): Node<Exercise3>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks in order:
- Use
score += 50 - Use
score *= 2 - Use
score -= 75 - Use
score //= 3
Expected Output
Your console output should display the final score after applying all four compound operations in sequence. The operations should be:
- Addition of 50
- Multiplication by 2
- Subtraction of 75
- Floor division by 3
The result will be an integer.
Verify Your Answer
Checklist
- All four compound operators are used
- The order of operations matches the assignment
- Console shows
ANSWER: 75
Exercise 4: Status Line Formatting ⭐⭐
Premise
You will constantly format text for labels, debug output, and status banners in Rive. String interpolation keeps those lines readable and avoids messy concatenation.
By the end of this exercise, you will build a formatted status line using interpolation and simple calculations.
Use Case
You are building a player status readout that appears in a corner HUD. It shows the character name, gold, active quests, and total play time in hours. This text is updated by the script and drives a Text Run or debug log.
Example scenarios:
- Debug overlays for UI state
- HUD labels built from multiple fields
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise4_StatusLine
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise4 = {}
function init(self: Exercise4): boolean
local playerName = "Shadowblade"
local gold = 250
local quests = 3
local minutesPlayed = 90
-- TODO 1: Compute hoursPlayed from minutesPlayed
local hoursPlayed = 0
-- TODO 2: Build the status line using interpolation
-- Format: "<name> | gold=<gold> | quests=<quests> | hours=<hours>"
local statusLine = ""
print(`ANSWER: {statusLine}`)
return true
end
function draw(self: Exercise4, renderer: Renderer)
end
return function(): Node<Exercise4>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
-
Compute hours
hoursPlayed = minutesPlayed / 60- With 90 minutes, hours should be
1.5
-
Create
statusLine- Use backticks and
{}interpolation - Match the exact formatting in the comment
- Use backticks and
-
Keep the validation print unchanged
Expected Output
Your console output should display a formatted status line with:
- The player name
- Gold amount prefixed with "gold="
- Quest count prefixed with "quests="
- Hours played (as a decimal) prefixed with "hours="
All sections separated by " | " (space-pipe-space).
Verify Your Answer
Checklist
- Interpolation uses backticks, not quotes
-
hoursPlayedequals 1.5 - Output formatting matches exactly
Exercise 5: Heal Logic ⭐⭐
Premise
Boolean logic is what turns state into decisions. In Rive, those decisions often drive which animation state plays or whether a visual change is allowed.
By the end of this exercise, you will combine and, or, and not to derive game-style conditions.
Use Case
You are building a healing interaction for a UI-based game animation. The heal button should only light up if the character is low on health, has a potion, and is not stunned. A critical warning should appear if health is extremely low. These two booleans would typically control visibility or opacity in the artboard.
Example scenarios:
- Button enable/disable logic
- Critical health warning states
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise5_HealLogic
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise5 = {}
function init(self: Exercise5): boolean
local health = 25
local maxHealth = 100
local hasPotion = true
local stunned = false
-- TODO 1: isLowHealth should be true when health <= 30
local isLowHealth = false
-- TODO 2: healthPercent should be health / maxHealth
local healthPercent = 0
-- TODO 3: canHeal should be true only if low health, has potion, and not stunned
local canHeal = false
-- TODO 4: isCritical should be true if health <= 10 OR healthPercent <= 0.1
local isCritical = false
print(`ANSWER: low={isLowHealth}, heal={canHeal}, critical={isCritical}`)
return true
end
function draw(self: Exercise5, renderer: Renderer)
end
return function(): Node<Exercise5>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Compute
isLowHealth - Compute
healthPercent - Compute
canHealusingand+not - Compute
isCriticalusingor
Expected Output
Your console output should display three boolean values:
low=— whether health is at or below the low thresholdheal=— whether all healing conditions are met (low health AND has potion AND not stunned)critical=— whether either critical condition is triggered
Each value will be true or false based on the given stats.
Verify Your Answer
Checklist
-
canHealuses all three conditions -
isCriticalusesorwith the percent check - Console shows the exact
ANSWER:line
Knowledge Check
Common Mistakes to Avoid
- Forgetting
local— Always declare withlocalto avoid globals - Using
!=instead of~=— Luau uses~=for not-equal - Using
+for strings — Use..for concatenation:"a" .. "b" - Expecting 0 to be falsy — In Luau,
0is truthy! - Using
.valueon Inputs —self.speed, notself.speed.value
Quick Reference
-- Variables
local x = 5 -- number
local s = "hello" -- string
local b = true -- boolean
local n = nil -- nil
-- Arithmetic
+ - * / // % ^
-- Comparison
== ~= < > <= >=
-- Logical
and or not
-- String
.. #string `interpolation {var}`
-- Compound assignment
+= -= *= /= //= %= ^= ..=
Next Steps
- Continue to Data Types
- Need a refresher? Review Quick Reference