Projectile and damage functionality
This is a generalized description of the chain of events in the code from the firing of a weapon to the allocation of damage, taken from the XCOM Nexus Modding forum thread Projectiles, Damage and how it works in XCOM. Understanding this sequence of events should aid modders attempting to make changes in any of these aspects of the game. Many thanks to Amineri for sharing her investigation.
There are five overall steps being performed:
- The firing action triggers the weapon to fire
- The weapon firing causes a projectile to be spawned
- The projectile travels along its designated path until the correct conditions are met, causing the projectile to explode (all projectiles explode)
- The projectile explodes
- Each unit that takes damage processes the effects
The call chain appears to dip into native/Unreal core code at least three times, so some of the steps in the sequence involved a little bit of guesswork and may not be 100% accurate.
(In case you are not aware, 'native code' are functions written by 2KGames in the game engine that have not so far been accessible to code divers. Such code is not visible nor alterable. Unreal code is part of the base game engine and requires the Unreal SDK to alter and then the necessary components to recompile the entire game engine, which are also not available.)
Programs and Tools
None are required to understand this general process description, but those that were used to develop the information may be found in the wiki article:
The details of each step are pretty long and ugly, so be warned! Here we breakdown the overall steps outlined above.
The firing action triggers the weapon to fire
It all begins in the XGAction_Firing.state'Firing'. There is a lot of code executed there, and hiding in there is a single small line:
Note that Disabling Shot does not get processed this way but instead has a separate earlier call:
The XGAction_Fire.state'Firing'.FireWeapon calls the function XGAction.DoFireUponUnitEvent which triggers a sequence event, diving into the core Unreal Event code. I'm pretty sure that eventually it ends up back at XComUnitPawnNativeBase.FireWeapon. There is no UPK code that invokes XComUnitPawnNativeBase.FireWeapon.
Weapon.CustomFire(); -- this uses the currently active weapon for the unit.
This in turn calls XComWeapon.CustomFire, which in turn calls WeaponComponent.CustomFire.
(The XCom prefix indicates this is a weapon pawn/mesh and not the game weapon which would be XGWeapon).
The weapon firing causes a projectile to be spawned
There are four different type of XComWeaponComponents : Grenade, Melee, Projectile, and Shotgun.
Grenades are special in that they follow a computed path (previewed when throwing the grenade).
Shotguns are special in that they don't generate the same type of projectile as most other weapons.
Most weapons however use the XComWeaponComponent_Projectile class. This includes most ballistic, laser, and plasma weapons. Rocket Launchers also fall in this class.
XComWeaponComponent_Projectile.CustomFire computes the start point and direction of the projectile (for most cases) using:
StartTrace = OwnerWeapon.Instigator.GetWeaponStartTraceLocation();
AimDir = vector(OwnerWeapon.GetAdjustedAim(StartTrace));
So far I've been unable to find this GetAdjustedAim function (which must be how the deviation for targeted shots that miss is computed).
It then calls :
SpawnProjectile(StartTrace, AimDir, bCanDoDamage, HACK_bMindMergeDeathProjectile);
This is handled via the parent's XComWeaponComponent.SpawnProjectile which has the code line:
OwnerWeapon.LastProjectile = PerformSpawnProjectile(OwnerWeapon, kFireAction, ClassToSpawn, NewPosition, NewDirection, bAnimNotify_FireWeaponCustom_DoDamage, HACK_bMindMergeDeathProjectile);
XComWeaponComponent.PerformSpawnProjectile does a variety of things (including InitFX) but the Projectile is actually created via the lines:
SpawnedProjectile = XComTacticalGRI(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kBattle.m_kLevel.m_kProjectileMgr.FindPooledProjectile(ClassToSpawn, OwnerWeapon, NewPosition);
SpawnedProjectile = OwnerWeapon.Spawn(ClassToSpawn, OwnerWeapon,, NewPosition,, OwnerWeapon.ProjectileTemplate, true);
InitProjectile(NewPosition, NewDirection, SpawnedProjectile, bAnimNotify_FireWeaponCustom_DoDamage);
SpawnedProjectile is a variable of type XComProjectile.
XComWeaponComponent.InitProjectile passes control to the XComProjectile class via the call:
SpawnedProjectile.InitProjectile(Direction, OwnerWeapon.bPreviewAim, bAnimNotify_FireWeaponCustom_DoDamage);
XComProjectile.InitProjectile does a few things. One of the more critical ones is call:
which sets the projectile damage and damage radius.
Projectiles can either be designated to strike a target pawn or a designated location. Targeted shots (as opposed to rocket launchers) that hit are designated to strike a target pawn. Scattered shots (Rocket Launchers) and missed shots are designated to strike a designated location.
XComProjectile' extends the core Unreal Projectile class. XComProjectile calls Projectile.Init, which invokes the core Unreal projectile code to begin animating the projectile.
The projectile travels along its designated path
The projectile travels along its path using the built-in Unreal Engine projectile handling. The Unreal Engine invokes two possible call-backs:
- XComProjectile.Tick -- a timer to check various conditions related to time
- XComProjectile.ProcessTouch -- invoked whenever the Projectile touches another actor (which can be an environmental object or a unit).
One of the primary functions of Tick is to destroy/shutdown the projectile if it has existed for too long. For example, if a miss is high and fails to hit any actors at all the projectile will timeout and be destroyed.
Tick also implements some of the Reflection code that enables Ethereals to reflect the projectile back at the originating unit.
The more interesting part is in ProcessTouch. This is invoked whenever the projectile "collides" with another actor.
ProcessTouch handles the following cases:
- if the projectile was reflected and the touched actor is the firing unit, then call Explode() to explode the projectile (damaging the firing unit).
- if the projectile was not reflected, the shot was a hit, and the touched actor is the target unit, then call Explode() to explode the projectile (damaging the targetted unit).
- The third case is a bit more complex, and is used to handle some incidental damage.
- If the touched actor is an environmental object, or if the shot is aimed at a target location AND the touched actor is an XComUnit, then one of two things can happen
- If the firing weapon is a Rocket Launcher and the projectile has moved at least 192 units (2 tiles), and the touched actor isn't fragile and the shot is not a hit the the rocket explodes immediately. (In practice I don't think this can happen).
- Otherwise if the touched actor is fragile and is at least 2 tiles from the source then damage is dealt to the touched actor but the projectile does not explode, and so continues moving.
- This last instance does happen in game. For example, it is how ballistic fire destroys windows in between the firing unit and the target unit (regardless of whether the shot was a hit or not).
- if the projectile is not designated with a target unit (and is aiming for a location instead). This breaks into two sub-cases:
- If the projectile is not the minimum required distance away from the firing unit then it deals damage to the actor but does not explode.
- If the projectile is at least the minimum required distance, then it explodes.
- This required minimum distance is the distance between the firing location and the target location.
All of the options above result in the projectile invoking XComProjectile.Explode.
The projectile explodes
XComProjectile.Explode handles most of the details of the projectile explosion. It calls:
DealSplashDamage(m_kFiredFromUnit, Damage, DamageRadius, MyDamageType, MomentumTransfer, HitLocation);
DealSplashDamage fills out a native Unreal Engine data structure ProjDamage:
ProjDamage.EventInstigator = m_kFiredFromUnit; ProjDamage.DamageCauser = self; ProjDamage.Target = ImpactedActor; ProjDamage.DamageAmount = int(DamageAmount); ProjDamage.Radius = InDamageRadius; ProjDamage.DamageType = DamageType; ProjDamage.Momentum = Velocity; ProjDamage.HitLocation = HurtOrigin; ProjDamage.bIsHit = bIsHit; ProjDamage.HitInfo = TracedImpactInfo; ProjDamage.bRadialDamage = InDamageRadius > float(0); ProjDamage.bCausesSurroundingAreaDamage = class'XComDamageType'.static.CausesSurroundingAreaDamage(DamageType);
It then calls:
bCausedDamage = class'XComDamageType'.static.DealDamage(ProjDamage);
passing control the the XComDamageType class in order to actually deal the damage.
After DealSplashDamage returns to Explode, Explode shuts down the projectile with the call:
This terminates any remaining visual effects and ensures that the projectile is properly cleaned up.
Each unit that takes damage processes the effects
XComDamageType.DealDamage is a native function. It is unclear how it functions but it likely passes information directly to the Unreal Engine to determine which actors are touched by the explosion (is often only the targeted unit, but may be more if the projectile has a damage radius).
Each of the three actor classes : XComUnitPawn, XComDestructibleActor, and XComFracLevelActor have a TakeSplashDamage function that is not invoked by the UPK code, so this function is the most likely callback point from the Unreal Engine/native code call initiated by XComDamageType.DealDamage. This allows Explode()-ed projectiles to deal damage to the environment as well as units.
XComUnitPawn.TakeSplashDamage invokes XComUnitPawn.TakeDirectDamage.
XComUnitPawn.TakeDirectDamage invokes XGUnit.OnTakeDamage.
OnTakeDamage is where the damage is applied. It applies some Combat Stims effects (so that they are effective against explosive damage) as well as HEAT bonus damage and Shredded bonus damage. It processes unit deaths and critical wounding as well.
Referred to by this article:
That refer to this article:
- None as yet.