Skip to main content

State flow

Phoenix Engine is a Flixel game, so every screen is an FlxState and every overlay an FlxSubState. The engine adds one layer on top: backend.MusicBeatState and backend.MusicBeatSubstate, which give each screen song-synchronised callbacks and mod-aware asset handling.

The map​

States​

StatePackageRole
InitStatestatesone-time setup, then hands off
StartupStatestatesengine splash
TitleStatestatestitle screen, intro text, update check
FlashingStatestatesphotosensitivity warning
OutdatedStatestatesshown when a newer release exists (CHECK_FOR_UPDATES)
MainMenuStatestateshub: story, freeplay, mods, credits, options, editors
StoryMenuStatestatesweek selection, difficulty, character display
FreeplayStatestatessong list, scores, the music player
ModsMenuStatestatesenable/disable/reorder mods
CreditsStatestatescredits list
AchievementsMenuStatestatesachievement gallery
OptionsStateoptionsoptions categories
LoadingStatestatesasync asset load before a song
PlayStateplaythe song itself
ErrorStatestatescrash screen
MasterEditorMenu + editorseditorsauthoring tools

Substates: PauseSubState, GameOverSubstate, GameplayChangersSubstate, ResetScoreSubState (states.substates), RenderingDoneSubState (states), the options category substates (options/), psychlua.CustomSubstate for scripts, states.ModState / states.substates.ModSubState for fully scripted screens, and mobile.MobileControlsSelectSubState.

Every MusicBeatState / MusicBeatSubstate loads optional HScript overlays from data/states/<ClassName>.hx. Details: Scripted states.

The MusicBeatState contract​

class MyState extends MusicBeatState {
override function create():Void { super.create(); }
override function update(elapsed:Float):Void { super.update(elapsed); }

override function stepHit():Void { super.stepHit(); } // 16× per section by default
override function beatHit():Void { super.beatHit(); } // 4× per section by default
override function sectionHit():Void { super.sectionHit(); } // 1× per section
}

A beat is always four steps, but a section is not always four beats: MusicBeatState.updateSection() advances by Math.round(getBeatsOnSection() * 4) steps, so a section with a non-default sectionBeats fires proportionally more or fewer steps and beats. Never assume 16 steps per section in timing-sensitive code — count beats instead.

MusicBeatState tracks curStep, curBeat and curSection from Conductor, handles the transition in/out, exposes controls (from PlayerSettings), and takes care of clearing per-state asset caches through Paths.

Menus use the beat callbacks for bopping icons and dancing characters; PlayState forwards them to the active stage and to every loaded script.

Transitions​

  • Switching is plain Flixel: FlxG.switchState(MainMenuState.new) (or FlxG.switchState(() -> new ModsMenuState()) when the constructor takes arguments).
  • FlxTransitionableState.skipNextTransIn is used to suppress the fade on the very first transition (set in InitState).
  • shaders.CustomFadeTransition provides the engine's fade; shaders.CrossFade is the separate character-crossfade effect used during gameplay.

Entering gameplay​

Both story and freeplay set up the same statics before switching:

StaticMeaning
PlayState.SONGthe loaded SwagSong
PlayState.isStoryModestory vs freeplay behaviour (next song, transitions, saves)
PlayState.storyPlaylistremaining songs in the week
PlayState.storyDifficultyselected difficulty index
PlayState.campaignScore / campaignMissesrunning week totals

LoadingState sits in between to preload the song's assets; on finish it constructs PlayState. When the song ends, PlayState either loads the next entry of storyPlaylist, returns to StoryMenuState, or returns to FreeplayState.

Adding a state​

  1. Extend MusicBeatState (or MusicBeatSubstate).
  2. Put heavy logic in a helper module under states/helpers/ instead of the state itself.
  3. Load assets through Paths, never with a literal path.
  4. If scripts should see it, broadcast a hook through the script host the way PlayState does — see Script hooks.
  5. Re-export it from headers.States if it belongs to an existing group.