-
Notifications
You must be signed in to change notification settings - Fork 0
Lua Cutscenes Recipe Book
This is a recipe-book intended to provide an overview of everything you might want to do with Lua Cutscenes, in approximate order of complexity. All code snippets here are free to use in your cutscenes and are released into the public domain.
This page is currently a work-in-progress!
A cutscene is a .lua file placed within your packaged mod; its path should usually be Assets/yournickname/yourmodname/cutscenename.lua, for which you would enter Assets/yournickname/yourmodname/cutscenename into an entity's parameters.
This path should use / characters rather than \, even on windows, due to the way Everest reads modded assets.
When something happens (like the player talking to a Lua Talker or entering a Lua Cutscene Trigger), one of the functions in that lua file will be run (provided it exists), and can take arguments (though it doesn't have to). All cutscenes can define the following functions:
function onBegin(room)
-- cutscene code goes here, and will be run when the cutscene begins
-- this one gets to do things that take time (more on that later!)
end
function onEnd(room, wasSkipped)
-- cutscene code goes here, and will be run when the cutscene ends
-- `room` is the current room, and `wasSkipped` is `true` if the cutscene was skipped
-- this one can't do things that take time
endAdditionally, triggers can define these (none of them can do things that take time):
function onEnter(player)
-- code here is run when the player enters the trigger
end
function onLeave(player)
-- code here is run when the player leaves the trigger
end
function onStay(player)
-- code here is run every frame while the player is touching the trigger
endYou can define functions other than these, and call them whenever you want, in order to decrease repetition in your code.
Lua Cutscenes provides many helper functions that let you do things in your cutscenes; a full list is available here, and the lua code those functions are written in is here.
To call a function, for example setCameraOffset, you write the name of the function (you don't need to put helpers), followed by the list of arguments you want to give it, like this:
function onBegin()
-- set the camera offset to 0.5 in the x direction
setCameraOffset(0.5)
-- wait for 1 second
wait(1)
-- set the camera offset to 0.2 in the x direction, and -0.3 in the y direction
setCameraOffset(0.2, -0.3)
-- wait again, for 1.5 seconds this time
wait(1.5)
-- make some variables to use (generally, pick an alphanumeric name without spaces, and starting with a letter):
local offset_x = -1
local offset_y = 0.1
-- then use them:
setCameraOffset(offset_x, offset_y)
endNote that in the documentation this function is written as helpers.setCameraOffset(x[, y]); the square brackets mean that that argument is optional.
You can get and set flags to store data, and this is typically used to
function onBegin()
-- find out if the cutscene has run before
local ran_before = getFlag("example_cutscene_ran")
-- if this is the first time:
if not ran_before then
-- set this for the next time round
setFlag("test_cutscene_ran", true)
-- do some cutscene stuff; in this case, trigger one of two dialog paths based on a different flag
if getFlag("some_other_flag") then
say("EXAMPLE_CUTSCENE_DIALOG_1")
else
say("EXAMPLE_CUTSCENE_DIALOG_2")
end
end
endYou can get a C# class like this:
local calc = require("#monocle.calc")It's good practice to have all your require statements at the top of your cutscene file, outside of any functions.
Some of these classes are already imported by the helper functions (see here)
Once you've done that, you can then call methods on the class:
local calc = require("#monocle.calc")
function onBegin()
-- output into the log the size of this angle, in radians:
-- . (1, 1)
-- /
-- /
-- /) <-- here
-- (0, 0) .----
log(calc.Angle(vector2(1, 1)))
endYou can also get static fields on classes:
local calc = require("#monocle.calc")
function onBegin()
-- how many radians are in a circle?
log(calc.Circle)
endYou can find a list of all the methods and fields of a class by decompiling it or looking at its metadata in an IDE.
Note that whether a method is marked as public or private is not relevant when using lua — you can call private methods and get private fields the same way as public ones.
When getting values or calling methods on C# objects, it's important to know about the differences between static and instance methods.
A static method or field exists on the class, like calc.Circle or csharpVector2.Lerp(start_pos, end_pos, amount), while an instance method or field exists on some instance of that class, like player.Position or level:GetSpawnPoint(pos).
In Lua, an instance method is called with instance:methodname() rather than instance.methodname().
Enum members, for technical reasons, cannot be retrieved like normal static fields.
Instead, you can use the getEnum(enum, value) helper function, called with the full name of the enum type and the name of the member you want.
For example, to get MoveBlock.Directions.Right, you can use getEnum("Celeste.MoveBlock.Directions", "Right").
The following functions (adapted from the vanilla cutscene in the last checkpoint of Farewell) allow you to create and remove a Badeline entity that doesn't do anything by itself, which is good for dialog:
-- store the badeline entity outside the functions, so it can be accessed whenever necessary
local badeline = nil
function badeline_appears(left_side)
-- determine the position and flipping properties
if left_side then
local pos_x = player.Position.X - 18
local scale = 1
else
local pos_x = player.Position.X + 18
local scale = -1
end
local pos_y = player.Position.Y - 8
-- create and add a new badeline dummy entity
badeline = celeste.BadelineDummy(vector2(pos_x, pos_y))
badeline.Sprite.Scale.X = scale;
getLevel():Add(badeline)
-- play sound + effect
getLevel().Displacement:AddBurst(badeline.Center, 0.5, 8, 32, 0.5);
Audio.Play("event:/char/badeline/maddy_split", badeline.Position);
-- wait until the next frame so all that can take effect properly
wait()
end
function badeline_vanishes()
-- tell the badeline entity to disappear
badeline:Vanish()
Input.Rumble(getEnum("Celeste.RumbleStrength", "Medium"), getEnum("Celeste.RumbleLength", "Medium"))
-- clear the stored variable so the memory can be used for something else
badeline = nil
-- wait until the next frame
wait()
endIn Monocle (the engine Celeste is built with), there's a concept of coroutines, which are a way to have a function pause and then continue later.
You can tell that a C# method is a coroutine because it will return an IEnumerator.
If a function or method is set up as a coroutine, you can do things like waiting (with wait), calling other coroutines, and switching to other coroutines entirely.
The coroutine.await() function allows you to call C# coroutines.
For example:
-- minimal version of CS02_Mirror
function onBegin()
-- get a DreamBlock from the room
local dream_block = getEntity("DreamBlock")
-- get a DreamMirror from the room
local mirror = getEntity("DreamMirror")
local break_direction = 0
-- break the mirror
coroutine.yield(mirror:BreakRoutine(break_direction))
-- this code only runs once it's finished breaking:
coroutine.yield(dream_block:Activate())
-- the cutscene only finishes once the dream block has activated
endZooming the camera is done like this:
-- zoom to a point
-- * position is a vector2 for where to zoom to
-- * zoom_amount is a number (how much to zoom)
-- * duration is a number (how many seconds the zoom should take)
-- because this uses coroutine.yield, your cutscene will only resume after the zoom is finished
coroutine.yield(engine.Scene:ZoomTo(position, zoom_amount, duration))
-- zoom out again
-- again, your cutscene will only resume after the zoom is finished
coroutine.yield(engine.Scene:ZoomBack(duration))
-- zoom instantly
engine.Scene:ZoomSnap(vector2(x_position, y_position), zoom_amount)
-- or
engine.Scene:ResetZoom()
-- zoom to a point, but from an already-zoomed-in state
-- works like ZoomTo, but cleanly transitions
coroutine.yield(engine.Scene:ZoomAcross(new_position, new_zoom_amount, duration))Note that the positions here are screen-space positions.
A Coroutine is a type of Component (a thing that can be put onto an entity, and which does extra stuff of some kind) that can be used as a sort of "glue" between an entity and a function that takes time.
You use them like this:
local monocle = require("#monocle")
-- store the coroutine out here so we can cancel it
local coroutine = nil
function onBegin()
-- create a Coroutine to zoom the camera
coroutine = monocle.Coroutine(engine.Scene:ZoomTo(position, zoom_amount, duration))
-- attach the Coroutine to the cutscene entity (either trigger or talker)
cutsceneEntity:Add(coroutine)
-- the camera zoom is now taking effect
-- this will pause the cutscene code until it's finished, but the camera zoom will continue in the background
say("EXAMPLE_CUTSCENE_DIALOG_1")
end
function onEnd(room, wasSkipped)
-- now, what if the player was really quick mashing buttons? let's make sure the zoom is cancelled
-- the if statement is to make sure we don't try to cancel nil
if coroutine then
coroutine:Cancel()
end
engine.Scene:ResetZoom()
endAltSides Home
AltSides Customisable Fields
Brokemia Helper Entities
Brokemia Helper Triggers
Brokemia Helper Extra Features
CollabUtils2 Home
CollabUtils2 Entities
CollabUtils2 Triggers
Contort Home
Contort Entities
Contort Triggers
Helping Hand Entities
Helping Hand Customisable Entities
Helping Hand Triggers
Helping Hand Customisable Stylegrounds
Helping Hand Extra Features