Diploma project β a 2D top-down roguelike shooter built in Unreal Engine 5.7 with Paper2D. The engineering deliverable is a custom Adaptive Enemy Behavior module: a 3-layer decision-making system implemented in pure C++ with no built-in UE AI infrastructure (no Behavior Trees, no Blackboards, no EQS, no ML).
Full video: https://youtu.be/e7XKQVwgk7Q
- Project Overview
- Key Features
- The Adaptive Behavior Module
- Tech Stack
- Source Layout
- Gameplay Loop
- Procedural Room Generation
- Item & Upgrade Systems
- Save System (SQLite)
- Running the Prebuilt Game
- Building and Running from Source
- Troubleshooting
- Console Commands
- Debug Tools
W, A, S, D - Movement
LMB β Attack
1 β Sword
2 β Bow
E β Use Potion
F3 β Open DebugWidget
Y β God Mode
U β Super Attack
Depthrun is a demonstration polygon for a diploma research project. The game is fully playable, but its primary purpose is to showcase a working software module that implements adaptive enemy behavior β a 3-layer pipeline that turns scalar context (distance, HP, weapon type, density, memory) into a high-level FSM decision while continuously adapting its own weights based on combat feedback.
The committee deliverable is not the game; it is the AdaptiveBehavior module. The game exists so the module has somewhere to live and something to react to.
Target platform: Windows x64. Target audience: Diploma defense committee + anyone interested in shippable, non-ML adaptive AI patterns in UE5.
- Procedurally generated dungeon of multi-cell rooms (1Γ1, 2Γ1, 1Γ2, 2Γ2, L-shape) on a grid of 8Γ6 tiles per cell.
- Two-slot weapon system β Sword (slot 1) and Bow (slot 2), instant switch with keys
1/2. - Per-run item system with 7 modifiers (Ricochet, Pierce, Extended Range, Double Swing, Flat Damage, Bonus HP, Move Speed, Multishot) applied at pickup and discarded on run end.
- Chest loot β every chest always rolls Diamonds + Health Potions + a 70% chance for a Run Item, configured via
DA_ChestLootConfig. - Hub metaprogression between runs β persistent upgrades (Damage, Range, Arrow Count, Max HP), bought with diamonds carried over from previous runs.
- Boss room ends the run with a Victory screen and 100 % diamond payout; death pays out 50 %.
- VFX powered by Niagara β hit (melee/ranged), death, chest open, victory, defeat.
- Music system β
UMusicSubsystemwith twoUAudioComponents and seamless crossfade; combat trigger reacts to active rooms.
- Pure C++ FSM with 5 states:
Idle / Chase / Attack / Retreat / Flankβ no Blueprint, no Behavior Tree. - 3-layer Adaptive Behavior overlay that decides which state to enter, with dynamic weights, time-decay memory, N-gram pattern recognition, and utility curves.
- A/B-testable via the
bAdaptiveEnabledflag on the component β disable the overlay to fall back to vanilla FSM, useful for empirical comparison during defense. - Real-time debug widget showing
T_final, every weightw_i, every state'sFinalScore, the recognized N-gram, and the current FSM state. - CSV export via
UDemonstrationSubsystemfor diploma plots and charts.
This is the heart of the project. It lives in Source/Depthrun/AdaptiveBehavior/ and is attached to every enemy as a UAdaptiveBehaviorComponent. Evaluation runs on a FTimerManager tick every 0.3 s (configurable 0.2β0.5 s) β never per-frame.
Reads raw world state for the owning enemy and normalizes everything into the [0, 1] range:
| Factor | Source | Normalization |
|---|---|---|
DistanceNorm |
distance to player | linear |
WeaponThreatNorm |
active player weapon (melee 0.6 / ranged 0.8) | discrete lookup |
EnemyHPRatioNorm |
enemy HP / MaxHP | quadratic (Ξ±=2) |
AllyCountNorm |
allies within support radius | clamp(count / 5) |
RoomDensityNorm |
entities within 600 units | clamp(count / 8) |
MemoryAggressiveness + MemoryMobility |
from AdaptiveMemory |
window counter |
Computes the scalar threat used by Layer 3:
T_final = clamp( Ξ£ w_i Γ f_i , 0, 1 )
No cross-terms, no exponential smoothing, no confidence fallback β deliberately kept simple and explainable for defense. Weights w_1β¦w_6 come from the DynamicWeightManager, which adjusts them with a deterministic feedback loop:
w_i += Ξ· Β· reward Β· contribution_i
contribution_i = f_i(x_i) / Ξ£ f_j(x_j)
Reward is +1.0 when the enemy successfully damages the player, β1.0 when the enemy takes damage. This is not machine learning β it is a transparent, fully traceable rule.
For each of the 5 FSM states evaluates:
FinalScore(s) = Utility(s, T_final, Context) β TransitionCost(current β s)
+ InertiaBonus(current, s, TimeInState)
+ PatternModifier(s)
- Utility β bell-curve / ramp-curve per state (
UtilityCurves), peaks at the threat level each state is designed to handle. - TransitionCost β 5Γ5 matrix (
TransitionCostMatrix), penalizes thrashing between distant states. - InertiaBonus β
min(InertiaMax, rate Γ TimeInState), rewards staying in the current state to suppress jitter. - PatternModifier β bonus/penalty per recognized N-gram (e.g. Shot+Shot boosts Flank by
+0.65).
The state with the highest FinalScore wins. If it differs from the current state, FSMComponent::TransitionTo fires.
AdaptiveMemoryβ short-term memory of player actions. Aggressiveness =count(Shot+Melee in last 10 s) / 10.PatternRecognizerβ sliding window of 15 player actions; counts 2-grams and 3-grams; returns the dominant pattern.UAdaptiveConfig(UDataAsset) β every tunable constant (Ξ·, Ξ», curve params, window sizes, hysteresis points) lives here and can be edited in the Editor without recompilation.
Each ABaseEnemy exposes split ranges:
MeleeAttackRangeβ melee attack entry (default 80)RangedAttackRangeβ optimal ranged distance (default 320, 450 forARangedEnemy)RangedTooFarRangeβ break out of ranged Attack back to Chase (default 480)MinAttackRangeβ too close β RetreatSafeDistanceβ Retreat exit distance
GetEffectiveAttackRange() is virtual; AAdaptiveEnemy overrides it based on bIsRangedMode. The meleeβranged switch uses hysteresis (separate Up/Down thresholds biased by personality CombatStyle) to avoid mode-flip oscillation.
| Layer | Technology |
|---|---|
| Engine | Unreal Engine 5.7 |
| Rendering | Paper2D (2D top-down, gravity = 0, all movement on XY plane) |
| Input | Enhanced Input |
| Effects | Niagara |
| Persistence | SQLiteCore (built-in UE 5 plugin) |
| UI | UMG layout + C++ logic (no Blueprint logic in widgets) |
| Audio | UAudioComponent Γ 2 + crossfade via UMusicSubsystem |
| Language | 100 % C++ for FSM, AdaptiveBehavior, gameplay, UI logic |
Source/Depthrun/
βββ Core/ β GameMode, GameInstance, log channels
βββ Player/ β DepthrunCharacter, CombatComponent, PlayerEconomy, ActionTracker
βββ Enemy/ β BaseEnemy, MeleeEnemy, RangedEnemy, AdaptiveEnemy
βββ FSM/ β FSMComponent + 5 FSMState_* subclasses
βββ AdaptiveBehavior/ β β
THE CORE MODULE
β βββ AdaptiveBehaviorComponent β orchestrator (timer tick, layer pipeline)
β βββ ContextEvaluator β Layer 1: factor normalization
β βββ ThreatCalculator β Layer 2: weighted sum β T_final
β βββ StateTransitionResolver β Layer 3: argmax FinalScore
β βββ AdaptiveMemory β short-term player-action memory
β βββ PatternRecognizer β 2/3-gram frequency analysis
β βββ DynamicWeightManager β feedback-loop weight adjustment
β βββ UtilityCurves β per-state utility functions
β βββ TransitionCostMatrix β 5Γ5 cost + inertia
β βββ AdaptiveConfig (DataAsset) β all tunables live here
βββ Combat/ β BaseWeapon, MeleeWeapon, RangedWeapon, Projectile, ChestActor
βββ Items/ β RunItemInventory, RunItemConfig, RunItemCollection
βββ RoomGeneration/ β RoomBase, RoomTemplate, RoomGeneratorSubsystem, TrapdoorActor
βββ UI/ β HUDOverlayWidget, HealthBar, PauseMenu, DeathScreen, VictoryScreen,
β DebugAdaptiveWidget, ChestRewardWidget, HubWidget, MainMenuWidget
βββ Data/ β DepthrunSaveSubsystem, SQLite schema
βββ Audio/ β MusicSubsystem, CombatMusicTrigger, UISoundLibrary
βββ Utils/ β math helpers (sigmoid, bell, decay)
L_MainMenu βββΆ L_Hub βββΆ L_Gameplay βββΆ (death OR boss cleared) βββΆ L_Hub
β² β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Main Menu β Play / Settings / Quit. Settings (master volume) persist in
UDepthrunGameInstance. - Hub β view profile, spend persistent diamonds on 4 upgrades, start a run.
- Run β procedural dungeon. Each combat room locks doors β spawns enemies β on clear unlocks doors + may spawn a chest (
bSpawnChest, 10 % default). - Boss room β clearing spawns the exit
ATrapdoorActor; entering it triggers Victory (100 % diamonds). - Death β Game Over screen, 3 s fade, 50 % diamonds banked, return to Hub.
- Every run records a row into
run_historyregardless of outcome.
- Grid-based, 8Γ6 tiles per cell (16Γ16 px each, ~332 units wide at scale 2.6).
- Multi-cell rooms supported: 1Γ1, 2Γ1, 1Γ2, L-shape (3 cells), 2Γ2.
- Z is single-source-of-truth via
RoomTemplate:PlayerLockedZ,EnemyLockedZ,DoorLockedZβ no hidden clamping. - Tile layers:
- Layer 0 β Floor (regular + Shadowed Floor in doorways)
- Layer 1 β Door layer
- Layer 2 β Walls (auto-built to seal blocked passages)
- Props placed in code: torches on top walls (not in corners, not in doorways, 50 % chance), bones on floor, chest 10 % chance.
- Start rooms never spawn doors and never lock β clean passage for the player.
- Enemy spawn safety:
OccupiedTiles(doorway cells) are skipped to prevent enemies blocking exits.
| Effect | Weapon | Description | Param |
|---|---|---|---|
ArrowRicochet |
Bow | Arrows ricochet between targets | RicochetCount = 2 |
ArrowPierce |
Bow | Arrows pierce enemies | β |
MeleeExtendedRange |
Sword | Increased swing radius | Γ1.5 |
MeleeDoubleSwing |
Sword | Two swings per attack | β |
FlatDamage |
Any | +5 flat damage | β |
BonusMaxHP |
Any | +25 MaxHP | 25 |
BonusMoveSpeed |
Any | +15 % move speed | 0.15 |
Multishot (BonusProjectileCount) |
Ranged | +1 arrow per shot (max 5) | 1 |
All items are pre-filled into a single DA_RunItemCollection DataAsset. Effects apply via ApplyToWeapon() + ApplyToCharacter().
| Upgrade | Max Level | Cost Formula |
|---|---|---|
| Damage | 5 | floor(50 Γ 1.6^level) |
| Range | 5 | floor(50 Γ 1.6^level) |
| ArrowCount | 3 | floor(150 Γ 2^level) |
| MaxHP | 5 | floor(50 Γ 1.6^level) |
Subsystem: UDepthrunSaveSubsystem (GameInstance scope)
Path in code: FPaths::ProjectSavedDir() / "Depthrun.db"
Viewer: the VSCode extension alexcvzz.vscode-sqlite works out of the box.
Where Depthrun.db actually lives, by launch mode:
| Launch | Path |
|---|---|
| Editor / PIE | <ProjectFolder>/Saved/Depthrun.db |
| Packaged build (Windows) | %LOCALAPPDATA%\Depthrun\Saved\Depthrun.db |
β In a packaged build the DB is stored under
%LOCALAPPDATA%and is not wiped by rebuilding or reinstalling the game. To reset progress, deleteDepthrun.dbat that path (or the wholeSavedfolder), or runResetProfileCmdin the in-game console.
player_profile (single row, id = 1)
| Column | Type | Notes |
|---|---|---|
id |
INTEGER PK | always 1 |
TotalDiamonds |
INTEGER | persistent diamonds |
Damage_Lvl |
INTEGER | 0β5 |
Range_Lvl |
INTEGER | 0β5 |
ArrowCount_Lvl |
INTEGER | 0β3 |
MaxHP_Lvl |
INTEGER | 0β5 |
run_history (one row per run)
| Column | Type | Notes |
|---|---|---|
id |
INTEGER PK | rowid |
Rooms |
INTEGER | cleared rooms count |
Won |
INTEGER | 1 = victory, 0 = death |
RunDuration |
REAL | seconds |
Timestamp |
TEXT | datetime('now') |
Run results are written once per run: ShowVictoryScreen() calls SaveRunResult(..., true) after committing 100 % diamonds; Die() calls SaveRunResult(..., false) after committing 50 % diamonds.
The simplest path. No additional software is required (except possibly the Visual C++ Redistributable, see Troubleshooting).
- Open the folder with the prebuilt game (
Game/from the archive, or your own build produced viaFile β Package Project). - Find
Depthrun.exe. - Double-click to launch.
- The game starts in fullscreen: main menu, Play button.
Quit via the in-game menu or Alt + F4.
Use this path to open the project in the Unreal Engine editor, study the code, modify logic, or produce your own .exe.
- Download Epic Games Launcher from the official site: https://store.epicgames.com/en-US/download
- Install and sign in (free Epic Games account).
- Open the Unreal Engine tab β Library.
- Click + next to Engine Versions and pick 5.7.
- Click Install (60β100 GB on disk).
Without Visual Studio the project will not build β the engine cannot compile the C++ sources.
- Download Visual Studio 2022 Community (free edition): https://visualstudio.microsoft.com/downloads/
- In the installer's Workloads tab, enable Game development with C++.
- In the Individual components list make sure these are checked:
- MSVC v143 (latest)
- Windows 10 SDK or Windows 11 SDK
- Click Install (~15β30 GB).
- Reboot after installation.
- Go to the source folder (
Source/from the archive or the repository root). - Right-click
Depthrun.uprojectβ Generate Visual Studio project files. Wait 1β3 minutes. - Double-click
Depthrun.uproject. A dialog may offer to rebuild modules β click Yes. The first compile takes 5β15 minutes. - The editor opens. The Play button in the top toolbar runs the game in PIE.
- Open the project in the editor.
- From the top menu pick Platforms β Windows β Package Project.
- Choose a target folder and wait for the build to finish.
- A standalone
Depthrun.exewith all assets appears in that folder.
The package must include all three levels. The project ships with the correct entry in Config/DefaultGame.ini:
[/Script/UnrealEd.ProjectPackagingSettings]
+MapsToCook=(FilePath="/Game/Levels/L_MainMenu")
+MapsToCook=(FilePath="/Game/Levels/L_Hub")
+MapsToCook=(FilePath="/Game/Levels/L_Gameplay")Without these entries the Play button in the packaged build silently fails β the cooker only ships the GameDefaultMap and OpenLevel("L_Hub") falls through.
Alternative for development:
- Open
Depthrun.sln(generated by Generate Visual Studio project files). - Set configuration to
Development Editorand platform toWin64. - Build β Build Solution or press F7.
- After a successful build, launch the editor via
Depthrun.uprojector run it from VS with F5.
β Critical: always use Development Editor for the editor, never DebugGame Editor. DebugGame produces a different
.dllthat the UE Editor refuses to load.
First thing to try for any weirdness β turn off your VPN.
Many Unreal Engine and project glitches come from an active VPN connection:
Depthrun.uprojectwon't open, the engine hangs or throws an error;- no audio in the Unreal Engine editor;
- assets fail to load, the editor takes ages to start;
- random compile failures.
Disable the VPN completely, close the editor, and try again β that fixes it most of the time.
Install Microsoft Visual C++ Redistributable for Visual Studio 2015β2022 (x64) from the official Microsoft site: https://aka.ms/vs/17/release/vc_redist.x64.exe Reboot and try again.
Click More info β Run anyway. The build has no paid code-signing certificate, which is expected.
Make sure the game uses the discrete GPU, not the integrated one.
Windows Settings β Display β Graphics β add Depthrun.exe β High performance.
Verify that Unreal Engine 5.7 and Visual Studio 2022 with the Game development with C++ workload are installed. Then right-click Depthrun.uproject β Generate Visual Studio project files, open Depthrun.sln, and build in Development Editor | Win64.
Restart the game. If that doesn't help, delete the Saved/ folder next to the .exe (or in the project's Source/ directory) β it is recreated automatically.
Open the console with ~ in PIE.
| Command | Effect |
|---|---|
AddRunDiamonds <N> |
+N diamonds to the current run |
AddProfileDiamonds <N> |
+N diamonds to the profile (persistent) |
BuyUpgradeCmd <Type> |
Buy upgrade: Damage / Range / ArrowCount / MaxHP |
ShowProfile |
Print profile (TotalDiamonds + upgrade levels) |
ResetProfileCmd |
Reset profile to defaults |
| Command | Effect |
|---|---|
ListItems |
List every item in DA_RunItemCollection |
GiveItem <partial-name> |
Give an item by partial name match |
ClearRunItems |
Clear run inventory, restore base stats |
| Command | Effect |
|---|---|
DBCheck |
Dump player_profile and all run_history rows to the Output Log |
| Command | Effect |
|---|---|
ToggleGodMode |
Invulnerability on/off |
ToggleSuperAttack |
Super-damage on/off |
UDebugAdaptiveWidgetβ real-time overlay above each enemy showingT_final, every weight, every state'sFinalScore, the recognized N-gram, and the current FSM state. Mandatory for defense β the committee must see the module thinking.bAdaptiveEnabledβ flag onUAdaptiveBehaviorComponent. Untick in the Editor or set at runtime to disable the overlay and fall back to vanilla FSM, side-by-side A/B comparison.UDemonstrationSubsystemβ exports decisions, weights and threat values into CSV files (underSaved/) for diploma charts.- Log categories β
LogDepthrun,LogAdaptiveBehavior,LogDepthrunLoot,LogDepthrunSave. Filter the Output Log to follow any subsystem.
A Russian version of this document is available at README_ru.md.