Engine
Templates & Starters
Tools
A collection of mods made by the community using Cool Engine. Want yours listed here? See the submission info below.
Cool Engine is a full-featured Friday Night Funkin' fork aimed at modders and developers.
It extends the base game with a robust asset cache, multi-layer scripting, mod compatibility
for Psych Engine / Codename / V-Slice formats, in-game editors, 3D scene support, and a
native C++ extension layer for Windows/macOS/Linux.
🗂️
3-Layer Asset Cache
PERMANENT / CURRENT / SECOND lifecycle management for textures, sounds and fonts between state switches.
📜
Lua + HScript Modding
Full Lua & HScript scripting with a rich API covering gameplay hooks, character overrides, shader injection and more.
🔄
Multi-Format Charts
Loads Psych Engine, Codename, V-Slice, OsuMania, and StepMania charts natively with automatic detection.
🎛️
In-Game Editors
Chart Editor, Stage Editor, Animation Debugger, Dialogue Editor, Cutscene Editor, and a ModChart Editor.
🎮
ModChart System
Timed note modifiers and visual effects controlled by a dedicated ModChartManager with event-driven API.
🖥️
3D Scene Support
Lightweight 3D rendering via Flx3DScene — OBJ loading, material support, lighting, custom shaders.
The engine follows a strict 11-step initialization order in Main.hx
to ensure each subsystem has its dependencies ready before it starts.
-
1
DPI-awareness + dark mode — InitAPI calls SetProcessDPIAware() and enables dark titlebar. Must run before any window is created.
-
2
GC tuning — Garbage collector parameters configured before any assets are loaded.
-
3
Stage config — OpenFL stage scale mode and alignment set up.
-
4
AudioConfig.load() — Must run before createGame() so audio settings are ready before OpenAL initializes.
-
5
CrashHandler, DebugConsole — Crash reporting and dev console registered.
-
6
createGame() — FlxGame instance created, FlxG becomes available.
-
7
AudioConfig.applyToFlixel() — Settings pushed to FlxG audio after Flixel is ready.
-
8
WindowManager.init() — Window resize subscription and scale mode set.
-
9
Save data, keybinds, note skins — All FlxG-dependent systems (save file, key bindings, NoteSkinSystem) initialized.
-
10
UI overlays — DataInfoUI, SoundTray, and ScreenshotPlugin added to the stage.
-
11
SystemInfo.init() — GPU/system information collected; requires context3D which is only available after the first rendered frame.
// NOTE
The initial state is
CacheState, which preloads essential assets, then transitions to
TitleState.
| Constant | Value | Description |
| DEFAULT_FPS | 60 | Default FPS target on desktop |
| MIN_FPS | 30 | Minimum accepted FPS |
| MAX_FPS | 2000 | Maximum accepted FPS (0 = unlimited) |
| DEFAULT_WIDTH | 1920 | Default resolution width (1080p) |
| DEFAULT_HEIGHT | 1080 | Default resolution height (1080p) |
| ENGINE_VERSION | "0.6.0B" | Reported in crash logs and UI |
// MIGRATION
The legacy save field
FPSCap is automatically migrated to
fpsTarget
on first run. Always use
EngineSettings.setFPS() from the options screen — never write directly to
FlxG.save.data.
FunkinCache manages all texture, sound, and font assets using a three-layer system that balances memory efficiency with smooth state transitions.
PERMANENT
Essential UI, fonts. Never evicted. Marked with markPermanentBitmap() or markPermanentSound().
CURRENT
Active session assets. Moved to SECOND on preStateSwitch. New state can rescue assets back to CURRENT.
SECOND
Previous session assets. Destroyed in postStateSwitch unless rescued. Batch eviction via clearSecondLayer().
Performance Improvements (v2)
- Batch eviction —
clearSecondLayer() collects all keys then does a single-pass removal, avoiding Map re-hashing on each individual delete.
- Hot path optimization —
getBitmapData() checks CURRENT first and short-circuits SECOND if not needed.
- O(1) counters — Bitmap/sound/font counts maintained as running totals, not computed by iterating.
- Mod fallback — Searches the active mod directory before falling back to disk.
- Eviction callbacks — Mods can register
onEvict callbacks to react when their assets are destroyed.
ScriptHandler v3 manages all scripts with a layered loading system. Scripts are organized by scope layer and loaded from predictable folder paths.
global
Active for the entire game session. Loaded at startup.
stage
Active during the current stage. Unloaded on stage change.
song
Active during the current song. Unloaded after results.
ui
HUD and UIScriptedManager scripts.
menu
State scripts: FreeplayState, TitleState, etc.
char
Per-character scripts bound to a specific character slot.
Script Folder Paths
BASE GAME:
assets/data/scripts/global/ ← always-on global scripts
assets/data/scripts/events/ ← custom event handlers
assets/songs/{song}/scripts/ ← per-song scripts
assets/songs/{song}/events/ ← per-song custom events
assets/stages/{stage}/scripts/ ← per-stage scripts
assets/characters/{char}/scripts/ ← per-character scripts
assets/states/{state}/ ← state / menu scripts
MODS:
mods/{mod}/scripts/global/
mods/{mod}/songs/{song}/scripts/
mods/{mod}/stages/{stage}/scripts/
mods/{mod}/characters/{char}/scripts/
mods/{mod}/states/{state}/
mods/{mod}/data/scripts/ ← additional alias
PSYCH COMPAT:
mods/{mod}/custom_events/{event}.hx
mods/{mod}/custom_notetypes/{type}.hx
Key Script Hooks (Gameplay)
| Hook | Trigger |
| onCreate | Script loaded and ready |
| onUpdate(elapsed) | Every game frame |
| onUpdatePost(elapsed) | After all update logic |
| onBeatHit(beat) | Every beat of the music |
| onStepHit(step) | Every step of the music |
| onNoteHit(note) | Player successfully hits a note |
| onNoteHitPre(note) | Before note-hit logic (can cancel) |
| onMiss(note) | Player misses a note |
| onHold(note) | Hold note sustained |
| onHoldEnd(note) | Hold note released |
| onSongStart | Song begins after countdown |
| onSongEnd | Song finishes |
| onCountdownTick(tick) | Countdown tick before song |
| onCountdownEnd | Countdown finished |
| onPause | Game paused |
| onResume | Game resumed |
| onGameOver | Player dies |
| onGameOverRestart | Player restarts from game over |
| onSectionHit(section) | Chart section crossed |
| onChartEvent(event) | Custom chart event fired |
| onKeyPressed(key) | Any key pressed |
| onKeyJustPressed(key) | Key pressed this frame |
| onKeyJustReleased(key) | Key released this frame |
| onFocusLost | Window loses focus |
| onFocusGained | Window gains focus |
| onDestroy | Script being unloaded |
RuleScript — LuaJIT Scripting
RuleScript is the primary scripting system in Cool Engine v0.6.0B.
Scripts are plain .lua files with a transparent bridge
to all Haxe classes and instances — no sandboxing,
no wrapper functions to memorise. Just real Lua with full OOP access.
RuleScript replaces the old handle-based
system. All legacy helper functions (newObject,
setProp, getProperty, etc.) remain available
for backward compatibility, but you no longer need them.
Enabling RuleScript
Add these lines to your Project.xml:
<haxedef name="LUA_ALLOWED"/>
<haxelib name="linc_luajit"/>
Then place .lua files in any of the script folder paths shown above.
Live Globals
These objects are available in every RuleScript automatically:
| Global | Type | Description |
game | PlayState | Current PlayState instance |
bf | Character | Boyfriend character |
dad | Character | Opponent character |
gf | Character | Girlfriend character |
stage | Stage | Current stage instance |
FlxG | flixel.FlxG | Flixel global access |
Conductor | Conductor | Music timing |
Paths | Paths | Asset path helpers |
OOP Access
All Haxe objects are accessed directly through Lua proxy tables. Fields are read/written with dot notation; methods are called with colon syntax (to pass self) or dot syntax.
-- Direct field access
game.health = 1.5
bf.x = 400
stage.zoom = 1.2
-- Calling methods
bf:playAnim("singLEFT", true)
FlxG.camera:shake(0.04, 0.3)
-- Chaining
FlxG.state:add(someSprite)
import
Imports a Haxe class or enum and registers it as a global short name.
You can use any of these syntaxes — they all do the same thing:
| Syntax | Where |
import "flixel.util.FlxColor" | Lua — no parens (recommended) |
import("flixel.util.FlxColor") | Lua — function-call style |
local C = import("flixel.util.FlxColor") | Lua — assign to local (also registers global) |
import flixel.util.FlxColor; | HScript (.hx) — same as Haxe syntax |
Imports placed at the top of the file are resolved before
the interpreter runs, so the class is available everywhere — including
inside other imports and module-level code.
-- Lua (.lua) — clean top-of-file style
import "flixel.util.FlxColor"
import "flixel.tweens.FlxTween"
import "flixel.tweens.FlxEase"
import "flixel.FlxSprite"
-- All four are now globals — use them anywhere:
local spr = FlxSprite.new(100, 200)
spr.color = FlxColor.RED
FlxG.state:add(spr)
FlxTween.tween(bf, {x = 500}, 0.5, {ease = FlxEase.quadOut})
-- HScript (.hx) — same syntax as real Haxe
import flixel.util.FlxColor;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
// Classes available immediately after the import lines
FlxTween.tween(bf, {x: 500}, 0.5, {ease: FlxEase.quadOut});
before() / after() / replace()
Three simple verbs for hooking into any method on any object. No original parameter, no self — just the actual arguments you care about.
| Function | When your code runs | Cancel original? |
before(obj, method, fn) | Before the original | Yes — return false |
after(obj, method, fn) | After the original | No |
replace(obj, method, fn) | Instead of the original | Always (original never runs) |
-- Add camera shake every beat — original logic still runs
before(game, "onBeatHit", function(beat)
FlxG.camera:shake(0.02, 0.1)
end)
-- Run extra animation after dad finishes dancing
after(dad, "dance", function()
dad:playAnim("extraBob", false)
end)
-- Fully replace bf's dance with a custom one
replace(bf, "dance", function()
bf:playAnim("myIdle", true)
end)
-- Cancel the original based on a condition
before(game, "onNoteHit", function(note)
if note.noteType == "ghost" then
return false -- skip the original hit logic entirely
end
end)
Legacy: overrideMethod(obj, name, function(original, self, ...)) still works for backward compat.
require()
Loads another .lua file as a reusable module. The module's global exports are returned as a table. Results are cached — the file runs only once.
-- songs/mymod/utils.lua → define exports in a table
local M = {}
function M.lerp(a, b, t) return a + (b - a) * t end
return M
-- In your song script:
local utils = require("songs/mymod/utils")
bf.x = utils.lerp(bf.x, 500, 0.1)
Custom Classes
Use the built-in Class helper to define OOP classes in Lua. Instances are created with ClassName.new(...) and the init method is called automatically.
MyBoss = Class {
init = function(self, x, y)
local Sprite = import("flixel.FlxSprite")
self.sprite = Sprite.new(x, y)
self.sprite:loadGraphic(Paths.image("boss"), true, 200, 200)
self.hp = 100
FlxG.state:add(self.sprite)
end,
takeDamage = function(self, amount)
self.hp = self.hp - amount
if self.hp <= 0 then self:die() end
end,
die = function(self)
local Tween = import("flixel.tweens.FlxTween")
Tween.tween(self.sprite, {alpha = 0}, 0.6)
end
}
-- Create an instance
local boss = MyBoss.new(640, 100)
Inheritance
Extend an existing class by passing it as the second argument to Class:
Animal = Class {
init = function(self, name) self.name = name end,
speak = function(self) return "..." end
}
Dog = Class({
speak = function(self) return self.name .. " says Woof!" end
}, Animal)
local d = Dog.new("Rex")
print(d:speak()) -- "Rex says Woof!"
switchState()
Transition to any engine state by name:
switchState("MainMenuState")
switchState("FreeplayState")
switchState("PlayState") -- restarts the song
Legacy API (backward compat)
All old Lua wrapper functions still work. You do not need to migrate existing scripts.
| Function | Description |
newObject(class, ...) | Create a Haxe object, returns handle |
setProp(handle, field, val) | Set a field on a handle object |
getProp(handle, field) | Get a field from a handle object |
callMethod(handle, name, ...) | Call a method on a handle object |
getGameProp(field) | Get a field from PlayState |
setGameProp(field, val) | Set a field on PlayState |
makeSprite(tag, x, y) | Create and register a sprite |
addSprite(tag) | Add registered sprite to scene |
triggerAnim(handle, name, forced) | Play animation on a character |
tweenPos(handle, x, y, t, ease) | Tween position |
tweenAlpha(handle, alpha, t, ease) | Tween alpha |
tweenColor(handle, color, t, ease) | Tween colour |
playSound(path, vol) | Play a sound |
playMusic(path, vol) | Play background music |
shakeCamera(intensity, duration) | Shake the camera |
flashCamera(color, duration) | Flash the camera |
setHealth(val) | Set player health (0–2) |
addHealth(val) | Add to player health |
addScore(val) | Add to score |
setScrollSpeed(val) | Change note scroll speed |
makeLuaText(tag, text, size, x, y) | Create an on-screen text label |
setTextString(tag, text) | Update text content |
Full RuleScript Example
-- Example: custom boss fight script (songs/bossfight/scripts/boss.lua)
local FlxColor = import("flixel.util.FlxColor")
local Tween = import("flixel.tweens.FlxTween")
local FlxEase = import("flixel.tweens.FlxEase")
local bossDefeated = false
local hitCount = 0
function onCreate()
log("Boss fight script loaded!")
-- Tint the stage slightly red
stage.background:setColorTransform(0, 0, 0, 1, 60, 0, 0, 0)
end
function onBeatHit(beat)
-- Make dad bounce harder on every 4th beat
if beat % 4 == 0 then
Tween.tween(dad, {y = dad.y - 20}, 0.1, {
ease = FlxEase.quadOut,
onComplete = function()
Tween.tween(dad, {y = dad.y + 20}, 0.1, {ease = FlxEase.quadIn})
end
})
end
end
function onNoteHit(note)
hitCount = hitCount + 1
if hitCount >= 50 and not bossDefeated then
bossDefeated = true
log("Boss defeated!")
Tween.tween(dad, {alpha = 0}, 1.0, {ease = FlxEase.quadIn})
end
end
function onDestroy()
log("Boss fight script unloaded.")
end
Minimal state example
// mods/my-mod/states/MyState/script.hx
var bg:FlxSprite;
var label:FlxText;
function onCreate() {
// Black background
bg = new FlxSprite(0, 0);
bg.makeGraphic(FlxG.width, FlxG.height, FlxColor.fromRGB(10, 10, 20));
add(bg);
// Title text
label = new FlxText(0, 100, FlxG.width, "MY CUSTOM STATE", 48);
label.alignment = "center";
label.color = FlxColor.fromRGB(255, 60, 120);
add(label);
}
function onUpdate(elapsed:Float) {
// Pulse the label
label.alpha = 0.5 + 0.5 * Math.sin(FlxG.game.ticks / 300.0);
}
function onKeyJustPressed(key) {
if (FlxG.keys.justPressed.ESCAPE) switchState("MainMenuState");
}
State with animated sprite & sounds
// mods/my-mod/states/IntroState/script.hx
var char:Dynamic;
function onCreate() {
var spr = new FunkinSprite(300, 200);
spr.loadSparrow("images/characters/bf/BF_assets");
spr.addAnim("idle", "BF idle dance", 24, true);
spr.playAnim("idle");
spr.screenCenter();
add(spr);
char = spr;
playSound("confirmMenu");
}
function onBeatHit(beat:Int) {
if (beat % 2 == 0) char.playAnim("idle", true);
}
function onKeyJustPressed(key) {
if (FlxG.keys.justPressed.ENTER) {
playSound("confirmMenu");
stickerSwitch(new funkin.gameplay.PlayState());
}
}
Scripted SubState
// File: mods/my-mod/states/MyPopup/popup.hx
function onCreate() {
// Semi-transparent overlay
var overlay = new FlxSprite(0, 0);
overlay.makeGraphic(FlxG.width, FlxG.height, FlxColor.fromRGBFloat(0, 0, 0, 0.6));
add(overlay);
var txt = new FlxText(0, FlxG.height / 2 - 30, FlxG.width, "Press ENTER to continue", 28);
txt.alignment = "center";
add(txt);
}
function onKeyJustPressed(key) {
if (FlxG.keys.justPressed.ENTER || FlxG.keys.justPressed.ESCAPE)
state.closeSubState();
}
// To open it from another state script:
// FlxG.state.openSubState(new funkin.scripting.ScriptableState("MyPopup"))
Available helpers inside StateScript
// Display list — same as a real FlxState
add(spr);
remove(spr);
insert(pos, spr);
// Read / write any field on the state
getField("fieldName") // read
setField("fieldName", value) // write
callMethod("methodName", [arg1, arg2])
// Refresh state fields into the script scope (after state creates new objects)
refreshFields()
// Sound — direct or via helpers
playSound("menus/confirmMenu");
playMusic("freakyMenu");
stopMusic();
FlxG.sound.play(Paths.sound("confirm")) // also works
Chart events are processed by EventManager, which dispatches typed events to registered handlers. Custom event types are registered in EventRegistry and can be defined by mods.
Built-in Event Types
| Event | Description |
| Camera Follow | Moves the camera to focus player or opponent. Used by Cool Engine instead of implicit mustHitSection. |
| BPM Change | Changes the song BPM mid-track, updates the Conductor. |
| Hey! | Triggers the "Hey!" animation on a character. |
| Set GF Speed | Changes the GF bopping speed. |
| Add Camera Zoom | Bump-zooms the game/HUD cameras. |
| Alt Anim | Forces alt animation on a character. |
// PSYCH COMPAT
When loading Psych Engine charts,
PsychConverter generates explicit "Camera Follow" events from
mustHitSection flags so the event system processes them consistently.
PlayState is the main gameplay state. It coordinates all gameplay subsystems through dedicated controller classes, keeping the core file manageable.
NoteManager
Spawning, culling, timing windows, sustain processing, and miss detection for all note types.
CameraController
Handles camera focusing, zoom bumps, and following based on Camera Follow events.
CharacterController
Manages character animations, alt-anim switching, dance cycles, and character freeze/unfreeze.
RatingManager
Tracks score, accuracy, combo, misses, and determines rating (Sick/Good/Bad/Shit) based on timing windows.
InputHandler
Processes key presses, maps them to note directions, handles hold states and ghost tapping.
UIScriptedManager
Manages the HUD — supports scripted/custom HUD replacement from mods.
The note system is built for performance, using object pooling and GPU batching to handle large note counts without hitching.
- NotePool — Object pool that recycles Note instances. Avoids garbage collection spikes during dense sections.
- NoteBatcher — Groups note draw calls into batches sent to the GPU renderer in a single pass.
- NoteSkinSystem — Loads per-skin note graphics from
assets/images/noteSkins/. Skins can override arrows, holds, splashes, and cover animations.
- NoteHoldCover — Animated cover that plays when a hold note starts, driven by a shader clip for seamless visual continuity.
- NoteSplash — Hit effect sprites pooled and reused on Sick hits.
// NoteData column layout (standard 4K)
// Columns 0-3 → Player (BF, right side)
// Columns 4-7 → Opponent (Dad, left side)
// Columns ≥ 8 → Extra groups (never swap)
// Column → Direction mapping
0 = Left 1 = Down 2 = Up 3 = Right
Every note skin and splash can optionally include a script (.lua or .hx)
that runs alongside the skin, giving you full access to gameplay hooks, the RGB color shader,
tween functions, and the OOP bridge — all scoped to that skin or splash.
// OVERVIEW
Three independent script layers exist — one per visual type. Each is loaded automatically
when its skin becomes active. Scripts are destroyed when PlayState exits.
🎯
Note Skin Script
Runs for the active note skin. Receives onNoteHit, onNoteMiss, onNoteSpawn and all standard gameplay hooks.
✨
Splash Script
Runs for the active hit-splash skin. Receives onSplashSpawn — called every time a splash appears on Sick hits.
💫
Hold Splash Script
Runs for the active hold-cover skin. Receives onHoldSplashSpawn — called whenever a hold-note cover starts.
Folder structure
Drop a skin.lua (or skin.hx) alongside your skin.json.
The filename can also be overridden with the "script" field in the JSON.
assets/notes/skins/MySkin/
├── skin.json ← skin descriptor
├── skin.lua ← auto-loaded script (or skin.hx)
└── MySkin.png ← atlas
assets/notes/splashes/MySplash/
├── splash.json
├── splash.lua ← auto-loaded splash script
└── MySplash.png
assets/notes/skins/MySkin/ ← hold splash is part of the splash system
└── (hold covers are configured in splash.json → holdCover section)
// CUSTOM FILENAME
Add
"script": "myScript.lua" inside
skin.json or
splash.json
to load a script with a different name or from a subdirectory.
Note skin script — skin.lua
The engine pre-injects these variables before calling onCreate:
| Variable | Type | Description |
skinName | String | Name of the active skin (matches skin.json "name") |
NoteSkinSystem | Class | Full static access to the skin system |
NoteRGBShader | Class | Direct shader class for manual instantiation |
applyRGBShader(spr, dir, mult) | Function | Apply direction-preset RGB shader to any sprite |
applyRGBColor(spr, hex, mult) | Function | Apply hex color RGB shader |
setRGBIntensity(spr, mult) | Function | Change intensity of existing shader |
removeRGBShader(spr) | Function | Remove RGB shader from sprite |
tweenRGBToDirection(spr, dir, dur, ease) | Function | Tween color → direction preset |
tweenRGBToColor(spr, hex, dur, ease) | Function | Tween color → hex color |
tweenRGBIntensity(spr, from, to, dur, ease) | Function | Tween shader intensity |
Note skin script — callbacks
-- assets/notes/skins/MySkin/skin.lua
function onCreate()
log("Skin loaded: " .. skinName)
end
-- Called when the player hits a note
-- note = note handle | rating = "sick" / "good" / "bad" / "shit"
function onNoteHit(note, rating)
if rating == "sick" then
-- Flash the note white then tween back to its direction color
applyRGBColor(note, 0xFFFFFF, 1.5)
tweenRGBToDirection(note, getNoteDir(note), 0.2, "quadOut")
end
end
-- Called when the player misses a note
-- direction = 0 (left) / 1 (down) / 2 (up) / 3 (right)
function onNoteMiss(note, direction)
applyRGBColor(note, 0x444444, 0.6) -- grey out missed note
end
-- Beat pulse: cycle direction colors across all notes
function onBeatHit(beat)
forEachNote(function(n)
tweenRGBToDirection(n, beat % 4, 0.15, "sineOut")
end)
end
function onDestroy()
log("Skin script unloaded")
end
Splash script — splash.lua
Variables injected before onCreate: same RGB helpers as above, plus splashName instead of skinName.
-- assets/notes/splashes/MySplash/splash.lua
function onCreate()
log("Splash script ready: " .. splashName)
end
-- Called every time a hit-splash appears
-- splash = sprite handle
-- noteData = 0-3 direction
-- x, y = spawn position
function onSplashSpawn(splash, noteData, x, y)
-- Apply the standard FNF direction color via RGB shader
applyRGBShader(splash, noteData, 1.0)
-- Or tween it in dramatically
applyRGBColor(splash, 0xFFFFFF, 2.0) -- start white
tweenRGBToDirection(splash, noteData, 0.1, "quadOut") -- → direction color
end
Hold splash (cover) script — inside splash.lua
Hold covers use the same splash script file. They fire a separate callback.
-- assets/notes/splashes/MySplash/splash.lua (continued)
-- Called when a hold-note cover starts
-- cover = sprite handle
-- direction = 0-3
-- x, y = center position of the strum
function onHoldSplashSpawn(cover, direction, x, y)
applyRGBShader(cover, direction, 0.85)
-- Pulse intensity on every beat while the cover is alive
-- (the cover stays alive as long as the hold is held)
tweenRGBIntensity(cover, 1.2, 0.85, 0.25, "sineOut")
end
HScript equivalent
Everything above works identically in skin.hx / splash.hx. You can also use the full OOP bridge:
// assets/notes/skins/MySkin/skin.hx
function onNoteHit(note:Dynamic, rating:String) {
// Direct shader access via OOP bridge
var s = Std.downcast(note.shader, funkin.shaders.NoteRGBShader);
if (s != null)
s.tweenToDirection(note.noteData % 4, 0.2, flixel.tweens.FlxEase.quadOut);
}
function onBeatHit(beat:Int) {
// Use NoteSkinSystem helpers directly
forEachNote(function(n) {
NoteSkinSystem.tweenRGBToDirection(n, beat % 4, 0.15, "sineOut");
});
}
JSON fields — skin.json
These fields are added to the root of your skin.json:
| Field | Type | Default | Description |
"script" | String | null | Custom script filename. If null, skin.lua or skin.hx are auto-detected. |
"colorAuto" | Bool | false | Apply NoteRGBShader automatically with standard FNF direction colors (Left=purple, Down=cyan, Up=green, Right=red). Ideal for grayscale/neutral atlases like NOTE_assets.png. |
"colorMult" | Float | 1.0 | Shader intensity (0.0–1.0). Used with colorAuto. |
"colorDirections" | Array | null | Custom 4-entry palette overriding the default presets. Each entry has r, g, b fields (vec3 column vectors). |
colorAuto — automatic RGB coloring
Inspired by Psych Engine and Nightmare Engine's RGB note coloring.
Uses a GLSL matrix shader that remaps the R, G, B channels of your note texture to any color
per direction — so a single grayscale/neutral atlas can look fully colored in-game.
// HOW IT WORKS
The shader maps:
red channel → base color ·
green channel → shadow/outline ·
blue channel → highlight tint.
Works the same as Psych Engine's
noteRGB.frag — fully compatible with atlases made for that engine.
Minimal — standard FNF colors
{
"name": "MySkin",
"colorAuto": true,
"texture": { "path": "NOTE_assets" }
}
With reduced intensity (70%)
{
"name": "MySkin",
"colorAuto": true,
"colorMult": 0.7,
"texture": { "path": "NOTE_assets" }
}
Custom palette (4 directions)
{
"name": "MySkin",
"colorAuto": true,
"colorDirections": [
{ "r": [0.76, 0.11, 0.67], "g": [0, 0, 0], "b": [0.09, 0.03, 0.94] },
{ "r": [0, 1, 1 ], "g": [0, 0, 0], "b": [0, 1, 0 ] },
{ "r": [0.07, 0.98, 0.02], "g": [0, 0, 0], "b": [0, 0.96, 0 ] },
{ "r": [0.98, 0.22, 0.25], "g": [0, 0, 0], "b": [0.96, 0.09, 0.12] }
],
"texture": { "path": "NOTE_assets" }
}
colorAuto also works on splashes & hold covers
Add the same fields to your splash.json. The shader is applied automatically on every setup() call for both NoteSplash and NoteHoldCover.
// splash.json
{
"name": "MySplash",
"colorAuto": true,
"colorMult": 0.9,
"assets": { "path": "noteSplashes" },
...
}
RGB shader tween API (Lua)
These functions are available in any Lua script (song, stage, skin, splash) — not just skin scripts.
| Function | Description |
applyRGBShader(spr, dir, mult) | Apply direction-preset shader. dir = 0–3. |
applyRGBColor(spr, hex, mult) | Apply hex color as RGB shader. Converts color to preset automatically. |
setRGBIntensity(spr, mult) | Change intensity of existing shader (0.0–1.0). |
removeRGBShader(spr) | Remove RGB shader from sprite. |
tweenRGBToDirection(spr, dir, dur, ease) | Smooth color tween → direction preset. Returns tween handle. |
tweenRGBToColor(spr, hex, dur, ease) | Smooth color tween → hex color. Returns tween handle. |
tweenRGBIntensity(spr, from, to, dur, ease) | Tween shader intensity. Great for beat pulses. |
-- Example: rainbow beat pulse on all notes (song script)
function onBeatHit(beat)
forEachNote(function(note)
-- Tween to direction color with a quick flash
tweenRGBIntensity(note, 1.8, 1.0, 0.2, "quadOut")
end)
end
-- Example: golden flash on Sick hit
function onNoteHit(note, rating)
if rating == "sick" then
tweenRGBToColor(note, 0xFFD700, 0.05, "linear")
timer(0.05, function()
tweenRGBToDirection(note, getNoteDir(note), 0.25, "sineOut")
end)
end
end
Default direction color presets
| Direction | Color | Hex | R vec3 | B vec3 |
| 0 — Left | ■ Purple | #C24B99 | [0.76, 0.11, 0.67] | [0.09, 0.03, 0.94] |
| 1 — Down | ■ Cyan | #00FFFF | [0.0, 1.0, 1.0] | [0.0, 1.0, 0.0] |
| 2 — Up | ■ Green | #12FA05 | [0.07, 0.98, 0.02] | [0.0, 0.96, 0.0] |
| 3 — Right | ■ Red | #F9393F | [0.98, 0.22, 0.25] | [0.96, 0.09, 0.12] |
All available callbacks
| Callback | Script type | Arguments |
onCreate() | skin · splash | — |
onNoteHit(note, rating) | skin | note handle, rating string |
onNoteMiss(note, direction) | skin | note handle (may be null), direction int |
onSplashSpawn(splash, noteData, x, y) | splash | splash handle, direction 0–3, world position |
onHoldSplashSpawn(cover, direction, x, y) | splash | cover handle, direction 0–3, strum center |
onBeatHit(beat) | skin · splash | beat integer |
onStepHit(step) | skin · splash | step integer |
onUpdate(elapsed) | skin · splash | delta time float |
onDestroy() | skin · splash | — |
Mods live in the mods/ directory. Each mod is a folder with a mod.json descriptor. Only one mod can be active at a time. Use ModManager.setActiveMod(id) to switch.
mod.json Structure
{
"name": "My Mod",
"description": "A cool mod.",
"author": "YourName",
"version": "1.0.0",
"priority": 0,
"color": "FF5599",
"website": "https://...",
"enabled": true,
"startupDefault": false,
"appTitle": "My Mod — FNF",
"appIcon": "icon",
"developerMode": true,
"discord": {
"clientId": "123456789",
"largeImageKey": "mymod_icon",
"largeImageText": "My Mod",
"menuDetails": "Playing My Mod"
}
}
Mod Folder Structure
mods/my-mod/
├── mod.json ← required
├── preview.mp4 ← video preview in selector (optional)
├── preview.png ← image preview fallback (optional)
├── icon.png ← square icon (optional)
├── songs/
├── characters/
├── stages/
├── images/
├── sounds/
├── music/
├── data/
├── scripts/
└── videos/
Engine Overrides
Mods can override engine behavior via mod_overrides.json or a ModEngineOverride class. Overrideable systems include: HUD class, note spawning system, character loading, stage loading.
Addons are lightweight plugins that run on top of any active mod. Unlike mods, multiple addons can be active simultaneously. They are loaded after mods so they can read the active folder.
// ADDON vs MOD
Mods replace assets and define full song/story content. Only one active at a time.
Addons inject gameplay mechanics or UI changes on top of any mod. Multiple can be active.
addon.json Hooks
"hooks": {
"onNoteHit": "scripts/onNoteHit.hx",
"onMissNote": "scripts/onMiss.hx",
"onSongStart": "scripts/onSongStart.hx",
"onSongEnd": "scripts/onSongEnd.hx",
"onBeat": "scripts/onBeat.hx",
"onStep": "scripts/onStep.hx",
"onUpdate": "scripts/onUpdate.hx",
"onCountdown": "scripts/onCountdown.hx",
"onGameOver": "scripts/onGameOver.hx",
"onStateCreate": "scripts/onStateCreate.hx",
"onStateSwitch": "scripts/onStateSwitch.hx",
"exposeAPI": "scripts/exposeAPI.hx"
}
ChartLoader auto-detects chart format and routes to the appropriate parser. No manual format specification required.
| Format | Parser | Detection |
| Psych Engine | PsychChartParser | mustHitSection field present |
| Codename Engine | CodenameChartParser | No V-Slice fields, has codename markers |
| V-Slice (FNF 2.0) | VSliceConverter | scrollSpeed + events array at root |
| OsuMania | OsuManiaParser | .osu file extension |
| StepMania | StepManiaParser | .sm / .ssc file extension |
// CAMERA EVENTS
Cool Engine uses explicit
"Camera Follow" events instead of Psych's implicit
mustHitSection. Psych charts are automatically converted during loading — existing mods do not need to be updated.
SpriteCutscene
Scripted sprite-based cutscenes defined in JSON. Supports tweens, sound effects, camera moves, and script callbacks.
Video (MP4/WebM)
Video playback via hxvlc with subtitle support (SRT parser). VideoManager handles state lifecycle and audio sync.
DialogueBox
Improved dialogue system with per-character portraits, typewriter effect, opacity/bold/fade transitions.
Subtitle Support
The SubtitleManager + SRTParser provides full SRT subtitle rendering for videos, with configurable styles (font, size, position, background).
CoreAudio wraps OpenAL for low-latency audio. MusicManager handles background music transitions. Character-specific vocals are loaded as streaming audio.
- AudioConfig — Loaded before
createGame(). Configures sample rate, period size, and latency before OpenAL initializes.
- VolumePlugin — Manages master, music, and SFX volume levels with smooth tweening.
- VSyncAPI — Native VSync control. Prevents audio drift on high-refresh-rate displays.
- FrameLimiterAPI — Improves OS timer precision (Windows:
timeBeginPeriod(1)) for accurate high-FPS pacing.
- GPURenderer — Manages sprite batching pools, submits batched draw calls to reduce GPU overhead during gameplay.
- RenderOptimizer — Tracks render budget per frame, applies adaptive quality (lower effect resolution, skip optional passes).
- OptimizationManager — Coordinates GPU and CPU optimization levels based on real-time performance metrics.
- FunkinCamera — Extended FlxCamera with built-in shader filter support and fixed BitmapData for zero-allocation frame capture.
- Flx3DScene — Lightweight 3D rendering layer built on Stage3D/Context3D. Supports OBJ models, .mtl materials, lighting, and custom GLSL shaders.
All editors are accessible from EditorHubState, opened via a debug menu shortcut during gameplay (when Developer Mode is enabled in mod settings).
Chart Editor
Full-featured chart editor with BPM detection, note placement, event sidebar, multi-selection, and undo/redo.
Stage Editor
Visual stage builder. Drag-and-drop sprites, set Z-order, configure parallax scrolling.
Animation Debugger
Preview and edit character animations. Add/remove animation entries, test playback, adjust offsets.
Dialogue Editor
Build dialogue sequences visually with character portraits, text timing, and emotion states.
Cutscene Editor
Timeline-based editor for SpriteCutscene JSON sequences.
Shader Editor
Live GLSL shader editing with real-time preview and hot-reload.
Script Watcher
Watches script files for changes and hot-reloads them without restarting the game.
Game Dev Console
In-game console for executing HScript/Lua snippets, inspecting state, and logging.
| Engine |
Charts |
Stages |
Scripts |
Characters |
| Psych Engine |
✔ auto |
✔ converter |
✔ Lua compat |
✔ |
| Codename Engine |
✔ auto |
✔ XML converter |
~ partial |
✔ |
| V-Slice (FNF 2.0) |
✔ auto |
~ partial |
✖ |
✔ |
| OsuMania |
✔ .osu |
✖ |
✖ |
✖ |
| StepMania |
✔ .sm/.ssc |
✖ |
✖ |
✖ |
assets/
images/
characters/ ← character spritesheets
stages/ ← stage sprites
ui/ ← UI elements (health icons, ratings…)
noteSkins/ ← note skin atlases
sounds/ ← SFX
music/ ← background music
songs/
{songName}/
{songName}.json ← chart data
Inst.ogg ← instrumental
Voices.ogg ← vocals (or Voices-bf.ogg / Voices-dad.ogg)
data/
characters/ ← character JSON definitions
stages/ ← stage JSON definitions
weeks/ ← week/level JSON
scripts/global/ ← always-on scripts
scripts/events/ ← event handler scripts
videos/ ← cutscene MP4/WebM files
mods/
{modName}/
mod.json ← mod metadata
[mirrors assets/ structure]
HScript files are placed in the appropriate folder (e.g. mods/{mod}/songs/{song}/scripts/myscript.hx). The engine injects all of these variables automatically — you don't need to import anything.
Flixel Core
// Available directly — no import needed
FlxG // FlxG global (cameras, sound, keys, etc.)
FlxSprite // Base sprite class
FlxTween // Tween engine
FlxEase // Easing functions (FlxEase.sineOut, etc.)
FlxColor // Color utilities (FlxColor.RED, FlxColor.fromRGB, etc.)
FlxTimer // Timer class
FlxSound // Sound class
FlxCamera // Camera class
FlxObject // Base object
FlxText // Text sprite
FlxGroup // Sprite group
FlxSpriteGroup
FlxBackdrop // Infinite scrolling background
FlxTrail // Trail effect
FlxMath // Math utils (lerp, bound, remapToRange, etc.)
FlxPoint // 2D point
FlxRect // Rectangle
FunkinSprite // Engine's extended sprite (auto-detects Sparrow/Atlas/Packer)
FlxAnimate // Adobe Animate texture atlas
BitmapData // OpenFL bitmap
WiggleEffect // Wave distortion
WaveEffect // Wave shader
BlendModeEffect
Gameplay Globals
game // PlayState.instance — access to everything in the current song
Conductor // Conductor.bpm, Conductor.songPosition, Conductor.curBeat
Paths // Asset paths helper (Paths.image("x"), Paths.sound("x"), etc.)
MetaData // Current song metadata (title, artist, difficulty, etc.)
Song // Song data class
Note // Note class
ModManager // Active mod info
ShaderManager // Apply/remove shaders
ModChartManager // ModChart system
Gameplay — game.*
// Access the PlayState directly
game.health // 0.0 – 2.0
game.score // current score (Int)
game.misses // total misses
game.combo // current combo
game.curBeat // current beat number
game.curStep // current step number
game.boyfriend // BF character instance
game.dad // opponent character instance
game.gf // GF character instance
game.camGame // game camera (FlxCamera)
game.camHUD // HUD camera
game.endSong() // end the current song
game.pauseSong() // pause
game.resumeSong() // resume
game.gameOver() // trigger game over
Score Object — score.*
score.getScore() // → Int
score.getAccuracy() // → Float (0.0–1.0)
score.getCombo() // → Int
score.getMisses() // → Int
score.getSicks() // → Int
score.addScore(100) // add to score
score.resetCombo() // break combo
score.isFullCombo() // → Bool
score.isSickCombo() // → Bool (all Sicks)
// Customize timing windows (in ms)
score.setWindow("sick", 45)
score.setWindow("good", 90)
score.setWindow("bad", 135)
score.setWindow("shit", 180)
// Customize point values
score.setPoints("sick", 350)
score.setPoints("good", 200)
score.setMissPenalty(10)
Camera Object — camera.*
camera.game() // → camGame FlxCamera
camera.hud() // → camHUD FlxCamera
camera.setZoom(0.9)
camera.getZoom() // → Float
camera.tweenZoom(0.9, 0.5, FlxEase.sineOut)
camera.shake() // default intensity 0.03, duration 0.2
camera.shake(0.05, 0.4, camera.game())
camera.flash() // white flash
camera.flash(FlxColor.RED, 0.5)
camera.fade(FlxColor.BLACK, 1.0)
camera.focusBf() // point camera at BF
camera.focusDad() // point camera at opponent
camera.setFollowLerp(0.04)
camera.bumpZoom()
camera.addShader("bloom")
camera.addShader("chromatic", camera.hud())
camera.clearShaders()
HUD Object — hud.*
hud.setVisible(false) // hide entire HUD
hud.setHealth(1.5) // set health bar value (0–2)
hud.getHealth() // → Float
hud.addHealth(0.3) // add/subtract health
hud.iconP1() // → health icon sprite (player)
hud.iconP2() // → health icon sprite (opponent)
hud.setScoreVisible(false) // hide score text
hud.showRating("sick", 142) // display a rating popup
Characters — chars.*
chars.bf() // → BF Character instance
chars.dad() // → opponent Character instance
chars.gf() // → GF Character instance
chars.getBySlot("bf") // → Character by slot name
// On a character instance:
var bf = chars.bf();
bf.playAnim("hey", true) // force play animation
bf.dance() // trigger dance cycle
bf.x = 100
bf.y = 200
bf.scale.x = 1.2
bf.flipX = true
bf.alpha = 0.5
bf.color = FlxColor.fromRGB(255, 200, 200)
bf.visible = false
bf.stunned = true // freeze character
States — states.*
states.goto("FreeplayState") // switch to a named state
states.open(new MyState()) // switch to a state instance
states.sticker(new MyState()) // switch with sticker transition
states.load(new PlayState(song, diff)) // switch via LoadingState
states.openSubState("MySubState") // open a scripted substate
states.close() // close current substate
states.current() // → current FlxState
Persistent Data — data.*
data.set("myKey", 42)
data.get("myKey") // → 42
data.get("myKey", 0) // → 0 if not found (fallback)
data.has("myKey") // → Bool
data.delete("myKey")
data.save() // flush to disk (FlxG.save)
Signal Bus — signal.*
signal.on("myEvent", function(data) {
trace("received: " + data);
});
signal.once("myEvent", function(data) { }); // fire once only
signal.emit("myEvent", { value: 42 });
signal.off("myEvent", myCallback);
signal.clear("myEvent");
Events — events.*
// Fire a chart event immediately (outside timeline)
events.fire("Camera Follow", "bf");
events.fire("Add Camera Zoom", "0.015", "0.5");
// Register a handler for a custom event
events.on("My Custom Event", function(v1, v2, time) {
trace("Event fired at beat " + Conductor.curBeat);
trace("Values: " + v1 + ", " + v2);
return false; // return true to cancel built-in behavior
});
// List all available event names
var names = events.list("chart"); // context-filtered
var all = events.list();
Shaders
// Apply a pre-built shader to a sprite
ShaderManager.applyShader(mySprite, "bloom");
// Apply a shader as a camera filter
camera.addShader("chromatic");
camera.addShader("vignette", camera.hud());
// Create an inline shader from GLSL code
var s = createShader("myWave", "
uniform float uTime;
void main() {
vec2 uv = openfl_TextureCoordv;
uv.x += sin(uv.y * 20.0 + uTime * 5.0) * 0.01;
gl_FragColor = flixel_texture2D(bitmap, uv);
}
");
s.applyTo(mySprite);
// Update uniforms each frame
function onUpdate(elapsed) {
s.set("uTime", Conductor.songPosition / 1000.0);
}
// Cleanup
s.remove(mySprite);
Math Utilities — math.*
math.lerp(a, b, 0.1) // linear interpolation
math.lerpSnap(a, b, 0.1, 1.0) // lerp that snaps when close
math.clamp(v, 0, 1)
math.map(v, 0, 100, 0, 1) // remap range
math.dist(x1, y1, x2, y2)
math.angle(x1, y1, x2, y2) // → degrees
math.sin(degrees) // trig in degrees (not radians)
math.cos(degrees)
math.rnd(1, 10) // random Int
math.rndf(0.0, 1.0) // random Float
math.chance(0.25) // 25% chance → Bool
math.bezier(t, p0, p1, p2, p3)
math.pingpong(time, 1.0)
math.snap(v, 0.5) // snap to grid
math.PI / math.TAU / math.E
Array Helpers — arr.*
arr.find([1,2,3], function(x) return x > 1) // → 2
arr.filter([1,2,3], function(x) return x > 1) // → [2, 3]
arr.map([1,2,3], function(x) return x * 2) // → [2, 4, 6]
arr.some([1,2,3], function(x) return x > 2) // → true
arr.every([1,2,3], function(x) return x > 0) // → true
arr.shuffle([1,2,3,4,5]) // → shuffled copy
arr.pick([1,2,3]) // → random element
ModChart — ModChartManager.*
// ModChart lets you move/rotate/scale strumlines over time
// Schedule a modifier event at a specific beat
ModChartManager.addEvent({
beat: 16.0,
target: "player", // "player", "opponent", or note index
type: ModEventType.MOVE_X,
value: 50.0,
duration: 1.0, // in beats
ease: ModEase.SINE_OUT
});
// Direct instant modifier (no easing)
ModChartManager.addEvent({
beat: 0.0,
target: "player",
type: ModEventType.ANGLE,
value: 15.0,
duration: 0.0,
ease: ModEase.INSTANT
});
// Reset all modifiers
ModChartManager.clearAll();
Defining Custom Classes (HScript)
// Define a reusable component with prototype pattern
defineClass("BouncySprite", {
"new": function(x, y, imagePath) {
var spr = new FunkinSprite(x, y);
spr.loadSparrow(imagePath);
spr.addAnim("idle", "idle", 24, true);
spr.playAnim("idle");
var self = { spr: spr, timer: 0.0 };
self.update = function(elapsed) {
self.timer += elapsed;
self.spr.y = y + Math.sin(self.timer * 5) * 8;
};
return self;
}
});
// Instantiate it
var bounce = newClass("BouncySprite", 200, 300, "images/mySprite");
add(bounce.spr);
function onUpdate(elapsed) {
bounce.update(elapsed);
}
Lua scripts work the same as in Psych Engine but with a much larger API. They are detected automatically by file extension and loaded from the same folder paths as HScript files.
Object Registry
-- Create a Lua-managed object reference
local id = newObject("FlxSprite", 100, 200) -- class name + constructor args
local id = makeFunkinSprite(100, 200) -- shorthand for FunkinSprite
local id = makeSprite(100, 200) -- blank sprite
-- Get/set properties on any object
getProp(id, "x") -- returns value
setProp(id, "x", 150) -- sets value
setProp(id, "alpha", 0.5)
-- Call a method
callMethod(id, "playAnim", "idle", true)
-- Destroy
destroyObject(id)
Sprites
local spr = makeFunkinSprite(100, 200)
loadSparrow(spr, "images/mySprite") -- PNG + XML
loadAtlas(spr, "images/myAtlas") -- Adobe Animate folder
loadGraphic(spr, "images/myPng") -- static PNG
addAnim(spr, "idle", "idle anim", 24, true) -- name, prefix, fps, loop
addAnimOffset(spr, "idle", -10, 5) -- offset x, y
playAnim(spr, "idle")
playAnim(spr, "idle", true) -- force restart
stopAnim(spr)
addSprite(spr) -- add to PlayState
addToState(spr) -- same as addSprite
addToGroup(spr, "grpStage") -- add to a named group
removeSprite(spr)
setSpriteScale(spr, 1.5) -- uniform scale
setSpriteScale(spr, 1.0, 1.5) -- x, y
setSpriteFlip(spr, true, false) -- flipX, flipY
setSpriteAlpha(spr, 0.8)
setSpriteColor(spr, colorRGB(255, 100, 100))
setSpritePosition(spr, 300, 400)
setSpriteScrollFactor(spr, 0.8, 0.8) -- parallax
setAntialiasing(spr, true)
screenCenter(spr)
screenCenter(spr, "x") -- center on axis only ("x" or "y")
Text
local txt = makeText(10, 10, 400, "Hello World!", 20)
setText(txt, "Updated text")
setTextSize(txt, 24)
setTextFont(txt, "vcr.ttf")
setTextBold(txt, true)
setTextItalic(txt, false)
setTextAlign(txt, "center") -- "left", "center", "right"
setTextColor(txt, colorHex("FF3C78"))
setTextBorder(txt, "outline", 2) -- style, size
setTextShadow(txt, 2, 2, colorRGB(0,0,0))
getText(txt) -- → current text string
addSprite(txt)
Camera
setCamZoom(0.9)
setCamZoomTween(0.9, 0.5, "sineOut") -- target, duration, ease
cameraFlash(id, "WHITE", 0.3) -- camHandle, color, duration
cameraShake(id, 0.03, 0.2)
cameraFade(id, "BLACK", 1.0)
cameraPan(id, 100, 0, 0.5, "linear") -- cam, dx, dy, dur, ease
cameraSnapTo(id, 400, 300) -- snap camera to world position
local camID = getCamHandle("game") -- "game", "hud", "other"
local newCam = makeCam() -- create new camera
Tweens
tweenProp(spr, "x", 500, 1.0, "sineOut") -- object, field, target, dur, ease
tween(spr, "alpha", 0, 0.5, "linear") -- alias for tweenProp
tweenColor(spr, 0.5, "FFFFFF", "FF3C78") -- obj, dur, fromHex, toHex
tweenAngle(spr, 360, 1.0, "quadOut")
tweenPosition(spr, 300, 400, 0.8, "sineInOut")
tweenAlpha(spr, 0.0, 0.5, "linear")
tweenScale(spr, 1.5, 0.5, "elasticOut")
tweenNumTween(0, 100, 1.0, "quadIn", function(v)
trace("value: " .. tostring(v))
end)
local id = tweenProp(spr, "x", 500, 1.0)
tweenCancel(id)
Timers
local t = timer(2.0, function()
trace("fired after 2 seconds")
end)
-- Repeating timer (loops parameter)
local t = timer(0.5, function()
trace("fires every 0.5s")
end, 0) -- 0 = infinite loops
timerCancel(t)
Gameplay
-- Score / health
addScore(100) setScore(0) getScore()
addHealth(0.2) setHealth(1.0) getHealth() -- 0–2
setMisses(0) getMisses()
setCombo(0) getCombo()
-- Song control
endSong()
gameOver()
pauseGame()
resumeGame()
-- Song info
getBeat() getStep() getBPM() getSongPos()
getSongName() getSongArtist()
getDifficulty() isStoryMode()
getAccuracy() getSicks() getGoods() getBads() getShits()
-- Note appearance
setScrollSpeed(1.5) getScrollSpeed()
setNoteAlpha(noteID, 0.5)
setNoteColor(noteID, colorRGB(255, 100, 100))
skipNote(noteID)
setNoteSkin("pixel")
Characters (Lua)
-- Get a character handle
local bfID = getCharHandle("bf") -- "bf", "dad", "gf"
-- Animations
triggerAnim("bf", "hey", true) -- charName, anim, force
characterDance("bf")
getCharAnim("bf") -- → animation name string
isAnimFinished("bf") -- → bool
-- Transform
setCharPos("bf", 300, 400)
setCharX("bf", 300) setCharY("bf", 400)
setCharScale("bf", 1.2)
setCharAlpha("bf", 0.8)
setCharColor("bf", colorRGB(255,200,200))
setCharAngle("bf", 15)
setCharFlip("bf", false, false)
setCharVisible("bf", true)
setCharScrollFactor("bf", 1.0, 1.0)
setCharPlaybackRate("bf", 1.5)
lockCharacter("bf", true) -- freeze in place
-- Swap characters mid-song
setBF("newBoyfriend")
setDAD("newDad")
setGF("newGF")
Notes (Lua)
-- Spawn a note at a specific time
local noteID = spawnNote(3000, 0, 0, "normal")
-- ms, dir, holdLen, type
-- Iterate over all active notes
forEachNote(function(noteID)
local dir = getNoteDir(noteID)
local time = getNoteTime(noteID)
setNoteAlpha(noteID, 0.5)
end)
ModChart (Lua)
-- Apply a modifier immediately (instant)
setModifier("player", "moveX", 50)
setModifier("opponent", "angle", 15)
setModifier("player", "alpha", 0.8)
setModifier("player", "scaleX", 1.5)
-- Read a modifier value
local val = getModifier("player", "moveX")
-- Reset all
clearModifiers()
Audio (Lua)
playMusic("music/myTrack")
playMusic("music/myTrack", 0.8) -- with volume
stopMusic()
pauseMusic()
resumeMusic()
playSound("sounds/hit")
playSound("sounds/hit", 0.6)
getMusicPos() -- → ms
setMusicPos(5000) -- seek to 5 seconds
setMusicPitch(1.25) -- pitch shift
Events (Lua)
-- Trigger a chart event immediately
triggerEvent("Camera Follow", "bf")
triggerEvent("Add Camera Zoom", "0.015", "0.5")
-- Register a custom event handler
registerEvent("My Event", function(v1, v2, time)
trace("My event fired: " .. v1)
end)
-- Register a new event definition (shows in Chart Editor)
registerEventDef({
name = "My Event",
description = "Does something cool",
color = "FF3C78",
params = {
{ name = "Target", type = "string", default = "bf" },
{ name = "Intensity", type = "float", default = "1.0" }
}
})
-- List events
local list = listEvents("chart")
Input (Lua)
keyPressed("SPACE") -- is SPACE held?
keyJustPressed("Z") -- just pressed this frame?
keyJustReleased("Z") -- just released?
mouseX() mouseY()
mousePressed() -- left button held
mouseJustPressed() -- just clicked
Script Communication (Lua)
-- Shared data between scripts
setShared("myKey", 42)
local v = getShared("myKey", 0) -- 0 = default if missing
deleteShared("myKey")
-- Broadcast an event to all active scripts
broadcast("onCustomEvent", arg1, arg2)
-- Call a function on all scripts by name
callOnScripts("myFunction", arg1)
-- Get/set variables in other scripts
setScriptVar("otherScript", "myVar", 100)
local v = getScriptVar("otherScript", "myVar")
Utils (Lua)
-- Math
lerp(a, b, 0.1)
clamp(v, 0, 100)
randomInt(1, 10)
randomFloat(0.0, 1.0)
-- Colors
colorRGB(255, 100, 100) -- → int color
colorRGBA(255, 100, 100, 200) -- → int color with alpha
colorHex("FF3C78") -- → int from hex string
-- Paths
getProperty("gf.x") -- Psych-style path access
setProperty("gf.x", 100)
getPropertyOf(sprID, "x")
setPropertyOf(sprID, "alpha", 0.5)
-- File I/O (desktop only)
fileExists("mods/myMod/data/x.json")
fileRead("mods/myMod/data/x.json")
fileWrite("mods/myMod/data/out.txt", "data")
-- Logging
log("My message")
trace("Also works")
You can build entirely new screens (states) using HScript without writing Haxe. Place a .hx file in mods/{mod}/states/{StateName}/ and navigate to it with states.goto("StateName").
Minimal state example
// mods/my-mod/states/MyState/script.hx
class MyState extends StateScript {
var bg: FlxSprite;
var label: FlxText;
override function onCreate() {
// Black background
bg = new FlxSprite(0, 0);
bg.makeGraphic(FlxG.width, FlxG.height, FlxColor.fromRGB(10, 10, 20));
addSprite(bg);
// Title text
label = createText(0, 100, FlxG.width, "MY CUSTOM STATE", 48);
label.alignment = "center";
label.color = FlxColor.fromRGB(255, 60, 120);
addSprite(label);
}
override function onUpdate(elapsed: Float) {
// Pulse the label
label.alpha = 0.5 + 0.5 * Math.sin(FlxG.game.ticks / 300);
}
// ESC goes back to main menu
override function onBack(): Bool {
states.goto("MainMenuState");
return true; // cancel default behavior
}
}
State with animated sprite & sounds
class IntroState extends StateScript {
var char: Dynamic;
override function onCreate() {
var spr = new FunkinSprite(300, 200);
spr.loadSparrow("images/characters/bf/BF_assets");
spr.addAnim("idle", "BF idle dance", 24, true);
spr.playAnim("idle");
spr.screenCenter();
addSprite(spr);
char = spr;
FlxG.sound.play(Paths.sound("confirmMenu"));
}
override function onBeatHit(beat: Int) {
if (beat % 2 == 0) char.playAnim("idle", true);
}
override function onAccept(): Bool {
FlxG.sound.play(Paths.sound("confirmMenu"));
states.sticker(new funkin.gameplay.PlayState());
return true;
}
}
Scripted SubState
// Open a scripted substate over the current state
// File: mods/my-mod/states/MyPopup/popup.hx
class MyPopup extends StateScript {
override function onCreate() {
// Semi-transparent overlay
var overlay = new FlxSprite(0, 0);
overlay.makeGraphic(FlxG.width, FlxG.height, FlxColor.fromRGBFloat(0,0,0,0.6));
addSprite(overlay);
var txt = createText(0, FlxG.height/2 - 30, FlxG.width, "Press ENTER to continue", 28);
txt.alignment = "center";
addSprite(txt);
}
override function onAccept(): Bool {
states.close();
return true;
}
override function onBack(): Bool {
states.close();
return true;
}
}
// To open it from another script:
// states.openSubState("MyPopup")
Available helpers inside StateScript
// In any StateScript method:
addSprite(spr) // add to state
removeSprite(spr) // remove from state
createText(x, y, w, txt, size) // → FlxText (not yet added)
getVar("fieldName") // read any field on the state
setVar("fieldName", val) // write any field on the state
callMethod("method", [arg1, arg2]) // call any method on the state
// Sound / music (same as Lua)
FlxG.sound.play(Paths.sound("confirm"))
FlxG.sound.playMusic(Paths.music("menu"))
FlxG.sound.music.stop()
Place a script at mods/{mod}/characters/scripts/{charName}/script.hx. The engine injects character (the Character instance) and game (PlayState) automatically.
// mods/my-mod/characters/scripts/bf/script.hx
function onCreate() {
// Scale up BF a little
character.scale.set(1.05, 1.05);
character.updateHitbox();
log("BF script ready!");
}
function onDance() {
// Extra bounce on health above 50%
if (game != null && game.health > 1.0) {
FlxTween.tween(character.scale, {x: 1.12, y: 0.9}, 0.05, {
ease: FlxEase.sineOut,
onComplete: function(_) {
FlxTween.tween(character.scale, {x: 1.05, y: 1.05}, 0.15, {ease: FlxEase.elasticOut});
}
});
}
}
function onBeatHit(beat) {
if (beat % 8 == 0) {
// Flash color briefly
character.color = FlxColor.fromRGB(255, 200, 200);
timer(0.15, function() { character.color = FlxColor.WHITE; });
}
}
function onSingStart(direction, anim) {
log("BF singing direction: " + direction);
}
// Return true to cancel the engine's default idle → dance transition
function onDanceEnd(): Bool {
return false;
}
All CharacterScript callbacks
| Callback | When it fires |
| onCreate() | Character fully initialized |
| onDance() | Dance animation triggered |
| onDanceEnd() → Bool | Dance animation ended. Return true to skip default transition |
| onSingStart(dir, anim) | Character begins singing (note hit) |
| onSingEnd(dir, anim) | Character finishes singing (hold released) |
| onBeatHit(beat) | Every musical beat |
| onStepHit(step) | Every musical step |
| onUpdate(elapsed) | Every game frame |
| onDestroy() | Character removed/destroyed |
| onAnimFinished(anim) | Any animation completes |
| onMiss(dir) | Player missed a note on this character's side |
| onGameOver() | Health reached 0 |
Cutscenes can be defined as JSON or built dynamically in Lua/HScript. Place them in mods/{mod}/videos/ or trigger them from a song script.
SpriteCutscene JSON Format
// mods/my-mod/data/cutscenes/myIntro.json
{
"skippable": true,
"sprites": [
{
"id": "bg",
"image": "images/cutscenes/myBg",
"x": 0, "y": 0,
"scrollX": 1.0, "scrollY": 1.0,
"depth": 0
},
{
"id": "char",
"image": "images/characters/dad/Dad_assets",
"atlas": "sparrow",
"x": 400, "y": 100,
"depth": 1
}
],
"timeline": [
{ "time": 0, "action": "playAnim", "target": "char", "anim": "idle", "loop": true },
{ "time": 0.5, "action": "sound", "file": "sounds/cutscene_start" },
{ "time": 1.0, "action": "tweenX", "target": "char", "to": 500, "duration": 0.8, "ease": "sineOut" },
{ "time": 2.0, "action": "subtitle", "text": "You ready for this?", "duration": 2.0 },
{ "time": 4.0, "action": "cameraZoom","to": 1.2, "duration": 0.5 },
{ "time": 5.0, "action": "end" }
]
}
Triggering cutscenes from scripts
// In a song HScript or Lua script
// Trigger a sprite cutscene by name (before song)
function onSongStart() {
// Note: normally done via meta.json, but can be forced:
VideoManager.playCutscene("myIntro", function() {
// Called when cutscene finishes
game.resumeSong();
});
}
// Play a video file
function onSongStart() {
VideoManager.playVideo("cutscene.mp4", function() {
game.startCountdown();
});
}
Lua cutscene builder API
-- Build a cutscene in Lua
function onCountdownStart()
local cut = newCutscene()
cutsceneSkippable(cut, true)
local sprID = cutsceneDefineSprite(cut, {
image = "images/cutscenes/dadAnim",
atlas = "sparrow",
x = 400, y = 100
})
cutsceneAdd(cut, 0.0, "playAnim", sprID, "idle", true)
cutscenePlaySound(cut, 0.5, "sounds/cutscene_intro")
cutsceneWait(cut, 1.5)
cutsceneCameraZoom(cut, 1.2, 0.5, "sineOut")
cutsceneAdd(cut, 3.0, "end")
cutscenePlay(cut)
end
Dialogue is defined in JSON and loaded by DialogueBoxImproved. Place files in assets/data/dialogue/ or mods/{mod}/data/dialogue/.
// assets/data/dialogue/week1/intro.json
{
"characters": {
"bf": { "portrait": "images/ui/dialogue/bf", "side": "right" },
"dad": { "portrait": "images/ui/dialogue/dad", "side": "left" }
},
"lines": [
{
"character": "dad",
"text": "You ready to go, kid?",
"expression": "normal",
"sound": "sounds/dialogue/grunt"
},
{
"character": "bf",
"text": "Yeah! Let's do it!",
"expression": "happy"
},
{
"character": "dad",
"text": "Don't disappoint me.",
"expression": "serious",
"sound": "sounds/dialogue/hmm"
}
]
}
// Trigger from a song script or cutscene:
// game.startDialogue("week1/intro")
// or from Lua:
// startDialogue("week1/intro")
Custom note types let you define notes that trigger special behavior on hit, miss, or hold. Register them from a global script that loads early.
// In a global HScript (assets/data/scripts/global/myNoteTypes.hx)
// Register a "mine" note type that damages the player on hit
noteTypes.register("mine", {
onHit: function(note) {
// Damage player when hitting a mine
hud.addHealth(-0.3);
camera.shake(0.04, 0.3);
FlxG.sound.play(Paths.sound("explosion"));
return true; // return true to cancel normal hit behavior
},
onMiss: function(note) {
// Missing a mine is fine — no penalty
return true; // cancel normal miss behavior
},
color: FlxColor.fromRGB(200, 50, 50),
hurtNote: true
});
// Register a "ghost" note — invisible but still hittable
noteTypes.register("ghost", {
onCreate: function(note) {
note.alpha = 0.0; // invisible
},
onHit: function(note) {
// Bonus points for hitting blind
score.addScore(500);
return false; // keep normal hit behavior
}
});
// REGISTERING FROM LUA
In Lua, use the global
registerEvent() and
registerEventDef() to register custom event types and handlers. Note types can also be registered from Lua via
noteTypes.register() — the same API applies.
Define these functions in your script and the engine will call them automatically at the right moment.
Lifecycle — all states
function onCreate() { } // when the state is created
function onUpdate(elapsed) { } // every frame, before update
function onUpdatePost(elapsed) { } // every frame, after update
function onBeatHit(beat) { } // every beat (4 per measure)
function onStepHit(step) { } // every step (16 per measure)
function onDestroy() { } // when the state is destroyed
PlayState only
function onSongStart() { }
function onSongEnd() { }
function onCountdownStarted() { }
function onSectionHit(section) { }
function onRestart() { }
function onResume() { }
// note = Note object, rating = "sick" / "good" / "bad" / "shit"
function onNoteHit(note, rating) { }
function onNoteHitPost(note, rating) { }
function onMiss(note, _) { }
function onMissPost(direction) { }
function onOpponentNoteHit(note, _) { }
function onCharacterSing(charId, noteData) { }
// Cancellable — return true to cancel (see table below)
function onPause() { }
function onGameOver() { }
Cancellable callbacks
| Callback | What gets cancelled when returning true |
| onNoteHit | Score, rating, health gain, confirm animation |
| onOpponentNoteHit | Score, rating, health loss, confirm animation |
| onMiss | Health loss, miss counter increment |
| onPause | Opening the pause menu |
| onGameOver | Game over screen |
| onCountdownStarted | The countdown |
HScript — cancel with return true
function onNoteHit(note, rating) {
if (note.noteType == 'mine') {
game.gameState.health -= 0.3;
return true; // cancels score, animation, normal logic
}
// returning nothing = does not cancel
}
function onPause() {
if (game.isBossSequence)
return cancelEvent(); // helper equivalent to return true
return continueEvent(); // helper equivalent to return false
}
// Note data available in callbacks
function onNoteHit(note, rating) {
trace(note.noteData); // 0=left 1=down 2=up 3=right
trace(note.strumTime); // time in ms
trace(note.noteType); // note type string
trace(note.isSustainNote); // true if hold piece
trace(note.sustainLength); // hold duration in ms
trace(rating); // "sick", "good", "bad", "shit"
}
Lua — cancelling
function onNoteHit(note)
if game.bossMode then
return false -- before() / cancellable: return false to cancel
end
end
function onMiss(note)
game.health = game.health - 0.05 -- custom penalty via OOP
return false -- cancel engine default penalty
end
// LUA
In RuleScript (Lua), use
return false from a
before() hook to cancel the original, or just
return false from any gameplay callback.
Cool Engine has two levels of control over engine behavior:
callbacks run alongside the engine, and
hooks let you run before, after, or instead of any method on any object.
// THREE LEVELS OF CONTROL
Callbacks (onCreate, onBeatHit…) — engine fires them, you add code on top.
before / after — wrap any method on any object, still call the original.
replace — fully replace any method, original never runs.
before() / after() / replace()
Works in both HScript and RuleScript (Lua). No original parameter — just the real arguments. The object you hook can be anything: game, bf, dad, a sprite, a class instance.
| Function | When your code runs | Can cancel original? |
before(obj, "method", fn) | Before the original | Yes — return false |
after(obj, "method", fn) | After the original | No |
replace(obj, "method", fn) | Instead of the original | Always (original never runs) |
Lua examples
-- Add camera shake on every beat without touching onBeatHit
before(game, "onBeatHit", function(beat)
FlxG.camera:shake(0.02, 0.1)
end)
-- Run extra code after dad finishes dancing
after(dad, "dance", function()
dad:playAnim("extraBob", false)
end)
-- Fully replace bf's dance
replace(bf, "dance", function()
bf:playAnim("myIdle", true)
end)
-- Cancel the original based on a condition
before(game, "onNoteHit", function(note)
if note.noteType == "ghost" then
return false -- original hit logic is skipped
end
end)
HScript examples
// Add shake on every beat
before(game, 'onBeatHit', function(beat) {
FlxG.camera.shake(0.02, 0.1);
});
// Extra anim after dad dances
after(dad, 'dance', function() {
dad.playAnim('extraBob', false);
});
// Replace bf's dance entirely
replace(bf, 'dance', function() {
bf.playAnim('myIdle', true);
});
// Cancel hit logic for a specific note type
before(game, 'onNoteHit', function(note) {
if (note.noteType == 'ghost') return false;
});
Example — fully custom scoring
// HScript: replace the entire note-hit scoring
replace(game, 'onNoteHit', function(note, rating) {
var pts = switch(rating) {
case 'sick': 350;
case 'good': 200;
case 'bad': 100;
default: 50;
};
game.gameState.score += pts;
// You control everything — play your own anims, sounds, etc.
});
// Replace miss penalty
replace(game, 'onMiss', function(note) {
game.health -= 0.08;
bf.playAnim('hurt', true);
});
-- Same thing in Lua
replace(game, "onNoteHit", function(note, rating)
local pts = 50
if rating == "sick" then pts = 350
elseif rating == "good" then pts = 200
elseif rating == "bad" then pts = 100 end
game.gameState.score = game.gameState.score + pts
end)
replace(game, "onMiss", function(note)
game.health = game.health - 0.08
bf:playAnim("hurt", true)
end)
StateScript overrides (HScript only)
Inside a StateScript you can also use overrideFunction to replace a named callback registered by the engine on that specific state. This is separate from the object-level before/after/replace system above.
// Replace a specific engine-registered hook on this state
overrideFunction('onNoteHit', function(note, rating) {
game.gameState.score += 500;
});
// Restore it
removeOverride('onNoteHit');
// Toggle without removing
toggleOverride('onNoteHit', false);
toggleOverride('onNoteHit', true);
if (hasOverride('onNoteHit')) trace('active');
// WHICH TO USE?
Use
before/after/replace for hooking into any object method — works in
both Lua and HScript, on anything.
Use
overrideFunction only for StateScript-specific named engine hooks —
HScript only.
Create full screens and menus entirely from HScript, without touching any Haxe code.
Folder structure
assets/states/mystate/
main.hx ← main script
helper.hx ← additional scripts (optional)
Navigating to the state
// From Haxe
StateTransition.switchState(new funkin.scripting.ScriptableState('mystate'));
// From another HScript or RuleScript
switchStateInstance(new funkin.scripting.ScriptableState('mystate'));
stickerSwitch(new funkin.scripting.ScriptableState('mystate'));
Example — full selection menu
// assets/states/myselectscreen/main.hx
var bg:FlxSprite;
var options = [];
var cursor = 0;
function onCreate() {
bg = new FlxSprite(0, 0);
bg.makeGraphic(FlxG.width, FlxG.height, 0xFF000000);
add(bg);
var title = new FlxText(0, 60, FlxG.width, "MY MENU", 52);
title.setFormat(Paths.font("vcr.ttf"), 52, 0xFFFFFFFF, "center");
title.alpha = 0;
add(title);
FlxTween.tween(title, {alpha: 1}, 0.4, {ease: FlxEase.quadOut});
var labels = ["Play", "Options", "Back"];
for (i in 0...labels.length) {
var opt = new FlxText(200, 200 + i * 70, 400, labels[i], 36);
add(opt);
options.push(opt);
}
_updateCursor();
}
function onUpdate(elapsed) {
if (FlxG.keys.justPressed.DOWN) {
cursor = (cursor + 1) % options.length;
_updateCursor();
FlxG.sound.play(Paths.sound('menus/scrollMenu'));
}
if (FlxG.keys.justPressed.UP) {
cursor = (cursor - 1 + options.length) % options.length;
_updateCursor();
}
if (FlxG.keys.justPressed.ENTER) _select();
if (FlxG.keys.justPressed.ESCAPE) switchState('MainMenuState');
}
function onBeatHit(beat) { zoomCamera(1.03, 0.1); }
function onDestroy() { options = []; }
function _updateCursor() {
for (i in 0...options.length)
options[i].color = i == cursor ? 0xFFFFFF00 : 0xFFFFFFFF;
}
function _select() {
switch (cursor) {
case 0: switchState('FreeplayState');
case 1: switchState('OptionsMenuState');
case 2: switchState('MainMenuState');
}
}
ScriptableSubState — popups
// Open a scripted substate from another script
FlxG.state.openSubState(new funkin.scripting.ScriptableState('confirmpopup'));
// assets/states/confirmpopup/main.hx — confirmation popup
var onYes;
function onCreate() {
// Receive a callback from the parent state via shared data
onYes = getShared('confirmCallback');
var overlay = new FlxSprite(0, 0);
overlay.makeGraphic(FlxG.width, FlxG.height, 0xAA000000);
add(overlay);
var msg = new FlxText(0, FlxG.height / 2 - 20, FlxG.width, "Are you sure?", 28);
msg.alignment = "center";
add(msg);
}
function onKeyJustPressed(key) {
if (FlxG.keys.justPressed.ENTER) {
if (onYes != null) onYes();
state.closeSubState();
}
if (FlxG.keys.justPressed.ESCAPE) state.closeSubState();
}
// ── In the parent state, set the callback before opening: ─────────
// setShared('confirmCallback', function() { trace('confirmed!'); });
// FlxG.state.openSubState(new funkin.scripting.ScriptableState('confirmpopup'));
Creating objects
// HScript — identical to writing a real FlxState in Haxe
var spr = new FlxSprite(x, y);
var spr2 = new FlxSprite(x, y); spr2.loadGraphic(Paths.image('img/path'));
var box = new FlxSprite(x, y); box.makeGraphic(w, h, 0xFFAA0000);
var txt = new FlxText(x, y, 0, 'Hello', 24);
var txt2 = new FlxText(x, y, 0, 'Hello', 24); txt2.font = Paths.font('vcr.ttf');
var grp = new FlxSpriteGroup();
var grp2 = new FlxGroup();
Display list
add(spr);
remove(spr);
insert(pos, spr); // specific position in display list
Tweens & timers
FlxTween.tween(spr, {alpha: 0}, 0.5);
FlxTween.tween(spr, {y: 100}, 0.3, {ease: FlxEase.quadOut});
FlxTween.tween(spr, {alpha: 0}, 1.0, {
ease: 'expoIn',
delay: 0.5,
onComplete: function(_) { trace('done'); }
});
cancelTweens(spr);
timer(1.5, function(t) { trace('tick'); });
interval(0.5, function(t) { }, 5); // 5 repetitions
Camera
shake(0.005, 0.25);
flash(0xFFFFFFFF, 0.5);
fade(0xFF000000, 0.5, false); // false=fadeOut, true=fadeIn
zoomCamera(1.05, 0.1);
Sound
playSound('menus/confirmMenu');
playSound('menus/scrollMenu', 0.7);
playMusic('freakyMenu');
stopMusic();
Centering & screen info
center(spr); // both axes
centerX(spr); // horizontal only
centerY(spr); // vertical only
var w = FlxG.width;
var h = FlxG.height;
Navigation
// Built-in engine states
switchState('MainMenuState');
switchState('FreeplayState');
switchState('OptionsMenuState');
switchState('TitleState');
switchState('PlayState');
switchState('StoryMenuState');
// Custom state instances
switchStateInstance(new funkin.scripting.ScriptableState('mystate'));
stickerSwitch(new funkin.scripting.ScriptableState('mystate'));
loadState(new funkin.scripting.ScriptableState('mystate'));
Shared data
// Script A — write
setShared('playerName', 'Kawai');
setShared('myCallback', function() { trace('called!'); });
// Script B — read
var name = getShared('playerName');
var cb = getShared('myCallback');
if (cb != null) cb();
deleteShared('playerName');
-- Lua: same functions available as globals
setShared('key', value)
local v = getShared('key')
deleteShared('key')
Broadcast
// Emit from any script
broadcast('phase2Started', {bgColor: 0xFF0000FF});
// Receive — function named after the event
function onPhase2Started(data) {
trace('color: ' + data.bgColor);
}
// Or register an explicit hook
registerHook('phase2Started', function(data) {
trace('received via hook');
});
-- Lua
broadcast('phase2Started')
function onPhase2Started(data)
-- called automatically
end
Get another script
// By filename (no extension)
var other = getScript('mySongScript');
if (other != null) other.set('someVar', 42);
// Assign and look up by tag
setTag('myTag');
var list = getScriptTag('myTag'); // array of scripts
Execution priority
setPriority(10); // runs before scripts at priority 0
setPriority(-5); // runs after
// If two scripts cancel the same event, the higher priority one wins
Hot-reload (developer mode)
hotReload(); // reload this script from disk
// F7 reloads ALL scripts globally
// Import another script as a module
var utils = require('path/to/utils.hx');
states API — navigation from scripts
// Navigate to a built-in engine state by name
switchState('MainMenuState');
switchState('FreeplayState');
switchState('PlayState');
// Navigate with a concrete state instance
switchStateInstance(new funkin.scripting.ScriptableState('mystate'));
stickerSwitch(new funkin.scripting.ScriptableState('mystate'));
loadState(new funkin.scripting.ScriptableState('mystate'));
// Sub-states
FlxG.state.openSubState(new funkin.scripting.ScriptableState('mypopup'));
state.closeSubState(); // close current substate
// Read current state
var cur = FlxG.state;
scripts API — internal broadcast
// From HScript — call a function on all active scripts
ScriptHandler.callOnScripts('myFunc', [arg1, arg2]);
// Set a variable on all active scripts
ScriptHandler.setOnScripts('myVar', value);
// Broadcast an event to all StateScript listeners (StateScriptHandler)
broadcast('myEvent', [arg1, arg2]);
// Get another script in the same state by name or tag
var other = getScript('otherScriptName');
var tagged = getScriptTag('myTag');
Cool Engine has a unified shader system built around ShaderManager and
FunkinRuntimeShader. Shaders live in assets/shaders/ or
mods/{mod}/shaders/ and are picked up automatically on startup.
Shader file types
| File | What it is |
name.frag | Plain GLSL fragment shader |
name.vert | Optional companion vertex shader (same name) |
name.lua | Script shader — defines GLSL inline + animates uniforms |
name.hx | Same as above but in HScript |
Using shaders from scripts
// Apply to a sprite
ShaderManager.applyShader(bf, "wave");
ShaderManager.applyShader(stage.background, "chromaShift");
// Apply to a camera (full-screen overlay)
ShaderManager.applyShaderToCamera("vignette");
ShaderManager.applyShaderToCamera("vignette", FlxG.cameras.list[1]);
// True post-process (reads camera pixels — shader must use #pragma header)
ShaderManager.applyPostProcessToCamera("blur");
// Update a uniform every frame
ShaderManager.setShaderParam("wave", "uTime", elapsed);
ShaderManager.setShaderParam("chromaShift", "uIntensity", 0.008);
// Remove
ShaderManager.removeShader(bf);
ShaderManager.removeShaderFromCamera("vignette");
Script shaders (.lua / .hx)
Put a .lua or .hx file in the shaders folder instead of a plain
.frag. The script defines the GLSL source in a frag variable and
updates uniforms in onUpdate(elapsed). No separate .frag file needed.
-- assets/shaders/wave.lua
frag = [[
#pragma header
uniform float uTime;
uniform vec2 uResolution;
void main() {
vec2 uv = openfl_TextureCoordv;
uv.x += sin(uv.y * 20.0 + uTime * 3.0) * 0.01;
gl_FragColor = flixel_texture2D(bitmap, uv);
}
]]
local time = 0.0
function onCreate()
setFloat2("uResolution", width, height)
end
function onUpdate(elapsed)
time = time + elapsed
setFloat("uTime", time)
end
// assets/shaders/wave.hx
var frag = "
#pragma header
uniform float uTime;
void main() {
vec2 uv = openfl_TextureCoordv;
uv.x += sin(uv.y * 20.0 + uTime * 3.0) * 0.01;
gl_FragColor = flixel_texture2D(bitmap, uv);
}
";
var time = 0.0;
function onUpdate(elapsed:Float) {
time += elapsed;
setFloat("uTime", time);
}
Once defined, use it exactly like any other shader:
ShaderManager.applyShader(bf, "wave");
ShaderManager.applyShaderToCamera("wave");
Script shader API
| Function | GLSL type |
setUniform(name, value) | auto-dispatch by type |
setFloat(name, v) | uniform float |
setFloat2(name, x, y) | uniform vec2 |
setFloat3(name, x, y, z) | uniform vec3 |
setFloat4(name, x, y, z, w) | uniform vec4 |
setInt(name, v) | uniform int |
setBool(name, v) | uniform bool |
setColor(name, 0xAARRGGBB) | uniform vec4 normalised |
recompile() | recompile from current frag/vert |
width, height | FlxG.width / FlxG.height |
Callbacks
| Callback | When |
onCreate() | Once, after the shader compiles |
onUpdate(elapsed) | Every frame while active |
onDestroy() | When the shader is unloaded |
Direct FunkinRuntimeShader API
If you import the shader class directly you get the full uniform API:
import "funkin.graphics.shaders.FunkinRuntimeShader"
// Create from a file
var fx = FunkinRuntimeShader.fromFile("assets/shaders/wave.frag");
// Create inline
var fx = new FunkinRuntimeShader("
#pragma header
uniform float uTime;
void main() {
gl_FragColor = flixel_texture2D(bitmap, openfl_TextureCoordv);
}
");
// Apply
bf.shader = fx;
FlxG.camera.filters = [new openfl.filters.ShaderFilter(fx)];
// Set uniforms
fx.setFloat("uTime", elapsed);
fx.setFloat2("uResolution", FlxG.width, FlxG.height);
fx.setColor("uTint", FlxColor.RED);
fx.setFloat4("uRect", 0.0, 0.0, 1.0, 1.0);
// Hot-reload from disk
fx.reload(); // re-reads the .frag file
fx.recompile(newFragCode);
Built-in engine shaders
| Shader | Applied to | Description |
BloomShader | Camera | Bloom glow effect — always on (can be disabled in options) |
NoteGlowShader | Notes | Coloured glow on arrows, one per direction |
ChromaticAberrationShader | Camera filter | RGB channel split |
FilmGrainShader | Camera filter | Animated film grain |
VignetteShader | Camera filter | Dark vignette border |