How-To: Projectiles
Goal: fire a replicated projectile that hits what it passes through, using the same profile assets as a melee swing.
1. Build a preset around Actor Sweep
Use Crimson Selection: Actor Sweep as the selection task. It sweeps a sphere along the path the projectile actually travelled since the last query, so it needs no configured length.
Crimson Selection: Actor Sweep with a Radius roughly matching the projectile's visual size.2. Create the projectile Blueprint
Right-click in the Content Browser: Crimson -> Hit Detection -> Crimson Projectile. In its Class Defaults set Impact Profile to the profile from step 1, and set Max Lifetime to something finite.
| Property | Meaning |
|---|---|
Impact Profile | Shape, filtering and dedup. Keep Dedup Policy on Once Per Window so a pierce cannot hit the same target twice |
Max Pierces | How many targets it passes through before stopping. 0 stops at the first; -1 (the default) pierces without limit, leaving Max Lifetime and Stop On World Block as the only things that end the flight |
Max Lifetime | Seconds before it removes itself. Always set this - a projectile that misses everything otherwise flies forever |
Stop On World Block | Stop at walls. Turn off for a projectile meant to pass through geometry |
FCrimsonProjectileLaunchParams can override two of them for a single shot: Lifetime Override (which also bounds the hit window, so a shot cannot outlive its own detection) and Max Range, a straight-line distance cutoff that is 0 - unlimited - by default.Max Range is free for an ordinary shot: a straight path at constant speed reaches a given distance at a predictable time, so the range folds into the lifetime timer and the projectile never ticks. Only a curving path - gravity, homing, bouncing - has to be measured in flight.BP_CrimsonProjectile_Example (/CrimsonHitDetection/Examples/) is this step already done: Impact Profile set to DA_CrimsonHitProfile_Projectile, a 3 second lifetime, no pierces. It ships with no mesh, so add your own visual to the Collision Component root.Its profile uses
TP_CrimsonProjectileSweep, which is built on Actor Sweep as described above.Collision Component root. The collision sphere itself only stops the projectile at walls - gameplay hits come from the profile.3. Launch it from an ability
In your ability graph: Event ActivateAbility -> Crimson Launch Projectile (Projectile Class, Spawn Transform from a muzzle socket). Split the Launch Params pin and set Velocity in cm/s world space; leave the rest at their defaults to use the projectile's own settings.
Drag off the red On Impact pin and add a custom event - that is where damage goes. Inside it, call Apply Effect Spec Detached with a spec you built before launching and the report's Target Data. Wire On Projectile Expired to End Ability.
Do NOT apply damage from On Projectile Hit: that pin belongs to the ability task, which dies with the ability, so a projectile still in the air would silently stop dealing damage.
On Impact exists for. Finally set Play As Client with a dedicated server: the projectile must appear and fly on the client, while impacts resolve only on the server.4. Add behaviour on impact
Override On Impact on the projectile Blueprint for anything the projectile itself should do regardless of who fired it - an explosion, a chain to the next target, spawning a hazard. It runs on the server. On Expired runs just before the projectile is destroyed or released. Not to be confused with the launch node's On Impact pin, which routes back to the firing ability.
On Impact and confirm it runs once per impact, on the server only.How replication works here
Only the server launches and only the server detects. The launch state - location, velocity, server launch time - replicates once, and clients then run the same Projectile Movement Component locally. The path is fully determined by the launch, so streaming positions every frame would pay repeatedly for something both sides can already compute.
Clients fast-forward by however long the spawn took to reach them, so a projectile is not permanently trailing the server by half a round trip. Their copy has collision disabled entirely, and Remaining Pierces is never replicated, so no client can make a gameplay decision from it.
Object pooling
ACrimsonProjectile implements ICrimsonPoolable, the pooling contract defined in CrimsonCommon. Any pool that speaks that interface - CrimsonObjectPool does - can recycle projectiles with nothing to wire up. The interface lives in the shared contract layer precisely so this works: the cardinal rule forbids CrimsonHitDetection from referencing a pooling plugin, and a shared interface means it does not have to.
Acquire an instance and call Launch Projectile. There is no release call to pair with it - the projectile decides when it is finished and asks to be reclaimed.
| Member | Who calls it | Role |
|---|---|---|
On Acquired From Pool | The pool | Marks the instance pool-owned and repositions it with a teleport. You do not call this |
Launch Projectile | You | The single activation entry point. Resets every per-flight value, so a recycled projectile behaves exactly like a fresh one |
On Released To Pool | The pool | A no-op here - expiry already closed the hit window, stopped the movement and hid the projectile |
Get Pool Release Delegate | The pool | C++ only. The pool binds this once per instance and reclaims the projectile when it fires |
On Projectile Released | - | The same moment, exposed for game code that wants to listen without a pool involved |
Is Projectile Active | - | False while dormant in a pool |
On Expired | - | The subclass reset hook. Clear any state your Blueprint added, here |
ICrimsonPoolable takes that over entirely; the pool then touches only ownership, net dormancy and its own bookkeeping.That exclusivity is deliberate, not a convenience. This projectile drives its visibility from the replicated
bActive flag, and bHidden replicates on its own. If the pool also wrote bHidden, the projectile's OnRep would never fire - its own state did not change - and clients would be left looking at a visible projectile the server considers parked. Only one side may own that state.Size the pool so an instance is not reused within a couple of network updates. A monotonic launch counter already guarantees that two consecutive launches are never byte-identical, so a projectile relaunched from the same muzzle with the same velocity is still seen as a new launch; it does not protect against a launch being skipped entirely.
Net dormancy while parked is the pool's responsibility, not the projectile's. A pool that omits it leaves parked projectiles paying relevancy cost for the whole match.
BeginPlay runs once per actor lifetime, not once per use. Per-shot state a Blueprint subclass initialises there is stale the second time a pooled projectile flies - move it into On Acquired From Pool or On Expired.One-way latches break reuse. Any "have I finished" flag must be cleared on acquire, or the second flight ends immediately.
Delegates bound in
BeginPlay must not be rebound per use, or handlers multiply with every reuse.Crimson Launch Projectile calls SpawnActor directly. To fire pooled projectiles, acquire the instance from your pool and call Launch Projectile on it yourself - see CrimsonObjectPool.See also
- Concept: Windows, Shapes and Filters
- How-To: Hit Zones
- API Reference