Third-person action-adventure. Ability and combat frameworks, plus the editor tools design used to populate the world.
Built the core ability and combat frameworks, then the developer tools that let the design team populate the world without constant code support.
Role: Gameplay & tools. Team: team of 9. Duration: 9 months. Platform: PC / Steam Deck. Tools: UE5 · C++ · Blueprints.
Problem: Nine people on one codebase, a design team iterating on abilities daily, and by the end a level that loaded 1.3 GiB of textures before anyone pressed a key.
Approach: Compose behaviour from components instead of inheriting it, push everything reusable out into its own plugin, and treat the asset reference graph as a system that needs maintaining like any other.
Result: 24 in-house plugins, four of which now run in other projects unmodified. Design authored quests, talents and encounters without engineering. The worst level went 1.3 GiB → 1.0 GiB.
The entity is a container, not a base class. ACharacterEntity holds no gameplay logic of its own. It implements six interfaces (team agent, stats, skills, character, floating numbers, gameplay tags) and owns seven components that carry the actual behaviour. Everything from a wandering slime to a two-phase boss is the same class with a different set of components attached, which is why a designer adding a behaviour never needed a programmer to find the right place in a hierarchy.
Abilities mount at runtime. A Generic Gameplay Action owns its own input binding and its own lifecycle. The character mounts them when it is possessed and unmounts them when it isn't, so an ability can be granted mid-game without a recompile and without touching the class hierarchy. Wall climbing, gliding and the grapple are all GGAs rather than character code.
The skill system had a split brain. For months skill logic lived in two places at once. ACharacterEntity kept its own map of skills and its own activation path, while UGenericSkillHandlerComponent, which already had cooldowns, slots and delegates, sat there being bypassed. Two sources of truth meant UI bound to whichever one the last person had touched, and skills could not be given to anything that wasn't a character. I made the component authoritative and reduced the entity to a forwarding agent.
Movement modes, not booleans. A slime glides, grapples, ground-slams and walks up walls. Tracking that with flags on the character would have produced the usual mess of mutually exclusive booleans nobody dares delete, so the movement component carries an explicit custom mode and broadcasts transitions. Wall walking is the odd one out: it doesn't use a custom mode at all, it re-points gravity.
Damage that carries. Armour and health are one attribute component, not two systems that argue about who absorbed what. When armour is depleted the remainder carries into health inside the same call, and each value broadcasts its own change event so the UI never has to poll or guess the order.
Fifty enemies without fifty polls. USlimeAIEnemySubsystem registers every AI agent centrally and holds one record per enemy: current state, whether it is targeting the player, creature type, registration time. Combat state is broadcast once from the subsystem rather than each actor asking every frame whether combat has started. Registrations are weak pointers throughout, so an enemy dying mid-encounter can't leave the subsystem owning a dangling reference.
1.3 GiB of textures before anyone pressed a key. Late in the project the cave level reported 1.3 GiB of texture memory. It was not a level-authoring problem, since both versions of the level were dominated by the same assets. It was an import-settings problem. I pulled asset-registry tags across all 2,421 Texture2D assets in the project and 79% of them were B8G8R8A8: no compression, four bytes per pixel, full resolution, never streamed out.
137 MiB to read one boolean. With the textures fixed, the size map stopped being an art inventory and started being what it always was: a reference graph. The largest remaining block was a behaviour-tree decorator. Three Cast To BP_PlayerSlime nodes existed for exactly one purpose, reading a Blueprint-only bIsInvisible flag, and a cast to a Blueprint class hard-references that Blueprint and everything it owns. Loading any AI loaded the player, which loaded the death screen, which loaded an 8K texture.
Rooms stream, chapters load. Moving between rooms in a chapter used to be an OpenLevel, which meant a hitch and a fresh GameMode every doorway. Rooms now stream in behind a transition screen while the player, GameMode and save state stay alive; only chapter boundaries still open a level. The loading screen blocks on a processor that reports real streaming progress rather than a fixed timer.
Everything reusable left the game module. By the end there were 24 in-house plugins in the project. That discipline is the reason four of them now run in other projects unmodified, and the reason a designer could work in the quest system without being able to break combat. Each one has its own page here rather than a paragraph in this one.