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 traveled 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 traveled 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 behavior 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.

Fire a volley

One shot per press is a special case of the same thing. Build Crimson Projectile Volley (UCrimsonProjectileVolleyLibrary::BuildProjectileVolley) takes an aim transform, one set of FCrimsonProjectileLaunchParams and a FCrimsonProjectileVolleyPattern, and returns one FCrimsonProjectileVolleyShot per shot - a spawn transform plus that shot's own launch params. It is pure geometry: it spawns nothing, acquires nothing and knows nothing about abilities, so you launch the shots exactly as you launched the single one above.

The aim direction is the base params' Velocity direction, falling back to the aim transform's forward axis when that velocity is zero; the velocity's magnitude is the speed every shot inherits, and the spread turns about the aim transform's up axis. Every shot copies the base params wholesale and only replaces Velocity, so Homing Target, Lifetime Override and Max Range carry across unchanged. Edit the returned array before launching it to make one shot differ from the rest.

Why looping the single-shot node leaves only one shot flying
Two things go wrong, and the volley fixes both.

The shots block each other. A projectile's collision sphere blocks dynamic actors, so two shots leaving the same muzzle are blocking hits for each other's movement sweep, and with Stop On World Block on the first one to move expires on its sibling. Fill each shot's Ignored Actors (on FCrimsonProjectileLaunchParams) with the volley's other projectiles and they fly straight through each other instead. Move-ignore is one-directional, so every shot needs the full list of its siblings - handing it to one of them is not enough.

Nothing computes per-shot directions. Crimson Launch Projectile takes a fully precomputed transform and velocity, so a loop that passes the same values stacks every shot on top of the others. That is what the builder is for.

Count is the knob a "+1 projectile" power-up turns; the pattern decides what the extra shots do with it. A Count of 1 flies dead straight down the aim direction whatever the pattern, and an even Count straddles the aim line rather than putting a shot on it. SpawnDistance is the other knob every pattern reads: each shot starts that many centimeters from the aim origin, measured along its own heading, so 0 starts them all on the aim origin.

PatternLayoutHow it scales with CountFields it uses, besides Count
FanAn arc centered on the aim direction - the classic multishot spreadArc = AnglePerProjectile x (Count - 1), capped at MaxArc. Past the cap the shots pack in tighter instead of widening, so a power-up can raise Count forever without the spread firing backwards. A MaxArc of 360 falls through to the Radial rule, so the first and last shot cannot land on top of each otherSpawnDistance, AnglePerProjectile, MaxArc
ParallelA line perpendicular to the aim direction; every shot flies the aim directionWidth = SpacingPerProjectile x (Count - 1), capped at MaxWidth. Same widen-then-densify rule as FanSpawnDistance, SpacingPerProjectile, MaxWidth
RadialA full circle around the caster, with shot 0 on the aim directionEach shot turns 360 / Count degrees from the one before it. Count changes density only. SpawnDistance is the radius of the ring the nova starts on, which is what keeps a burst from spawning inside the caster - the caster's capsule radius plus the projectile's radius is a good first value. RadialStartAngle turns the whole ring away from the aim direction, so turning the second of two novas by half a step interleaves them instead of overlapping themSpawnDistance, RadialStartAngle
Random ConeA random scatter inside the cone around the aim direction; spacing is not guaranteed and two shots may overlapEvery shot draws its own direction within ConeHalfAngle degrees of the aim direction, so Count changes density only. bConstrainToAimPlane (true by default) keeps the scatter level - the right answer for a top-down game, where vertical scatter is invisible and only makes shots miss. RandomSeed 0 draws a fresh seed for every volley; set a nonzero value for a repeatable spraySpawnDistance, ConeHalfAngle, bConstrainToAimPlane, RandomSeed
Fields you do not see
The Details panel hides the fields that do not apply to the selected Pattern, so author the pattern as a variable or a class default to get that filtered view. A Make Crimson Projectile Volley Pattern node cannot hide pins and shows all eleven fields whatever the pattern is; the builder simply ignores the ones that do not belong to it.

Build the shots, then launch them yourself.

Event ActivateAbility -> Build Crimson Projectile Volley: wire Aim Transform from your muzzle socket, split Base Params and set Velocity in cm/s world space, then set Pattern's Count and its pattern fields. The node returns one Crimson Projectile Volley Shot per shot, each with its own Spawn Transform and Launch Params.

The node is deliberately not pure: a pure node re-executes once per pin that reads it, so a Random Cone volley would hand out a different set of shots to every consumer. Call it once and promote the result to a variable.

Then two loops, in this order. First For Each Loop over the shots: Spawn Actor From Class (your projectile class, at the shot's Spawn Transform) and add each one to a projectile array. Second For Each Loop over that array: Set the matching shot's Launch Params -> Ignored Actors to the volley's other projectiles, then call Launch Projectile on it with those params.

Spawning is server-only, so gate the whole graph on Has Authority. For damage, bind the projectile's own On Projectile Hit before launching it - it fires on the server for every impact and, unlike an ability task's pin, is not torn down when the ability ends.

Verify
Set Pattern to Fan and fire with Count 1, then 2, then 5, then 12. One shot flies dead straight; the rest widen into a fan until it reaches MaxArc and then pack in tighter. Every shot must still be in the air a second later - a volley where all but one vanish at the muzzle means the shots are not ignoring each other. Then set Play As Client with a dedicated server: every shot must appear and fly on the client, while impacts resolve only on the server.
Also using CrimsonCore? It ships the volley as one node
(CrimsonCore - requires all Crimson plugins) Crimson Launch Pooled Projectile Volley takes the shot array this builder returns, and Crimson Launch Pooled Projectile Pattern takes the aim transform, base params and pattern and runs the builder for you. Both acquire every shot from an actor pool, fill each one's Ignored Actors with its siblings and launch them in the same frame, reporting On Launched, On Projectile Hit, On Projectile Expired and On Volley Finished. CrimsonCore is free to download, but compiles only with all Crimson plugins installed - the steps above are the standalone path and need nothing but this plugin.

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 is pool-ready but implements no pooling interface, so CrimsonHitDetection never references a pooling implementation. Your pool integration drives it through four public hooks: Set Pool Owned (deactivate instead of destroy on expiry) and Acquire From Pool (reposition a dormant instance) to bring an instance back into play, Deactivate For Reuse to stand a still-live one down, and the native OnPoolReleaseRequested delegate, which a pool binds once per instance so the projectile can ask to be reclaimed when it finishes. A projectile that is not pool-owned simply destroys itself on expiry.

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
Set Pool Owned + Acquire From PoolThe poolYour pool integration: marks the instance pool-owned and repositions it with a teleport
Launch ProjectileYouThe single activation entry point. Resets every per-flight value, so a recycled projectile behaves exactly like a fresh one
Deactivate For ReuseThe poolServer only. Stands a still-live instance down; after expiry there is nothing left to do - the hit window is closed, movement stopped and the projectile hidden
OnPoolReleaseRequestedThe poolC++ only (native delegate). 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 pool-owned projectile takes that over entirely (your pool integration must skip its own hide/disable step for it); 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 initializes there is stale the second time a pooled projectile flies - move it into 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.
Pooling and the launch nodes
Crimson Launch Projectile calls SpawnActor directly - it never pools. To fire pooled projectiles with this plugin alone, acquire the instance from your own actor pool and call Launch Projectile on it yourself; any pool works once it drives the hooks listed in Object pooling above.

(CrimsonCore - requires all Crimson plugins) With CrimsonCore installed you get that for free: Crimson Launch Pooled Projectile is a drop-in replacement for Crimson Launch Projectile with the same pins and delegates, and Crimson Launch Pooled Projectile Volley and Crimson Launch Pooled Projectile Pattern do the same for a whole volley. A cold pool still does one real spawn per shot in the frame a volley fires, so give the projectile class a prewarm count and a capacity sized for Count times the number of volleys you expect in the air at once.

See also

  • Concept: Windows, Shapes and Filters
  • How-To: Hit Zones
  • API Reference