Glitchblade

Role: Team Lead, Programmer, Game Designer, Level Designer


Tool: CUGL (Cornell University Game Library) with C++, GitHub


Team: Heptabyte, Team of 9


Timeline: 1 semester (5 months)


Completion Date: May, 2025

Role: Team Lead, Programmer, Game Designer, Level Designer


Tool: CUGL (Cornell University Game Library) with C++, GitHub


Team: Heptabyte, Team of 9


Timeline: 1 semester (5 months)


Completion Date: May, 2025

Role: Team Lead, Programmer, Game Designer, Level Designer


Tool: CUGL (Cornell University Game Library) with C++, GitHub


Team: Heptabyte, Team of 9


Timeline: 1 semester (5 months)


Completion Date: May, 2025

Play ->

Play ->

Play ->

GAME DESIGN

GAME DESIGN

GAME DESIGN

Glitchblade is a fast-paced action game centered on close-range combat and high-risk engagements. Players rely on precise timing, positioning, and aggressive decision-making to defeat enemies and bosses in tightly designed encounters. The core experience emphasizes momentum, responsiveness, and mastery through repeated combat challenges.

design pillars

design pillars

design pillars

Fast-Paced Combat

Fast-Paced Combat

Fast-Paced Combat

Aggressive enemies keep players on edge, forcing quick reactions and seamless shifts between attack and defense. The result is a tense, adrenaline-fueled flow.

Aggressive enemies keep players on edge, forcing quick reactions and seamless shifts between attack and defense. The result is a tense, adrenaline-fueled flow.

High Risk, High Reward

High Risk, High Reward

High Risk, High Reward

Every move has stakes. Bold parries and risky positioning can backfire — or unleash stuns, deflections, and powerful finishers that reward skill and mastery.

Every move has stakes. Bold parries and risky positioning can backfire — or unleash stuns, deflections, and powerful finishers that reward skill and mastery.

Intuitive Controls

Intuitive Controls

Intuitive Controls

Minimal UI and familiar swipe gestures make the game instantly accessible. Simple inputs let newcomers dive in and veterans focus on precision, not complexity.

Minimal UI and familiar swipe gestures make the game instantly accessible. Simple inputs let newcomers dive in and veterans focus on precision, not complexity.

mechanics

mechanics

mechanics

TECHNICAL CHALLENGES

TECHNICAL CHALLENGES

TECHNICAL CHALLENGES

Frame-Accurate Hitbox System

Frame-Accurate Hitbox System

Frame-Accurate Hitbox System

CHALLENGE

In a game where parrying is a core mechanic, hitboxes that appear one frame too early or persist one frame too long break the game's fairness contract with the player. The challenge is twofold: first, getting hitbox spawn to align exactly with the animation frame where an attack becomes "active"; second, keeping the hitbox anchored to the enemy's moving body for its lifetime without the physics system fighting the anchor every frame. A Box2D sensor attached directly to the enemy body would work but creates fixture management complexity on a body that's already being manipulated by AI. A free-floating sensor that moves independently needs to be manually repositioned every frame.

THOUGHT PROCESS

I made hitboxes free-floating Box2D sensors that track the enemy via a stored reference and a cached offset. The hitbox's update() moves it to enemy->getPosition() + _offset every frame, and if the enemy is stunned mid-attack, the hitbox self-destructs via markRemoved. This means stunned enemies can't have lingering hitboxes, which is critical for parry feeling fair.

For spawn timing, the enemy model's getDamagingAction() returns a MeleeActionModel only on the exact frame where the attack becomes active — checking sprite->getFrame() == action->getHitboxStartFrame() - 1. LevelController polls this every fixed update; when it returns non-null, it spawns a hitbox whose duration is computed from the action's frame window: 4 * (endFrame - startFrame + 1) ticks. This makes hitbox lifetime directly proportional to the animation window defined in JSON, with no hardcoded magic numbers.

Facing direction is handled at spawn: the hitbox offset's X is negated if the enemy is facing left, and the hitbox angle is mirrored. This means artists define hitboxes in right-facing space and the code handles the flip.

SOLUTION

A) Hitbox anchoring and stun-triggered self-removal:

void Hitbox::update(float dt) {
    BoxObstacle::update(dt);
    setPosition(_enemy->getPosition() + _offset);

    _duration -= dt / (1.0 / FPS);
    if (_duration <= 0 || _enemy->isStunned()) {
        markRemoved(true

B) Facing-aware spawn from LevelController::createHitbox:

void LevelController::createHitbox(std::shared_ptr<EnemyModel> enemy, Vec2 pos, Size size,
    float rotation, int damage, float knockback, float duration, bool parriable) {

    auto hitbox = Hitbox::alloc(enemy, pos, size, _scale, damage, knockback, duration, parriable);
    hitbox->setAngle(enemy->isFacingRight() ? -rotation : rotation);

    if (_worldRef->inBounds(hitbox.get())) {
        addObstacle(ObstacleNodePair(hitbox, sprite

And inside Hitbox::init, the offset X is negated for left-facing enemies at construction time, so the stored _offset is already in world space:

bool Hitbox::init(...) {
    if (!enemy->isFacingRight()) {
        pos.x = -pos.x;
    }
    // ...
    _offset = pos

C) Frame-accurate spawn gating in enemy modelsgetDamagingAction() returns non-null only on the exact trigger frame. For Minion1B's punch:

std::shared_ptr<MeleeActionModel> Minion1BModel::getDamagingAction() {
    if (_isPunching && _punchSprite->getFrame() == _punch->getHitboxStartFrame() - 1) {
        return _punch;
    }
    if (_isSlamming && _slamSprite->getFrame() == _slam->getHitboxStartFrame() - 1) {
        return _slam;
    }
    return nullptr

Since LevelController calls this every fixed update and creates a hitbox when it gets a non-null result, the hitbox appears in the physics world on precisely the frame defined in the action's JSON data.

Enemy AI

Enemy AI

Enemy AI

CHALLENGE

Simple timer-based AI (attack every N seconds) is readable but predictable and flat. Random action selection without memory is chaotic and unfair. The real challenge is building AI that feels reactive and escalating — enemies that pressure you more as a fight goes on, without becoming a coin-flip machine. Additionally, action selection can't just pick an intent; it needs to account for the enemy's current animation state, since triggering a new action mid-animation causes visual and hitbox corruption.

THOUGHT PROCESS

I used an aggression meter as a shared currency that builds over time and gets spent on actions. Each enemy accumulates aggression passively per frame in preUpdate, and aggression also increases when the enemy takes damage — meaning the player landing hits makes the enemy more dangerous, not less. Each action checks if (rand() % 100 <= _aggression) before firing, and spending aggression on an action resets a portion of it. This creates natural pacing: early in a fight, low aggression means actions rarely fire; as the fight continues, the threshold is crossed more reliably.

The state guard at the top of nextAction() prevents new action selection while any current action flag is set. This is a flat boolean check rather than a formal FSM, but it achieves the same safety guarantee: the selector is only active when the enemy is actually free to act. Boss 3 extends this with form switching — stun recovery flips _isGroundForm, which changes the entire action vocabulary available to the selector.

SOLUTION

A) Aggression-gated probabilistic action selection — aggression accumulates in the controller's preUpdate, and each action's firing condition samples against it:

// In Minion1AController::preUpdate
_enemy->setAggression(std::min(100.0f, _enemy->getAggression() + dt * 5));

// In Minion1AModel::shoot
void Minion1AModel::shoot() {
    faceTarget();
    if (_aggression > 8) {
        _aggression = 0;
        _isShooting = true;
        setMovement(0);
    }
}

// In Minion1AModel::explode (used as a desperation move below 25 HP)
void Minion1AModel::explode() {
    faceTarget();
    if (rand() % 100 <= _aggression) {
        _aggression -= std::max(0.0f, _aggression - 50);
        _isExploding = true;
        setMovement(0

shoot() uses a flat threshold (fires when aggression exceeds 8) for consistent ranged pressure. explode() uses a probabilistic check (rand() % 100 <= _aggression) so the desperation move becomes more likely the longer the fight runs.

B) State-guarded action selection in nextAction() — the selector only runs when no action flag is set:

void Minion1AModel::nextAction() {
    AIMove();

    if (!_isShooting && !_isExploding && _moveDuration <= 0 && !isStunned()) {
        if (isTargetClose()) {
            if (_hp < 25) {
                explode();
            } else {
                avoidTarget(45);
            }
        } else if (isTargetFar()) {
            approachTarget(45);
        } else {
            faceTarget();
            shoot();
        }
    } else {
        if (isStunned()) {
            _isShooting = false;
            _isExploding = false;
            setMovement(0);
        }
        if (_isShooting && _shootSprite->getFrame() >= _shootSprite->getCount() - 1) {
            _isShooting = false

Distance thresholds (isTargetClose(), isTargetFar()) drive positioning decisions, and HP threshold drives the desperation branch. The stun handler in the else block clears all flags immediately, so the selector regains control on the next tick once stun clears.

C) Minion2B's directional damage override — a concrete example of per-enemy behavior extending the base system without touching EnemyModel. The guard check intercepts damage() before any HP reduction occurs:

void Minion2BModel::damage(float value) {
    bool targetAtFront = ((_targetPos - getPosition()).x > 0 && isFacingRight())
        || ((_targetPos - getPosition()).x < 0 && !isFacingRight());

    if (targetAtFront && _targetPos.y <= getPosition().y + 2.1 && !isStunned()) {
        if (value <= 10) {
            _attackGuarded = true;  // trigger counter-attack
            _moveDuration = 0;
        } else {
            setStun(stunFrames);    // heavy hit breaks guard
        }
    } else {
        EnemyModel::damage(value);  // flanked or attacked from above: take full damage

Low-damage hits from the front set _attackGuarded, which causes nextAction() to immediately prioritize a guard counter on the next free tick. High-damage hits from the front bypass the guard and apply stun directly. This directional check is purely in model space — no physics queries needed.

Parry/Guard System and Combo Meter

Parry/Guard System and Combo Meter

Parry/Guard System and Combo Meter

CHALLENGE

Parrying in action games lives and dies on frame windows. Too generous and it's a spam button; too tight and it's a skill check that feels unfair. But tight parry windows aren't enough on their own — you need to reward skilled play with something more than "you didn't take damage." The design also needs to handle three distinct defensive outcomes (parry, guard, hit) from a single input, with each having different consequences for physics, animation, and game state.

THOUGHT PROCESS

I modeled the guard state as a 3-phase countdown: a short parry window at the beginning of the guard duration, a longer block window for the remainder, and a recovery state after the guard expires or is hit. _parryRem and _guardRem count down independently, meaning the player starts in the parry-active phase and transitions to guard-only after _parryRem reaches zero. The CollisionController checks these flags at collision time to route to the correct outcome: full damage, half damage (guard), or the parry response.

For the parry reward, I built a combo meter that fills on successful parries and attacks and decays on inactivity. Filling the meter to 100 triggers one of two rewards based on current HP: if the player is damaged, it heals them; if already at full HP, it grants _isNextAttackEnhanced, which deals amplified damage and a larger screen shake on the next hit. This creates a meaningful strategic choice — playing aggressively at low HP heals you, but at full HP it powers up your offense.

SOLUTION

A) Three-phase guard state managed in PlayerController::updateCooldowns():

if (_player->isGuardBegin()) {
    _player->setGuardCDRem();
    _player->setGuardRem();
    _player->setParryRem();    // parry window starts here
    _player->setGuardState(1);
    _player->resetKnocked();
}
if (_player->isGuardActive() && !_player->isGuardBegin()) {
    int guardRem = _player->getGuardRem();
    _player->setGuardRem(guardRem - 1);
    if (guardRem == 1) {
        _player->setGuardReleaseRem(PLAYER_HIT_COLOR_DURATION);
        _player->setGuardState(3);    // guard expired: recovery state
    }
    int parryRem = _player->getParryRem();
    _player->setParryRem(parryRem > 0 ? parryRem - 1 : 0

_parryRem counts down within the guard window. CollisionController checks isParryActive() (parryRem > 0) before checking isGuardActive() to correctly prioritize the parry outcome.

B) Collision routing in playerHitboxCollision():

if (!_player->isGuardActive() && !_player->isParryActive()) {
    _player->damage(damage);
    _player->resetCombo();
    _player->setKnocked(true, knockback);
    _screenShake(damage, 3);
}
else if (_player->isParryActive()) {
    AudioHelper::playSfx("parry");
    _player->incrementComboCounterByParry();
    _player->_parryCounter++;
    _player->setParryRem(0);
    _player->setGuardRem(0);
    _screenShake(40, 1);
    if (hitbox->getIsParriable()) {
        enemy->setStun(enemy->stunFrames);
    }
}
else if (_player->isGuardActive()) {
    _player->damage(hitbox->getDamage() / 2);
    _screenShake(halfDamage, 3

The parry outcome also checks getIsParriable() on the hitbox — unparriable attacks (like Boss 3's laser) go through the block branch regardless of timing. Parrying a projectile additionally reverses its velocity and marks it as player-fired, turning it into a homing counter-attack:

projectile->setIsPlayerFired(true);
projectile->setVX(-projectile->getVX());
projectile->setVY(-projectile->getVY());
projectile->getSceneNode()->setColor(cugl::Color4(0, 255, 150, 255

C) Combo meter accumulation and dual-reward resolution:

// In PlayerController::fixedUpdate
_player->_lastComboElapsedTime += timestep;
if (_player->_lastComboElapsedTime >= 5 && _player->_comboMeter > 0) {
    _player->_comboMeter = std::max(_player->_comboMeter - timestep * 10, 0.0f);
}

if (_player->_comboMeter >= 100) {
    if (_player->getHP() < _player->getMaxHP()) {
        _player->setHP(std::min(_player->getHP() + 20.0f, 100.0f));
    } else {
        _player->_isNextAttackEnhanced = true;
    }
    _player->_comboMeter = 0

The meter decays after 5 seconds of inactivity, preventing passive accumulation between encounters. The enhanced attack flag is consumed on the next collision hit and cleared immediately after:

if (_player->_isNextAttackEnhanced) {
    _player->_isNextAttackEnhanced = false;
    _screenShake(30, 5);   // amplified feedback
} else {
    _player->incrementComboCounterByAttack

The screen shake magnitude — 30 versus the standard 3 — is the primary player-facing signal that the enhanced hit landed.

LEVEL DESIGN

LEVEL DESIGN

LEVEL DESIGN

Challenges

Challenges

Challenges

Boss Encounters

Boss Encounters

How can we design bosses that feel challenging and memorable without overwhelming the player?

How can we design bosses that feel challenging and memorable without overwhelming the player?

How can we design bosses that feel challenging and memorable without overwhelming the player?

Enemy Placement and Pacing

Enemy Placement and Pacing

Enemy Placement and Pacing

How do we arrange enemies to keep combat dynamic while maintaining a fair difficulty curve (i.e. how to keep player in a flow state)?

How do we arrange enemies to keep combat dynamic while maintaining a fair difficulty curve (i.e. how to keep player in a flow state)?

Introducing New Mechanics

Introducing New Mechanics

Introducing New Mechanics

How can the game teach mechanics naturally through play instead of heavy-handed tutorials?

How can the game teach mechanics naturally through play instead of heavy-handed tutorials?

How can the game teach mechanics naturally through play instead of heavy-handed tutorials?

Solutions

Solutions

Solutions

Boss Encounters

Designed unique, well-telegraphed attack patterns with clear phase transitions, supported by minions introduced earlier in the level to ease players into the boss’s moveset.

Enemy Placement and Pacing

Combined enemy types so players could apply familiar strategies in new contexts, while alternating combat arenas with platforming sections to balance intensity and give players breathing room.

Introducing New Mechanics

Built levels that use safe spaces, escalating obstacles, and enemy encounters as organic teaching moments, reinforced with subtle UI prompts to guide players without breaking immersion.

Boss Encounters

Boss Encounters

Designed unique, well-telegraphed attack patterns with clear phase transitions, supported by minions introduced earlier in the level to ease players into the boss’s moveset.

Designed unique, well-telegraphed attack patterns with clear phase transitions, supported by minions introduced earlier in the level to ease players into the boss’s moveset.

Enemy Placement and Pacing

Enemy Placement and Pacing

Combined enemy types so players could apply familiar strategies in new contexts, while alternating combat arenas with platforming sections to balance intensity and give players breathing room.

Combined enemy types so players could apply familiar strategies in new contexts, while alternating combat arenas with platforming sections to balance intensity and give players breathing room.

Introducing New Mechanics

Built levels that use safe spaces, escalating obstacles, and enemy encounters as organic teaching moments, reinforced with subtle UI prompts to guide players without breaking immersion.

Built levels that use safe spaces, escalating obstacles, and enemy encounters as organic teaching moments, reinforced with subtle UI prompts to guide players without breaking immersion.

SAMPLE Enemy Moveset (Boss 1 Animations and VFX)

SAMPLE Enemy Moveset (Boss 1 Animations and VFX)

SAMPLE Enemy Moveset (Boss 1 Animations and VFX)

boss1_idle

boss1_slam

boss1_shoot

boss1_stunned

damaged_vfx

projectile

boss1_walk

boss1_stab

boss1_explode

boss1_dead

spawn_vfx

explode_vfx

boss1_idle

boss1_slam

boss1_shoot

boss1_stunned

damaged_vfx

projectile

boss1_walk

boss1_stab

boss1_explode

boss1_dead

spawn_vfx

explode_vfx

boss1_idle

boss1_walk

boss1_slam

boss1_stab

boss1_shoot

boss1_explode

boss1_stunned

boss1_dead

damaged_vfx

spawn_vfx

projectile

explode_vfx

Copyright © 2026, Andy Pang. All rights reserved.

Copyright © 2026, Andy Pang. All rights reserved.

Copyright © 2026, Andy Pang. All rights reserved.