Skip to main content

Lesson 1.1: Variables, Types & Operators

Before You Begin

Make sure you've completed one of the Getting Started pages first:

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:

JavaScript / After Effects Comparison
LuauJavaScriptAfter Effects
local x = 5let x = 5;var x = 5;
-- comment// comment// comment
function f() endfunction 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
JavaScript Comparison
LuauJavaScript
local x = 5let x = 5; or const x = 5;
No local = globalvar 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
After Effects Comparison

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:

TypeExampleDescription
nilnilAbsence of value (like null in JS)
booleantrue, falseLogical values
number42, 3.14, 0xFFAll numbers (integers and decimals)
string"hello", 'world'Text
table{}, {1,2,3}Arrays and objects
functionfunction() endCallable code
JavaScript Comparison
LuauJavaScript
nilnull or undefined
booleanboolean
numbernumber (no separate int/float)
stringstring
table (array)Array
table (dict)Object
functionfunction

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
JavaScript Comparison
OperationLuauJavaScript
Floor division10 // 33Math.floor(10 / 3)3
Exponentiation2 ^ 382 ** 38
All othersSameSame

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 Comparison

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
Critical Difference: Not Equal

Luau uses ~= for "not equal", NOT !=!

LuauJavaScript / After Effects
x ~= yx !== 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
JavaScript Comparison
LuauJavaScript
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!"
JavaScript Comparison
LuauJavaScript
"a" .. "b""a" + "b"
#strstr.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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_CharacterStats
  2. Create required elements:

    • Draw any shape on the artboard (this is just a node to attach to)
  3. Attach the script:

    • Drag the script onto the shape
  4. Prepare the Console:

    • View → Console
    • Clear previous output
  5. Run:


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:

  1. Declare characterName

    • Type: string
    • Value: "Warrior"
  2. Declare level

    • Type: number
    • Value: 5
  3. Declare experience

    • Type: number
    • Value: 1250.5
  4. Declare hasUltimate

    • Type: boolean
    • Value: true
  5. 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

Verify Your Answer

Checklist

  • All four variables are declared with local
  • Variable names match exactly (case-sensitive)
  • --!strict is 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_DamageBreakdown
  2. Create required elements:

    • Draw any shape on the artboard
  3. 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:

  1. Calculate critical damage

    • critDamage = baseDamage * critMultiplier
  2. Calculate final damage

    • finalDamage = critDamage - armorReduction
  3. Calculate blocked percent

    • percentBlocked = math.floor(armorReduction / critDamage * 100)
  4. 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

Verify Your Answer

Checklist

  • critDamage uses multiplication, not hard-coded values
  • percentBlocked uses 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_ScoreUpdates
  2. 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:

  1. Use score += 50
  2. Use score *= 2
  3. Use score -= 75
  4. 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

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_StatusLine
  2. 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:

  1. Compute hours

    • hoursPlayed = minutesPlayed / 60
    • With 90 minutes, hours should be 1.5
  2. Create statusLine

    • Use backticks and {} interpolation
    • Match the exact formatting in the comment
  3. 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

Verify Your Answer

Checklist

  • Interpolation uses backticks, not quotes
  • hoursPlayed equals 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_HealLogic
  2. 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:

  1. Compute isLowHealth
  2. Compute healthPercent
  3. Compute canHeal using and + not
  4. Compute isCritical using or

Expected Output

Your console output should display three boolean values:

  • low= — whether health is at or below the low threshold
  • heal= — 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

Verify Your Answer

Checklist

  • canHeal uses all three conditions
  • isCritical uses or with the percent check
  • Console shows the exact ANSWER: line

Knowledge Check

Q:What keyword should you always use when declaring variables inside a function?
Q:What operator is used for 'not equal' in Luau?
Q:What is the result of: 10 // 3
Q:Where do print() statements appear in Rive?
Q:Which values are considered 'falsy' in Luau?

Common Mistakes to Avoid

Top 5 Mistakes
  1. Forgetting local — Always declare with local to avoid globals
  2. Using != instead of ~= — Luau uses ~= for not-equal
  3. Using + for strings — Use .. for concatenation: "a" .. "b"
  4. Expecting 0 to be falsy — In Luau, 0 is truthy!
  5. Using .value on Inputsself.speed, not self.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