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 traveled 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 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.
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.
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.
| Pattern | Layout | How it scales with Count | Fields it uses, besides Count |
|---|---|---|---|
| Fan | An arc centered on the aim direction - the classic multishot spread | Arc = 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 other | SpawnDistance, AnglePerProjectile, MaxArc |
| Parallel | A line perpendicular to the aim direction; every shot flies the aim direction | Width = SpacingPerProjectile x (Count - 1), capped at MaxWidth. Same widen-then-densify rule as Fan | SpawnDistance, SpacingPerProjectile, MaxWidth |
| Radial | A full circle around the caster, with shot 0 on the aim direction | Each 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 them | SpawnDistance, RadialStartAngle |
| Random Cone | A random scatter inside the cone around the aim direction; spacing is not guaranteed and two shots may overlap | Every 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 spray | SpawnDistance, ConeHalfAngle, bConstrainToAimPlane, RandomSeed |
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.
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.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.
| Member | Who calls it | Role |
|---|---|---|
Set Pool Owned + Acquire From Pool | The pool | Your pool integration: marks the instance pool-owned and repositions it with a teleport |
Launch Projectile | You | The single activation entry point. Resets every per-flight value, so a recycled projectile behaves exactly like a fresh one |
Deactivate For Reuse | The pool | Server 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 |
OnPoolReleaseRequested | The pool | C++ 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 |
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 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.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