Skip to content

Lua Scripting API

SomeGuyWhoLovesCoding edited this page Jun 23, 2026 · 20 revisions

Note

This document was generated via claude sonnet 4.6 (and later deepseek, qwen, and then GLM 5 turbo) 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.

Refer to image Progamming in Lua to learn the basics of the Lua scripting language.


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. Also manages a dedicated isolated VM for custom note movement formulas.
FunkinViewLuaScript Wraps a single Lua VM (llua.State). Handles callback registration.

Components are attached to each FunkinViewLua instance and extend the base class LuaComponentObject, which provides shared access to the FunkinViewLua instance and its associated PlayField:

LuaComponentObject

Member Type Description
parent FunkinViewLua The owning FunkinViewLua instance.
playField PlayField The PlayField associated with parent. Resolved from parent.parent in the constructor.
new(parent) Constructor Initialises parent and resolves playField from parent.parent.
addCallbacksList(vm) Method Override in subclasses to register callbacks and variables on a FunkinViewLuaScript VM. No-op in base.
updateVariablesList(vm) Method Override in subclasses to update runtime variables on each frame. No-op in base.
dispose() Method Nulls out parent and playField. Always call super.dispose() in subclasses.

The three main components registered on each FunkinViewLua instance all extend LuaComponentObject:

Class Responsibility
CustomLuaSpriteComponent Exposes sprite/text/program/buffer APIs to Lua scripts.
CustomPlayFieldComponent Exposes PlayField component APIs to Lua scripts, including the custom note movement formula system.
CustomAnimationComponent Exposes animated sprite (Actor) 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.


Important: Returning From Callbacks

Warning

Every Lua callback function must return Function_Continue at the end. Returning any other value — or accidentally falling off the end of a function without returning — can halt execution across all subsequently loaded scripts for that callback. Always include an explicit return Function_Continue as the last line of every callback.

function beatHit(beat)
    -- do your thing
    return Function_Continue  -- REQUIRED
end

The three control values are:

Value Effect
Function_Continue Default. Allows execution to continue in subsequent VM instances.
Function_Stop Stops the callback from executing in all subsequent VMs for this call.
Function_StopLua Stops execution in the current VM only.

Note

missNote is the one exception where the return value has gameplay meaning: returning Function_Continue from missNote cancels the miss, preventing health loss, score penalty, and combo break. Return nothing or Function_Stop to allow the miss to proceed normally.


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 and note formula registration.
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.
startCountdown(title, difficulty)

Stage Events

Function Arguments Notes
onChangeStage(stage) stage: String Called when setStage is used to override the active stage.

Conductor

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

Note Events

Function Arguments Notes
hitNote(pos, index, duration, type, timing, notesInOne)
hitNotePost(pos, index, duration, type, timing, notesInOne) Also callable as postHitNote.
missNote(pos, index, duration, type, notesInOne) Returning Function_Continue cancels the miss — health/score penalties are skipped.
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.

Custom Note Movement Formulas

Funkin' View includes a dedicated, high-performance system for dynamically calculating note positions via Lua. Unlike standard lifecycle callbacks which run in the main script VMs, the note formula executes in its own isolated Lua VM that is optimized for per-frame, per-note calls.

How It Works

  1. Call setCustomNoteMoveFormula(luaString) from a standard Lua script (typically in createPost).
  2. The engine compiles the string once and caches the resulting chunk in the isolated VM.
  3. Every frame, for each active note, the engine calls the global noteFormula function inside the isolated VM.
  4. The returned values are applied directly to the note sprite and its associated sustain sprite.
  5. Call resetCustomNoteMoveFormula() to clear the formula and revert to default engine movement.

The noteFormula Function

Your script string must define a global function named noteFormula with the following signature:

function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    -- calculations
    return x, y, scale, sustainRot, scrollMultiplier
end

Arguments

Argument Type Description
diff Float Time difference in milliseconds until the note must be hit. Decreases as the note approaches the receptor.
scrollSpeed Float The chart's scroll speed value.
receptorX Float X position of the receptor for this note's lane.
receptorY Float Y position of the receptor for this note's lane.
index Float The lane index (0-based).
type Float The note type ID.

Return Values

The function must return exactly 4 values. All returns must be numbers — returning nil for any value cancels the entire result for that note (see Cancelling the Formula below).

Return Type Default Description
x Float 0 Note sprite X position.
y Float 0 Note sprite Y position.
scale Float 1 Note sprite scale multiplier.
sustainRot Float 0 Sustain sprite rotation in degrees.
scrollMutliplier Float 1 Scroll speed multiplier .

Cancelling the Formula

If noteFormula returns nil for any of the four return values, the entire result is discarded for that note on that frame, and the engine falls back to its default movement logic. This lets you conditionally opt out per-note:

function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    -- Only apply custom movement to notes within 1 second of the receptor
    if diff > 1000 then
        return nil  -- too far away, let the engine handle it
    end

    local x = receptorX + (index * 55)
    local y = receptorY - (diff * scrollSpeed)
    return x, y, 1.0, 0, 0.45
end

Isolated VM Environment

The note formula runs in a separate Lua state from your main chart scripts. This is an intentional design choice for performance and safety. Key implications:

  • No access to FunkinView callbacks (trace, setCameraScroll, getProperty, etc.)
  • No access to global variables from other scripts (scrollSpeed, curBeat, health, etc.) — you must rely entirely on the 6 arguments passed to noteFormula
  • Full access to standard Lua libraries (math, string, table, io, etc.)
  • Use math.sin(x), math.cos(x), etc. — bare sin(x) is not available (standard Lua places these in the math table, not as globals)

Performance Notes

Important

The noteFormula function is called every frame, for every active note on screen. In a 9-key chart with 50 visible notes at 60 FPS, that is 3,000 calls per second.

  • The script string is compiled once when setCustomNoteMoveFormula is called. Subsequent frames reuse the cached function — there is no per-frame parsing overhead.
  • Arguments are pushed as raw floats and return values are read as raw floats via direct Lua.pushnumber / Lua.tonumber calls. There is no Dynamic boxing or Convert.fromLua overhead.
  • Avoid allocating tables, strings, or closures inside noteFormula. Allocating objects in a hot loop creates GC pressure that can cause frame stutters. Precompute any lookup tables outside the function body.

Minimal Example

function createPost()
    setCustomNoteMoveFormula([[
function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    local x = receptorX + (index * 55)
    local y = receptorY - (diff * scrollSpeed * 0.45)
    return x, y, 1.0, 0
end
    ]])
    return Function_Continue
end

Wave Effect Example

function createPost()
    setCustomNoteMoveFormula([[
function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    local spacing = 55
    local x = receptorX + (index * spacing)
    local y = receptorY - (diff * scrollSpeed * 0.45)

    -- Add a subtle sine wave offset based on time and lane
    local wave = math.sin(diff * 0.005 + index * 0.8) * 8
    x = x + wave

    -- Scale notes slightly as they approach the receptor
    local proximity = math.max(0, 1.0 - diff * 0.0002)
    local scale = 0.8 + (proximity * 0.2)

    return x, y, scale, 0
end
    ]])
    return Function_Continue
end

Spiral Example

function createPost()
    setCustomNoteMoveFormula([[
-- Precompute center position (adjust these to your stage layout)
local centerX = 640
local centerY = 360

function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    -- Normalize diff into a 0-1 range based on a 2-second window
    local t = math.max(0, math.min(1, diff / 2000))

    -- Angle spreads lanes out, and rotates over time
    local angle = (index * 0.7) + (t * math.pi * 4)

    -- Radius shrinks as the note approaches hit time
    local radius = 50 + (t * 250)

    local x = centerX + math.cos(angle) * radius
    local y = centerY + math.sin(angle) * radius

    -- Scale down as notes spiral inward
    local scale = 0.5 + (t * 0.5)

    return x, y, scale, angle * (180 / math.pi)
end
    ]])
    return Function_Continue
end

Lua API Reference

All functions below are available from any loaded Lua script.

Utility

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

Global Variables

These variables are set once at script load time and reflect the state of the engine at that moment.

Control Flow

Variable Type Description
Function_Stop String Return this to stop the callback from executing in all subsequent VM instances.
Function_Continue String Return this to continue execution in the next VM. Required at the end of every callback.
Function_StopLua String Return this to stop execution in the current VM only.

Engine / Build

Variable Type Description
buildTarget String The current platform name (e.g. "Windows", "Linux", "Mac").
version String The current engine version string (MainMenu.fnfpVer).
modFolder String Path to the active mod/custom asset folder. Alias of currentModDirectory.
currentModDirectory String Path to the current mod/custom asset directory.

Song / Chart

Variable Type Description
songName String The display song name (respects any custom name override).
loadedSongName String The raw song name from the chart header.
chartPath String The directory path of the loaded chart.
songLength Float Total length of the song in milliseconds (Mixer.length).
songPosition Float The song position in milliseconds at the time the script was loaded.
startedCountdown Bool Whether the countdown has started. Initially false.
loadedStage String The stage name from the chart header.
curStage String The current active stage name (respects any custom stage override).
scrollSpeed Float The chart's scroll speed.
playbackRate Float The current playback speed multiplier (Mixer.speed).

Conductor

Variable Type Description
curBpm Float The BPM value from the chart header.
bpm Float The conductor's current live BPM (Main.conductor.bpm).
crochet Float Duration of one beat in milliseconds (Main.conductor.crochet).
stepCrochet Float Duration of one step in milliseconds (Main.conductor.stepCrochet).
measureCrochet Float Duration of one measure in milliseconds (Main.conductor.measureCrochet).
curStep Float The conductor's current step counter (Main.conductor.curStep).
curBeat Float The conductor's current beat counter (Main.conductor.curBeat).
curMeasure Float The conductor's current measure counter (Main.conductor.curMeasure).

Score / Gameplay State

Variable Type Description
score number The player's current score. Converted from Haxe Int64 to Lua number.
misses number The player's current miss count. Converted from Haxe Int64 to Lua number.
combo number The player's current combo. Converted from Haxe Int64 to Lua number.
deaths Int The number of times the player has died (playField.deathCounter).
totalNotesHit number Accuracy numerator — total weighted notes hit (playField.accuracy.left).
totalPlayed number Accuracy denominator — total weighted notes played (playField.accuracy.right).
inGameOver Bool Whether the field is currently in the game over sequence (playField.field?.isInGameOver).
botPlay Bool Whether bot play is enabled (playField.botplay).
practice Bool Whether practice mode is enabled (playField.practiceMode).
health Float The player's current health (playField.health).

Important

Int64 Number Handling

The engine uses Haxe Int64 internally for score, misses, and combo to support large numbers without overflow. However, Lua uses double-precision floating point numbers with a maximum safe integer of 2^53 (≈9 quadrillion).

Automatic Conversion:

  • Haxe → Lua: Int64 values are automatically converted to Lua numbers using Tools.int64ToFloat()
  • Lua → Haxe: Lua numbers are converted back to Int64 using Int64.fromFloat()

Precision Warning: Values beyond 2^53 will lose precision when passed to Lua. For normal gameplay, this limit is practically unreachable (it would take billions of years of max score per frame to exceed).

Lua Script Usage:

function update()
    -- Works normally - Lua treats them as numbers
    setTextString(getScoreText(), "Score: " .. score)
    setTextString(getWatermarkText(), "Misses: " .. misses)
end

function hitNote()
    -- Adding values works seamlessly
    addScore(100)  -- Lua number 100 → Int 100
    addMisses(1)   -- Lua number 1 → Int 1
end

Troubleshooting: If you see errors like attempt to concatenate global 'score' (a nil value) or attempt to concatenate global 'misses' (a table value), ensure updateVariablesList is being called each frame to refresh these values in the Lua context.

Screen / Settings

Variable Type Description
screenWidth Int The current render width of the game window (Main.VARIABLE_WIDTH).
screenHeight Int The current render height of the game window (Main.VARIABLE_HEIGHT).
downscroll Bool Whether downscroll is enabled in preferences.
frameRate Int The configured frame rate from graphics settings.
hideHud Bool Whether the HUD is hidden in preferences.
smoothHealthbar Bool Whether smooth healthbar is enabled.
scoreZoom Bool Whether score text bopping is enabled.
cameraZoomOnBeat Bool Whether camera zooming on beat is enabled.

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"

Actors (Animated Sprites)

Actors are animated sprites managed by the CustomAnimationComponent. The API uses String Keys to reference actors. When you create or retrieve an actor, you get a string identifier which you then pass to all subsequent manipulation functions.

Retrieving Vanilla Actors

For vanilla characters that are always present in the playfield, you can retrieve their string keys directly:

getPlayer() --> String
-- Returns the string key for the player (Boyfriend).

getBF() --> String
-- Alias for getPlayer().

getSpectator() --> String
-- Returns the string key for the spectator (Girlfriend).

getGF() --> String
-- Alias for getSpectator().

getOpponent() --> String
-- Returns the string key for the opponent (Dad).

Creating Custom Actors

customActorNew(actorName, toDisplay, actorType, actorChar, x, y, fps)
-- Creates a new custom actor and registers it under the specified `actorName`.
-- actorName:  String (must not be empty or nil)
-- toDisplay:  String (e.g., "view", "display")
-- actorType:  String (the character type/spritesheet to load)
-- actorChar:  String (specific character configuration)
-- x, y:       Int
-- fps:        Int

addCustomActor(actorName)
-- Adds the registered custom actor to its display/buffer so it can be rendered.
-- actorName: String (the key used in customActorNew)

Animation Control

playCustomActorAnimation(actorName, anim, loop)
-- Plays a specific animation.
-- actorName: String
-- anim:      String
-- loop:      Bool (default: false)
 
stopCustomActorAnimation(actorName)
-- Stops the currently playing animation.
 
setCustomActorFinishAnim(actorName, anim)
-- Sets the animation to automatically play once the current animation finishes.
-- actorName: String
-- anim:      String

Mania / Pose Helpers

playSingIdCustomActorAnimation(actorName, index, loop)
-- Plays an animation mapped to a specific sing index (useful for >4 mania).
-- actorName: String
-- index:     Int
-- loop:      Bool
 
playMissIdCustomActorAnimation(actorName, index, loop)
-- Plays an animation mapped to a specific miss index.
 
preComputeCustomActorSingPoses(actorName, anims)
-- Precomputes sing poses for a list of animations to optimize runtime performance.
-- actorName: String
-- anims:     Array<String>
 
preComputeCustomActorMissPoses(actorName, anims)
-- Precomputes miss poses for a list of animations.

Properties

setCustomActorPos(actorName, x, y)
getCustomActorPosX(actorName) --> Float
getCustomActorPosY(actorName) --> Float
 
setCustomActorAngle(actorName, r)
getCustomActorAngle(actorName) --> Float
 
setCustomActorFPS(actorName, fps)
getCustomActorFPS(actorName) --> Float
 
getCustomActorTag(actorName) --> String
 
setCustomActorShake(actorName, shake)
-- Enables or disables the shaking effect.
-- actorName: String
-- shake:     Bool
 
setCustomActorStartToEndShakeFrames(actorName, start, end)
-- Sets the frame range for the shake effect.
-- actorName: String
-- start, end: Int
 
getCustomActorFrameRange(actorName) --> Int
-- Returns the total number of frames in the current animation range.

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.
-- textElem:     String  — unique identifier for this text element
-- x, y:         Int     — position
-- 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")
 
-- Visibility (use these to show/hide without recreating)
customTextHide(textElem)
-- Hides the text element by removing its program from the display.
-- textElem: String
 
customTextShow(textElem)
-- Shows the text element by re-adding its program to the display.
-- textElem: String
 
hideText(textElem)  -- Alias for customTextHide
showText(textElem)  -- Alias for customTextShow
 
-- 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"
 
-- Text Layout & Formatting
setTextMultiline(textElem, value)
-- Enables or disables multiline text rendering.
-- textElem: String
-- value:    Bool
 
getTextMultiline(textElem) --> Bool
-- Returns whether multiline is enabled.
 
setTextAlignment(textElem, value)
-- Sets the text alignment.
-- textElem: String
-- value:    String — "LEFT", "CENTER", or "RIGHT" (case-insensitive)
 
getTextAlignment(textElem) --> String
-- Returns the current alignment as "LEFT", "CENTER", or "RIGHT".
 
setTextSpacerPercent(textElem, value)
-- Sets the spacing between lines as a percentage of text size.
-- textElem: String
-- value:    Float (e.g., 1.0 = 100% of text height)
 
getTextSpacerPercent(textElem) --> Float
-- Returns the current line spacing percentage.

Built-in HUD Text Access

The engine provides direct access to the built-in HUD text elements for easy modification.

getScoreText() --> String
-- Returns the string key for the built-in score text element.
-- Use this key with other text manipulation functions to modify the score display.

getWatermarkText() --> String
-- Returns the string key for the built-in watermark text element.
-- Use this key with other text manipulation functions to modify the watermark display.

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:

customTextNew("myText", 0, 0, "display", "#Hello# ^World^", "vcr", "white")
 
setTextFormatMarkerPairs("myText", {
    { marker = "#", color = "0xffff0000" },
    { marker = "^", color = "0xff00ddff", outlineColor = "black", outlineSize = 2.0 }
})
 
screenCenterText("myText", "XY")

PlayField Functions

These functions are provided by CustomPlayFieldComponent and expose gameplay-level controls to Lua scripts.

-- Property Access
getProperty(name) --> Dynamic
-- Returns the value of a named property on the PlayField instance via reflection.
-- name: String
 
setProperty(name, value)
-- Sets a named property on the PlayField instance via reflection.
-- name:  String
-- value: Dynamic
 
-- Song Control
loadSong(songDir)
-- Exits the current state and loads a new song by directory path.
-- songDir: String
 
endSong()
-- Ends the current song and returns to the main menu. Also known as `exitSong()`
 
restartSong(skipTransition)
-- Restarts the current song.
-- skipTransition: Bool (default: false)
 
setSongPosition(time)
-- Seeks the song to the given time in milliseconds.
-- Clamps to [0, songLength - 1000]. Has no effect if the song hasn't started,
-- has ended, is paused, or the playfield is disposed.
-- time: Float
 
setStage(stage)
-- Overrides the current stage name and fires the onChangeStage callback.
-- stage: String (must not be empty or nil)
 
setPlaybackRate(speed)
-- Sets the playback speed multiplier (Mixer.speed).
-- speed: Float
 
-- Custom Note Movement
setCustomNoteMoveFormula(script)
-- Sets a custom Lua script string to calculate note positions each frame.
-- The string must define a global `noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)`
-- function that returns 4 values: x, y, scale, sustainRot.
-- The script is compiled once and executed in an isolated VM.
-- script: String (must not be nil; use resetCustomNoteMoveFormula to clear)

resetCustomNoteMoveFormula()
-- Clears the custom note movement formula and closes its isolated VM,
-- reverting to default engine note movement.

-- Accuracy
getAccuracyString() --> String
-- Returns a formatted accuracy string from the Accuracy object.
 
-- Health Multipliers
healthGainMult(lane) --> Float
-- Returns the health gain multiplier for the given lane index.
-- lane: Int
 
healthLossMult(lane) --> Float
-- Returns the health loss multiplier for the given lane index.
-- lane: Int
 
setHealthGainMult(lane, value)
-- Sets the health gain multiplier for the given lane index.
-- lane: Int
-- value: Float
 
setHealthLossMult(lane, value)
-- Sets the health loss multiplier for the given lane index.
-- lane: Int
-- value: Float
 
setHealth(health)
-- Directly sets the player's health.
-- health: Float
 
-- Score Manipulation
addScore(value)
-- Adds an integer value to the current score.
-- value: number (automatically converted to Int)
 
addMisses(value)
-- Adds an integer value to the current miss counter.
-- value: number (automatically converted to Int)
 
addCombo(value)
-- Adds an integer value to the current combo.
-- value: number (automatically converted to Int)
 
resetCombo()
-- Resets the combo counter to 0.
 
addDeath()
-- Increments the death counter by 1.
 
-- Camera Control
turnOnCustomCamera()
-- Enables custom camera control, allowing you to manipulate the camera freely.
 
turnOffCustomCamera()
-- Disables custom camera control, returning to engine-controlled camera.
 
setDefaultCameraPosition(lane, x, y)
-- Sets the default camera position for a specific lane.
-- lane: Int
-- x, y: Float
 
addZoom(fromDisplay, value)
-- Adds zoom from a specific camera.
-- fromDisplay: String ("view" or "display")
-- value: Float
 
setCameraScroll(x, y)
-- Sets the target camera position on the field's targetCamera point.
-- x, y: Float
 
addCameraScroll(x, y)
-- Adds an offset to the target camera position.
-- x: Float (default: 0)
-- y: Float (default: 0)
 
getCameraScrollX() --> Float
-- Returns the current target camera X position.
 
getCameraScrollY() --> Float
-- Returns the current target camera Y position.
 
setCameraFollowPoint(x, y)
-- Sets the view display's scroll position directly.
-- x, y: Float
 
addCameraFollowPoint(x, y)
-- Adds an offset to the view display's scroll position.
-- x: Float (default: 0)
-- y: Float (default: 0)
 
getCameraFollowX() --> Float
-- Returns the current view scroll X.
 
getCameraFollowY() --> Float
-- Returns the current view scroll Y.
 
setCameraShake(camera, x, y)
-- Sets the shake offset on the playfield's shake point.
-- camera: String ("view" or "display")
-- x, y:   Float
 
-- Health Bar Customization
turnOnCustomHealthBarColor()
-- Enables custom health bar coloring.
 
turnOffCustomHealthBarColor()
-- Disables custom health bar coloring, returning to default.
 
setHealthBarColorsLeft(colors)
-- Sets the left side (player) health bar gradient colors.
-- colors: Array<String> — array of hex color strings (e.g., `{"0xFF0000", "0x00FF00"}`)
 
setHealthBarColorsRight(colors)
-- Sets the right side (opponent) health bar gradient colors.
-- colors: Array<String> — array of hex color strings
 
-- Event Trigger (WIP)
triggerEvent(name, values)
-- Triggers a named chart event. (Currently a no-op stub.)
-- name:   String
-- values: Array<String>

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

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()
    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")
    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)
 
    return Function_Continue
end
 
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

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)
 
    return Function_Continue
end
 
function beatHit(beat)
    setElementAngle(mySprite, beat * 15)
    updateElementToBuffer(mySprite, myBuf)
    updateBuffer(myBuf)
 
    return Function_Continue
end

Custom Note Movement — Wave Effect

function createPost()
    setCustomNoteMoveFormula([[
function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    local spacing = 55
    local x = receptorX + (index * spacing)
    local y = receptorY - (diff * scrollSpeed * 0.45)

    -- Sine wave offset per lane
    local wave = math.sin(diff * 0.005 + index * 0.8) * 8
    x = x + wave

    -- Scale notes as they approach
    local proximity = math.max(0, 1.0 - diff * 0.0002)
    local scale = 0.8 + (proximity * 0.2)

    return x, y, scale, 0
end
    ]])
    return Function_Continue
end

Custom Note Movement — Spiral

function createPost()
    setCustomNoteMoveFormula([[
local centerX = 640
local centerY = 360

function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    local t = math.max(0, math.min(1, diff / 2000))
    local angle = (index * 0.7) + (t * math.pi * 4)
    local radius = 50 + (t * 250)

    local x = centerX + math.cos(angle) * radius
    local y = centerY + math.sin(angle) * radius
    local scale = 0.5 + (t * 0.5)

    return x, y, scale, angle * (180 / math.pi)
end
    ]])
    return Function_Continue
end

Custom Note Movement — Conditional with Nil Cancellation

function createPost()
    setCustomNoteMoveFormula([[
function noteFormula(diff, scrollSpeed, receptorX, receptorY, index, type)
    -- Only override movement for note type 1 (e.g. special notes)
    if type ~= 1 then
        return nil  -- fall back to default engine movement
    end

    local x = receptorX + (index * 55)
    local y = receptorY - (diff * scrollSpeed * 0.45)

    -- Special notes pulse in scale
    local pulse = 1.0 + math.sin(diff * 0.01) * 0.2
    return x, y, pulse, 0
end
    ]])
    return Function_Continue
end

Custom Health Bar Colors

function createPost()
    turnOnCustomHealthBarColor()
    setHealthBarColorsLeft({"0xFF0000", "0xFF6666", "0xFF0000"})  -- Red gradient
    setHealthBarColorsRight({"0x0000FF", "0x6666FF", "0x0000FF"}) -- Blue gradient
    return Function_Continue
end

Text Visibility Toggle

local textKey = nil
local visible = true

function createPost()
    textKey = "myText"
    customTextNew(textKey, 100, 100, "display", "Press SPACE to hide/show me!", "vcr", "white")
    return Function_Continue
end

function update(deltaTime)
    if keyJustPressed("space") then
        visible = not visible
        if visible then
            customTextShow(textKey)
        else
            customTextHide(textKey)
        end
    end
    return Function_Continue
end

Custom Camera Control

function createPost()
    turnOnCustomCamera()
    return Function_Continue
end

function hitNote(pos, index, duration, type, timing, notesInOne)
    if type == 1 then
        setCameraScroll(100, -100)
        addZoom("view", 0.01)
    else
        setCameraScroll(-100, -100)
    end
    return Function_Continue
end

Animated Character Setup

local bfKey = nil

function createPost()
    bfKey = getBF()
    preComputeCustomActorSingPoses(bfKey, {"singLEFT", "singDOWN", "singUP", "singRIGHT"})
    playCustomActorAnimation(bfKey, "idle", true)
    return Function_Continue
end

function hitNote(pos, index, duration, type, timing, notesInOne)
    playSingIdCustomActorAnimation(bfKey, index, false)
    setCustomActorFinishAnim(bfKey, "idle")
    return Function_Continue
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 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.
  • Score, miss, and combo manipulation functions accept Int values.
  • resetCombo() is a convenience wrapper that sets the combo to 0 immediately.
  • addDeath() increments the engine's death counter. This is tracked separately from the automatic increment during a game over sequence.
  • The Actor API uses String Keys to reference actors. Vanilla actors (Boyfriend, Girlfriend, Dad) are automatically registered with internal keys (FV_BF_099, FV_GF_099, FV_OP_099).
  • Built-in HUD text elements (getScoreText() and getWatermarkText()) are automatically managed by the engine and should not be manually disposed.
  • Use customTextHide and customTextShow (or hideText/showText) to toggle text element visibility efficiently.
  • Health bar color functions expect arrays of 3-6 hex color strings for smooth gradient interpolation.
  • Custom camera control must be enabled with turnOnCustomCamera() before camera manipulation functions will have effect.
  • The note movement formula VM is isolated from the main script VMs. It has no access to FunkinView callbacks or global variables — only the 6 arguments passed to noteFormula and standard Lua libraries (math, string, table, etc.). Use math.sin(x), not bare sin(x).
  • The note movement formula string is compiled once on setCustomNoteMoveFormula. Changing the source string recompiles automatically; identical strings are skipped.
  • Returning nil from any slot of noteFormula's 4 return values discards the entire result for that note and falls back to default engine movement for that frame.

Hello sidebar test.

Clone this wiki locally