A spaceship shooter prototype exploring projectile systems, boss AI phases, and advanced collision configuration in Unreal Engine 5.4.
This project is a spaceship shooter prototype built entirely in Blueprints with Unreal Engine 5.4, developed as part of this Udemy course. It covers the core systems of the genre — player movement, projectile combat, enemy waves, and a boss encounter — with a deliberate focus on Unreal’s collision system as the layer that makes all of them interact correctly.
You can watch the prototype in action here: YouTube
Spaceship Movement and Constraints
The player controls a spaceship in a constrained 2D plane — movement is limited to horizontal and vertical axes within the play area, with the camera fixed in position above. This constraint is enforced through clamped position values rather than physics — the ship moves at a defined speed per input axis, and its world position is clamped to the visible play area boundaries each frame. Using physics-based movement for this genre would introduce unnecessary complexity: inertia, angular momentum, and drag that work against the precise, responsive feel a shooter requires.
The ship’s movement speed and play area bounds are exposed as configurable parameters, making it easy to tune the feel without touching logic nodes.
Projectile System
The projectile system follows the standard pattern for this genre: the player ship spawns projectile actors at a fire point, which travel in a fixed direction (upward, in a vertical scroller) at a set velocity. The fire rate is controlled by a cooldown timer — a simple boolean flag that blocks subsequent spawns until the cooldown completes, then resets.
Projectiles are lightweight actors: they have a ProjectileMovementComponent for velocity, a collision primitive for overlap detection, and a lifespan that auto-destroys them if they leave the play area without hitting anything. The lifespan is important — without it, projectiles that miss their target continue moving indefinitely, accumulating as invisible actors until they cause a memory or performance problem.
Enemy projectiles use the same actor class as player projectiles, differentiated only by their collision profile — which determines what they can hit. This is where the collision system becomes central.
Advanced Collision: Channels, Profiles, and Overlap Filtering
Unreal’s collision system is one of the most powerful and most misunderstood parts of the engine. The naive approach — putting everything on the default channel and using overlap events to filter by class — technically works but produces unnecessary overlap events between actors that should never interact, and creates logic that belongs in the physics layer mixed into gameplay code.
The correct approach uses custom collision channels and collision profiles to encode interaction rules at the physics level:
Collision channels define categories of objects — Player, PlayerProjectile, Enemy, EnemyProjectile, Environment. Each actor is assigned to one channel. Collision profiles define, for each channel, whether to ignore, overlap, or block other channels. A PlayerProjectile profile blocks Enemy and Environment, ignores EnemyProjectile and Player. An EnemyProjectile profile blocks Player, ignores PlayerProjectile and Enemy. The physics engine filters interactions before any Blueprint code runs — only the collisions that are supposed to happen generate events.
This has two concrete benefits. First, performance: ignored interactions cost nothing. Second, correctness: the logic for “player projectiles don’t hit other player projectiles” lives in the collision profile where it belongs, not scattered across overlap event handlers with class-check conditions.
For the boss encounter, a separate collision profile allows boss projectiles to behave differently from regular enemy projectiles — potentially penetrating shields, having a larger hit radius, or triggering different damage events — without modifying any existing actor class.
Boss AI and Phase Design
The boss enemy is the most architecturally complex actor in the prototype. It introduces the concept of phases — distinct behavioral states that activate based on health thresholds. A boss at full health behaves differently from one at 50% or 25%. This requires a state machine, even a simple one.
The phase system works as follows: the boss monitors its current health value. When health crosses a threshold, it transitions to the next phase — changing movement pattern, fire rate, projectile type, or all three. Each phase is a self-contained behavioral configuration rather than a completely separate code path, which keeps the phase transition logic simple: change the parameters, keep the underlying update loop the same.
The boss also has a larger, multi-part collision body compared to regular enemies. Different regions of the boss (a core, wing segments, a turret) can be configured as separate collision primitives with independent hit responses — allowing design space for weak points that take extra damage or sections that must be destroyed in sequence before the core becomes vulnerable.
Game Loop: Waves and Progression
Enemy waves are managed by a spawner that releases groups of enemies on a timer, with each successive wave increasing in density or speed. This is a simple but effective progression model for the genre: the player faces escalating pressure until the boss appears, which is spawned as a special case after the final regular wave completes.
The wave system uses a data-driven approach — wave configurations are stored as arrays of structs defining enemy count, spawn interval, and enemy type — rather than hardcoded sequences. This makes adding or reordering waves a matter of editing data rather than modifying logic.
Reflection
The spaceship shooter genre is a classic learning context for a reason: it’s simple enough to implement quickly, but the systems it requires — movement, projectiles, collision, AI, wave management — are the same systems that appear in far more complex games. Getting the collision configuration right in a project like this builds habits that carry over to any genre.
The key takeaway from the collision work is the value of moving filtering logic out of Blueprint event handlers and into the physics configuration layer. The more that “what interacts with what” is encoded in collision profiles, the less defensive checking is needed in gameplay code — and the more the engine can do the right thing automatically.
In C++, custom collision channels are defined in DefaultEngine.ini and referenced via ECollisionChannel enums. Collision profiles are configured in the same file or via FCollisionResponseContainer in component setup. The Blueprint surface maps directly to these underlying structures.
Leave a comment