-
Notifications
You must be signed in to change notification settings - Fork 4
Lua Scripting API
Note
Note: This document was AI-generated via claude sonnet 4.6 and is a work in progress. Things may change in the future.
A LuaJIT-powered scripting layer for Funkin' View that lets chart creators hook into gameplay events, create custom visual elements, and manipulate the playfield at runtime.
The system is composed of two main lua classes:
| Class | Responsibility |
|---|---|
FunkinViewLua |
Top-level manager. Loads .lua files, routes function calls to all active VM instances. |
FunkinViewLuaScript |
Wraps a single Lua VM (llua.State). Handles callback registration. |
And also, there are 3 main components that will be included:
| Class | Responsibility |
|---|---|
CustomLuaSpriteComponent |
Exposes sprite/text/program/buffer APIs to Lua scripts. |
CustomLuaActorsComponent |
Exposes animated sprite (Actor) APIs to Lua scripts. |
CustomPlayFieldComponent |
Exposes PlayField component APIs to Lua scripts. |
Scripts are loaded from the chart's directory. Optionally, a stage-specific script can be placed at stages/<stageName>.lua relative to the chart root and will be loaded automatically.
These are Lua functions your script can define. They are called automatically by the engine at the appropriate point in the gameplay lifecycle.
| Function | Arguments | Notes |
|---|---|---|
create() |
— | Called once on script load, before createPost. |
createPost() |
— | Called after the playfield finishes initializing. Recommended entry point for stage setup. |
update(deltaTime) |
deltaTime: Float |
Called every frame before gameplay logic updates. |
updatePost(deltaTime) |
deltaTime: Float |
Called every frame after gameplay logic updates. Also callable as postUpdate. |
render() |
— | Called every render frame. |
renderPost() |
— | Called after the render pass. Also callable as postRender. |
dispose() |
— | Called when the playfield begins tearing down. |
disposePost() |
— | Called after disposal is complete. Also callable as postDispose. |
| Function | Arguments |
|---|---|
startSong(title, difficulty) |
title: String, difficulty: String
|
startSongPost(title, difficulty) |
Also callable as postStartSong. |
stopSong(title, difficulty) |
title: String, difficulty: String
|
stopSongPost(title, difficulty) |
Also callable as postStopSong. |
| Function | Arguments |
|---|---|
stepHit(step) |
step: Float |
beatHit(beat) |
beat: Float |
measureHit(measure) |
measure: Float |
| Function | Arguments |
|---|---|
hitNote(pos, index, duration, type, timing, notesInOne) |
|
hitNotePost(pos, index, duration, type, timing, notesInOne) |
Also callable as postHitNote. |
missNote(pos, index, duration, type, notesInOne) |
|
missNotePost(pos, index, duration, type, notesInOne) |
Also callable as postMissNote. |
completeSustain(pos, index, duration, type) |
|
completeSustainPost(pos, index, duration, type) |
Also callable as postCompleteSustain. |
releaseSustain(pos, index, duration, type) |
|
releaseSustainPost(pos, index, duration, type) |
Also callable as postReleaseSustain. |
| Function | Arguments |
|---|---|
pause() |
— |
pausePost() |
Also callable as postPause. |
resume() |
— |
resumePost() |
Also callable as postResume. |
| Function | Arguments |
|---|---|
gameOver() |
— |
gameOverPost() |
Also callable as postGameOver. |
All functions below are available from any loaded Lua script.
trace(message)
-- Prints a message to the console.Buffers hold collections of sprite elements for batch rendering. You must call updateBuffer after modifying any element inside it.
customBufferNew(bufferName, minSize, growSize, autoShrink)
-- Creates a new named buffer.
-- bufferName: String (must not be empty or nil)
-- minSize: Int
-- growSize: Int (default: 0)
-- autoShrink: Bool (default: false)
updateBuffer(bufferName)
-- Flushes and re-uploads the buffer's data to the GPU.
-- Call this after any element modification to see the change rendered.A program links a buffer to a texture for rendering. Programs are what get added to displays.
customProgramNew(programName, customBuffer)
-- Creates a new render program attached to an existing buffer.
addTextureToProgram(programName, texturePNG, disableAntialiasing)
-- Loads a PNG from the assets folder and binds it to the program.
-- texturePNG: String (relative asset path, e.g. "images/myTexture.png")
-- disableAntialiasing: Bool (default: false)
wipeTextureFromProgram(programName)
-- Unbinds and removes the texture from the program.
addProgramToDisplay(programName, toDisplay, isBehind, atCustomProgram)
-- Adds the program to a named display ("display", "view", or "roof").
-- isBehind: Bool (default: false) — render behind existing programs
-- atCustomProgram: String (optional) — insert relative to another named program
removeProgramFromDisplay(programName, fromDisplay)
-- Removes the program from the specified display.
-- NOTE: When the scripting system is disposed, all programs are automatically
-- removed from their parent displays. You only need to call this manually
-- if you want to remove a program mid-game.
getTextureCoordinateX(programName) --> Int -- texture pixel width
getTextureCoordinateY(programName) --> Int -- texture pixel heightSprite elements live inside buffers. After changing any property, call updateElementToBuffer then updateBuffer to push the changes to the GPU.
customElementNew(elem, x, y, w, h, color)
-- Creates a new sprite element.
-- x, y: Int (position)
-- w, h: Float (size / UV coordinates)
-- color: String (color name or "0xAARRGGBB" hex, default: "white")
-- Buffer management
addElementToBuffer(elemName, bufferName)
removeElementFromBuffer(elemName, bufferName)
updateElementToBuffer(elemName, bufferName)
-- Marks the element dirty so the buffer re-uploads it on the next updateBuffer call.
-- Position
setElementPos(elemName, x, y)
getElementPosX(elemName) --> Float
getElementPosY(elemName) --> Float
-- Size / UV coordinates
setElementCoordinate(elemName, w, h)
getElementCoordinateX(elemName) --> Float
getElementCoordinateY(elemName) --> Float
-- Color / tint
setElementTint(elemName, color)
getElementTint(elemName) --> String -- returns "0xAARRGGBB"
-- Alpha (0.0 – 1.0)
setElementAlpha(elemName, alpha)
getElementAlpha(elemName) --> Float
-- Rotation (degrees)
setElementAngle(elemName, rotation)
getElementAngle(elemName) --> Float
-- Centering
screenCenterElement(elemName, fromDisplay, axis)
-- axis: "X", "Y", or "XY"Text elements automatically bind to a live gameplay value (such as score or combo) and update it on screen each frame.
customTextNew(textElem, x, y, toDisplay, text, font, color, outlineSize, outlineColor)
-- Creates a new text element.
-- toDisplay: String — a PlayField field name to track (e.g. "score", "combo", "accuracy")
-- text: String — the format string displayed (use markers for styled sections, see below)
-- font: String (default: "vcr")
-- color: String (default: "white")
-- outlineSize: Int (default: 0)
-- outlineColor: String (default: "black")
-- Position
setTextPos(textElem, x, y)
getTextPosX(textElem) --> Float
getTextPosY(textElem) --> Float
-- Content
setTextString(textElem, text)
getTextString(textElem) --> String
-- Color
setTextColor(textElem, color)
getTextColor(textElem) --> String -- returns "0xAARRGGBB"
setTextOutlineColor(textElem, color)
getTextOutlineColor(textElem) --> String
setTextOutlineSize(textElem, size)
getTextOutlineSize(textElem) --> Float
-- Alpha (0.0 – 1.0)
setTextAlpha(textElem, alpha)
getTextAlpha(textElem) --> Float
-- Centering
screenCenterText(textElem, axis)
-- axis: "X", "Y", or "XY"
-- Visibility
hideText(textElem)
showText(textElem)Text elements support inline color formatting via marker pairs. A marker is a delimiter string embedded in your text string that switches the color (and optional outline) for everything between a pair of identical markers.
setTextFormatMarkerPairs(textElem, pairs)
-- Applies a list of marker-color rules to the text element.
-- textElem: String — name of an existing text element
-- pairs: Array<Table> — list of marker definitions (see format below)Each entry in pairs is a table with the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
marker |
String | Yes | The delimiter string used in the text (e.g. "#", "^", "_bold_") |
color |
String | Yes | Color applied to text between this marker pair |
outlineColor |
String | No | Outline color for this section (default: "0x00000000") |
outlineSize |
Float | No | Outline thickness for this section (default: 0.0) |
Example:
-- Text string uses markers as delimiters.
-- "#!#" means: '#' toggles the red color, '!' is literal content, '#' toggles it off.
customTextNew("myText", 0, 0, "display", "#Hello# ^World^", "vcr", "white")
setTextFormatMarkerPairs("myText", {
{ marker = "#", color = "0xffff0000" }, -- red
{ marker = "^", color = "0xff00ddff", outlineColor = "black", outlineSize = 2.0 } -- cyan with outline
})
screenCenterText("myText", "XY")In this example, the word Hello renders in red and World renders in cyan with a black outline.
setDisplayAngle(fromDisplay, rotation)
-- Rotates a named display (e.g. "display", "view", "roof").Named colors are matched case-insensitively, so "white", "White", and "WHITE" all resolve to the same value.
Note
Hex values are shown in both the engine's Lua-facing format (0xAARRGGBB) and as HTML rgba() for reference. The internal Color.hx constants use 0xRRGGBBAA byte order, which is the reverse.
| Name | 0xAARRGGBB |
rgba() |
Preview |
|---|---|---|---|
Black |
0xFF000000 |
rgba(0, 0, 0, 1.0) |
|
White |
0xFFFFFFFF |
rgba(255, 255, 255, 1.0) |
|
Grey |
0xFF7F7F7F |
rgba(127, 127, 127, 1.0) |
|
Grey1 |
0xFF1F1F1F |
rgba(31, 31, 31, 1.0) |
|
Grey2 |
0xFF3F3F3F |
rgba(63, 63, 63, 1.0) |
|
Grey3 |
0xFF5F5F5F |
rgba(95, 95, 95, 1.0) |
|
Grey4 |
0xFF7F7F7F |
rgba(127, 127, 127, 1.0) |
|
Grey5 |
0xFF9F9F9F |
rgba(159, 159, 159, 1.0) |
|
Grey6 |
0xFFBFBFBF |
rgba(191, 191, 191, 1.0) |
|
Grey7 |
0xFFDFDFDF |
rgba(223, 223, 223, 1.0) |
|
Red |
0xFFFF0000 |
rgba(255, 0, 0, 1.0) |
|
Red1 |
0xFF3F0000 |
rgba(63, 0, 0, 1.0) |
|
Red2 |
0xFF7F0000 |
rgba(127, 0, 0, 1.0) |
|
Red3 |
0xFFBF0000 |
rgba(191, 0, 0, 1.0) |
|
Green |
0xFF00FF00 |
rgba(0, 255, 0, 1.0) |
|
Green1 |
0xFF003F00 |
rgba(0, 63, 0, 1.0) |
|
Green2 |
0xFF007F00 |
rgba(0, 127, 0, 1.0) |
|
Green3 |
0xFF00BF00 |
rgba(0, 191, 0, 1.0) |
|
Blue |
0xFF0000FF |
rgba(0, 0, 255, 1.0) |
|
Blue1 |
0xFF00003F |
rgba(0, 0, 63, 1.0) |
|
Blue2 |
0xFF00007F |
rgba(0, 0, 127, 1.0) |
|
Blue3 |
0xFF0000BF |
rgba(0, 0, 191, 1.0) |
|
Yellow |
0xFFFFFF00 |
rgba(255, 255, 0, 1.0) |
|
Magenta |
0xFFFF00FF |
rgba(255, 0, 255, 1.0) |
|
Cyan |
0xFF00FFFF |
rgba(0, 255, 255, 1.0) |
|
Gold |
0xFFFFD700 |
rgba(255, 215, 0, 1.0) |
|
Orange |
0xFFFFA500 |
rgba(255, 165, 0, 1.0) |
|
Brown |
0xFF8B4513 |
rgba(139, 69, 19, 1.0) |
|
Purple |
0xFF800080 |
rgba(128, 0, 128, 1.0) |
|
Pink |
0xFFFFC0CB |
rgba(255, 192, 203, 1.0) |
|
Lime |
0xFFCCFF00 |
rgba(204, 255, 0, 1.0) |
This example shows the typical pattern for building a stage in createPost. A local helper function (setupStageObject) is used to bundle the boilerplate of creating a buffer, program, texture, and element into a single call.
local stageback_img = "assets/stages/stage/stageback.png"
local stagefront_img = "assets/stages/stage/stagefront.png"
local stage_light_img = "assets/stages/stage/stage_light.png"
local stagecurtains_img = "assets/stages/stage/stagecurtains.png"
function createPost()
-- Static background layers
setupStageObject("stageback", stageback_img, -450, -300, 0.9, 0.9, "view", true)
setupStageObject("stagefront", stagefront_img, -550, 450, 0.9 * 1.1, 0.9, "view", false, "stageback")
-- Lighting and curtains
setupStageObject("stage_light", stage_light_img, -125, -400, 0.9 * 1.1, 0.9, "view", false, "stageback")
setupStageObject("stage_light2", stage_light_img, -1225, -400, -0.9 * 1.1, -0.9, "view", false, "stageback")
setupStageObject("stagecurtains", stagecurtains_img, -1300, -1000, 1.3 * 0.9, 1.3, "view", false)
end
-- Helper: creates a buffer, program, texture, and sprite element in one call.
-- scaleX/scaleY are multiplied against the texture's native pixel dimensions.
-- Pass a negative scale to flip the image on that axis.
function setupStageObject(key, img, x, y, scaleX, scaleY, cam, behind, atProgram)
customBufferNew(key, 1)
customProgramNew(key, key)
addTextureToProgram(key, img)
addProgramToDisplay(key, cam, behind, atProgram)
customElementNew(key, x, y,
getTextureCoordinateX(key) * scaleX,
getTextureCoordinateY(key) * scaleY)
addElementToBuffer(key, key)
end
function dispose()
-- Programs are removed from their displays automatically on dispose,
-- so explicit removeProgramFromDisplay calls are not required here.
-- Only needed if you want to detach a program earlier during gameplay.
endlocal mySprite = "box"
local myBuf = "myBuffer"
local myProg = "myProgram"
function createPost()
customBufferNew(myBuf, 64)
customProgramNew(myProg, myBuf)
addTextureToProgram(myProg, "images/myTexture.png")
addProgramToDisplay(myProg, "display")
customElementNew(mySprite, 0, 0, 100, 100, "white")
screenCenterElement(mySprite, "display", "XY")
addElementToBuffer(mySprite, myBuf)
updateElementToBuffer(mySprite, myBuf)
updateBuffer(myBuf)
end
function beatHit(beat)
setElementAngle(mySprite, beat * 15)
updateElementToBuffer(mySprite, myBuf)
updateBuffer(myBuf)
end
function dispose()
-- Programs are automatically removed from displays on dispose.
-- removeProgramFromDisplay(myProg, "display") is not required.
endfunction createPost()
customTextNew("label", 0, 0, "display", "#Score:# ^{value}^", "vcr", "white")
setTextFormatMarkerPairs("label", {
{ marker = "#", color = "0xffffcc00" }, -- gold label
{ marker = "^", color = "0xffffffff", outlineColor = "black", outlineSize = 1.5 } -- white value with outline
})
screenCenterText("label", "X")
setTextPos("label", getTextPosX("label"), 40)
end
function dispose()
-- text elements are cleaned up automatically, but you can hide them early:
hideText("label")
end- Scripts are compiled and executed via LuaJIT using the
linc_luajitbindings. The#if linc_luajit_funkinviewcompile flag must be set for the scripting system to be active. - All callbacks silently no-op if the corresponding Lua function is not defined in the script.
- Multiple
.luafiles in the chart directory are loaded and called in sequence. - Returning
"##FUNKINVIEWLUA_FUNCTION_STOP"from a callback halts execution of that callback across all subsequent VM instances for that call. - Stage scripts are loaded from
stages/<stageName>.luarelative to the chart root. - Always call
updateElementToBufferfollowed byupdateBufferafter modifying a sprite's properties, or changes will not appear on screen. - Programs are automatically removed from their parent displays when the scripting system is disposed. You do not need to call
removeProgramFromDisplayin yourdisposecallback unless you want to detach a program earlier during gameplay.
But on one condition... You will most likely misuse the optimization techniques in your own game or overcomplicate them.
Please don't read this it's a joke I cocked up early in the wiki
Hello sidebar test.