2D Fighter Game — Java (OOP)

Arcade-style 2D side-view fighter supporting PvP (same keyboard) , with rounds, health bars, timers, and snappy animations. Built as a Bachelor project to practice real-time input, sprite handling, and collision/hitbox logic.

Stack: Java + Java2D (AWT/Swing), sprite/animation sheets, lightweight SFX. Java’s event loop and robust input handling make it ideal for clean game-loop architecture, per-frame collision checks, and sprite animation control.

Java Java2D / AWT Game loop Finite State Machine Collision / Hitboxes Sprite Animation Sound OOP
2D Fighter — hero

Code Map

  • game.java — entry point, window init, main loop
  • input.java — keyboard input (pressed/held state)
  • player.java / player2.java — player actors, state & animation
  • object.java — base entity (pos/vel/size, update/draw)
  • processor.java — update pipeline (physics, collisions)
  • menu.java — main menu / pause / round UI
  • res.java — sprite & sound loading (assets/)
  • ID.java — enums (EntityID, State, Animation)
  • bar.java — health/timer UI elements
  • assets/ — spritesheets, backgrounds
  • sound/ — sfx/music (optional)

Note: filenames derived from project; exact responsibilities may vary slightly.

Gameplay — sample 1

Features

Responsive Controls

Keyboard mapping for move, jump, crouch, light/heavy attack, block. Debounce for taps + hold detection.

Combo & Hit Detection

Per-frame hitboxes apply damage/knockback on contact; blocking reduces or nullifies damage (if enabled).

Health & Rounds

HP bars, round timer, KO detection, and best-of-N round logic.

Sprite Animation

Idle/run/jump/attack/hurt/KO states with frame timing, state-driven animation switching.

Audio

Punch/kick SFX and optional background music.

Pause & Restart

Game state transitions for pause, restart, and next round.

Architecture & Code

High-Level Design

  • Game Loop: handleInput → update → physics/collision → render (~60 FPS).
  • Entities: Player, Enemy extend Object (pos/vel/size, update/draw).
  • State Machine: Idle / Run / Jump / Attack / Hit / KO, with cooldowns.
  • Systems: Input, Collision/Hitboxes, UI (health bars & timer), Audio.

Implementation Notes

  • Composition: entities hold current animation, state, direction, and physics params.
  • Hitboxes: per-frame rectangles (or masks) checked each update.
  • Animation: spritesheet slicing; frame index advances at fixed ticks; resets on state change.
  • Input: pressed/held maps support simultaneous keys; optional tap vs hold differentiation.
Gameplay — sample 2

Game loop — window, timing, and render cycle

public class Game implements Runnable {
    private boolean running = true;
    private final int targetFps = 60;
    private final Input input = new Input();          // keyboard listener
    private final Processor processor = new Processor(); // physics/collisions

    @Override public void run() {
        long nsPerTick = 1_000_000_000L / targetFps;
        long last = System.nanoTime(), acc = 0;

        while (running) {
            long now = System.nanoTime();
            acc += (now - last); last = now;

            // Handle input events (non-blocking)
            input.poll();

            // Fixed updates (catch up if needed)
            while (acc >= nsPerTick) {
                update();
                acc -= nsPerTick;
            }
            render();
        }
    }

    private void update() {
        processor.updateAll(); // moves entities, resolves hits, timers, round logic
    }
    private void render() {
        // draw background → entities → UI (health bars, timer)
    }
}

Fixed-step update keeps physics stable; rendering occurs as often as possible.

Finite State Machine — player actions & transitions

enum State { IDLE, RUN, JUMP, ATTACK_LIGHT, ATTACK_HEAVY, HIT, KO }

public class Player extends Entity {
    private State state = State.IDLE;
    private int facing = 1; // 1: right, -1: left
    private int attackCooldown = 0;

    @Override public void update() {
        // resolve input → desired state
        if (hp <= 0) state = State.KO;
        else if (wasHit()) state = State.HIT;
        else if (input.attackHeavy() && attackCooldown == 0) { state = State.ATTACK_HEAVY; attackCooldown = 20; }
        else if (input.attackLight() && attackCooldown == 0) { state = State.ATTACK_LIGHT; attackCooldown = 10; }
        else if (input.jump()) state = State.JUMP;
        else if (input.left() ^ input.right()) state = State.RUN;
        else state = State.IDLE;

        // physics + animation
        applyPhysics();
        updateAnimationFor(state, facing);

        if (attackCooldown > 0) attackCooldown--;
    }
}

Transitions are input-driven with cooldowns to gate combos and maintain readable combat.

Hitbox collision — damage & knockback

public class Processor {
    public void resolveHit(Player attacker, Player defender){
        Rectangle hit = attacker.getActiveHitbox();
        Rectangle hurt = defender.getHurtbox();

        if (hit != null && hurt != null && hit.intersects(hurt)) {
            int dmg = attacker.isHeavy() ? 16 : 8;
            if (defender.isBlocking(attacker.facing())) dmg /= 3; // block mitigates
            defender.applyDamage(dmg);
            defender.applyKnockback(attacker.facing() * (attacker.isHeavy() ? 6 : 4), 2);
            defender.markHit(); // state → HIT
        }
    }
}

Simple rectangle checks per frame; blocking reduces damage; heavier attacks add knockback.

Animation update — spritesheet frames

public class Animator {
    private BufferedImage[] frames; // current state's frames
    private int idx = 0, tick = 0, ticksPerFrame = 5;

    public void setClip(BufferedImage[] newFrames){
        frames = newFrames; idx = 0; tick = 0;
    }
    public BufferedImage next(){
        if (frames == null || frames.length == 0) return null;
        if (++tick >= ticksPerFrame){ tick = 0; idx = (idx + 1) % frames.length; }
        return frames[idx];
    }
}

State changes swap animation clips; frame index advances at fixed cadence for consistent timing.

UI — health bar & round timer

public class HUD {
    public void draw(Graphics2D g, int p1hp, int p2hp, int time, int round, int bestOf){
        int w = 280, h = 16;
        g.setColor(Color.darkGray);
        g.fillRoundRect(40, 28, w, h, 8, 8);
        g.fillRoundRect(720-w-40, 28, w, h, 8, 8);

        g.setColor(new Color(34,197,94)); // emerald
        g.fillRoundRect(40, 28, (int)(w * Math.max(0, p1hp)/100.0), h, 8, 8);
        g.fillRoundRect(720-w-40 + (w - (int)(w * Math.max(0, p2hp)/100.0)), 28,
                        (int)(w * Math.max(0, p2hp)/100.0), h, 8, 8);

        g.setColor(Color.white);
        g.drawString("Round " + round + " / " + bestOf, 360, 24);
        g.setFont(g.getFont().deriveFont(Font.BOLD, 16f));
        g.drawString(String.valueOf(time), 380, 44);
    }
}

Lightweight immediate-mode UI: draw rects/text each frame for bars & timers.

Demo & How to Run

Run Locally

# Clone repo
git clone https://github.com/sabers13/bachelor-projects.git
cd "bachelor-projects/semester 3/2D Fighter Game"

# Compile & run (Java 17+, adjust if using older JDK)
javac -d out src/*.java
java -cp out Game

Controls (example)

  • P1: A/D = move, W = jump, S = crouch, J = light, K = heavy, L = block
  • P2 (optional): arrows + numpad 1/2/3
  • Esc = pause menu, R = restart round

Exact keys depend on input.java; adjust here if mappings differ.