-
Notifications
You must be signed in to change notification settings - Fork 5
Lua Scripting API
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.
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
Progamming in Lua to learn the basics of the Lua scripting language.
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:
| 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.
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
endThe 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.
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 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. |
| 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) |
— |
| Function | Arguments | Notes |
|---|---|---|
onChangeStage(stage) |
stage: String |
Called when setStage is used to override the active stage. |
| Function | Arguments |
|---|---|
stepHit(step) |
step: Float |
beatHit(beat) |
beat: Float |
measureHit(measure) |
measure: Float |
| 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. |
| Function | Arguments |
|---|---|
pause() |
— |
pausePost() |
Also callable as postPause. |
resume() |
— |
resumePost() |
Also callable as postResume. |
| Function | Arguments |
|---|---|
gameOver() |
— |
gameOverPost() |
Also callable as postGameOver. |
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.
- Call
setCustomNoteMoveFormula(luaString)from a standard Lua script (typically increatePost). - The engine compiles the string once and caches the resulting chunk in the isolated VM.
- Every frame, for each active note, the engine calls the global
noteFormulafunction inside the isolated VM. - The returned values are applied directly to the note sprite and its associated sustain sprite.
- Call
resetCustomNoteMoveFormula()to clear the formula and revert to default engine movement.
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| 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. |
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 . |
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
endThe 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 tonoteFormula -
Full access to standard Lua libraries (
math,string,table,io, etc.) - Use
math.sin(x),math.cos(x), etc. — baresin(x)is not available (standard Lua places these in themathtable, not as globals)
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
setCustomNoteMoveFormulais 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.tonumbercalls. There is noDynamicboxing orConvert.fromLuaoverhead. -
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.
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
endfunction 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
endfunction 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
endAll functions below are available from any loaded Lua script.
trace(message)
-- Prints a message to the console.These variables are set once at script load time and reflect the state of the engine at that moment.
| 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. |
| 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. |
| 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). |
| 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). |
| 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
endTroubleshooting: 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.
| 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 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"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.
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).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)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: StringplaySingIdCustomActorAnimation(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.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 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.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.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")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>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) |
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)
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)
return Function_Continue
end
function beatHit(beat)
setElementAngle(mySprite, beat * 15)
updateElementToBuffer(mySprite, myBuf)
updateBuffer(myBuf)
return Function_Continue
endfunction 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
endfunction 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
endfunction 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
endfunction createPost()
turnOnCustomHealthBarColor()
setHealthBarColorsLeft({"0xFF0000", "0xFF6666", "0xFF0000"}) -- Red gradient
setHealthBarColorsRight({"0x0000FF", "0x6666FF", "0x0000FF"}) -- Blue gradient
return Function_Continue
endlocal 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
endfunction 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
endlocal 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- 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
Function_Stopfrom 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. - Score, miss, and combo manipulation functions accept
Intvalues. -
resetCombo()is a convenience wrapper that sets the combo to0immediately. -
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()andgetWatermarkText()) are automatically managed by the engine and should not be manually disposed. - Use
customTextHideandcustomTextShow(orhideText/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
noteFormulaand standard Lua libraries (math,string,table, etc.). Usemath.sin(x), not baresin(x). - The note movement formula string is compiled once on
setCustomNoteMoveFormula. Changing the source string recompiles automatically; identical strings are skipped. - Returning
nilfrom any slot ofnoteFormula's 4 return values discards the entire result for that note and falls back to default engine movement for that frame.
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.