Weapon System Design


1 Basic Principles for Shooting Weapon


1.1 Weapon Shooting Flow


For the weapon system in game development, one of the most important thing is to make the shooting process closer to reality. The basic weapon shooting flow is given as following picture.


shootingflow


Key parts in the above shooting flow:

  • Weapon: The weapon entity is the basic element in a weapon system, so that the weapon creation is the first step.

  • Bullet: In order to handle the shooting process, we need to create bullet entity to simulate real movement and hit.

  • Damage (Hit Target): Damage event will be triggered by bullet entity while hitting the target.

  • CrossHair: Use to aim at the target and usually located at the center of screen in the game development.

  • Shooting Direction (Trajectory): In order to promise the shooting process closer to reality, you need to calculate the movement direction of bullet, which is determined by weapon recoil, physical environmental factor and other random factor.

  • Shooting Start Location: Use weapon muzzle location or camera location, which is depended on calculation strategy.


1.2 Trajectory Calculation (Bullet Direction)


In a word, the result of trajectory calculation is getting the final shooting direction for bullet.

In the actual shooting, the bullet should be hit the same position due to different physical reasons, especially for the continuous shooting. The following picture shows the continuous shooting process.


shootspread


In game development, we usually use some factors to calculate the shooting spread, such as move speed, recoil, heat or random factors. The result of shooting spread will impact following parameters in game.

  • Deviation of CrossHair

  • Yaw and Pitch of Character

  • Camera Shake


2 Comparision with Lyra


2.1 Lyra Trajectory


In the Lyra project, the key idea is to calculate the final bullet direction from a cone as following picture.

lyratrajectory


The code is given below.


...
const float BaseSpreadAngle = WeaponData->GetCalculatedSpreadAngle();
const float SpreadAngleMultiplier = WeaponData->GetCalculatedSpreadAngleMultiplier();
const float ActualSpreadAngle = BaseSpreadAngle * SpreadAngleMultiplier;

const float HalfSpreadAngleInRadians = FMath::DegreesToRadians(ActualSpreadAngle * 0.5f);

const FVector BulletDir = VRandConeNormalDistribution(InputData.AimDir, HalfSpreadAngleInRadians, WeaponData->GetSpreadExponent());

const FVector EndTrace = InputData.StartTrace + (BulletDir * WeaponData->GetMaxDamageRange());
FVector HitLocation = EndTrace;
...

The function GetCalculatedSpreadAngleMultiplier returns the multiplier related about the character state, such as crouch or move. Mainly, computed by following function.


bool ULyraRangedWeaponInstance::UpdateMultipliers(float DeltaSeconds)
{
	const float MultiplierNearlyEqualThreshold = 0.05f;

	APawn* Pawn = GetPawn();
	check(Pawn != nullptr);
	UCharacterMovementComponent* CharMovementComp = Cast<UCharacterMovementComponent>(Pawn->GetMovementComponent());

	// See if we are standing still, and if so, smoothly apply the bonus
	const float PawnSpeed = Pawn->GetVelocity().Size();
	const float MovementTargetValue = FMath::GetMappedRangeValueClamped(
		/*InputRange=*/ FVector2D(StandingStillSpeedThreshold, StandingStillSpeedThreshold + StandingStillToMovingSpeedRange),
		/*OutputRange=*/ FVector2D(SpreadAngleMultiplier_StandingStill, 1.0f),
		/*Alpha=*/ PawnSpeed);
	StandingStillMultiplier = FMath::FInterpTo(StandingStillMultiplier, MovementTargetValue, DeltaSeconds, TransitionRate_StandingStill);
	const bool bStandingStillMultiplierAtMin = FMath::IsNearlyEqual(StandingStillMultiplier, SpreadAngleMultiplier_StandingStill, SpreadAngleMultiplier_StandingStill*0.1f);

	// See if we are crouching, and if so, smoothly apply the bonus
	const bool bIsCrouching = (CharMovementComp != nullptr) && CharMovementComp->IsCrouching();
	const float CrouchingTargetValue = bIsCrouching ? SpreadAngleMultiplier_Crouching : 1.0f;
	CrouchingMultiplier = FMath::FInterpTo(CrouchingMultiplier, CrouchingTargetValue, DeltaSeconds, TransitionRate_Crouching);
	const bool bCrouchingMultiplierAtTarget = FMath::IsNearlyEqual(CrouchingMultiplier, CrouchingTargetValue, MultiplierNearlyEqualThreshold);

	// See if we are in the air (jumping/falling), and if so, smoothly apply the penalty
	const bool bIsJumpingOrFalling = (CharMovementComp != nullptr) && CharMovementComp->IsFalling();
	const float JumpFallTargetValue = bIsJumpingOrFalling ? SpreadAngleMultiplier_JumpingOrFalling : 1.0f;
	JumpFallMultiplier = FMath::FInterpTo(JumpFallMultiplier, JumpFallTargetValue, DeltaSeconds, TransitionRate_JumpingOrFalling);
	const bool bJumpFallMultiplerIs1 = FMath::IsNearlyEqual(JumpFallMultiplier, 1.0f, MultiplierNearlyEqualThreshold);

	// Determine if we are aiming down sights, and apply the bonus based on how far into the camera transition we are
	float AimingAlpha = 0.0f;
	if (const ULyraCameraComponent* CameraComponent = ULyraCameraComponent::FindCameraComponent(Pawn))
	{
		float TopCameraWeight;
		FGameplayTag TopCameraTag;
		CameraComponent->GetBlendInfo(/*out*/ TopCameraWeight, /*out*/ TopCameraTag);

		AimingAlpha = (TopCameraTag == TAG_Lyra_Weapon_SteadyAimingCamera) ? TopCameraWeight : 0.0f;
	}
	const float AimingMultiplier = FMath::GetMappedRangeValueClamped(
		/*InputRange=*/ FVector2D(0.0f, 1.0f),
		/*OutputRange=*/ FVector2D(1.0f, SpreadAngleMultiplier_Aiming),
		/*Alpha=*/ AimingAlpha);
	const bool bAimingMultiplierAtTarget = FMath::IsNearlyEqual(AimingMultiplier, SpreadAngleMultiplier_Aiming, KINDA_SMALL_NUMBER);

	// Combine all the multipliers
	const float CombinedMultiplier = AimingMultiplier * StandingStillMultiplier * CrouchingMultiplier * JumpFallMultiplier;
	CurrentSpreadAngleMultiplier = CombinedMultiplier;

	// need to handle these spread multipliers indicating we are not at min spread
	return bStandingStillMultiplierAtMin && bCrouchingMultiplierAtTarget && bJumpFallMultiplerIs1 && bAimingMultiplierAtTarget;
}

The code to compute final direction:


FVector VRandConeNormalDistribution(const FVector& Dir, const float ConeHalfAngleRad, const float Exponent)
{
	if (ConeHalfAngleRad > 0.f)
	{
		const float ConeHalfAngleDegrees = FMath::RadiansToDegrees(ConeHalfAngleRad);

		// consider the cone a concatenation of two rotations. one "away" from the center line, and another "around" the circle
		// apply the exponent to the away-from-center rotation. a larger exponent will cluster points more tightly around the center
		const float FromCenter = FMath::Pow(FMath::FRand(), Exponent);
		const float AngleFromCenter = FromCenter * ConeHalfAngleDegrees;
		const float AngleAround = FMath::FRand() * 360.0f;

		FRotator Rot = Dir.Rotation();
		FQuat DirQuat(Rot);
		FQuat FromCenterQuat(FRotator(0.0f, AngleFromCenter, 0.0f));
		FQuat AroundQuat(FRotator(0.0f, 0.0, AngleAround));
		FQuat FinalDirectionQuat = DirQuat * AroundQuat * FromCenterQuat;
		FinalDirectionQuat.Normalize();

		return FinalDirectionQuat.RotateVector(FVector::ForwardVector);
	}
	else
	{
		return Dir.GetSafeNormal();
	}
}

For the continuous shooting, Lyra mainly used three curves about heat to describe the trend.

lyracurve

Finally, the deviation (screen radius) of crosshair need to be computed as well.


float ULyraReticleWidgetBase::ComputeMaxScreenspaceSpreadRadius() const
{
	const float LongShotDistance = 10000.f;

	APlayerController* PC = GetOwningPlayer();
	if (PC && PC->PlayerCameraManager)
	{
		// A weapon's spread can be thought of as a cone shape. To find the screenspace spread for reticle visualization,
		// we create a line on the edge of the cone at a long distance. The end of that point is on the edge of the cone's circle.
		// We then project it back onto the screen. Its distance from screen center is the spread radius.

		// This isn't perfect, due to there being some distance between the camera location and the gun muzzle.

		const float SpreadRadiusRads = FMath::DegreesToRadians(ComputeSpreadAngle() * 0.5f);
		const float SpreadRadiusAtDistance = FMath::Tan(SpreadRadiusRads) * LongShotDistance;

		FVector CamPos;
		FRotator CamOrient;
		PC->PlayerCameraManager->GetCameraViewPoint(CamPos, CamOrient);

		FVector CamForwDir = CamOrient.RotateVector(FVector::ForwardVector);
		FVector CamUpDir   = CamOrient.RotateVector(FVector::UpVector);

		FVector OffsetTargetAtDistance = CamPos + (CamForwDir * LongShotDistance) + (CamUpDir * SpreadRadiusAtDistance);

		FVector2D OffsetTargetInScreenspace;

		if (PC->ProjectWorldLocationToScreen(OffsetTargetAtDistance, OffsetTargetInScreenspace, true))
		{
			int32 ViewportSizeX(0), ViewportSizeY(0);
			PC->GetViewportSize(ViewportSizeX, ViewportSizeY);

			const FVector2D ScreenSpaceCenter(FVector::FReal(ViewportSizeX) * 0.5f, FVector::FReal(ViewportSizeY) * 0.5f);

			return (OffsetTargetInScreenspace - ScreenSpaceCenter).Length();
		}
	}
	
	return 0.0f;
}

In the Lyra demo, there doesn’t seem to have the configuration about camera shake and character yaw/pitch. We will consider this from our project.


2.2 USGT Trajectory


In our project, we can use following factors to compute the shooting direction. Mainly use the weapon recoil to describe the spread. Larger recoil will cause larger shooting spread.

We also add the camera shake and character yaw/pitch properties, which is influenced by shooting.

usgttrajectory


  • Multiplier factor in different states(e.g. Crouch, ADS), which can be modified according to your requirement. Also, you can add more factor for more complex situations.

  • RecoilToSpreadCurve: Curve corresponding the relationship between recoil and spread.

  • RecoilToPitchCurve: Curve corresponding the relationship between recoil and pitch.

  • SpeedToMultipilerCurve: Curve corresponding the relationship between speed and multiplier.


3 Framework Design


The weapon system is one of the most important systems for a shooting game. In our project, we design a universal weapon system framework which can be migrated easily.

The structure of weapon system is given below.

systemstructure

The weapon system mainly includes the following parts:

  • WeaponManager(BP_WeaponComponent_USG):As a component of character, it is used to manage all weapons and weapon actions.

    • Weapon Slots: Determine how many weapons that can be equipped in the character and the weapon type for each slot.

    • Weapon Operaions: Equip weapon, unequip weapon, switch weapon, discard weapon, start attack, stop attack.

  • WeaponBase(BP_WeaponBase_USG):Basic class of weapon and used to define weapon basic functions and properties, including animation class, weapon id, damage type and so on. For different types of weapon, you can create child class of this basic weapon class.

  • ShootingWeapon(BP_ShootingWeapon_USG):This is the child class of BP_WeaponBase_USG and mainly including the features of shooting type weapon.

    • Bullet: Use bullet class to notify damage events and bullets should be recycled.

    • Trajectory: The weapon trajectory is one of the most important part for shooting game, which determine the bullet spread of different weapon with different recoil, and promise the shooting process closer to reality. In our project, we use different curves to compute this weapon trajectory.

    • CrossHair: The cross hair is relative of the bullet spread and different weapon has different cross hair type.


4 Core Data Structures


4.1 BP_WeaponComponent_USG


BP_WeaponComponent_USG is the weapon management component for different actors who designed to have the ability to use weapon.

The important design rules of weapon management is given blew.

  • The weapon slots is configurable, which will decide how many weapons the owner actor can equip.

  • When the slot was occupied by one weapon, the new equipped weapon will replace the last weapon in this slot if there are no avaiable empty slots.

  • There is only one in use weapon. We suppose the owner actor can only use one weapon in a moment. While switching next weapon, the last weapon will be holstered and the next weapon will be in use.

Use following property to config weapon slots.

  • WeaponSlots: One player can have several weapon slots which will decide how many weapons the player can equip.

weaponsetup


Use following functions to activate and deactivate weapon attack.

  • StartAttack: General function to start weapon attack.

  • StopAttack: General function to stop weapon attack.

These two function to notify the specific weapon to process attack, such as shooting weapon and melee weapon.

Use following functins to operate weapon.

  • CreateAndEquipWeapon: Create and equip the weapon by class and output weapon instance.

  • EquipWeapon: Equip the input weapon.

  • UnEquipWeapon: Unequip the input weapon.

  • UnEquipCurrentWeapon: Unequip(holster) current in use weapon.

  • SwitchWeapon: Switch to the input weapon.

  • SwitchWeaponBySlot: Switch to the weapon in target slot.

  • SwitchToNextWeapon: Switch to the weapon in next slot.

  • DiscardWeapon: Discard the input weapon.

  • DiscardWeaponBySlotID: Discard the weapon in target slot.

  • DiscardAllWeapons: Discard all weapons in owner actor.

  • DestroyAllWeapons: Destroy all weapons in owner actor.

Use following functions to get weapon informations.

  • GetCurrentWeapon: Return current in use weapon.

  • GetCurrentWeaponSlot: Return slot id of current in use weapon.

  • GetWeaponInSlot: Return the weapon from target slot.

  • GetWeaponSlotID: Return slot id of given weapon.

  • GetWeaponData: Return replicated weapon data.


The cross hair is another important part for shooting game. In our project, we use weapon component to handle the cross hair and the specific logic of cross hair is inside the following object blueprint.

  • /Game/USGT/Framework/Weapon/CrossHair/BP_CrossHairProxy_USG

The cross hair will be changed when current weapon changed, so that this proxy object should listen the weapon changed event and get cross hair style and screen radius for current weapon.

Creat child blueprint of following basic widget blueprint to customize your crosshair.

  • /Game/USGT/Framework/Weapon/CrossHair/WB_CrossHair_USG

4.2 BP_WeaponBase_USG


The basic weapon class is used to define common properties or abilities for weapon. In order to make weapon system uncoupled, we mainly add two interfaces blueprint for weapon owner and damage receiver.

  • IF_WeaponOwner_USG: Define interfaces about weapon owner actor.

    • OnWeaponEquipped

    • OnWeaponUnEquipped

    • IsFemaleWeaponOwner

  • IF_WeaponDamage_USG: Define interfaces for actor who wants to receive weapon damage.

    • TakeWeaponDamage

Notice that these interfaces are called from server. You can implement these interfaces and notify to clients by yourself. For example, implement the damage interface in the character blueprint and broadcast to clients.

chardamageif


The basic properties for common weapons.

  • MaleLinkAnimClass: Linked amin class for male owner (usea as default if the interface IsFemaleWeaponOwner is not implemented).

  • FemaleLinkAnimClass: Linked amin class for female owner.

  • BaseDamage: The basic damage value for this weapon.

  • DamageTypeClass: The damage type class for this weapon.

  • bIsPointDamage: If trigger point damage for this weapon.

  • EquipAttachInfo: The attachment info of owner actor for this weapon.

  • DefaultUnequipAttachInfo: Default attachment info when unequip this weapon if there is only one slot for this weapon type or you want to attach unequipped weapon in the same place.

  • UnequipAttachSlotInfo: If the owner actor has more than one slot for this weapon type, you should config attachment info for each one.

  • CrossHairConfig: Cross hair configuration for this weapon. We assume every weapon can have the cross hair, no just shooting weapon. For melee weapon, you may have the circle point cross hair.

The basic function triggered from weapon manager component. Please implement these two functions for specific type of weapons (e.g. BP_ShootingWeapon_USG).

  • StartAttack: function called while starting weapon attack.

  • StopAttack: function called while stopping weapon attack.

If you have some logic to do when equipping or unequipping weapon, you can implement following two functions. Notice that the basic equippment logic has been implemented in these two functions so that you need to calls the parent function first if you implment these two functions.

  • OnEquipWeapon: function called while equipping weapon.

  • OnUnEquipWeapon: function called while unequipping weapon.

To promise the performance, the weapon tick is triggered from weapon manager component. If you have logic to do in tick, please implment following function.

  • OnTick

For the weapon damage, please called the following function for specific type of weapons when damage triggered. For new weapon type, the damage way is different but the final damage event is simular so that you should call following function to promise the damage actor receive this event.

  • OnServerHit: Trigger damage and call the damage interface (TakeWeaponDamage in IF_WeaponDamage_USG).

  • CheckLegalDamage: Verify whether the damage is valid, if not, the damage event will not be triggered.

Use following functions to get weapon informations.

  • GetCrossHairOffset: Return crosshair offset for this weapon.

  • GetCrossHairMultiplier: Return crosshair size multiplier for this weapon.

  • GetWeaponID: Return weapon instance id.

  • IsEquipped: Return if the weapon equipped or not.


5 Development Flow


See “The Usage of Weapon System”.