TheDebugThugs is a 2D escape game built using the libGDX framework. The game challenges players to navigate through a university campus, collect items, avoid obstacles and enemies, and ultimately escape within a time limit.
- Framework: libGDX (Java-based game development framework)
- Language: Java 17
- Build System: Gradle
- Architecture: Multi-module project (core, lwjgl3, headless)
- Code Quality: JaCoCo for test coverage, Checkstyle for code standards
The project follows a modular design with three key modules:
Team10Assessment2-Self-test/
├── core/ # Core game logic (platform-independent)
├── lwjgl3/ # Desktop launcher (LWJGL3 backend)
└── headless/ # Headless testing environment
Important
Design Choice: This separation allows the game logic to remain platform-independent while enabling headless testing without graphics initialization, crucial for CI/CD pipelines.
Main Systems:
- Main.java - Application entry point with dual viewport system
- FirstScreen.java - Main gameplay screen (549 lines)
- Player.java - Player controller with state machine
Game Entities:
- Enemy, Duck, Exam, LongBoi, DuoAuth, WetFloor (various events/obstacles)
- Key, EnergyDrink, Coin, HelperCharacter (collectibles)
- Bus, BusStop (transportation system)
Supporting Systems:
- AchievementManager.java - Achievement tracking with singleton pattern
- Pathfinding.java - A* pathfinding algorithm
- Collision.java - Collision detection system
UI Screens:
- MenuScreen, WinScreen, LoseScreen, SettingsScreen, LeaderBoardScreen, Tutorial
Implementation (Main.java):
// World camera - for game world rendering
worldCamera = new OrthographicCamera();
worldViewport = new FitViewport(800, 600, worldCamera);
// UI camera - for HUD/menu rendering
uiCamera = new OrthographicCamera();
uiViewport = new FitViewport(1280, 720, uiCamera);Tip
Design Rationale: Separating world and UI rendering allows independent scaling. The world can zoom (0.6x default) while UI remains crisp and pixel-perfect. This prevents UI distortion during camera movements.
State Management (Player.java):
public enum State {
WALK, // Walking down
WALK_L, // Walking left
WALK_R, // Walking right
WALK_UP, // Walking up
FALL // Falling animation (triggered by events)
}Animation Frame Trimmer (FirstScreen.java):
public static Animation<TextureRegion> frameTrimmer(TextureRegion[] array, int empty) {
TextureRegion[] arrayOut = new TextureRegion[array.length - empty];
for (int i = 0; i < (array.length - empty); i++) {
arrayOut[i] = array[i];
}
return new Animation<>(0.05f, arrayOut);
}Note
Design Choice: The frame trimmer removes empty frames from sprite sheets, optimizing memory and preventing unnecessary frame rendering. This keeps animations smooth at 0.05s per frame.
The game follows a clean render pattern with separation of concerns (FirstScreen.java):
@Override
public void render(float delta) {
logic(delta); // Update game state
renderWorld(); // Render world objects
renderUI(delta); // Render UI overlay
}Logic Phase (~76 lines):
- Pause/unpause handling
- Player input processing
- Enemy AI updates
- Event system updates
- Score decay calculation
- Win/lose condition checks
World Rendering Phase:
- Camera positioning (follows player)
- Tiled map rendering
- Sprite batch rendering in proper z-order
UI Rendering Phase:
- HUD elements (timer, score, events)
- Achievement popups
- Pause menu overlay
Tile-based Collision (Collision.java):
The system uses corner-point collision detection - checking the four corners of the player's bounding box against the tilemap:
public static boolean collisionCheck(Player p) {
// Check all 4 corners of player rectangle
if (isCellBlocked(p.playerX, p.playerY)) return true;
if (isCellBlocked(p.playerX + p.playerWidth, p.playerY)) return true;
if (isCellBlocked(p.playerX, p.playerY + p.playerHeight)) return true;
if (isCellBlocked(p.playerX + p.playerWidth, p.playerY + p.playerHeight)) return true;
return false;
}Important
Design Choice: Corner-point collision provides pixel-perfect collision while remaining computationally efficient. Only 4 tile lookups per movement frame.
Implementation (AchievementManager.java):
private static AchievementManager instance;
public static AchievementManager get() {
if (instance == null) {
instance = new AchievementManager();
}
return instance;
}Achievements Tracked:
- ESCAPED, ENCOUTERED_DEAN, FOUND_KEY, UNLOCKED_DOOR
- ENERGISED, FLAWLESS_RUN, TELEPORTED, DUO_AUTHENTICATED
- WATCH_YOUR_STEP, QUACK, DUCK_OF_RESETTING, HELPER_FOUND
Persistence: Uses libGDX Preferences API for persistent storage across game sessions.
Pop-up System:
- Queue-based achievement notifications
- Fade-in/fade-out animations (2s display, 0.5s fade)
- Non-blocking UI updates
Tip
Design Rationale: Singleton ensures consistent achievement state across all game screens. The queue prevents notification spam when multiple achievements unlock simultaneously.
Implementation (Pathfinding.java):
The enemy AI uses A pathfinding* with the following features:
Key Components:
- Node-based graph - Each tile is a node with g, h, and f costs
- Octile distance heuristic - Accounts for diagonal movement
- Priority queue - Efficiently selects lowest f-cost nodes
- Path reconstruction - Backtracks from goal to start via parent links
- Path pruning - Removes redundant waypoints near start position
private float heuristic(int ax, int ay, int bx, int by) {
float dx = Math.abs(ax - bx);
float dy = Math.abs(ay - by);
float min = Math.min(dx, dy);
float max = Math.max(dx, dy);
return 1.41421356f * min + (max - min); // Octile distance
}Diagonal Movement Handling:
if (d[0] != 0 && d[1] != 0) {
// Diagonal movement - check corners aren't blocked
if (!isWalkable(current.x + d[0], current.y) ||
!isWalkable(current.x, current.y + d[1])) {
continue; // Prevent corner-cutting
}
}Performance Optimization:
- Repath interval: 0.5s (reduces CPU usage)
- Path pruning: Removes waypoints within half-tile radius of enemy
- Early termination: Stops when goal is reached
Note
Design Choice: A* provides optimal pathfinding while octile distance heuristic ensures realistic diagonal movement. The repath interval balances responsiveness with performance.
Three Event Categories tracked on player:
public int goodEvent = 0; // Beneficial events (energy drink, coin)
public int badEvent = 0; // Negative events (enemy, exam, duck)
public int hiddenEvent = 0; // Secret events (bus teleportation)Event Types:
| Event | Type | Effect | Duration |
|---|---|---|---|
| Enemy | Bad | -30s time, +1 counter | 2s cooldown |
| DuoAuth | Bad | 10s freeze, achievement | One-time |
| WetFloor | Bad | 5s freeze | One-time |
| Exam | Bad | Patrol movement, time penalty | N/A |
| Duck | Bad | Random position reset | One-time |
| EnergyDrink | Good | +25% speed boost | Permanent |
| Coin | Good | +50 score | N/A |
| HelperCharacter | Good | +20s time | N/A |
| Bus | Hidden | Random teleport | Unlimited |
Freeze Event Pattern (DuoAuth example):
if (duoAuth.active) {
return; // Skip all player input processing
}Important
Design Rationale: The event counter system supports achievement tracking (e.g., "FLAWLESS_RUN" requires badEvent == 0) and provides gameplay statistics for player feedback.
Design:
- Single bus entity at fixed location (608, 512)
- 10 bus stops (A-J) scattered across map
- Random teleportation to any stop on interaction
Implementation (Player.java):
private void teleportToRandomStop(java.util.List<BusStop> busStops) {
int index = MathUtils.random(0, busStops.size() - 1);
BusStop targetStop = busStops.get(index);
playerX = targetStop.bounds.x;
playerY = targetStop.bounds.y;
lastBusMessage = "You took a bus to " + targetStop.name;
needsBusMessage = true;
busMessageTimer = 0f;
hiddenEvent += 1;
AchievementManager.get().unlock("TELEPORTED");
}UX Polish:
- Context-sensitive message: "Press 'E' to ride the bus"
- Notification displays destination for 3 seconds
- Unlocks "TELEPORTED" achievement on first use
Tip
Design Rationale: The bus provides strategic depth - players can use it to quickly navigate the large map or escape enemies, but the random destination adds risk/reward decision-making.
Score Calculation:
public float maxScore = 500f;
public float playerScore = maxScore;
// In render loop:
float decayRate = maxScore / 300f; // 500/300 = ~1.67 points/second
playerScore -= decayRate * delta;Score Modifiers:
- Base: 500 points
- Time decay: Linear decay to 0 over 5 minutes
- Bonus: +50 from coin collection
- Penalty: Time lost from events indirectly reduces score
Note
Design Choice: Linear score decay incentivizes fast completion while bonus collectibles reward exploration. The system creates tension between speed and thoroughness.
Headless Testing Support (FirstScreen.java):
public void initLogic(Player player, Key key, EnergyDrink energyDrink,
Bus bus, java.util.List<BusStop> busStops,
DuoAuth duoAuth, WetFloor wetFloor,
float enemyX, float enemyY) {
// Initialize game objects without graphics/audio
// Enables unit testing of game logic
}Test Mode Constructors:
// Enemy.java - test constructor
public Enemy(float x, float y) {
this.testMode = true;
// Skip texture/pathfinding initialization
}Important
Design Rationale: Separating logic initialization from graphics/audio enables comprehensive unit testing without LibGDX graphics context. This supports CI/CD with headless test runners.
Texture Loading Strategy:
- All textures loaded in
show()method - Proper disposal in
dispose()method (60+ lines) - Animation textures separated from sprite textures
Example (FirstScreen.java):
@Override
public void dispose() {
if (renderer != null) renderer.dispose();
if (map != null) map.dispose();
if (keyTexture != null) keyTexture.dispose();
// ... 50+ more resource disposals
}Warning
Critical Choice: LibGDX requires manual memory management. Failing to dispose textures causes memory leaks. The comprehensive disposal pattern prevents crashes in long play sessions.
Map Layers:
map = new TmxMapLoader().load("maps/maze_map.tmx");
MapLayer wallsLayer = map.getLayers().get("Walls");
MapLayer doorsLayer = map.getLayers().get("Doors");
collisionLayer = (TiledMapTileLayer) wallsLayer;
doorLayer = (TiledMapTileLayer) doorsLayer;Benefits:
- Visual level design using Tiled Map Editor
- Non-programmers can create levels
- Collision data embedded in map
- Easy iteration without code changes
Input Handling:
- WASD and Arrow Keys for movement
- E for interaction (doors, bus)
- ESC for pause menu
- C for debug win (testing shortcut)
Input Processor Swapping:
if (paused)
Gdx.input.setInputProcessor(pauseStage); // UI handles input
else
Gdx.input.setInputProcessor(null); // Game handles inputTip
Design Rationale: Swapping input processors cleanly separates game input from UI input, preventing movement during pause menu interaction.
Camera Following:
game.worldCamera.position.set(
playerChar.playerX + playerChar.playerWidth / 2f,
playerChar.playerY + playerChar.playerHeight / 2f,
0
);Zoom Level: 0.6x (shows more of the world while maintaining visibility)
Note
Design Choice: Centered camera follows player smoothly. The 0.6x zoom provides strategic view of surroundings, important for spotting enemies and planning routes.
Test Organization:
- Core module:
core/src/test/- 5 test classes - Headless module:
headless/src/test/- 13+ test classes
Test Categories:
- Unit Tests: Individual component testing (Player, Collision, Key, Door)
- Integration Tests: Multi-component interactions (BusStop, DuoAuth, Enemy)
- Headless Tests: Full game logic without graphics
- Asset Tests: Verify all game assets load correctly
Coverage Reporting:
jacoco {
toolVersion = "0.8.10"
}
jacocoTestReport {
reports {
html.required = true
xml.required = true
csv.required = false
}
}Checkstyle Integration:
checkstyle {
toolVersion = '10.12.0'
configFile = rootProject.file("${rootDir}/config/checkstyle/checkstyle.xml")
ignoreFailures = true
}Enforced Standards:
- Consistent naming conventions
- Javadoc comments for public methods
- Proper indentation and formatting
- Maximum line length constraints
SpriteBatch Pattern:
game.batch.begin();
// All sprite drawing here
game.batch.end();Tip
Batching all sprite draws between single begin/end calls minimizes GPU state changes, crucial for 60 FPS performance.
Enemy Pathfinding:
- Repath interval: 0.5s (not every frame)
- Path pruning eliminates unnecessary waypoints
- Early goal detection terminates search
Cost Analysis:
- Pathfinding: ~0.5-2ms per repath (depends on distance)
- 2 repaths/second = negligible CPU impact
Frame Timing:
if (playerChar.isMoving)
stateTime += delta * animSpeed; // Only advance when moving
else
stateTime = 0f; // Reset to idle frameThis prevents unnecessary GPU texture swaps when player is stationary.
Problem: Detecting when player is near door and has key.
Solution: Static door state flags in Player class:
public static boolean doorInfront = false;
public static boolean open = false;Collision detection sets these flags, player input reads them.
Problem: Events (DuoAuth, WetFloor, Enemy) need to coordinate without interfering.
Solution: Individual active flags and update loops:
if (duoAuth.active) return; // Skip input processing
if (wetFloor.active) return; // Skip input processing
// Normal movement hereProblem: LibGDX rendering requires OpenGL context, unavailable in CI/CD.
Solution:
- Headless backend (no graphics initialization)
- Test-specific constructors for entities
initLogic()method to set up game state for testing
✅ Modular Architecture - Clean separation enables testing and future platform expansion
✅ Dual Viewport System - Professional rendering with independent world/UI cameras
✅ A Pathfinding* - Sophisticated AI with optimal path calculation
✅ Singleton Achievement System - Persistent player progression across sessions
✅ Comprehensive Testing - Headless testing enables CI/CD integration
✅ Resource Management - Proper disposal prevents memory leaks
✅ Event-Driven Design - Flexible event system supports diverse gameplay mechanics
- Testability First - Every component designed with testing in mind
- Performance - Optimizations at every layer (batching, pathfinding intervals, frame skipping)
- Maintainability - Clear separation of concerns, consistent patterns
- Player Experience - Smooth animations, responsive controls, clear feedback
- Code Quality - Checkstyle enforcement, comprehensive Javadoc, code reviews
Potential Enhancements:
- Entity Component System (ECS) - Refactor entities to component-based architecture for flexibility
- Level Editor - In-game level designer using Tiled integration
- Multiplayer - Add networked co-op using Kryonet
- Mobile Port - Android/iOS modules (architecture already supports it)
- Procedural Generation - Randomized map layouts for replayability
- Save System - Checkpoint system using LibGDX Preferences
- Sound Effects - Currently only background music, add SFX for events
TheDebugThugs demonstrates professional game development practices with a focus on:
- Clean architecture with separation of concerns
- Advanced algorithms (A*, state machines, event systems)
- Comprehensive testing infrastructure
- Performance-conscious implementation
- Extensible design for future features
The project showcases both technical depth (pathfinding, dual viewports, resource management) and creative design (unique bus teleportation, multi-layered event system, achievement progression).
| Function | Primary File | LOC |
|---|---|---|
| Main Game Loop | FirstScreen.java | 549 |
| Player Control | Player.java | 248 |
| Enemy AI | Enemy.java | 181 |
| Pathfinding | Pathfinding.java | 198 |
| Achievements | AchievementManager.java | 216 |
| Collision | Collision.java | ~145 |
- Singleton: AchievementManager
- State Machine: Player animation states
- Observer: Achievement unlock notifications
- Template Method: Screen lifecycle (show/render/dispose)
- Strategy: Different event behaviors (DuoAuth, WetFloor, Duck)
- libGDX: 1.12.x (game framework)
- Java: 17
- Gradle: Build automation
- JUnit 5: Testing framework
- JaCoCo: Code coverage
- Checkstyle: Code quality
- Tiled: Map editor
End of Presentation Guide