Skip to main content

Rive Environment

Learning Objectives

  • Understand how Rive scripts run inside the editor
  • Use the Console panel for debugging
  • Know the difference between Node and Util scripts
  • Work within sandbox limitations

AE/JS Syntax Comparison

ConceptAfter Effects / JavaScriptLuau (Rive Scripts)
Debug outputconsole.log() or alert()print() to Console panel
Script executionRuns in browser JS engineRuns in sandboxed Luau VM
File accessFile.read() (ExtendScript)Not available (sandboxed)
Network requestsfetch() or XMLHttpRequestNot available (sandboxed)
Module importimport or require() (Node.js)require("ScriptName")
Type checkingTypeScript (optional)--!strict (recommended)
RuntimeJavaScript (dynamic)Luau (statically typed variant of Lua)
Critical Difference: Where Your Code Runs

After Effects: Expressions run in the AE JavaScript engine. ExtendScript runs as a separate process that can access files, network, etc.

JavaScript (Web): Code runs in the browser's V8/SpiderMonkey engine with full DOM access.

Rive Scripts: Code runs in a sandboxed Luau virtual machine inside the Rive editor. There is:

  • ❌ No file system access
  • ❌ No network requests
  • ❌ No system commands
  • ❌ No dynamic code loading
  • ✅ Full access to Rive's graphics APIs (Path, Paint, Vector, etc.)
  • ✅ Standard math, string, and table libraries

Rive Context: Scripts Run Inside the Editor

Rive scripts execute in a sandboxed Luau environment inside the Rive editor. This is not a standard Lua runtime - it has specific APIs, restrictions, and behaviors designed for animation development.

Key points to understand:

  • Scripts run in the Rive editor, not in your browser's JavaScript environment
  • Output goes to Rive's Console panel, not the browser developer console
  • The runtime provides specific APIs (Path, Paint, Vector, etc.)
  • Certain Lua capabilities are restricted for security and performance

The Console Panel

The Console panel is your primary debugging tool in Rive. All print() statements from your scripts appear here.

Opening the Console:

  1. View menu > Console (or use the keyboard shortcut)
  2. The panel appears at the bottom of the editor

Console features:

  • Shows print() output from all scripts
  • Displays error messages with line numbers
  • Clears when you restart the animation
Debug Strategy

Use print() liberally during development. Unlike browser console logs, these don't affect your exported animation - they only appear in the editor.

-- Rive debugging
print("Debug message")
print(`Value is {myValue}`) -- String interpolation
// JavaScript equivalent:
console.log("Debug message");
console.log(`Value is ${myValue}`);

Script Types

Rive has two types of scripts with different purposes:

Node Scripts

Node scripts attach to Artboard nodes and have a lifecycle:

--!strict

export type MyNode = {}

function init(self: MyNode): boolean
print("I am a Node script")
return true
end

function draw(self: MyNode, renderer: Renderer)
end

return function(): Node<MyNode>
return {
init = init,
draw = draw,
}
end
// JavaScript/TypeScript mental model:
// A Node script is like a class with lifecycle methods

class MyNode {
constructor() {
console.log("I am a Node script"); // Like init()
}

draw(renderer: Renderer) {
// Called every frame
}
}

Characteristics:

  • Must have init and draw functions
  • Can have update, advance, and pointer handlers
  • Attach to nodes in the artboard hierarchy
  • Have access to a Renderer for drawing

Util Scripts

Util scripts are reusable modules with no lifecycle. To create one, select Blank Script in the editor dropdown—there is no dedicated "Util Script" option. See the Rive docs for the full pattern.

--!strict

local MyUtil = {}

function MyUtil.helper(x: number): number
return x * 2
end

return MyUtil
// JavaScript/TypeScript equivalent:
// A Util script is like an ES6 module

export function helper(x: number): number {
return x * 2;
}

// Or as a module object:
const MyUtil = {
helper: (x: number) => x * 2
};
export default MyUtil;

Characteristics:

  • No lifecycle functions
  • Return a table of functions and types
  • Imported with require("ScriptName")
  • Shared across all scripts that import them

Artboard References (self.artboard)

In Rive scripts, self only contains fields you explicitly define in your exported type. There is no built-in self.artboard unless you add it.

If you see self.artboard in examples, it means the script defines an artboard Input:

--!strict
export type Spawner = {
artboard: Input<Artboard<Data.ComponentVM>>,
}

function init(self: Spawner): boolean
local instance = self.artboard:instance()
return true
end

Key takeaway: To access an artboard, declare it as an Input<Artboard<...>> (or late() if assigned in the editor) and then use self.artboard in your logic.


Practice Exercises

Exercise 1: Console Exploration ⭐

Premise

Understanding the lifecycle order (init → advance → draw) is fundamental to Rive scripting. The milestone detection shows how to track state across frames.

Goal

By the end of this exercise, you will be able to Complete the advance function to track frames and detect when frameCount reaches exactly 60. Print 'ANSWER: milestone' when you hit that frame.

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_Exercise1ConsoleExploration
  2. Attach and run:

    • Attach to any shape and press Play
  3. Open the Console:

    • View → Console

Starter Code

--!strict
-- This script explores the Console panel and lifecycle callbacks

export type ConsoleExplorer = {
frameCount: number,
}

function init(self: ConsoleExplorer): boolean
print("init() called")
self.frameCount = 0
return true
end

function advance(self: ConsoleExplorer, seconds: number): boolean
-- TODO 1: Increment frameCount by 1

-- TODO 2: If this is one of the first 3 frames, print "Frame {frameCount}"

-- TODO 3: When frameCount reaches exactly 60, print "ANSWER: milestone"

return true
end

function draw(self: ConsoleExplorer, renderer: Renderer)
end

return function(): Node<ConsoleExplorer>
return {
init = init,
advance = advance,
draw = draw,
frameCount = 0,
}
end

Assignment

Complete these tasks:

  1. Complete the advance function to track frames and detect when frameCount reaches exactly 60. Print 'ANSWER: milestone' when you hit that frame.
  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: Using Vector Constructors ⭐

Premise

Rive provides Vector as a core type with multiple constructors. Understanding these constructors is essential for positioning, scaling, and movement calculations in your animations.

Goal

By the end of this exercise, you will create vectors using different constructors and calculate a distance.

Use Case

Vectors are fundamental to all positioning and movement in Rive scripts.

Example scenarios:

  • Positioning elements on the artboard
  • Calculating distances between points
  • Defining directions for movement

Setup

In Rive Editor:

  1. Create the script:

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

    • Attach to any shape and press Play
  3. Open the Console:

    • View → Console

Starter Code

--!strict
-- Practice using Vector constructors

export type VectorPractice = {}

function init(self: VectorPractice): boolean
print("=== Vector Constructors ===")

-- TODO 1: Create a vector at position (100, 50) using Vector.xy()
local start = Vector.xy(0, 0) -- Replace with correct values

-- TODO 2: Create another vector at position (100, 150) using Vector.xy()
local finish = Vector.xy(0, 0) -- Replace with correct values

-- TODO 3: Calculate the difference (finish - start)
local diff = finish - start

-- TODO 4: Calculate the distance using diff:length()
local distance = diff:length()

print(`Start: ({start.x}, {start.y})`)
print(`Finish: ({finish.x}, {finish.y})`)
print(`Distance: {distance}`)
print(`ANSWER: {distance}`)
return true
end

function draw(self: VectorPractice, renderer: Renderer)
end

return function(): Node<VectorPractice>
return {
init = init,
draw = draw,
}
end

Assignment

Complete these tasks:

  1. Create start at position (100, 50)
  2. Create finish at position (100, 150)
  3. Calculate the distance between them
  4. Copy the ANSWER: line into the validator

Expected Output

=== Vector Constructors ===
Start: (100, 50)
Finish: (100, 150)
Distance: 100
ANSWER: 100

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: Type Safety with --!strict ⭐

Premise

Strict mode catches type mismatches at edit time rather than runtime. This is similar to TypeScript vs JavaScript—you get errors in the editor before running your code. Without --!strict, Node scripts won't be recognized by Rive.

Goal

By the end of this exercise, you will be able to Complete the type annotations for the Player type. Each field needs the correct type: number, string, or Vector.

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_Exercise3TypeSafetyWithStrict
  2. Attach and run:

    • Attach to any shape and press Play
  3. Open the Console:

    • View → Console

Starter Code

--!strict
-- Type annotations help catch errors at edit time

export type Player = {
-- TODO 1: Add type annotation for name (should be a string)
name: TODO,
-- TODO 2: Add type annotation for health (should be a number)
health: TODO,
-- TODO 3: Add type annotation for position (should be a Vector)
position: TODO,
}

function init(self: Player): boolean
-- Initialize with specific values
self.name = "Hero"
self.health = 100
self.position = Vector.xy(50, 75)

-- Print the typed values
print("Player created:")
print(` name = {self.name}`)
print(` health = {self.health}`)
print(` position = ({self.position.x}, {self.position.y})`)
print("ANSWER: typed")

return true
end

function draw(self: Player, renderer: Renderer)
end

return function(): Node<Player>
return {
init = init,
draw = draw,
name = "",
health = 0,
position = Vector.xy(0, 0),
}
end

Assignment

Complete these tasks:

  1. Complete the type annotations for the Player type. Each field needs the correct type: number, string, or Vector.
  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 4: Working Within the Sandbox ⭐⭐

Premise

The sandbox provides standard Luau math functions (sin, cos, rad) plus Rive graphics APIs. You can perform complex calculations and create procedural graphics—just no file I/O or network access.

Goal

By the end of this exercise, you will be able to Complete the trigonometry calculations: convert 45 degrees to radians, then calculate sin and cos. Store the cos value (rounded to 3 decimals) as the answer.

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_Exercise4WorkingWithinTheSandbox
  2. Attach and run:

    • Attach to any shape and press Play
  3. Open the Console:

    • View → Console

Starter Code

--!strict
-- Demonstrate math operations available in the sandbox

export type SandboxDemo = {
angle: number,
sinValue: number,
cosValue: number,
}

function init(self: SandboxDemo): boolean
print("Calculating for 45 degrees...")

-- TODO 1: Convert 45 degrees to radians using math.rad()
-- Store in self.angle
self.angle = 0

-- TODO 2: Calculate sin and cos of the angle
-- Store in self.sinValue and self.cosValue
self.sinValue = 0
self.cosValue = 0

-- Print results (these will use your calculated values)
print(`Radians: {string.format("%.3f", self.angle)}`)
print(`Sin: {string.format("%.3f", self.sinValue)}`)
print(`Cos: {string.format("%.3f", self.cosValue)}`)

-- TODO 3: Print the answer (cos value rounded to 3 decimals)
-- Format: print(`ANSWER: {string.format("%.3f", self.cosValue)}`)

return true
end

function draw(self: SandboxDemo, renderer: Renderer)
end

return function(): Node<SandboxDemo>
return {
init = init,
draw = draw,
angle = 0,
sinValue = 0,
cosValue = 0,
}
end

Assignment

Complete these tasks:

  1. Complete the trigonometry calculations: convert 45 degrees to radians, then calculate sin and cos. Store the cos value (rounded to 3 decimals) as the answer.
  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


Sandbox Limitations

The Rive scripting environment has intentional restrictions:

Not Available

FeatureReasonAlternative
File I/OSecurity - scripts cannot access filesystemUse ViewModel to store data
Network requestsSecurity - no HTTP or socket accessPre-load data into ViewModel
os.executeSecurity - cannot run system commandsN/A
loadstringSecurity - cannot dynamically load codeUse require() for Util scripts
require for external filesOnly Util scripts in your projectCreate all code in Rive

Available

FeatureNotesJavaScript Equivalent
math libraryFull math functionsMath.sin(), Math.cos(), etc.
string libraryString manipulationString methods
table libraryTable utilitiesArray/Object methods
printOutput to Consoleconsole.log()
Rive APIsVector, Path, Paint, Color, etc.Canvas 2D API
Luau, Not Lua

Rive uses Luau (Roblox's Lua variant) which includes features like type annotations, string interpolation ({variable}), and compound assignment (+=). These features don't exist in standard Lua.


Editor Integration

Script Creation Workflow

  1. Assets panel > + button > Script
  2. Choose Node or Util
  3. Name your script
  4. Write your code in the built-in editor

Attaching Node Scripts

  1. Select a node in the artboard
  2. In the properties panel, find the Script section
  3. Select your Node script from the dropdown

Script Errors

When your script has errors:

  • Red underlines appear in the code editor
  • Error messages show in the Console
  • The script won't appear in the add-to-scene menu until fixed

Knowledge Check

Q:Where does print() output appear in Rive?
Q:What is the difference between a Node script and a Util script?
Q:Why is --!strict recommended at the top of Rive scripts?
Q:Which of the following is NOT available in Rive scripts?

Common Mistakes

1. Expecting Browser Console Output

-- WRONG expectation: Looking in browser DevTools
print("Where is this?")

-- CORRECT: Check Rive's Console panel (View > Console)
print("I appear in Rive's Console!")

2. Trying to Access External Resources

-- WRONG: These don't exist in Rive
-- local file = io.open("data.txt") -- NO file access
-- local response = http.get(url) -- NO network

-- CORRECT: Use ViewModel for external data
-- Data is passed into Rive through runtime API
-- WORKS but not recommended: Missing strict directive
export type MyNode = {}
-- Script runs, but type errors won't be caught

-- RECOMMENDED: Include --!strict for type checking
--!strict
export type MyNode = {}
-- Type checker catches errors before runtime

Script Compilation Settings

Rive provides compilation settings that affect debugging and performance. Find these in the Design/Animate panel → Scripting section.

Optimization Level

Controls how aggressively the Luau compiler optimizes your code:

LevelDescriptionUse Case
NoneNo optimizations. Code runs as-written.Development - easier to debug
MediumBalanced tradeoff between performance and debuggability.General development
FullMaximum optimizations. Best runtime performance.Production - final export

Debug Level

Controls how much debug information is included in the compiled bytecode:

LevelDescriptionUse Case
NoneSmallest bytecode. No line numbers or stack traces.Production - smallest file size
MediumBasic debug info. Line numbers for errors and stack traces.General development
FullFull debug info. Line numbers, variable names, detailed error info.Development - best error messages

During development:

  • Optimization: None or Medium
  • Debug: Full

For production export:

  • Optimization: Full
  • Debug: None or Medium

Why It Matters

-- With Debug: None
-- Error: "error in script"

-- With Debug: Full
-- Error: "error at line 42: attempt to index nil value 'myVariable'"

Higher debug levels give you detailed error messages with line numbers and variable names, making it much easier to find and fix bugs.


Summary

  • Rive scripts run in a sandboxed Luau environment inside the editor
  • The Console panel shows all print() output and errors
  • Node scripts have a lifecycle (init, update, advance, draw)
  • Util scripts are reusable modules with no lifecycle
  • --!strict enables type safety and is strongly recommended
  • Script compilation settings affect debugging (Debug Level) and performance (Optimization Level)
  • The sandbox restricts file I/O, network access, and dynamic code loading

Next Steps