Skip to content

Lua Scripting API

SomeGuyWhoLovesCoding edited this page Mar 15, 2026 · 5 revisions

Note

Note: This document was AI-generated via claude sonnet 4.6 and is a work in progress. Things may change in the future.

FunkinView Lua Scripting

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.


Architecture Overview

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.


Lifecycle Callbacks

These are Lua functions your script can define. They are called automatically by the engine at the appropriate point in the gameplay lifecycle.

Core

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.

Song Events

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.

Conductor

Function Arguments
stepHit(step) step: Float
beatHit(beat) beat: Float
measureHit(measure) measure: Float

Note Events

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.

Pause / Resume

Function Arguments
pause()
pausePost() Also callable as postPause.
resume()
resumePost() Also callable as postResume.

Game Over

Function Arguments
gameOver()
gameOverPost() Also callable as postGameOver.

Lua API Reference

All functions below are available from any loaded Lua script.

Utility

trace(message)
-- Prints a message to the console.

Buffers

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.

Programs

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 height

Elements (Sprites)

Sprite 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

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)

Inline Text Formatting — setTextFormatMarkerPairs

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.


Display Utilities

setDisplayAngle(fromDisplay, rotation)
-- Rotates a named display (e.g. "display", "view", "roof").

Named Color Reference

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)

Example Scripts

Simple Stage Setup

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.
end

Animated Element on Beat

local 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.
end

Inline-Styled Text

function 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

Notes

  • Scripts are compiled and executed via LuaJIT using the linc_luajit bindings. The #if linc_luajit_funkinview compile 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 .lua files 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>.lua relative to the chart root.
  • Always call updateElementToBuffer followed by updateBuffer after 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 removeProgramFromDisplay in your dispose callback unless you want to detach a program earlier during gameplay.