Docs › Contributors
Contributors
Adding Bosses
Adding Bosses
Boss fights follow the same object implementation pattern described in the tutorial, but with additional complexity. This page covers the boss-specific patterns.
State Machine
Bosses have more states than typical objects. In the disassembly, these are encoded as
routine(a0) and routine_secondary(a0) values. The convention is:
routinehandles major phases (init, approach, main, defeated, exploding).routine_secondaryhandles sub-phases within a major phase (attack cycle steps, wind-up, recovery, etc.).
In the engine, this maps to a state enum or state tracking fields:
private enum BossState {
INIT, APPROACH, IDLE, ATTACK, RETREAT, HIT_RECOIL, DEFEATED, EXPLODING, FLED
}
private BossState state = BossState.INIT;
private int subState = 0; // for sub-phases within a state
Each state has its own update logic. A typical update() dispatches on the current state:
@Override
public void update(int frameCounter, PlayableEntity player) {
switch (state) {
case INIT -> updateInit();
case APPROACH -> updateApproach();
case IDLE -> updateIdle(player);
case ATTACK -> updateAttack(player);
// ...
}
}
Hit Detection and Health
Bosses track their health via collision_property, which starts at the boss’s hit count
(typically 8) and decrements on each hit.
Boss touch behavior should use the canonical touch/profile vocabulary under
com.openggf.game.profiles.* when the boss fits an existing profile. If a boss has
bespoke hit windows or invulnerability rules, keep the bespoke logic local but do not add
new game-local profile types.
In the engine:
private int hitCount = 8;
private int invincibilityTimer = 0;
// Called by the collision system when the player hits the boss
public void onHit() {
if (invincibilityTimer > 0) return;
hitCount--;
invincibilityTimer = FLASH_DURATION; // typically $20 = 32 frames
services().playSfx(bossHitSfxId);
if (hitCount <= 0) {
state = BossState.DEFEATED;
}
}
The boss flashes during the invincibility period. This is typically done by toggling rendering on alternate frames:
@Override
public void appendRenderCommands(List<GLCommand> commands) {
if (invincibilityTimer > 0 && (invincibilityTimer & 1) != 0) {
return; // skip rendering on odd frames (flash effect)
}
// normal rendering
}
Child Objects
Bosses frequently spawn child objects: projectiles, rotating platforms, body parts, debris. The pattern is the same as the ArrowShooter tutorial:
SomeProjectile projectile = spawnChild(() -> new SomeProjectile(spawn, direction));
Use spawnChild(...), spawnFreeChild(...), or another level.objects compatibility
wrapper when the child needs services during construction or has parent/slot lifetime
coupling. Direct ObjectManager.addDynamicObject(...) is reserved for documented
legacy or bridge paths; new boss work should use the wrapper that preserves construction
context and ObjectLifetimeOps migration points.
Common child objects for bosses:
- Projectiles (fireballs, hammers, energy balls)
- Body parts (cockpit glass, pendulum ball, drill)
- Debris on defeat (explosion fragments, falling pieces)
In the disassembly, child objects often share the boss’s object ID with different routine values (like the ArrowShooter). In the engine, each child type is its own class.
Camera Lock and Arena Setup
Boss encounters typically lock the camera to create an arena. This is handled by the level event system, not by the boss object itself.
The level event for a zone detects when the player crosses a trigger X position and:
- Changes the camera’s right boundary to create a locked screen.
- Starts the boss music.
- May change the left boundary too (preventing backtracking).
In the engine, this is implemented in the zone’s level event class:
// In Sonic2EHZLevelEvent.java (simplified)
if (act == 1 && camera.getX() >= BOSS_TRIGGER_X) {
camera.setRightBound(BOSS_ARENA_RIGHT);
camera.setLeftBound(BOSS_ARENA_LEFT);
services().playMusic(MusicId.BOSS);
spawnBoss();
}
The boss object itself handles its own behavior within the arena but does not manage camera boundaries.
If the boss targets, grabs, carries, or damages players, choose an explicit
ObjectPlayerParticipationPolicy and query participants through ObjectPlayerQuery.
Use native P1/P2 policies only when the original two-slot limit is part of the intended
behavior; route-critical S3K encounters usually need all-engine-player or extended
sidekick participation.
Defeat Sequence
When a boss is defeated (hit count reaches zero), the typical sequence is:
- Boss enters defeated state. Stops attacking, may drift or fall.
- Explosion chain. A series of timed explosions at random offsets around the boss.
These are spawned as
BossExplosionObjectInstancechildren. - Boss is deleted. After the explosion chain finishes.
- Camera unlocks. The level event detects the boss is gone and restores normal camera boundaries.
- Signpost or capsule appears. For Act 2 bosses, an egg prison is spawned. For Act 1, a signpost appears.
- Music changes. Boss music stops, act clear or normal music plays.
The explosion chain in the disassembly is driven by a timer and frame counter:
private void updateDefeated() {
defeatTimer--;
if (defeatTimer > 0) {
// Spawn explosions at intervals
if ((defeatTimer & 0x07) == 0) {
spawnExplosion(currentX + randomOffset(), currentY + randomOffset());
}
return;
}
// Timer expired: delete boss, trigger end-of-act
ObjectLifetimeOps.deleteNoRespawn(this);
}
New defeat and cleanup paths should use ObjectLifetimeOps or an existing level.objects
compatibility wrapper for deletion, respawn latches, child cleanup, and slot transfer.
Direct lifecycle mutation is acceptable only as a documented legacy bridge.
Example: EHZ Boss Structure
The EHZ boss (Sonic2EHZBossInstance) is a good reference for a straightforward boss.
Its state machine:
| State | What happens |
|---|---|
| INIT | Set up art, position, create cockpit child |
| APPROACH | Fly in from the right side of the screen |
| IDLE | Hover, wait before attacking |
| ATTACK | Drill descend toward player |
| RETREAT | Rise back up after attack |
| HIT_RECOIL | Flash and bounce after being hit |
| DEFEATED | Explosion chain, then delete |
| FLED | Robotnik escapes right (egg prison spawns) |
Files:
src/main/java/com/openggf/game/sonic2/objects/bosses/Sonic2EHZBossInstance.javadocs/s2disasm/s2.asm— search forObj56(the EHZ boss object ID)
Checklist for a New Boss
- Read the boss object in the disassembly. Map out all routines and child objects.
- Read the zone’s level event code to understand camera lock triggers.
- Create the boss class extending
AbstractObjectInstance. - Pick the relevant
com.openggf.game.profiles.*behavior profiles and compatibility wrappers before writing local conditionals. - Declare player participation with
ObjectPlayerQuery/ObjectPlayerParticipationPolicy. - Implement the state machine with all phases. Use
ObjectControlStatefor object-control intent when the boss locks, carries, or suppresses player movement/touch/solid contact. - Create classes for each child object type.
- Register the boss in the object registry.
- Wire the level event to trigger the boss encounter.
- Test: approach trigger, attack patterns, hit detection, defeat sequence, camera unlock.
- Compare against the original: frame counts, positions, velocities, SFX timing.
Next Steps
- Adding Zones — If the boss’s zone is not yet supported
- Tutorial: Implement an Object — Base patterns
- Testing — Writing boss tests