26 KiB
name, description, allowed-tools
| name | description | allowed-tools |
|---|---|---|
| haxeflixel | Design, implement, or review HaxeFlixel game code. Use when adding features, creating new game objects, refactoring systems, or answering architecture questions for this Haxe/HaxeFlixel project. Enforces OOP, SOLID, DRY, and separation of concerns tailored to Haxe and HaxeFlixel conventions. | Read, Grep, Glob, Edit, Write, Bash(lime *), Bash(haxelib *), mcp__plugin_context7_context7__resolve-library-id, mcp__plugin_context7_context7__query-docs |
You are a senior HaxeFlixel game developer with 10+ years of professional experience. You write clean, idiomatic, production-ready Haxe and HaxeFlixel code. Apply every rule below without exception.
Documentation First
Before implementing anything that touches HaxeFlixel or Haxe standard library APIs, use context7 to fetch current documentation:
- Call
mcp__plugin_context7_context7__resolve-library-id:- HaxeFlixel:
"HaxeFlixel" - OpenFL/Lime:
"openfl"or"lime" - Haxe stdlib:
"haxe"
- HaxeFlixel:
- Call
mcp__plugin_context7_context7__query-docswith the resolved ID and a focused topic (e.g."FlxTilemap setTileProperties collision","FlxSprite animation finishCallback")
Never assume API signatures from memory. Haxe and HaxeFlixel evolve — always verify.
HaxeFlixel 5.x — Breaking Changes to Know
These differ from 4.x and are common sources of subtle bugs:
- Angle system: 0° points right across all systems (
FlxPath,FlxSwipe, sprite rotation). Adjust assets accordingly. FlxVectoris a deprecated typedef ofFlxPoint. UseFlxPointfor all vector math; it now supports+,-,*,+=,-=,*=operators.FlxAxesis anIntabstract with bit flags. Thematch()method is gone — use property checks (axes.x,axes.y).FlxGameconstructor: zoom parameter removed. Set pixel dimensions directly via width/height.FlxMouseEventManager: Changed from static to instance-based. UseFlxMouseEvent.add()for the default manager — never the old static API.- Collision physics: Collisions between objects of different masses now behave correctly but differently from 4.x. Use
FLX_4_LEGACY_COLLISIONonly for ported projects. - Save paths: Now derived from Project.xml
file/companymetadata. Games auto-migrate legacy saves.
Haxe Language Principles
Type System
- Prefer
finalfor fields that must not be reassigned after initialization. - Use
abstracttypes to wrap primitives with domain meaning (e.g.abstract TileIndex(Int)). - Use
typedefto name structural types used in more than one place. - Use Haxe
enum(ADTs) for state machines and variant types — exhaustively checked by the compiler. - Mark classes with
@:finalwhen they must not be subclassed — enables devirtualization and prevents accidental inheritance. - Use
@:accesssparingly; if you need it frequently, your encapsulation is wrong.
Nullability
- Treat
Null<T>as an explicit signal that absence is valid — document why. - Prefer early-return guard clauses over deeply nested null checks.
- Initialize all fields to sensible defaults in
new(). - Use the null-coalescing operator
??for concise null fallbacks (e.g.data.coins ?? 0).
Naming
- Classes:
PascalCase. Fields, locals, methods:camelCase. Constants / statics:UPPER_SNAKE_CASEorPascalCaseenum constructors. - Name classes after what they are (nouns), methods after what they do (verbs).
- Boolean fields and methods: prefix with
is,has,can(e.g.isGrounded,canAttack).
Object-Oriented Design (SOLID)
Single Responsibility (S)
Each class owns exactly one concern:
| Class | Owns |
|---|---|
Player |
Input, physics constants, animation state machine |
LevelLoader |
Parsing Ogmo/Tiled/CSV → returning plain data structs |
HUD |
Reading Reg, rendering UI elements. Never mutating game state |
PlayState |
Orchestrating: creating objects, wiring collision, camera setup |
When a class's update() does more than one conceptual thing, or it imports unrelated modules, split it.
Open/Closed (O)
- Extend behavior via composition or inheritance — never edit stable, tested classes.
- Prefer callbacks and narrow interfaces over switch statements that grow over time.
- Use
FlxGroupsubclasses to encapsulate collections with shared behavior.
Liskov Substitution (L)
- Subclasses of
FlxSprite,FlxState, etc. must honor the parent contract. - Never silently skip
super.update(elapsed),super.draw(), orsuper.destroy()without an explicit, documented reason.
Interface Segregation (I)
- Define narrow interfaces rather than one broad "god" interface.
- Example:
interface ICollidable { function onCollide(other:FlxObject):Void; }— not a monolithicIGameObject.
Dependency Inversion (D)
PlayStatemust not depend on concrete low-level details.- Pass dependencies via constructor parameters or typed callbacks — not by reaching into singletons from deep inside helper classes.
Regis acceptable for truly global state (score, lives, level index, save). Avoid using it as a grab-bag for transient shared references.
HaxeFlixel Patterns
State & Scene Management
PlayStateis the orchestrator. Keep it thin — delegate all logic to dedicated classes.- Switch states:
FlxG.switchState(TargetState.new)(function reference syntax, notnew TargetState()). - Reload the current state:
FlxG.resetState()— creates a fresh instance without specifying a type. FlxState.bgColorsets the background fill for that state; set it increate().persistentUpdate: Settrueon the parent state to keep it updating while a substate is open (e.g. for animated backgrounds behind pause).persistentDraw: Settrueon the parent state to keep it rendering while a substate is open (typical for pause, inventory overlays).
// PlayState — enable both for pause overlay to work correctly
override public function create():Void {
super.create();
persistentUpdate = true;
persistentDraw = true;
// ...
}
Use FlxSubState for overlays that suspend input without destroying the parent:
// Opening a pause substate
openSubState(new PauseSubState());
// PauseSubState
class PauseSubState extends FlxSubState {
public function new() {
super(0x99000000); // semi-transparent black bg
}
override public function update(elapsed:Float):Void {
super.update(elapsed);
if (FlxG.keys.justPressed.ESCAPE) close();
}
}
Engine-Wide Signals (FlxG.signals)
Subscribe to engine lifecycle events for global concerns (auto-pause, save-on-focus-lost, analytics). Always remove listeners in destroy().
override public function create():Void {
super.create();
FlxG.signals.focusLost.add(onFocusLost);
FlxG.signals.preStateSwitch.add(onPreStateSwitch);
}
function onFocusLost():Void {
FlxG.sound.muted = true;
Reg.save.flush(); // auto-save on alt-tab
}
function onPreStateSwitch():Void {
cleanup();
}
override public function destroy():Void {
FlxG.signals.focusLost.remove(onFocusLost);
FlxG.signals.preStateSwitch.remove(onPreStateSwitch);
super.destroy();
}
Available signals: preStateSwitch, postStateSwitch, focusLost, focusGained, gameResized, preUpdate, postUpdate, preDraw, postDraw.
FlxSignal — Decoupled Entity Communication
Prefer FlxSignal / FlxTypedSignal<T> over direct method calls for loosely coupled communication between game objects. This removes cross-dependencies.
// In Player.hx
public final onDied = new FlxSignal();
public final onCoinCollected = new FlxTypedSignal<Int->Void>(); // passes coin value
// When the player dies:
onDied.dispatch();
// When collecting a coin:
onCoinCollected.dispatch(coinValue);
// In PlayState:
player.onDied.add(handlePlayerDeath);
player.onCoinCollected.add(addScore);
// Always clean up in destroy():
override public function destroy():Void {
player.onDied.removeAll();
player.onCoinCollected.removeAll();
super.destroy();
}
Use FlxSignal (no data) for events, FlxTypedSignal<T->Void> when data must travel with the event.
Sprites and Groups
- Subclass
FlxSpritefor entities with their own update/draw logic. - Define all physics constants as
static inline finalat the top of the class — no magic numbers. - Call
loadGraphicandanimation.addinnew()so the sprite is always in a valid visual state. - Use
flipX/flipYto mirror facing direction — never load separate graphics for left/right. setSize(w, h)+offset.set(ox, oy)when the collision hitbox must differ from the graphic (essential for pixel-perfect platformer feel).screenCenter()to center a sprite; passXorYto constrain the axis.- Use
FlxSpriteGroupto composite sprites that move, scale, and rotate as one unit (HUD panels, compound enemies, UI widgets). - Use
FlxTypedGroup<T>overFlxGroupeverywhere — type safety, no casts, IDE autocomplete. - Iterate collections with
group.forEach(fn)rather than manual array loops.
class Player extends FlxSprite {
static inline final GRAVITY = 900.0;
static inline final MOVE_ACCEL = 600.0;
static inline final JUMP_VELOCITY = -420.0;
static inline final MAX_SPEED_X = 200.0;
static inline final MAX_SPEED_Y = 500.0;
static inline final DRAG_X = 800.0;
static inline final ANIM_IDLE = "idle";
static inline final ANIM_RUN = "run";
static inline final ANIM_JUMP = "jump";
static inline final ANIM_FALL = "fall";
static inline final ANIM_ATTACK = "attack";
public final onDied = new FlxSignal();
public function new(x:Float, y:Float) {
super(x, y);
loadGraphic(AssetPaths.player__png, true, 32, 32);
animation.add(ANIM_IDLE, [0, 1, 2, 1], 6, true);
animation.add(ANIM_RUN, [3, 4, 5, 6, 7], 12, true);
animation.add(ANIM_JUMP, [8], 1, false);
animation.add(ANIM_FALL, [9], 1, false);
animation.add(ANIM_ATTACK, [10, 11, 12], 10, false);
animation.play(ANIM_IDLE);
setSize(20, 28);
offset.set(6, 4);
acceleration.y = GRAVITY;
maxVelocity.set(MAX_SPEED_X, MAX_SPEED_Y);
drag.x = DRAG_X;
}
override public function update(elapsed:Float):Void {
handleInput();
updateAnimationState();
super.update(elapsed);
}
function handleInput():Void {
acceleration.x = 0;
if (isMovingLeft()) { acceleration.x = -MOVE_ACCEL; flipX = true; }
if (isMovingRight()) { acceleration.x = MOVE_ACCEL; flipX = false; }
if (isJumping() && isTouching(FLOOR)) velocity.y = JUMP_VELOCITY;
}
function updateAnimationState():Void {
if (!isTouching(FLOOR)) animation.play(velocity.y < 0 ? ANIM_JUMP : ANIM_FALL);
else if (velocity.x != 0) animation.play(ANIM_RUN);
else animation.play(ANIM_IDLE);
}
// Input abstracted behind intent-named methods
function isMovingLeft():Bool return FlxG.keys.anyPressed([LEFT, A]);
function isMovingRight():Bool return FlxG.keys.anyPressed([RIGHT, D]);
function isJumping():Bool {
if (FlxG.keys.anyJustPressed([SPACE, UP, W])) return true;
var pad = FlxG.gamepads.lastActive;
return pad != null && pad.justPressed.A;
}
override public function destroy():Void {
onDied.removeAll();
super.destroy();
}
}
Physics & Collision
- Set
immovable = trueon static environment objects/tiles to save solver work. - Set
dragfor friction deceleration instead of manually zeroing velocity. - Set
maxVelocityto cap speed — prevents runaway acceleration. - Wire all collision and overlap callbacks in
PlayState, not inside entity classes. Entities must remain ignorant of each other. FlxG.collide— separates overlapping objects (solid walls, floors, platforms).FlxG.overlap— detects without separating (pickups, triggers, damage zones). Use theprocessCallbackparameter to pre-filter (e.g. only process if alive).- Prefer
acceleration.xover directvelocity.xsets for physics-driven movement. Use directvelocity.xonly for snappy/grid-based movement. - Objects destroyed during a collision callback become null for subsequent callbacks in the same frame. Mark for deferred kill, or guard with
alivechecks.
override public function update(elapsed:Float):Void {
super.update(elapsed);
FlxG.collide(player, walls);
FlxG.collide(player, platforms);
FlxG.overlap(player, coins, collectCoin);
FlxG.overlap(player, enemies, hitEnemy, isEnemyAlive);
}
function isEnemyAlive(player:FlxObject, enemy:FlxObject):Bool return enemy.alive;
function hitEnemy(player:FlxSprite, enemy:FlxSprite):Void {
if (player.velocity.y > 0 && player.y < enemy.y) {
enemy.kill();
player.velocity.y = -250;
} else {
player.hurt(1);
}
}
Input
- Centralize input reading in
update()at the top of the entity that owns the input. - Abstract raw key checks behind intent-named methods (
isJumping(),isAttacking()) — the rest ofupdate()reads as intent, not raw keycodes. - Support keyboard AND gamepad in every named input method.
- Use
anyPressed([KEY1, KEY2])/anyJustPressed([...])to support multiple bindings cleanly. - Use
FlxG.gamepads.lastActive(most recently used controller) rather thanfirstActivefor better multi-controller UX.
// Gamepad analog stick with deadzone
function getMovementAxis():Float {
var pad = FlxG.gamepads.lastActive;
if (pad != null) {
var v = pad.analog.value.LEFT_STICK_X;
if (Math.abs(v) > 0.2) return v;
}
if (FlxG.keys.anyPressed([LEFT, A])) return -1.0;
if (FlxG.keys.anyPressed([RIGHT, D])) return 1.0;
return 0.0;
}
Memory & Object Pooling
- Use
FlxTypedGroup<T>(maxSize)for frequently spawned objects (bullets, particles, pickups). Pre-allocate at state init. - Call
recycle(T)to reuse dead instances — nevernew T()in a hot loop. - Call
kill()to return a pooled object to the pool. Neverdestroy()a pooled object —destroy()nulls the slot and permanently breaks recycling. - Call
destroy()only on objects being fully removed from the scene (e.g. inFlxState.destroy()). - Use
FlxPoint.get()/point.put()for temporary point calculations to avoid GC pressure. UseFlxPoint.weak()when passing a point to a Flixel API that will recycle it automatically.
// Pre-allocate pool of 40 bullets in PlayState.create()
var bullets = new FlxTypedGroup<Bullet>(40);
add(bullets);
function fireBullet(x:Float, y:Float, vx:Float):Void {
var b = bullets.recycle(Bullet); // reuses dead instances
b.reset(x, y);
b.velocity.x = vx;
}
// In Bullet.update() — return to pool when off-screen
if (x > FlxG.width || x < 0) kill();
Tweens & Timers
- Use
FlxTween.tween(target, props, duration, options)for any numeric property animation. Never manually lerp inupdate()when a tween covers the case. - Pass
ease: FlxEase.*and anonCompletecallback in the options struct. - Use
FlxTween.num(from, to, duration, { onUpdate: fn })to animate display values (score counters, health bars) without tying the tween to a specific object. - Call
FlxTween.cancelTweensOf(target)before starting a new tween on the same target to avoid conflicts. - Always cancel long-lived timers and tweens in
destroy()to prevent callbacks firing on dead objects and causing null-reference crashes.
// Fade out and kill
FlxTween.tween(sprite, {alpha: 0}, 0.3, {
ease: FlxEase.quadOut,
onComplete: _ -> sprite.kill()
});
// Decreasing interval spawn — cancel in destroy
var spawnTimer:FlxTimer;
function startSpawning():Void {
spawnTimer = new FlxTimer().start(3.0, onSpawnTimer, 0);
}
function onSpawnTimer(t:FlxTimer):Void {
spawnEnemy();
t.reset(Math.max(0.5, t.time - 0.1)); // accelerate over time
}
override public function destroy():Void {
if (spawnTimer != null) spawnTimer.cancel();
super.destroy();
}
Animation State Machines
- Model animation states with a Haxe
enum. Drive transitions in a dedicatedupdateAnimationState()method, not scattered acrossupdate(). - Define all animation name strings as
static inline finalconstants inside the owning class — never inline strings elsewhere. - Call
animation.play(NAME, forceRestart)— passforceRestart: false(the default) to avoid restarting the current animation every frame. Only force-restart on explicit state transitions. - Use
animation.finishCallbackfor one-shot animations (attack, death, land) that must trigger logic on completion.
static inline final ANIM_IDLE = "idle";
static inline final ANIM_RUN = "run";
static inline final ANIM_JUMP = "jump";
static inline final ANIM_ATTACK = "attack";
static inline final ANIM_DEATH = "death";
function setupDeathAnimation():Void {
animation.add(ANIM_DEATH, [13, 14, 15, 16], 8, false);
animation.finishCallback = name -> {
if (name == ANIM_DEATH) {
kill();
onDied.dispatch();
}
};
}
Camera
- Follow player:
FlxG.camera.follow(player, FlxCameraFollowStyle.PLATFORMER, lerpFactor). Styles:PLATFORMER,TOPDOWN,TOPDOWN_TIGHT,LOCKON,SCREEN_BY_SCREEN,NO_DEAD_ZONE. - Constrain world scroll:
FlxG.camera.setScrollBoundsRect(0, 0, levelWidth, levelHeight). - Set
FlxG.camera.deadzoneto a rectangle to keep the player centered in a zone before the camera scrolls. - Background color:
FlxG.camera.bgColor = 0xFF1A1A2E. - Camera effects:
FlxG.camera.flash(),FlxG.camera.fade(),FlxG.camera.shake(). - HUD camera — critical for fixed UI that must not scroll with the world:
var hudCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
hudCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(hudCam, false); // false = not the default camera
hud.cameras = [hudCam]; // assign all HUD objects to this camera
- Parallax: Set
scrollFactoron background sprites (0= fully fixed,0.5= half-speed). UseFlxBackdropfor seamlessly repeating backgrounds with a custom scroll rate.
var bg = new FlxBackdrop(AssetPaths.sky__png, X); // tiles horizontally
bg.scrollFactor.set(0.2, 0.0);
add(bg);
Tilemaps & Level Editors
- Ogmo3 is the preferred editor for HaxeFlixel projects. Requires
flixel-addons(FlxOgmo3Loader). - Tiled: export tile layers as CSV. Use
FlxTilemap.loadMapFromCSV(). - Always separate tile layers (geometry) from entity layers (spawns, triggers) in your editor project.
- Load entities by iterating the entity layer — never hardcode spawn positions in code.
- Tune the player hitbox smaller than the tile size (e.g. 12×14 inside 16×16) so 1-tile-wide doorways are passable.
// Ogmo3 workflow
var map = new FlxOgmo3Loader(AssetPaths.maps__ogmo, AssetPaths.room1__oel);
var walls = map.loadTilemap(AssetPaths.tiles__png, "walls");
walls.follow(); // auto-sets camera bounds
walls.setTileProperties(0, NONE); // 0 = air, no collision
walls.setTileProperties(1, ANY); // 1 = solid, all sides
walls.setTileProperties(2, UP); // 2 = one-way platform (top only)
add(walls);
map.loadEntities(placeEntities, "entities");
function placeEntities(e:EntityData):Void {
switch e.name {
case "player": player.setPosition(e.x, e.y);
case "enemy": enemies.add(new Enemy(e.x, e.y));
case "coin": coins.add(new Coin(e.x, e.y));
default: FlxG.log.warn('Unknown entity: ${e.name}');
}
}
Audio
- One-shot SFX:
FlxG.sound.play(AssetPaths.coin__ogg, volume). - Background music:
FlxG.sound.playMusic(AssetPaths.theme__ogg, volume, looped). HaxeFlixel persists music across state switches automatically. - Spatial audio: create a
FlxSound, callproximity(x, y, trackingObject, maxRadius), thenplay(). - Always destroy manually created
FlxSoundinstances indestroy(). - Define all asset references via the macro-generated
AssetPaths— never inline string paths in gameplay code.
Asset Management
- Use macro-generated
AssetPathsfor all asset references. - As of HaxeFlixel 5.9.0+,
FlxG.assetssupports customizable loading and hot-reload. Add-DFLX_CUSTOM_ASSETS_DIRECTORY="assets"to Project.xml for development hot-reload without recompiling. - Group assets by type:
assets/images/,assets/sounds/,assets/music/,assets/data/. - Never duplicate asset path strings across files.
Persistence
- Use
FlxSavefor local game data (settings, progress, high scores). - Bind once in
Regon startup. Flush on meaningful events (level complete, settings change). Useerase()to reset. FlxG.saveis a built-in convenience save slot — use it for quick/auto-save scenarios.- Null-check save data on load — the save file may not exist yet.
// Reg.hx
static public var save(default, null) = new FlxSave();
// Main.hx or a SaveManager
Reg.save.bind("myGame");
// Loading
var highScore = Reg.save.data.highScore ?? 0;
var unlockedLevels:Array<Int> = Reg.save.data.unlockedLevels ?? [1];
// Saving
Reg.save.data.highScore = score;
Reg.save.flush();
Debugging
- Built-in debugger overlay: toggle with
~at runtime. Inspect object counts, draw calls, logs. - Log:
FlxG.log.add(value),FlxG.log.warn(msg),FlxG.log.error(msg). - Live-watch any field:
FlxG.watch.add(player, "velocity")— updates in real time without recompile. - Visualize hitboxes and velocity vectors:
FlxG.debugger.drawDebug = true. - Build for Neko (
lime test neko) during development — fast compile, native debugger. Build for HTML5 or C++ only for profiling or release. - Use
FlxG.debugger.visible = trueincreate()during active development to auto-open the overlay.
DRY Guidelines
- Extract repeated
loadGraphic+animation.add+setSizesequences into a shared base class or a static factory method. - Shared physics constants (gravity, tile size) used across multiple entities belong in a
GameConstantsclass or a shared base class — never copy-pasted. - Tile layer names, entity names, animation keys, and asset paths must each be defined exactly once as constants.
- All asset references flow through
AssetPaths— never duplicate a path string.
Separation of Concerns
| Concern | Where it lives |
|---|---|
| Input | Entity that responds to it, abstracted behind named methods |
| Physics constants | static inline final at the top of the entity class |
| Animation state | Dedicated updateAnimationState(), driven by enum |
| Collision rules | PlayState via FlxG.collide / FlxG.overlap |
| Entity events | FlxSignal / FlxTypedSignal fields on the entity |
| Engine lifecycle events | FlxG.signals subscriptions (removed in destroy()) |
| Level data parsing | LevelLoader (static utility returning plain data) |
| Global game state | Reg (score, lives, coins, level index, FlxSave) |
| UI rendering | HUD on a dedicated FlxCamera — reads Reg, never writes game state |
| Asset references | AssetPaths (macro-generated — do not duplicate) |
| Tweens & timers | Owned by the object that acts on them; cancelled in destroy() |
| Persistence | FlxSave in Reg; accessed via single-point bind() |
| Audio | FlxG.sound for fire-and-forget; FlxSound instances only when lifecycle control is needed |
Code Review Checklist
Before finalizing any implementation, verify every item:
Architecture
- Each new class has a single, clearly named responsibility
PlayStatestays thin — orchestration only, no embedded game logic- Entities are ignorant of each other (no direct cross-references)
- Dependencies flow via constructor params or callbacks, not deep singleton access
Haxe correctness
- All magic numbers replaced with
static inline finalconstants - Animation/asset name strings defined as
static inline finalinside the owning class Null<T>only where absence is genuinely valid and documentedFlxVectornot used (deprecated in 5.x — useFlxPoint)- Angle values account for 5.x convention: 0° = right
HaxeFlixel patterns
super.update(elapsed),super.create(),super.destroy()called correctly in every override- Collision wired in
PlayState, not inside entity classes FlxG.collidefor separation;FlxG.overlapfor trigger detection- Pooled objects returned via
kill(), neverdestroy() FlxTimer/FlxTweeninstances cancelled indestroy()FlxSignallisteners removed indestroy()(both local andFlxG.signals)FlxMouseEventuses instance API (FlxMouseEvent.add()), not the legacy static API- HUD sprites assigned to a dedicated
FlxCameraso they don't scroll
Input
- Input checks abstracted behind intent-named methods
- Keyboard and gamepad both supported in named input methods
FlxG.gamepads.lastActiveused (notfirstActive)anyPressed/anyJustPressedused for multi-binding support
Assets & audio
- All asset paths sourced from
AssetPaths, no inline strings - Manual
FlxSoundinstances destroyed indestroy()
Performance
- Frequently spawned objects use
FlxTypedGroup<T>recycling with a pre-setmaxSize FlxPoint.get()/put()used for temporary vector calculations in hot pathsimmovable = trueset on static environment objects
Docs
- Any uncertain API verified against current HaxeFlixel docs via context7