// Developer Reference

Cool Engine

A feature-rich Friday Night Funkin' engine built on HaxeFlixel / OpenFL. Supports Lua & HScript modding, multi-format chart compatibility, a 3-layer asset cache, and a full suite of in-game editors.
Version 0.6.0B
Language Haxe 4+
Framework HaxeFlixel
Resolution 1920×1080
📦
Downloads
Engine releases, templates, tools and example mods

Engine

Templates & Starters

Tools

🏆
Mod Showcase
Mods and projects built with Cool Engine

A collection of mods made by the community using Cool Engine. Want yours listed here? See the submission info below.

📬 Submit your mod
Made something with Cool Engine? Open an issue or PR on the GitHub wiki repo and we'll add it here.

→ Submit via GitHub Issues  ·  → Post in #showcase on Discord
Overview
What is Cool Engine?

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.
Initialization Order
Main.hx bootstrap sequence

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 modeInitAPI 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 overlaysDataInfoUI, 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.
Engine Settings
EngineSettings.hx — centralized configuration
ConstantValueDescription
DEFAULT_FPS60Default FPS target on desktop
MIN_FPS30Minimum accepted FPS
MAX_FPS2000Maximum accepted FPS (0 = unlimited)
DEFAULT_WIDTH1920Default resolution width (1080p)
DEFAULT_HEIGHT1080Default 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.

Asset Cache (FunkinCache v2)
3-layer lifecycle management

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 evictionclearSecondLayer() collects all keys then does a single-pass removal, avoiding Map re-hashing on each individual delete.
  • Hot path optimizationgetBitmapData() 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.
📜
Scripting System
ScriptHandler v3 — Lua & HScript

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)

HookTrigger
onCreateScript 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
onSongStartSong begins after countdown
onSongEndSong finishes
onCountdownTick(tick)Countdown tick before song
onCountdownEndCountdown finished
onPauseGame paused
onResumeGame resumed
onGameOverPlayer dies
onGameOverRestartPlayer 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
onFocusLostWindow loses focus
onFocusGainedWindow gains focus
onDestroyScript 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:

GlobalTypeDescription
gamePlayStateCurrent PlayState instance
bfCharacterBoyfriend character
dadCharacterOpponent character
gfCharacterGirlfriend character
stageStageCurrent stage instance
FlxGflixel.FlxGFlixel global access
ConductorConductorMusic timing
PathsPathsAsset 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:

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

FunctionWhen your code runsCancel original?
before(obj, method, fn)Before the originalYes — return false
after(obj, method, fn)After the originalNo
replace(obj, method, fn)Instead of the originalAlways (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.

FunctionDescription
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
Event System
EventManager + EventRegistry

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

EventDescription
Camera FollowMoves the camera to focus player or opponent. Used by Cool Engine instead of implicit mustHitSection.
BPM ChangeChanges the song BPM mid-track, updates the Conductor.
Hey!Triggers the "Hey!" animation on a character.
Set GF SpeedChanges the GF bopping speed.
Add Camera ZoomBump-zooms the game/HUD cameras.
Alt AnimForces 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.
🎮
Gameplay / PlayState
Core gameplay loop

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.
Note System
NoteSkinSystem + NotePool + NoteBatcher

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
🎨
Note Skin Scripting
Lua · HScript · JSON — skins, splashes & hold splashes

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:

VariableTypeDescription
skinNameStringName of the active skin (matches skin.json "name")
NoteSkinSystemClassFull static access to the skin system
NoteRGBShaderClassDirect shader class for manual instantiation
applyRGBShader(spr, dir, mult)FunctionApply direction-preset RGB shader to any sprite
applyRGBColor(spr, hex, mult)FunctionApply hex color RGB shader
setRGBIntensity(spr, mult)FunctionChange intensity of existing shader
removeRGBShader(spr)FunctionRemove RGB shader from sprite
tweenRGBToDirection(spr, dir, dur, ease)FunctionTween color → direction preset
tweenRGBToColor(spr, hex, dur, ease)FunctionTween color → hex color
tweenRGBIntensity(spr, from, to, dur, ease)FunctionTween 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:

FieldTypeDefaultDescription
"script"StringnullCustom script filename. If null, skin.lua or skin.hx are auto-detected.
"colorAuto"BoolfalseApply 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"Float1.0Shader intensity (0.0–1.0). Used with colorAuto.
"colorDirections"ArraynullCustom 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.

FunctionDescription
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

DirectionColorHexR vec3B 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

CallbackScript typeArguments
onCreate()skin · splash
onNoteHit(note, rating)skinnote handle, rating string
onNoteMiss(note, direction)skinnote handle (may be null), direction int
onSplashSpawn(splash, noteData, x, y)splashsplash handle, direction 0–3, world position
onHoldSplashSpawn(cover, direction, x, y)splashcover handle, direction 0–3, strum center
onBeatHit(beat)skin · splashbeat integer
onStepHit(step)skin · splashstep integer
onUpdate(elapsed)skin · splashdelta time float
onDestroy()skin · splash
📦
Mod System
ModManager + mod.json

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.

🔌
Addon System
AddonManager + addon.json

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" }
Chart Formats
Multi-format chart loading

ChartLoader auto-detects chart format and routes to the appropriate parser. No manual format specification required.

FormatParserDetection
Psych EnginePsychChartParsermustHitSection field present
Codename EngineCodenameChartParserNo V-Slice fields, has codename markers
V-Slice (FNF 2.0)VSliceConverterscrollSpeed + events array at root
OsuManiaOsuManiaParser.osu file extension
StepManiaStepManiaParser.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.
🎬
Cutscenes
SpriteCutscene + Video + Dialogue
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).

🔊
Audio System
CoreAudio + AudioConfig

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.
Rendering & GPU
GPURenderer + OptimizationManager
  • 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.
🛠
Debug Tools
EditorHubState + in-game editors

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.
Mod Compatibility
Cross-engine mod support
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
📁
Canonical Folder Paths
Asset organization reference
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 — Full API Reference
All exposed globals you can use in .hx scripts

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 — Full Function Reference
All registered functions available in .lua scripts

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")
🏗️
Creating States from Scratch
StateScript — custom screens, menus & substates

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()
🕺
Character Scripts
CharacterScript — customize characters without editing JSON

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

CallbackWhen it fires
onCreate()Character fully initialized
onDance()Dance animation triggered
onDanceEnd() → BoolDance 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
🎬
Cutscene Scripting
SpriteCutscene JSON + Lua cutscene builder

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 System
DialogueData JSON format

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
NoteTypeManager — register new note behaviors

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.
🔔
Callbacks & Cancelling Logic
Automatically called functions — HScript & Lua

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

CallbackWhat gets cancelled when returning true
onNoteHitScore, rating, health gain, confirm animation
onOpponentNoteHitScore, rating, health loss, confirm animation
onMissHealth loss, miss counter increment
onPauseOpening the pause menu
onGameOverGame over screen
onCountdownStartedThe 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.
⚙️
Hooking & Overrides
before() / after() / replace() — HScript and RuleScript (Lua)

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.

FunctionWhen your code runsCan cancel original?
before(obj, "method", fn)Before the originalYes — return false
after(obj, "method", fn)After the originalNo
replace(obj, "method", fn)Instead of the originalAlways (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.
🖥️
Custom States in Script
ScriptableState & ScriptableSubState — 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'));
🎨
The ui API
Only available inside ScriptableState and ScriptableSubState

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'));
📡
Script Communication
Shared data, broadcast, hot-reload — HScript & Lua

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');
Shaders
Runtime GLSL shaders — sprites, cameras, and script shaders

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

FileWhat it is
name.fragPlain GLSL fragment shader
name.vertOptional companion vertex shader (same name)
name.luaScript shader — defines GLSL inline + animates uniforms
name.hxSame 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

FunctionGLSL 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, heightFlxG.width / FlxG.height

Callbacks

CallbackWhen
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

ShaderApplied toDescription
BloomShaderCameraBloom glow effect — always on (can be disabled in options)
NoteGlowShaderNotesColoured glow on arrows, one per direction
ChromaticAberrationShaderCamera filterRGB channel split
FilmGrainShaderCamera filterAnimated film grain
VignetteShaderCamera filterDark vignette border