How-To: Projectiles

Goal: fire a replicated projectile that hits what it passes through, using the same profile assets as a melee swing.

Prerequisites
Quick Start through step 4, so you understand profiles and presets. How-To: Get the Building Blocks for how to reach the component and settings.

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.

Do not use an area overlap for a projectile
A projectile at 5000 cm/s covers over 80cm in one frame at 60fps. An overlap test at its new position steps straight over anything thinner than that, and the miss gets worse the faster the projectile or the lower the frame rate. Sweeping the distance travelled is what makes fast projectiles reliable.
Verify
The preset's first task is 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.

PropertyMeaning
Impact ProfileShape, filtering and dedup. Keep Dedup Policy on Once Per Window so a pierce cannot hit the same target twice
Max PiercesHow 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 LifetimeSeconds before it removes itself. Always set this - a projectile that misses everything otherwise flies forever
Stop On World BlockStop at walls. Turn off for a projectile meant to pass through geometry
Per-shot overrides
Those are the class defaults. 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.
Reference implementation
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.
Verify
Attach a mesh and any trail effects to the 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.

Verify
Fire at an enemy and confirm one hit. Then end the ability immediately after launching - before the projectile lands - and confirm damage still applies: that is the case 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.

Verify
Breakpoint or log in 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.

MemberWho calls itRole
On Acquired From PoolThe poolMarks the instance pool-owned and repositions it with a teleport. You do not call this
Launch ProjectileYouThe single activation entry point. Resets every per-flight value, so a recycled projectile behaves exactly like a fresh one
On Released To PoolThe poolA no-op here - expiry already closed the hit window, stopped the movement and hid the projectile
Get Pool Release DelegateThe poolC++ 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
Implementing the interface opts out of the pool's default handling
A pool normally hides, stops and disables collision on an actor it parks. A projectile that implements 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.
Pool sizing affects correctness, not just performance
Replication is state-based, not event-based. If a projectile is recycled faster than a client processes the property update - plausible for a high rate-of-fire weapon with a small pool - that client never observes the intermediate state and sees one fewer projectile than the server fired.

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.
Three rules for anything you add to a pooled projectile
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.
The launch task does not pool
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