一、框架视图
二、关键代码
PlayerInputHandler
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class PlayerInputHandler : MonoBehaviour
{
[Tooltip("Sensitivity multiplier for moving the camera around")]
public float LookSensitivity = 1f;
[Tooltip("Additional sensitivity multiplier for WebGL")]
public float WebglLookSensitivityMultiplier = 0.25f;
[Tooltip("Limit to consider an input when using a trigger on a controller")]
public float TriggerAxisThreshold = 0.4f;
[Tooltip("Used to flip the vertical input axis")]
public bool InvertYAxis = false;
[Tooltip("Used to flip the horizontal input axis")]
public bool InvertXAxis = false;
GameFlowManager m_GameFlowManager;
PlayerCharacterController m_PlayerCharacterController;
bool m_FireInputWasHeld;
void Start()
{
m_PlayerCharacterController = GetComponent<PlayerCharacterController>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerCharacterController, PlayerInputHandler>(
m_PlayerCharacterController, this, gameObject);
m_GameFlowManager = FindObjectOfType<GameFlowManager>();
DebugUtility.HandleErrorIfNullFindObject<GameFlowManager, PlayerInputHandler>(m_GameFlowManager, this);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
void LateUpdate()
{
m_FireInputWasHeld = GetFireInputHeld();
}
public bool CanProcessInput()
{
return Cursor.lockState == CursorLockMode.Locked && !m_GameFlowManager.GameIsEnding;
}
public Vector3 GetMoveInput()
{
if (CanProcessInput())
{
Vector3 move = new Vector3(Input.GetAxisRaw(GameConstants.k_AxisNameHorizontal), 0f,
Input.GetAxisRaw(GameConstants.k_AxisNameVertical));
// constrain move input to a maximum magnitude of 1, otherwise diagonal movement might exceed the max move speed defined
move = Vector3.ClampMagnitude(move, 1);
return move;
}
return Vector3.zero;
}
public float GetLookInputsHorizontal()
{
return GetMouseOrStickLookAxis(GameConstants.k_MouseAxisNameHorizontal,
GameConstants.k_AxisNameJoystickLookHorizontal);
}
public float GetLookInputsVertical()
{
return GetMouseOrStickLookAxis(GameConstants.k_MouseAxisNameVertical,
GameConstants.k_AxisNameJoystickLookVertical);
}
public bool GetJumpInputDown()
{
if (CanProcessInput())
{
return Input.GetButtonDown(GameConstants.k_ButtonNameJump);
}
return false;
}
public bool GetJumpInputHeld()
{
if (CanProcessInput())
{
return Input.GetButton(GameConstants.k_ButtonNameJump);
}
return false;
}
public bool GetFireInputDown()
{
return GetFireInputHeld() && !m_FireInputWasHeld;
}
public bool GetFireInputReleased()
{
return !GetFireInputHeld() && m_FireInputWasHeld;
}
public bool GetFireInputHeld()
{
if (CanProcessInput())
{
bool isGamepad = Input.GetAxis(GameConstants.k_ButtonNameGamepadFire) != 0f;
if (isGamepad)
{
return Input.GetAxis(GameConstants.k_ButtonNameGamepadFire) >= TriggerAxisThreshold;
}
else
{
return Input.GetButton(GameConstants.k_ButtonNameFire);
}
}
return false;
}
public bool GetAimInputHeld()
{
if (CanProcessInput())
{
bool isGamepad = Input.GetAxis(GameConstants.k_ButtonNameGamepadAim) != 0f;
bool i = isGamepad
? (Input.GetAxis(GameConstants.k_ButtonNameGamepadAim) > 0f)
: Input.GetButton(GameConstants.k_ButtonNameAim);
return i;
}
return false;
}
public bool GetSprintInputHeld()
{
if (CanProcessInput())
{
return Input.GetButton(GameConstants.k_ButtonNameSprint);
}
return false;
}
public bool GetCrouchInputDown()
{
if (CanProcessInput())
{
return Input.GetButtonDown(GameConstants.k_ButtonNameCrouch);
}
return false;
}
public bool GetCrouchInputReleased()
{
if (CanProcessInput())
{
return Input.GetButtonUp(GameConstants.k_ButtonNameCrouch);
}
return false;
}
public bool GetReloadButtonDown()
{
if (CanProcessInput())
{
return Input.GetButtonDown(GameConstants.k_ButtonReload);
}
return false;
}
public int GetSwitchWeaponInput()
{
if (CanProcessInput())
{
bool isGamepad = Input.GetAxis(GameConstants.k_ButtonNameGamepadSwitchWeapon) != 0f;
string axisName = isGamepad
? GameConstants.k_ButtonNameGamepadSwitchWeapon
: GameConstants.k_ButtonNameSwitchWeapon;
if (Input.GetAxis(axisName) > 0f)
return -1;
else if (Input.GetAxis(axisName) < 0f)
return 1;
else if (Input.GetAxis(GameConstants.k_ButtonNameNextWeapon) > 0f)
return -1;
else if (Input.GetAxis(GameConstants.k_ButtonNameNextWeapon) < 0f)
return 1;
}
return 0;
}
public int GetSelectWeaponInput()
{
if (CanProcessInput())
{
if (Input.GetKeyDown(KeyCode.Alpha1))
return 1;
else if (Input.GetKeyDown(KeyCode.Alpha2))
return 2;
else if (Input.GetKeyDown(KeyCode.Alpha3))
return 3;
else if (Input.GetKeyDown(KeyCode.Alpha4))
return 4;
else if (Input.GetKeyDown(KeyCode.Alpha5))
return 5;
else if (Input.GetKeyDown(KeyCode.Alpha6))
return 6;
else if (Input.GetKeyDown(KeyCode.Alpha7))
return 7;
else if (Input.GetKeyDown(KeyCode.Alpha8))
return 8;
else if (Input.GetKeyDown(KeyCode.Alpha9))
return 9;
else
return 0;
}
return 0;
}
float GetMouseOrStickLookAxis(string mouseInputName, string stickInputName)
{
if (CanProcessInput())
{
// Check if this look input is coming from the mouse
bool isGamepad = Input.GetAxis(stickInputName) != 0f;
float i = isGamepad ? Input.GetAxis(stickInputName) : Input.GetAxisRaw(mouseInputName);
// handle inverting vertical input
if (InvertYAxis)
i *= -1f;
// apply sensitivity multiplier
i *= LookSensitivity;
if (isGamepad)
{
// since mouse input is already deltaTime-dependant, only scale input with frame time if it's coming from sticks
i *= Time.deltaTime;
}
else
{
// reduce mouse input amount to be equivalent to stick movement
i *= 0.01f;
#if UNITY_WEBGL
// Mouse tends to be even more sensitive in WebGL due to mouse acceleration, so reduce it even more
i *= WebglLookSensitivityMultiplier;
#endif
}
return i;
}
return 0f;
}
}
}
PlayerWeaponsManager
using System.Collections.Generic;
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(PlayerInputHandler))]
public class PlayerWeaponsManager : MonoBehaviour
{
public enum WeaponSwitchState
{
Up,
Down,
PutDownPrevious,
PutUpNew,
}
[Tooltip("List of weapon the player will start with")]
public List<WeaponController> StartingWeapons = new List<WeaponController>();
[Header("References")] [Tooltip("Secondary camera used to avoid seeing weapon go throw geometries")]
public Camera WeaponCamera;
[Tooltip("Parent transform where all weapon will be added in the hierarchy")]
public Transform WeaponParentSocket;
[Tooltip("Position for weapons when active but not actively aiming")]
public Transform DefaultWeaponPosition;
[Tooltip("Position for weapons when aiming")]
public Transform AimingWeaponPosition;
[Tooltip("Position for innactive weapons")]
public Transform DownWeaponPosition;
[Header("Weapon Bob")]
[Tooltip("Frequency at which the weapon will move around in the screen when the player is in movement")]
public float BobFrequency = 10f;
[Tooltip("How fast the weapon bob is applied, the bigger value the fastest")]
public float BobSharpness = 10f;
[Tooltip("Distance the weapon bobs when not aiming")]
public float DefaultBobAmount = 0.05f;
[Tooltip("Distance the weapon bobs when aiming")]
public float AimingBobAmount = 0.02f;
[Header("Weapon Recoil")]
[Tooltip("This will affect how fast the recoil moves the weapon, the bigger the value, the fastest")]
public float RecoilSharpness = 50f;
[Tooltip("Maximum distance the recoil can affect the weapon")]
public float MaxRecoilDistance = 0.5f;
[Tooltip("How fast the weapon goes back to it's original position after the recoil is finished")]
public float RecoilRestitutionSharpness = 10f;
[Header("Misc")] [Tooltip("Speed at which the aiming animatoin is played")]
public float AimingAnimationSpeed = 10f;
[Tooltip("Field of view when not aiming")]
public float DefaultFov = 60f;
[Tooltip("Portion of the regular FOV to apply to the weapon camera")]
public float WeaponFovMultiplier = 1f;
[Tooltip("Delay before switching weapon a second time, to avoid recieving multiple inputs from mouse wheel")]
public float WeaponSwitchDelay = 1f;
[Tooltip("Layer to set FPS weapon gameObjects to")]
public LayerMask FpsWeaponLayer;
public bool IsAiming { get; private set; }
public bool IsPointingAtEnemy { get; private set; }
public int ActiveWeaponIndex { get; private set; }
public UnityAction<WeaponController> OnSwitchedToWeapon;
public UnityAction<WeaponController, int> OnAddedWeapon;
public UnityAction<WeaponController, int> OnRemovedWeapon;
WeaponController[] m_WeaponSlots = new WeaponController[9]; // 9 available weapon slots
PlayerInputHandler m_InputHandler;
PlayerCharacterController m_PlayerCharacterController;
float m_WeaponBobFactor;
Vector3 m_LastCharacterPosition;
Vector3 m_WeaponMainLocalPosition;
Vector3 m_WeaponBobLocalPosition;
Vector3 m_WeaponRecoilLocalPosition;
Vector3 m_AccumulatedRecoil;
float m_TimeStartedWeaponSwitch;
WeaponSwitchState m_WeaponSwitchState;
int m_WeaponSwitchNewWeaponIndex;
void Start()
{
ActiveWeaponIndex = -1;
m_WeaponSwitchState = WeaponSwitchState.Down;
m_InputHandler = GetComponent<PlayerInputHandler>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerInputHandler, PlayerWeaponsManager>(m_InputHandler, this,
gameObject);
m_PlayerCharacterController = GetComponent<PlayerCharacterController>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerCharacterController, PlayerWeaponsManager>(
m_PlayerCharacterController, this, gameObject);
SetFov(DefaultFov);
OnSwitchedToWeapon += OnWeaponSwitched;
// Add starting weapons
foreach (var weapon in StartingWeapons)
{
AddWeapon(weapon);
}
SwitchWeapon(true);
}
void Update()
{
// shoot handling
WeaponController activeWeapon = GetActiveWeapon();
if (activeWeapon != null && activeWeapon.IsReloading)
return;
if (activeWeapon != null && m_WeaponSwitchState == WeaponSwitchState.Up)
{
if (!activeWeapon.AutomaticReload && m_InputHandler.GetReloadButtonDown() && activeWeapon.CurrentAmmoRatio < 1.0f)
{
IsAiming = false;
activeWeapon.StartReloadAnimation();
return;
}
// handle aiming down sights
IsAiming = m_InputHandler.GetAimInputHeld();
// handle shooting
bool hasFired = activeWeapon.HandleShootInputs(
m_InputHandler.GetFireInputDown(),
m_InputHandler.GetFireInputHeld(),
m_InputHandler.GetFireInputReleased());
// Handle accumulating recoil
if (hasFired)
{
m_AccumulatedRecoil += Vector3.back * activeWeapon.RecoilForce;
m_AccumulatedRecoil = Vector3.ClampMagnitude(m_AccumulatedRecoil, MaxRecoilDistance);
}
}
// weapon switch handling
if (!IsAiming &&
(activeWeapon == null || !activeWeapon.IsCharging) &&
(m_WeaponSwitchState == WeaponSwitchState.Up || m_WeaponSwitchState == WeaponSwitchState.Down))
{
int switchWeaponInput = m_InputHandler.GetSwitchWeaponInput();
if (switchWeaponInput != 0)
{
bool switchUp = switchWeaponInput > 0;
SwitchWeapon(switchUp);
}
else
{
switchWeaponInput = m_InputHandler.GetSelectWeaponInput();
if (switchWeaponInput != 0)
{
if (GetWeaponAtSlotIndex(switchWeaponInput - 1) != null)
SwitchToWeaponIndex(switchWeaponInput - 1);
}
}
}
// Pointing at enemy handling
IsPointingAtEnemy = false;
if (activeWeapon)
{
if (Physics.Raycast(WeaponCamera.transform.position, WeaponCamera.transform.forward, out RaycastHit hit,
1000, -1, QueryTriggerInteraction.Ignore))
{
if (hit.collider.GetComponentInParent<Health>() != null)
{
IsPointingAtEnemy = true;
}
}
}
}
// Update various animated features in LateUpdate because it needs to override the animated arm position
void LateUpdate()
{
UpdateWeaponAiming();
UpdateWeaponBob();
UpdateWeaponRecoil();
UpdateWeaponSwitching();
// Set final weapon socket position based on all the combined animation influences
WeaponParentSocket.localPosition =
m_WeaponMainLocalPosition + m_WeaponBobLocalPosition + m_WeaponRecoilLocalPosition;
}
// Sets the FOV of the main camera and the weapon camera simultaneously
public void SetFov(float fov)
{
m_PlayerCharacterController.PlayerCamera.fieldOfView = fov;
WeaponCamera.fieldOfView = fov * WeaponFovMultiplier;
}
// Iterate on all weapon slots to find the next valid weapon to switch to
public void SwitchWeapon(bool ascendingOrder)
{
int newWeaponIndex = -1;
int closestSlotDistance = m_WeaponSlots.Length;
for (int i = 0; i < m_WeaponSlots.Length; i++)
{
// If the weapon at this slot is valid, calculate its "distance" from the active slot index (either in ascending or descending order)
// and select it if it's the closest distance yet
if (i != ActiveWeaponIndex && GetWeaponAtSlotIndex(i) != null)
{
int distanceToActiveIndex = GetDistanceBetweenWeaponSlots(ActiveWeaponIndex, i, ascendingOrder);
if (distanceToActiveIndex < closestSlotDistance)
{
closestSlotDistance = distanceToActiveIndex;
newWeaponIndex = i;
}
}
}
// Handle switching to the new weapon index
SwitchToWeaponIndex(newWeaponIndex);
}
// Switches to the given weapon index in weapon slots if the new index is a valid weapon that is different from our current one
public void SwitchToWeaponIndex(int newWeaponIndex, bool force = false)
{
if (force || (newWeaponIndex != ActiveWeaponIndex && newWeaponIndex >= 0))
{
// Store data related to weapon switching animation
m_WeaponSwitchNewWeaponIndex = newWeaponIndex;
m_TimeStartedWeaponSwitch = Time.time;
// Handle case of switching to a valid weapon for the first time (simply put it up without putting anything down first)
if (GetActiveWeapon() == null)
{
m_WeaponMainLocalPosition = DownWeaponPosition.localPosition;
m_WeaponSwitchState = WeaponSwitchState.PutUpNew;
ActiveWeaponIndex = m_WeaponSwitchNewWeaponIndex;
WeaponController newWeapon = GetWeaponAtSlotIndex(m_WeaponSwitchNewWeaponIndex);
if (OnSwitchedToWeapon != null)
{
OnSwitchedToWeapon.Invoke(newWeapon);
}
}
// otherwise, remember we are putting down our current weapon for switching to the next one
else
{
m_WeaponSwitchState = WeaponSwitchState.PutDownPrevious;
}
}
}
public WeaponController HasWeapon(WeaponController weaponPrefab)
{
// Checks if we already have a weapon coming from the specified prefab
for (var index = 0; index < m_WeaponSlots.Length; index++)
{
var w = m_WeaponSlots[index];
if (w != null && w.SourcePrefab == weaponPrefab.gameObject)
{
return w;
}
}
return null;
}
// Updates weapon position and camera FoV for the aiming transition
void UpdateWeaponAiming()
{
if (m_WeaponSwitchState == WeaponSwitchState.Up)
{
WeaponController activeWeapon = GetActiveWeapon();
if (IsAiming && activeWeapon)
{
m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition,
AimingWeaponPosition.localPosition + activeWeapon.AimOffset,
AimingAnimationSpeed * Time.deltaTime);
SetFov(Mathf.Lerp(m_PlayerCharacterController.PlayerCamera.fieldOfView,
activeWeapon.AimZoomRatio * DefaultFov, AimingAnimationSpeed * Time.deltaTime));
}
else
{
m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition,
DefaultWeaponPosition.localPosition, AimingAnimationSpeed * Time.deltaTime);
SetFov(Mathf.Lerp(m_PlayerCharacterController.PlayerCamera.fieldOfView, DefaultFov,
AimingAnimationSpeed * Time.deltaTime));
}
}
}
// Updates the weapon bob animation based on character speed
void UpdateWeaponBob()
{
if (Time.deltaTime > 0f)
{
Vector3 playerCharacterVelocity =
(m_PlayerCharacterController.transform.position - m_LastCharacterPosition) / Time.deltaTime;
// calculate a smoothed weapon bob amount based on how close to our max grounded movement velocity we are
float characterMovementFactor = 0f;
if (m_PlayerCharacterController.IsGrounded)
{
characterMovementFactor =
Mathf.Clamp01(playerCharacterVelocity.magnitude /
(m_PlayerCharacterController.MaxSpeedOnGround *
m_PlayerCharacterController.SprintSpeedModifier));
}
m_WeaponBobFactor =
Mathf.Lerp(m_WeaponBobFactor, characterMovementFactor, BobSharpness * Time.deltaTime);
// Calculate vertical and horizontal weapon bob values based on a sine function
float bobAmount = IsAiming ? AimingBobAmount : DefaultBobAmount;
float frequency = BobFrequency;
float hBobValue = Mathf.Sin(Time.time * frequency) * bobAmount * m_WeaponBobFactor;
float vBobValue = ((Mathf.Sin(Time.time * frequency * 2f) * 0.5f) + 0.5f) * bobAmount *
m_WeaponBobFactor;
// Apply weapon bob
m_WeaponBobLocalPosition.x = hBobValue;
m_WeaponBobLocalPosition.y = Mathf.Abs(vBobValue);
m_LastCharacterPosition = m_PlayerCharacterController.transform.position;
}
}
// Updates the weapon recoil animation
void UpdateWeaponRecoil()
{
// if the accumulated recoil is further away from the current position, make the current position move towards the recoil target
if (m_WeaponRecoilLocalPosition.z >= m_AccumulatedRecoil.z * 0.99f)
{
m_WeaponRecoilLocalPosition = Vector3.Lerp(m_WeaponRecoilLocalPosition, m_AccumulatedRecoil,
RecoilSharpness * Time.deltaTime);
}
// otherwise, move recoil position to make it recover towards its resting pose
else
{
m_WeaponRecoilLocalPosition = Vector3.Lerp(m_WeaponRecoilLocalPosition, Vector3.zero,
RecoilRestitutionSharpness * Time.deltaTime);
m_AccumulatedRecoil = m_WeaponRecoilLocalPosition;
}
}
// Updates the animated transition of switching weapons
void UpdateWeaponSwitching()
{
// Calculate the time ratio (0 to 1) since weapon switch was triggered
float switchingTimeFactor = 0f;
if (WeaponSwitchDelay == 0f)
{
switchingTimeFactor = 1f;
}
else
{
switchingTimeFactor = Mathf.Clamp01((Time.time - m_TimeStartedWeaponSwitch) / WeaponSwitchDelay);
}
// Handle transiting to new switch state
if (switchingTimeFactor >= 1f)
{
if (m_WeaponSwitchState == WeaponSwitchState.PutDownPrevious)
{
// Deactivate old weapon
WeaponController oldWeapon = GetWeaponAtSlotIndex(ActiveWeaponIndex);
if (oldWeapon != null)
{
oldWeapon.ShowWeapon(false);
}
ActiveWeaponIndex = m_WeaponSwitchNewWeaponIndex;
switchingTimeFactor = 0f;
// Activate new weapon
WeaponController newWeapon = GetWeaponAtSlotIndex(ActiveWeaponIndex);
if (OnSwitchedToWeapon != null)
{
OnSwitchedToWeapon.Invoke(newWeapon);
}
if (newWeapon)
{
m_TimeStartedWeaponSwitch = Time.time;
m_WeaponSwitchState = WeaponSwitchState.PutUpNew;
}
else
{
// if new weapon is null, don't follow through with putting weapon back up
m_WeaponSwitchState = WeaponSwitchState.Down;
}
}
else if (m_WeaponSwitchState == WeaponSwitchState.PutUpNew)
{
m_WeaponSwitchState = WeaponSwitchState.Up;
}
}
// Handle moving the weapon socket position for the animated weapon switching
if (m_WeaponSwitchState == WeaponSwitchState.PutDownPrevious)
{
m_WeaponMainLocalPosition = Vector3.Lerp(DefaultWeaponPosition.localPosition,
DownWeaponPosition.localPosition, switchingTimeFactor);
}
else if (m_WeaponSwitchState == WeaponSwitchState.PutUpNew)
{
m_WeaponMainLocalPosition = Vector3.Lerp(DownWeaponPosition.localPosition,
DefaultWeaponPosition.localPosition, switchingTimeFactor);
}
}
// Adds a weapon to our inventory
public bool AddWeapon(WeaponController weaponPrefab)
{
// if we already hold this weapon type (a weapon coming from the same source prefab), don't add the weapon
if (HasWeapon(weaponPrefab) != null)
{
return false;
}
// search our weapon slots for the first free one, assign the weapon to it, and return true if we found one. Return false otherwise
for (int i = 0; i < m_WeaponSlots.Length; i++)
{
// only add the weapon if the slot is free
if (m_WeaponSlots[i] == null)
{
// spawn the weapon prefab as child of the weapon socket
WeaponController weaponInstance = Instantiate(weaponPrefab, WeaponParentSocket);
weaponInstance.transform.localPosition = Vector3.zero;
weaponInstance.transform.localRotation = Quaternion.identity;
// Set owner to this gameObject so the weapon can alter projectile/damage logic accordingly
weaponInstance.Owner = gameObject;
weaponInstance.SourcePrefab = weaponPrefab.gameObject;
weaponInstance.ShowWeapon(false);
// Assign the first person layer to the weapon
int layerIndex =
Mathf.RoundToInt(Mathf.Log(FpsWeaponLayer.value,
2)); // This function converts a layermask to a layer index
foreach (Transform t in weaponInstance.gameObject.GetComponentsInChildren<Transform>(true))
{
t.gameObject.layer = layerIndex;
}
m_WeaponSlots[i] = weaponInstance;
if (OnAddedWeapon != null)
{
OnAddedWeapon.Invoke(weaponInstance, i);
}
return true;
}
}
// Handle auto-switching to weapon if no weapons currently
if (GetActiveWeapon() == null)
{
SwitchWeapon(true);
}
return false;
}
public bool RemoveWeapon(WeaponController weaponInstance)
{
// Look through our slots for that weapon
for (int i = 0; i < m_WeaponSlots.Length; i++)
{
// when weapon found, remove it
if (m_WeaponSlots[i] == weaponInstance)
{
m_WeaponSlots[i] = null;
if (OnRemovedWeapon != null)
{
OnRemovedWeapon.Invoke(weaponInstance, i);
}
Destroy(weaponInstance.gameObject);
// Handle case of removing active weapon (switch to next weapon)
if (i == ActiveWeaponIndex)
{
SwitchWeapon(true);
}
return true;
}
}
return false;
}
public WeaponController GetActiveWeapon()
{
return GetWeaponAtSlotIndex(ActiveWeaponIndex);
}
public WeaponController GetWeaponAtSlotIndex(int index)
{
// find the active weapon in our weapon slots based on our active weapon index
if (index >= 0 &&
index < m_WeaponSlots.Length)
{
return m_WeaponSlots[index];
}
// if we didn't find a valid active weapon in our weapon slots, return null
return null;
}
// Calculates the "distance" between two weapon slot indexes
// For example: if we had 5 weapon slots, the distance between slots #2 and #4 would be 2 in ascending order, and 3 in descending order
int GetDistanceBetweenWeaponSlots(int fromSlotIndex, int toSlotIndex, bool ascendingOrder)
{
int distanceBetweenSlots = 0;
if (ascendingOrder)
{
distanceBetweenSlots = toSlotIndex - fromSlotIndex;
}
else
{
distanceBetweenSlots = -1 * (toSlotIndex - fromSlotIndex);
}
if (distanceBetweenSlots < 0)
{
distanceBetweenSlots = m_WeaponSlots.Length + distanceBetweenSlots;
}
return distanceBetweenSlots;
}
void OnWeaponSwitched(WeaponController newWeapon)
{
if (newWeapon != null)
{
newWeapon.ShowWeapon(true);
}
}
}
}
ObjectiveKillEnemies
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class ObjectiveKillEnemies : Objective
{
[Tooltip("Chose whether you need to kill every enemies or only a minimum amount")]
public bool MustKillAllEnemies = true;
[Tooltip("If MustKillAllEnemies is false, this is the amount of enemy kills required")]
public int KillsToCompleteObjective = 5;
[Tooltip("Start sending notification about remaining enemies when this amount of enemies is left")]
public int NotificationEnemiesRemainingThreshold = 3;
int m_KillTotal;
protected override void Start()
{
base.Start();
EventManager.AddListener<EnemyKillEvent>(OnEnemyKilled);
// set a title and description specific for this type of objective, if it hasn't one
if (string.IsNullOrEmpty(Title))
Title = "Eliminate " + (MustKillAllEnemies ? "all the" : KillsToCompleteObjective.ToString()) +
" enemies";
if (string.IsNullOrEmpty(Description))
Description = GetUpdatedCounterAmount();
}
void OnEnemyKilled(EnemyKillEvent evt)
{
if (IsCompleted)
return;
m_KillTotal++;
if (MustKillAllEnemies)
KillsToCompleteObjective = evt.RemainingEnemyCount + m_KillTotal;
int targetRemaining = MustKillAllEnemies ? evt.RemainingEnemyCount : KillsToCompleteObjective - m_KillTotal;
// update the objective text according to how many enemies remain to kill
if (targetRemaining == 0)
{
CompleteObjective(string.Empty, GetUpdatedCounterAmount(), "Objective complete : " + Title);
}
else if (targetRemaining == 1)
{
string notificationText = NotificationEnemiesRemainingThreshold >= targetRemaining
? "One enemy left"
: string.Empty;
UpdateObjective(string.Empty, GetUpdatedCounterAmount(), notificationText);
}
else
{
// create a notification text if needed, if it stays empty, the notification will not be created
string notificationText = NotificationEnemiesRemainingThreshold >= targetRemaining
? targetRemaining + " enemies to kill left"
: string.Empty;
UpdateObjective(string.Empty, GetUpdatedCounterAmount(), notificationText);
}
}
string GetUpdatedCounterAmount()
{
return m_KillTotal + " / " + KillsToCompleteObjective;
}
void OnDestroy()
{
EventManager.RemoveListener<EnemyKillEvent>(OnEnemyKilled);
}
}
}
ObjectivePickupItem
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class ObjectivePickupItem : Objective
{
[Tooltip("Item to pickup to complete the objective")]
public GameObject ItemToPickup;
protected override void Start()
{
base.Start();
EventManager.AddListener<PickupEvent>(OnPickupEvent);
}
void OnPickupEvent(PickupEvent evt)
{
if (IsCompleted || ItemToPickup != evt.Pickup)
return;
// this will trigger the objective completion
// it works even if the player can't pickup the item (i.e. objective pickup healthpack while at full heath)
CompleteObjective(string.Empty, string.Empty, "Objective complete : " + Title);
if (gameObject)
{
Destroy(gameObject);
}
}
void OnDestroy()
{
EventManager.RemoveListener<PickupEvent>(OnPickupEvent);
}
}
}
ObjectiveReachPoint
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(Collider))]
public class ObjectiveReachPoint : Objective
{
[Tooltip("Visible transform that will be destroyed once the objective is completed")]
public Transform DestroyRoot;
void Awake()
{
if (DestroyRoot == null)
DestroyRoot = transform;
}
void OnTriggerEnter(Collider other)
{
if (IsCompleted)
return;
var player = other.GetComponent<PlayerCharacterController>();
// test if the other collider contains a PlayerCharacterController, then complete
if (player != null)
{
CompleteObjective(string.Empty, string.Empty, "Objective complete : " + Title);
// destroy the transform, will remove the compass marker if it has one
Destroy(DestroyRoot.gameObject);
}
}
}
}
AmmoPickup
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class AmmoPickup : Pickup
{
[Tooltip("Weapon those bullets are for")]
public WeaponController Weapon;
[Tooltip("Number of bullets the player gets")]
public int BulletCount = 30;
protected override void OnPicked(PlayerCharacterController byPlayer)
{
PlayerWeaponsManager playerWeaponsManager = byPlayer.GetComponent<PlayerWeaponsManager>();
if (playerWeaponsManager)
{
WeaponController weapon = playerWeaponsManager.HasWeapon(Weapon);
if (weapon != null)
{
weapon.AddCarriablePhysicalBullets(BulletCount);
AmmoPickupEvent evt = Events.AmmoPickupEvent;
evt.Weapon = weapon;
EventManager.Broadcast(evt);
PlayPickupFeedback();
Destroy(gameObject);
}
}
}
}
}
ChargedProjectileEffectsHandler
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class ChargedProjectileEffectsHandler : MonoBehaviour
{
[Tooltip("Object that will be affected by charging scale & color changes")]
public GameObject ChargingObject;
[Tooltip("Scale of the charged object based on charge")]
public MinMaxVector3 Scale;
[Tooltip("Color of the charged object based on charge")]
public MinMaxColor Color;
MeshRenderer[] m_AffectedRenderers;
ProjectileBase m_ProjectileBase;
void OnEnable()
{
m_ProjectileBase = GetComponent<ProjectileBase>();
DebugUtility.HandleErrorIfNullGetComponent<ProjectileBase, ChargedProjectileEffectsHandler>(
m_ProjectileBase, this, gameObject);
m_ProjectileBase.OnShoot += OnShoot;
m_AffectedRenderers = ChargingObject.GetComponentsInChildren<MeshRenderer>();
foreach (var ren in m_AffectedRenderers)
{
ren.sharedMaterial = Instantiate(ren.sharedMaterial);
}
}
void OnShoot()
{
ChargingObject.transform.localScale = Scale.GetValueFromRatio(m_ProjectileBase.InitialCharge);
foreach (var ren in m_AffectedRenderers)
{
ren.sharedMaterial.SetColor("_Color", Color.GetValueFromRatio(m_ProjectileBase.InitialCharge));
}
}
}
}
ChargedWeaponEffectsHandler
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(AudioSource))]
public class ChargedWeaponEffectsHandler : MonoBehaviour
{
[Header("Visual")] [Tooltip("Object that will be affected by charging scale & color changes")]
public GameObject ChargingObject;
[Tooltip("The spinning frame")] public GameObject SpinningFrame;
[Tooltip("Scale of the charged object based on charge")]
public MinMaxVector3 Scale;
[Header("Particles")] [Tooltip("Particles to create when charging")]
public GameObject DiskOrbitParticlePrefab;
[Tooltip("Local position offset of the charge particles (relative to this transform)")]
public Vector3 Offset;
[Tooltip("Parent transform for the particles (Optional)")]
public Transform ParentTransform;
[Tooltip("Orbital velocity of the charge particles based on charge")]
public MinMaxFloat OrbitY;
[Tooltip("Radius of the charge particles based on charge")]
public MinMaxVector3 Radius;
[Tooltip("Idle spinning speed of the frame based on charge")]
public MinMaxFloat SpinningSpeed;
[Header("Sound")] [Tooltip("Audio clip for charge SFX")]
public AudioClip ChargeSound;
[Tooltip("Sound played in loop after the change is full for this weapon")]
public AudioClip LoopChargeWeaponSfx;
[Tooltip("Duration of the cross fade between the charge and the loop sound")]
public float FadeLoopDuration = 0.5f;
[Tooltip(
"If true, the ChargeSound will be ignored and the pitch on the LoopSound will be procedural, based on the charge amount")]
public bool UseProceduralPitchOnLoopSfx;
[Range(1.0f, 5.0f), Tooltip("Maximum procedural Pitch value")]
public float MaxProceduralPitchValue = 2.0f;
public GameObject ParticleInstance { get; set; }
ParticleSystem m_DiskOrbitParticle;
WeaponController m_WeaponController;
ParticleSystem.VelocityOverLifetimeModule m_VelocityOverTimeModule;
AudioSource m_AudioSource;
AudioSource m_AudioSourceLoop;
float m_LastChargeTriggerTimestamp;
float m_ChargeRatio;
float m_EndchargeTime;
void Awake()
{
m_LastChargeTriggerTimestamp = 0.0f;
// The charge effect needs it's own AudioSources, since it will play on top of the other gun sounds
m_AudioSource = gameObject.AddComponent<AudioSource>();
m_AudioSource.clip = ChargeSound;
m_AudioSource.playOnAwake = false;
m_AudioSource.outputAudioMixerGroup =
AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponChargeBuildup);
// create a second audio source, to play the sound with a delay
m_AudioSourceLoop = gameObject.AddComponent<AudioSource>();
m_AudioSourceLoop.clip = LoopChargeWeaponSfx;
m_AudioSourceLoop.playOnAwake = false;
m_AudioSourceLoop.loop = true;
m_AudioSourceLoop.outputAudioMixerGroup =
AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponChargeLoop);
}
void SpawnParticleSystem()
{
ParticleInstance = Instantiate(DiskOrbitParticlePrefab,
ParentTransform != null ? ParentTransform : transform);
ParticleInstance.transform.localPosition += Offset;
FindReferences();
}
public void FindReferences()
{
m_DiskOrbitParticle = ParticleInstance.GetComponent<ParticleSystem>();
DebugUtility.HandleErrorIfNullGetComponent<ParticleSystem, ChargedWeaponEffectsHandler>(m_DiskOrbitParticle,
this, ParticleInstance.gameObject);
m_WeaponController = GetComponent<WeaponController>();
DebugUtility.HandleErrorIfNullGetComponent<WeaponController, ChargedWeaponEffectsHandler>(
m_WeaponController, this, gameObject);
m_VelocityOverTimeModule = m_DiskOrbitParticle.velocityOverLifetime;
}
void Update()
{
if (ParticleInstance == null)
SpawnParticleSystem();
m_DiskOrbitParticle.gameObject.SetActive(m_WeaponController.IsWeaponActive);
m_ChargeRatio = m_WeaponController.CurrentCharge;
ChargingObject.transform.localScale = Scale.GetValueFromRatio(m_ChargeRatio);
if (SpinningFrame != null)
{
SpinningFrame.transform.localRotation *= Quaternion.Euler(0,
SpinningSpeed.GetValueFromRatio(m_ChargeRatio) * Time.deltaTime, 0);
}
m_VelocityOverTimeModule.orbitalY = OrbitY.GetValueFromRatio(m_ChargeRatio);
m_DiskOrbitParticle.transform.localScale = Radius.GetValueFromRatio(m_ChargeRatio * 1.1f);
// update sound's volume and pitch
if (m_ChargeRatio > 0)
{
if (!m_AudioSourceLoop.isPlaying &&
m_WeaponController.LastChargeTriggerTimestamp > m_LastChargeTriggerTimestamp)
{
m_LastChargeTriggerTimestamp = m_WeaponController.LastChargeTriggerTimestamp;
if (!UseProceduralPitchOnLoopSfx)
{
m_EndchargeTime = Time.time + ChargeSound.length;
m_AudioSource.Play();
}
m_AudioSourceLoop.Play();
}
if (!UseProceduralPitchOnLoopSfx)
{
float volumeRatio =
Mathf.Clamp01((m_EndchargeTime - Time.time - FadeLoopDuration) / FadeLoopDuration);
m_AudioSource.volume = volumeRatio;
m_AudioSourceLoop.volume = 1 - volumeRatio;
}
else
{
m_AudioSourceLoop.pitch = Mathf.Lerp(1.0f, MaxProceduralPitchValue, m_ChargeRatio);
}
}
else
{
m_AudioSource.Stop();
m_AudioSourceLoop.Stop();
}
}
}
}
HealthPickup
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class HealthPickup : Pickup
{
[Header("Parameters")] [Tooltip("Amount of health to heal on pickup")]
public float HealAmount;
protected override void OnPicked(PlayerCharacterController player)
{
Health playerHealth = player.GetComponent<Health>();
if (playerHealth && playerHealth.CanPickup())
{
playerHealth.Heal(HealAmount);
PlayPickupFeedback();
Destroy(gameObject);
}
}
}
}
Jetpack
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(AudioSource))]
public class Jetpack : MonoBehaviour
{
[Header("References")] [Tooltip("Audio source for jetpack sfx")]
public AudioSource AudioSource;
[Tooltip("Particles for jetpack vfx")] public ParticleSystem[] JetpackVfx;
[Header("Parameters")] [Tooltip("Whether the jetpack is unlocked at the begining or not")]
public bool IsJetpackUnlockedAtStart = false;
[Tooltip("The strength with which the jetpack pushes the player up")]
public float JetpackAcceleration = 7f;
[Range(0f, 1f)]
[Tooltip(
"This will affect how much using the jetpack will cancel the gravity value, to start going up faster. 0 is not at all, 1 is instant")]
public float JetpackDownwardVelocityCancelingFactor = 1f;
[Header("Durations")] [Tooltip("Time it takes to consume all the jetpack fuel")]
public float ConsumeDuration = 1.5f;
[Tooltip("Time it takes to completely refill the jetpack while on the ground")]
public float RefillDurationGrounded = 2f;
[Tooltip("Time it takes to completely refill the jetpack while in the air")]
public float RefillDurationInTheAir = 5f;
[Tooltip("Delay after last use before starting to refill")]
public float RefillDelay = 1f;
[Header("Audio")] [Tooltip("Sound played when using the jetpack")]
public AudioClip JetpackSfx;
bool m_CanUseJetpack;
PlayerCharacterController m_PlayerCharacterController;
PlayerInputHandler m_InputHandler;
float m_LastTimeOfUse;
// stored ratio for jetpack resource (1 is full, 0 is empty)
public float CurrentFillRatio { get; private set; }
public bool IsJetpackUnlocked { get; private set; }
public bool IsPlayergrounded() => m_PlayerCharacterController.IsGrounded;
public UnityAction<bool> OnUnlockJetpack;
void Start()
{
IsJetpackUnlocked = IsJetpackUnlockedAtStart;
m_PlayerCharacterController = GetComponent<PlayerCharacterController>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerCharacterController, Jetpack>(m_PlayerCharacterController,
this, gameObject);
m_InputHandler = GetComponent<PlayerInputHandler>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerInputHandler, Jetpack>(m_InputHandler, this, gameObject);
CurrentFillRatio = 1f;
AudioSource.clip = JetpackSfx;
AudioSource.loop = true;
}
void Update()
{
// jetpack can only be used if not grounded and jump has been pressed again once in-air
if (IsPlayergrounded())
{
m_CanUseJetpack = false;
}
else if (!m_PlayerCharacterController.HasJumpedThisFrame && m_InputHandler.GetJumpInputDown())
{
m_CanUseJetpack = true;
}
// jetpack usage
bool jetpackIsInUse = m_CanUseJetpack && IsJetpackUnlocked && CurrentFillRatio > 0f &&
m_InputHandler.GetJumpInputHeld();
if (jetpackIsInUse)
{
// store the last time of use for refill delay
m_LastTimeOfUse = Time.time;
float totalAcceleration = JetpackAcceleration;
// cancel out gravity
totalAcceleration += m_PlayerCharacterController.GravityDownForce;
if (m_PlayerCharacterController.CharacterVelocity.y < 0f)
{
// handle making the jetpack compensate for character's downward velocity with bonus acceleration
totalAcceleration += ((-m_PlayerCharacterController.CharacterVelocity.y / Time.deltaTime) *
JetpackDownwardVelocityCancelingFactor);
}
// apply the acceleration to character's velocity
m_PlayerCharacterController.CharacterVelocity += Vector3.up * totalAcceleration * Time.deltaTime;
// consume fuel
CurrentFillRatio = CurrentFillRatio - (Time.deltaTime / ConsumeDuration);
for (int i = 0; i < JetpackVfx.Length; i++)
{
var emissionModulesVfx = JetpackVfx[i].emission;
emissionModulesVfx.enabled = true;
}
if (!AudioSource.isPlaying)
AudioSource.Play();
}
else
{
// refill the meter over time
if (IsJetpackUnlocked && Time.time - m_LastTimeOfUse >= RefillDelay)
{
float refillRate = 1 / (m_PlayerCharacterController.IsGrounded
? RefillDurationGrounded
: RefillDurationInTheAir);
CurrentFillRatio = CurrentFillRatio + Time.deltaTime * refillRate;
}
for (int i = 0; i < JetpackVfx.Length; i++)
{
var emissionModulesVfx = JetpackVfx[i].emission;
emissionModulesVfx.enabled = false;
}
// keeps the ratio between 0 and 1
CurrentFillRatio = Mathf.Clamp01(CurrentFillRatio);
if (AudioSource.isPlaying)
AudioSource.Stop();
}
}
public bool TryUnlock()
{
if (IsJetpackUnlocked)
return false;
OnUnlockJetpack.Invoke(true);
IsJetpackUnlocked = true;
m_LastTimeOfUse = Time.time;
return true;
}
}
}
JetpackPickup
namespace Unity.FPS.Gameplay
{
public class JetpackPickup : Pickup
{
protected override void OnPicked(PlayerCharacterController byPlayer)
{
var jetpack = byPlayer.GetComponent<Jetpack>();
if (!jetpack)
return;
if (jetpack.TryUnlock())
{
PlayPickupFeedback();
Destroy(gameObject);
}
}
}
}
OverheatBehavior
using UnityEngine;
using System.Collections.Generic;
using Unity.FPS.Game;
namespace Unity.FPS.Gameplay
{
public class OverheatBehavior : MonoBehaviour
{
[System.Serializable]
public struct RendererIndexData
{
public Renderer Renderer;
public int MaterialIndex;
public RendererIndexData(Renderer renderer, int index)
{
this.Renderer = renderer;
this.MaterialIndex = index;
}
}
[Header("Visual")] [Tooltip("The VFX to scale the spawn rate based on the ammo ratio")]
public ParticleSystem SteamVfx;
[Tooltip("The emission rate for the effect when fully overheated")]
public float SteamVfxEmissionRateMax = 8f;
//Set gradient field to HDR
[GradientUsage(true)] [Tooltip("Overheat color based on ammo ratio")]
public Gradient OverheatGradient;
[Tooltip("The material for overheating color animation")]
public Material OverheatingMaterial;
[Header("Sound")] [Tooltip("Sound played when a cell are cooling")]
public AudioClip CoolingCellsSound;
[Tooltip("Curve for ammo to volume ratio")]
public AnimationCurve AmmoToVolumeRatioCurve;
WeaponController m_Weapon;
AudioSource m_AudioSource;
List<RendererIndexData> m_OverheatingRenderersData;
MaterialPropertyBlock m_OverheatMaterialPropertyBlock;
float m_LastAmmoRatio;
ParticleSystem.EmissionModule m_SteamVfxEmissionModule;
void Awake()
{
var emissionModule = SteamVfx.emission;
emissionModule.rateOverTimeMultiplier = 0f;
m_OverheatingRenderersData = new List<RendererIndexData>();
foreach (var renderer in GetComponentsInChildren<Renderer>(true))
{
for (int i = 0; i < renderer.sharedMaterials.Length; i++)
{
if (renderer.sharedMaterials[i] == OverheatingMaterial)
m_OverheatingRenderersData.Add(new RendererIndexData(renderer, i));
}
}
m_OverheatMaterialPropertyBlock = new MaterialPropertyBlock();
m_SteamVfxEmissionModule = SteamVfx.emission;
m_Weapon = GetComponent<WeaponController>();
DebugUtility.HandleErrorIfNullGetComponent<WeaponController, OverheatBehavior>(m_Weapon, this, gameObject);
m_AudioSource = gameObject.AddComponent<AudioSource>();
m_AudioSource.clip = CoolingCellsSound;
m_AudioSource.outputAudioMixerGroup = AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponOverheat);
}
void Update()
{
// visual smoke shooting out of the gun
float currentAmmoRatio = m_Weapon.CurrentAmmoRatio;
if (currentAmmoRatio != m_LastAmmoRatio)
{
m_OverheatMaterialPropertyBlock.SetColor("_EmissionColor",
OverheatGradient.Evaluate(1f - currentAmmoRatio));
foreach (var data in m_OverheatingRenderersData)
{
data.Renderer.SetPropertyBlock(m_OverheatMaterialPropertyBlock, data.MaterialIndex);
}
m_SteamVfxEmissionModule.rateOverTimeMultiplier = SteamVfxEmissionRateMax * (1f - currentAmmoRatio);
}
// cooling sound
if (CoolingCellsSound)
{
if (!m_AudioSource.isPlaying
&& currentAmmoRatio != 1
&& m_Weapon.IsWeaponActive
&& m_Weapon.IsCooling)
{
m_AudioSource.Play();
}
else if (m_AudioSource.isPlaying
&& (currentAmmoRatio == 1 || !m_Weapon.IsWeaponActive || !m_Weapon.IsCooling))
{
m_AudioSource.Stop();
return;
}
m_AudioSource.volume = AmmoToVolumeRatioCurve.Evaluate(1 - currentAmmoRatio);
}
m_LastAmmoRatio = currentAmmoRatio;
}
}
}
Pickup
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(Rigidbody), typeof(Collider))]
public class Pickup : MonoBehaviour
{
[Tooltip("Frequency at which the item will move up and down")]
public float VerticalBobFrequency = 1f;
[Tooltip("Distance the item will move up and down")]
public float BobbingAmount = 1f;
[Tooltip("Rotation angle per second")] public float RotatingSpeed = 360f;
[Tooltip("Sound played on pickup")] public AudioClip PickupSfx;
[Tooltip("VFX spawned on pickup")] public GameObject PickupVfxPrefab;
public Rigidbody PickupRigidbody { get; private set; }
Collider m_Collider;
Vector3 m_StartPosition;
bool m_HasPlayedFeedback;
protected virtual void Start()
{
PickupRigidbody = GetComponent<Rigidbody>();
DebugUtility.HandleErrorIfNullGetComponent<Rigidbody, Pickup>(PickupRigidbody, this, gameObject);
m_Collider = GetComponent<Collider>();
DebugUtility.HandleErrorIfNullGetComponent<Collider, Pickup>(m_Collider, this, gameObject);
// ensure the physics setup is a kinematic rigidbody trigger
PickupRigidbody.isKinematic = true;
m_Collider.isTrigger = true;
// Remember start position for animation
m_StartPosition = transform.position;
}
void Update()
{
// Handle bobbing
float bobbingAnimationPhase = ((Mathf.Sin(Time.time * VerticalBobFrequency) * 0.5f) + 0.5f) * BobbingAmount;
transform.position = m_StartPosition + Vector3.up * bobbingAnimationPhase;
// Handle rotating
transform.Rotate(Vector3.up, RotatingSpeed * Time.deltaTime, Space.Self);
}
void OnTriggerEnter(Collider other)
{
PlayerCharacterController pickingPlayer = other.GetComponent<PlayerCharacterController>();
if (pickingPlayer != null)
{
OnPicked(pickingPlayer);
PickupEvent evt = Events.PickupEvent;
evt.Pickup = gameObject;
EventManager.Broadcast(evt);
}
}
protected virtual void OnPicked(PlayerCharacterController playerController)
{
PlayPickupFeedback();
}
public void PlayPickupFeedback()
{
if (m_HasPlayedFeedback)
return;
if (PickupSfx)
{
AudioUtility.CreateSFX(PickupSfx, transform.position, AudioUtility.AudioGroups.Pickup, 0f);
}
if (PickupVfxPrefab)
{
var pickupVfxInstance = Instantiate(PickupVfxPrefab, transform.position, Quaternion.identity);
}
m_HasPlayedFeedback = true;
}
}
}
PlayerCharacterController
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(CharacterController), typeof(PlayerInputHandler), typeof(AudioSource))]
public class PlayerCharacterController : MonoBehaviour
{
[Header("References")] [Tooltip("Reference to the main camera used for the player")]
public Camera PlayerCamera;
[Tooltip("Audio source for footsteps, jump, etc...")]
public AudioSource AudioSource;
[Header("General")] [Tooltip("Force applied downward when in the air")]
public float GravityDownForce = 20f;
[Tooltip("Physic layers checked to consider the player grounded")]
public LayerMask GroundCheckLayers = -1;
[Tooltip("distance from the bottom of the character controller capsule to test for grounded")]
public float GroundCheckDistance = 0.05f;
[Header("Movement")] [Tooltip("Max movement speed when grounded (when not sprinting)")]
public float MaxSpeedOnGround = 10f;
[Tooltip(
"Sharpness for the movement when grounded, a low value will make the player accelerate and decelerate slowly, a high value will do the opposite")]
public float MovementSharpnessOnGround = 15;
[Tooltip("Max movement speed when crouching")] [Range(0, 1)]
public float MaxSpeedCrouchedRatio = 0.5f;
[Tooltip("Max movement speed when not grounded")]
public float MaxSpeedInAir = 10f;
[Tooltip("Acceleration speed when in the air")]
public float AccelerationSpeedInAir = 25f;
[Tooltip("Multiplicator for the sprint speed (based on grounded speed)")]
public float SprintSpeedModifier = 2f;
[Tooltip("Height at which the player dies instantly when falling off the map")]
public float KillHeight = -50f;
[Header("Rotation")] [Tooltip("Rotation speed for moving the camera")]
public float RotationSpeed = 200f;
[Range(0.1f, 1f)] [Tooltip("Rotation speed multiplier when aiming")]
public float AimingRotationMultiplier = 0.4f;
[Header("Jump")] [Tooltip("Force applied upward when jumping")]
public float JumpForce = 9f;
[Header("Stance")] [Tooltip("Ratio (0-1) of the character height where the camera will be at")]
public float CameraHeightRatio = 0.9f;
[Tooltip("Height of character when standing")]
public float CapsuleHeightStanding = 1.8f;
[Tooltip("Height of character when crouching")]
public float CapsuleHeightCrouching = 0.9f;
[Tooltip("Speed of crouching transitions")]
public float CrouchingSharpness = 10f;
[Header("Audio")] [Tooltip("Amount of footstep sounds played when moving one meter")]
public float FootstepSfxFrequency = 1f;
[Tooltip("Amount of footstep sounds played when moving one meter while sprinting")]
public float FootstepSfxFrequencyWhileSprinting = 1f;
[Tooltip("Sound played for footsteps")]
public AudioClip FootstepSfx;
[Tooltip("Sound played when jumping")] public AudioClip JumpSfx;
[Tooltip("Sound played when landing")] public AudioClip LandSfx;
[Tooltip("Sound played when taking damage froma fall")]
public AudioClip FallDamageSfx;
[Header("Fall Damage")]
[Tooltip("Whether the player will recieve damage when hitting the ground at high speed")]
public bool RecievesFallDamage;
[Tooltip("Minimun fall speed for recieving fall damage")]
public float MinSpeedForFallDamage = 10f;
[Tooltip("Fall speed for recieving th emaximum amount of fall damage")]
public float MaxSpeedForFallDamage = 30f;
[Tooltip("Damage recieved when falling at the mimimum speed")]
public float FallDamageAtMinSpeed = 10f;
[Tooltip("Damage recieved when falling at the maximum speed")]
public float FallDamageAtMaxSpeed = 50f;
public UnityAction<bool> OnStanceChanged;
public Vector3 CharacterVelocity { get; set; }
public bool IsGrounded { get; private set; }
public bool HasJumpedThisFrame { get; private set; }
public bool IsDead { get; private set; }
public bool IsCrouching { get; private set; }
public float RotationMultiplier
{
get
{
if (m_WeaponsManager.IsAiming)
{
return AimingRotationMultiplier;
}
return 1f;
}
}
Health m_Health;
PlayerInputHandler m_InputHandler;
CharacterController m_Controller;
PlayerWeaponsManager m_WeaponsManager;
Actor m_Actor;
Vector3 m_GroundNormal;
Vector3 m_CharacterVelocity;
Vector3 m_LatestImpactSpeed;
float m_LastTimeJumped = 0f;
float m_CameraVerticalAngle = 0f;
float m_FootstepDistanceCounter;
float m_TargetCharacterHeight;
const float k_JumpGroundingPreventionTime = 0.2f;
const float k_GroundCheckDistanceInAir = 0.07f;
void Awake()
{
ActorsManager actorsManager = FindObjectOfType<ActorsManager>();
if (actorsManager != null)
actorsManager.SetPlayer(gameObject);
}
void Start()
{
// fetch components on the same gameObject
m_Controller = GetComponent<CharacterController>();
DebugUtility.HandleErrorIfNullGetComponent<CharacterController, PlayerCharacterController>(m_Controller,
this, gameObject);
m_InputHandler = GetComponent<PlayerInputHandler>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerInputHandler, PlayerCharacterController>(m_InputHandler,
this, gameObject);
m_WeaponsManager = GetComponent<PlayerWeaponsManager>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerWeaponsManager, PlayerCharacterController>(
m_WeaponsManager, this, gameObject);
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, PlayerCharacterController>(m_Health, this, gameObject);
m_Actor = GetComponent<Actor>();
DebugUtility.HandleErrorIfNullGetComponent<Actor, PlayerCharacterController>(m_Actor, this, gameObject);
m_Controller.enableOverlapRecovery = true;
m_Health.OnDie += OnDie;
// force the crouch state to false when starting
SetCrouchingState(false, true);
UpdateCharacterHeight(true);
}
void Update()
{
// check for Y kill
if (!IsDead && transform.position.y < KillHeight)
{
m_Health.Kill();
}
HasJumpedThisFrame = false;
bool wasGrounded = IsGrounded;
GroundCheck();
// landing
if (IsGrounded && !wasGrounded)
{
// Fall damage
float fallSpeed = -Mathf.Min(CharacterVelocity.y, m_LatestImpactSpeed.y);
float fallSpeedRatio = (fallSpeed - MinSpeedForFallDamage) /
(MaxSpeedForFallDamage - MinSpeedForFallDamage);
if (RecievesFallDamage && fallSpeedRatio > 0f)
{
float dmgFromFall = Mathf.Lerp(FallDamageAtMinSpeed, FallDamageAtMaxSpeed, fallSpeedRatio);
m_Health.TakeDamage(dmgFromFall, null);
// fall damage SFX
AudioSource.PlayOneShot(FallDamageSfx);
}
else
{
// land SFX
AudioSource.PlayOneShot(LandSfx);
}
}
// crouching
if (m_InputHandler.GetCrouchInputDown())
{
SetCrouchingState(!IsCrouching, false);
}
UpdateCharacterHeight(false);
HandleCharacterMovement();
}
void OnDie()
{
IsDead = true;
// Tell the weapons manager to switch to a non-existing weapon in order to lower the weapon
m_WeaponsManager.SwitchToWeaponIndex(-1, true);
EventManager.Broadcast(Events.PlayerDeathEvent);
}
void GroundCheck()
{
// Make sure that the ground check distance while already in air is very small, to prevent suddenly snapping to ground
float chosenGroundCheckDistance =
IsGrounded ? (m_Controller.skinWidth + GroundCheckDistance) : k_GroundCheckDistanceInAir;
// reset values before the ground check
IsGrounded = false;
m_GroundNormal = Vector3.up;
// only try to detect ground if it's been a short amount of time since last jump; otherwise we may snap to the ground instantly after we try jumping
if (Time.time >= m_LastTimeJumped + k_JumpGroundingPreventionTime)
{
// if we're grounded, collect info about the ground normal with a downward capsule cast representing our character capsule
if (Physics.CapsuleCast(GetCapsuleBottomHemisphere(), GetCapsuleTopHemisphere(m_Controller.height),
m_Controller.radius, Vector3.down, out RaycastHit hit, chosenGroundCheckDistance, GroundCheckLayers,
QueryTriggerInteraction.Ignore))
{
// storing the upward direction for the surface found
m_GroundNormal = hit.normal;
// Only consider this a valid ground hit if the ground normal goes in the same direction as the character up
// and if the slope angle is lower than the character controller's limit
if (Vector3.Dot(hit.normal, transform.up) > 0f &&
IsNormalUnderSlopeLimit(m_GroundNormal))
{
IsGrounded = true;
// handle snapping to the ground
if (hit.distance > m_Controller.skinWidth)
{
m_Controller.Move(Vector3.down * hit.distance);
}
}
}
}
}
void HandleCharacterMovement()
{
// horizontal character rotation
{
// rotate the transform with the input speed around its local Y axis
transform.Rotate(
new Vector3(0f, (m_InputHandler.GetLookInputsHorizontal() * RotationSpeed * RotationMultiplier),
0f), Space.Self);
}
// vertical camera rotation
{
// add vertical inputs to the camera's vertical angle
m_CameraVerticalAngle += m_InputHandler.GetLookInputsVertical() * RotationSpeed * RotationMultiplier;
// limit the camera's vertical angle to min/max
m_CameraVerticalAngle = Mathf.Clamp(m_CameraVerticalAngle, -89f, 89f);
// apply the vertical angle as a local rotation to the camera transform along its right axis (makes it pivot up and down)
PlayerCamera.transform.localEulerAngles = new Vector3(m_CameraVerticalAngle, 0, 0);
}
// character movement handling
bool isSprinting = m_InputHandler.GetSprintInputHeld();
{
if (isSprinting)
{
isSprinting = SetCrouchingState(false, false);
}
float speedModifier = isSprinting ? SprintSpeedModifier : 1f;
// converts move input to a worldspace vector based on our character's transform orientation
Vector3 worldspaceMoveInput = transform.TransformVector(m_InputHandler.GetMoveInput());
// handle grounded movement
if (IsGrounded)
{
// calculate the desired velocity from inputs, max speed, and current slope
Vector3 targetVelocity = worldspaceMoveInput * MaxSpeedOnGround * speedModifier;
// reduce speed if crouching by crouch speed ratio
if (IsCrouching)
targetVelocity *= MaxSpeedCrouchedRatio;
targetVelocity = GetDirectionReorientedOnSlope(targetVelocity.normalized, m_GroundNormal) *
targetVelocity.magnitude;
// smoothly interpolate between our current velocity and the target velocity based on acceleration speed
CharacterVelocity = Vector3.Lerp(CharacterVelocity, targetVelocity,
MovementSharpnessOnGround * Time.deltaTime);
// jumping
if (IsGrounded && m_InputHandler.GetJumpInputDown())
{
// force the crouch state to false
if (SetCrouchingState(false, false))
{
// start by canceling out the vertical component of our velocity
CharacterVelocity = new Vector3(CharacterVelocity.x, 0f, CharacterVelocity.z);
// then, add the jumpSpeed value upwards
CharacterVelocity += Vector3.up * JumpForce;
// play sound
AudioSource.PlayOneShot(JumpSfx);
// remember last time we jumped because we need to prevent snapping to ground for a short time
m_LastTimeJumped = Time.time;
HasJumpedThisFrame = true;
// Force grounding to false
IsGrounded = false;
m_GroundNormal = Vector3.up;
}
}
// footsteps sound
float chosenFootstepSfxFrequency =
(isSprinting ? FootstepSfxFrequencyWhileSprinting : FootstepSfxFrequency);
if (m_FootstepDistanceCounter >= 1f / chosenFootstepSfxFrequency)
{
m_FootstepDistanceCounter = 0f;
AudioSource.PlayOneShot(FootstepSfx);
}
// keep track of distance traveled for footsteps sound
m_FootstepDistanceCounter += CharacterVelocity.magnitude * Time.deltaTime;
}
// handle air movement
else
{
// add air acceleration
CharacterVelocity += worldspaceMoveInput * AccelerationSpeedInAir * Time.deltaTime;
// limit air speed to a maximum, but only horizontally
float verticalVelocity = CharacterVelocity.y;
Vector3 horizontalVelocity = Vector3.ProjectOnPlane(CharacterVelocity, Vector3.up);
horizontalVelocity = Vector3.ClampMagnitude(horizontalVelocity, MaxSpeedInAir * speedModifier);
CharacterVelocity = horizontalVelocity + (Vector3.up * verticalVelocity);
// apply the gravity to the velocity
CharacterVelocity += Vector3.down * GravityDownForce * Time.deltaTime;
}
}
// apply the final calculated velocity value as a character movement
Vector3 capsuleBottomBeforeMove = GetCapsuleBottomHemisphere();
Vector3 capsuleTopBeforeMove = GetCapsuleTopHemisphere(m_Controller.height);
m_Controller.Move(CharacterVelocity * Time.deltaTime);
// detect obstructions to adjust velocity accordingly
m_LatestImpactSpeed = Vector3.zero;
if (Physics.CapsuleCast(capsuleBottomBeforeMove, capsuleTopBeforeMove, m_Controller.radius,
CharacterVelocity.normalized, out RaycastHit hit, CharacterVelocity.magnitude * Time.deltaTime, -1,
QueryTriggerInteraction.Ignore))
{
// We remember the last impact speed because the fall damage logic might need it
m_LatestImpactSpeed = CharacterVelocity;
CharacterVelocity = Vector3.ProjectOnPlane(CharacterVelocity, hit.normal);
}
}
// Returns true if the slope angle represented by the given normal is under the slope angle limit of the character controller
bool IsNormalUnderSlopeLimit(Vector3 normal)
{
return Vector3.Angle(transform.up, normal) <= m_Controller.slopeLimit;
}
// Gets the center point of the bottom hemisphere of the character controller capsule
Vector3 GetCapsuleBottomHemisphere()
{
return transform.position + (transform.up * m_Controller.radius);
}
// Gets the center point of the top hemisphere of the character controller capsule
Vector3 GetCapsuleTopHemisphere(float atHeight)
{
return transform.position + (transform.up * (atHeight - m_Controller.radius));
}
// Gets a reoriented direction that is tangent to a given slope
public Vector3 GetDirectionReorientedOnSlope(Vector3 direction, Vector3 slopeNormal)
{
Vector3 directionRight = Vector3.Cross(direction, transform.up);
return Vector3.Cross(slopeNormal, directionRight).normalized;
}
void UpdateCharacterHeight(bool force)
{
// Update height instantly
if (force)
{
m_Controller.height = m_TargetCharacterHeight;
m_Controller.center = Vector3.up * m_Controller.height * 0.5f;
PlayerCamera.transform.localPosition = Vector3.up * m_TargetCharacterHeight * CameraHeightRatio;
m_Actor.AimPoint.transform.localPosition = m_Controller.center;
}
// Update smooth height
else if (m_Controller.height != m_TargetCharacterHeight)
{
// resize the capsule and adjust camera position
m_Controller.height = Mathf.Lerp(m_Controller.height, m_TargetCharacterHeight,
CrouchingSharpness * Time.deltaTime);
m_Controller.center = Vector3.up * m_Controller.height * 0.5f;
PlayerCamera.transform.localPosition = Vector3.Lerp(PlayerCamera.transform.localPosition,
Vector3.up * m_TargetCharacterHeight * CameraHeightRatio, CrouchingSharpness * Time.deltaTime);
m_Actor.AimPoint.transform.localPosition = m_Controller.center;
}
}
// returns false if there was an obstruction
bool SetCrouchingState(bool crouched, bool ignoreObstructions)
{
// set appropriate heights
if (crouched)
{
m_TargetCharacterHeight = CapsuleHeightCrouching;
}
else
{
// Detect obstructions
if (!ignoreObstructions)
{
Collider[] standingOverlaps = Physics.OverlapCapsule(
GetCapsuleBottomHemisphere(),
GetCapsuleTopHemisphere(CapsuleHeightStanding),
m_Controller.radius,
-1,
QueryTriggerInteraction.Ignore);
foreach (Collider c in standingOverlaps)
{
if (c != m_Controller)
{
return false;
}
}
}
m_TargetCharacterHeight = CapsuleHeightStanding;
}
if (OnStanceChanged != null)
{
OnStanceChanged.Invoke(crouched);
}
IsCrouching = crouched;
return true;
}
}
}
PositionBobbing
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class PositionBobbing : MonoBehaviour
{
[Tooltip("Frequency at which the item will move up and down")]
public float VerticalBobFrequency = 1f;
[Tooltip("Distance the item will move up and down")]
public float BobbingAmount = 0.5f;
Vector3 m_StartPosition;
void Start()
{
// Remember start position for animation
m_StartPosition = transform.position;
}
void Update()
{
// Handle bobbing
float bobbingAnimationPhase = ((Mathf.Sin(Time.time * VerticalBobFrequency) * 0.5f) + 0.5f) * BobbingAmount;
transform.position = m_StartPosition + Vector3.up * bobbingAnimationPhase;
}
}
}
ProjectileChargeParameters
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class ProjectileChargeParameters : MonoBehaviour
{
public MinMaxFloat Damage;
public MinMaxFloat Radius;
public MinMaxFloat Speed;
public MinMaxFloat GravityDownAcceleration;
public MinMaxFloat AreaOfEffectDistance;
ProjectileBase m_ProjectileBase;
void OnEnable()
{
m_ProjectileBase = GetComponent<ProjectileBase>();
DebugUtility.HandleErrorIfNullGetComponent<ProjectileBase, ProjectileChargeParameters>(m_ProjectileBase,
this, gameObject);
m_ProjectileBase.OnShoot += OnShoot;
}
void OnShoot()
{
// Apply the parameters based on projectile charge
ProjectileStandard proj = GetComponent<ProjectileStandard>();
if (proj)
{
proj.Damage = Damage.GetValueFromRatio(m_ProjectileBase.InitialCharge);
proj.Radius = Radius.GetValueFromRatio(m_ProjectileBase.InitialCharge);
proj.Speed = Speed.GetValueFromRatio(m_ProjectileBase.InitialCharge);
proj.GravityDownAcceleration =
GravityDownAcceleration.GetValueFromRatio(m_ProjectileBase.InitialCharge);
}
}
}
}
ProjectileStandard
using System.Collections.Generic;
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class ProjectileStandard : ProjectileBase
{
[Header("General")] [Tooltip("Radius of this projectile's collision detection")]
public float Radius = 0.01f;
[Tooltip("Transform representing the root of the projectile (used for accurate collision detection)")]
public Transform Root;
[Tooltip("Transform representing the tip of the projectile (used for accurate collision detection)")]
public Transform Tip;
[Tooltip("LifeTime of the projectile")]
public float MaxLifeTime = 5f;
[Tooltip("VFX prefab to spawn upon impact")]
public GameObject ImpactVfx;
[Tooltip("LifeTime of the VFX before being destroyed")]
public float ImpactVfxLifetime = 5f;
[Tooltip("Offset along the hit normal where the VFX will be spawned")]
public float ImpactVfxSpawnOffset = 0.1f;
[Tooltip("Clip to play on impact")]
public AudioClip ImpactSfxClip;
[Tooltip("Layers this projectile can collide with")]
public LayerMask HittableLayers = -1;
[Header("Movement")] [Tooltip("Speed of the projectile")]
public float Speed = 20f;
[Tooltip("Downward acceleration from gravity")]
public float GravityDownAcceleration = 0f;
[Tooltip(
"Distance over which the projectile will correct its course to fit the intended trajectory (used to drift projectiles towards center of screen in First Person view). At values under 0, there is no correction")]
public float TrajectoryCorrectionDistance = -1;
[Tooltip("Determines if the projectile inherits the velocity that the weapon's muzzle had when firing")]
public bool InheritWeaponVelocity = false;
[Header("Damage")] [Tooltip("Damage of the projectile")]
public float Damage = 40f;
[Tooltip("Area of damage. Keep empty if you don<t want area damage")]
public DamageArea AreaOfDamage;
[Header("Debug")] [Tooltip("Color of the projectile radius debug view")]
public Color RadiusColor = Color.cyan * 0.2f;
ProjectileBase m_ProjectileBase;
Vector3 m_LastRootPosition;
Vector3 m_Velocity;
bool m_HasTrajectoryOverride;
float m_ShootTime;
Vector3 m_TrajectoryCorrectionVector;
Vector3 m_ConsumedTrajectoryCorrectionVector;
List<Collider> m_IgnoredColliders;
const QueryTriggerInteraction k_TriggerInteraction = QueryTriggerInteraction.Collide;
void OnEnable()
{
m_ProjectileBase = GetComponent<ProjectileBase>();
DebugUtility.HandleErrorIfNullGetComponent<ProjectileBase, ProjectileStandard>(m_ProjectileBase, this,
gameObject);
m_ProjectileBase.OnShoot += OnShoot;
Destroy(gameObject, MaxLifeTime);
}
new void OnShoot()
{
m_ShootTime = Time.time;
m_LastRootPosition = Root.position;
m_Velocity = transform.forward * Speed;
m_IgnoredColliders = new List<Collider>();
transform.position += m_ProjectileBase.InheritedMuzzleVelocity * Time.deltaTime;
// Ignore colliders of owner
Collider[] ownerColliders = m_ProjectileBase.Owner.GetComponentsInChildren<Collider>();
m_IgnoredColliders.AddRange(ownerColliders);
// Handle case of player shooting (make projectiles not go through walls, and remember center-of-screen trajectory)
PlayerWeaponsManager playerWeaponsManager = m_ProjectileBase.Owner.GetComponent<PlayerWeaponsManager>();
if (playerWeaponsManager)
{
m_HasTrajectoryOverride = true;
Vector3 cameraToMuzzle = (m_ProjectileBase.InitialPosition -
playerWeaponsManager.WeaponCamera.transform.position);
m_TrajectoryCorrectionVector = Vector3.ProjectOnPlane(-cameraToMuzzle,
playerWeaponsManager.WeaponCamera.transform.forward);
if (TrajectoryCorrectionDistance == 0)
{
transform.position += m_TrajectoryCorrectionVector;
m_ConsumedTrajectoryCorrectionVector = m_TrajectoryCorrectionVector;
}
else if (TrajectoryCorrectionDistance < 0)
{
m_HasTrajectoryOverride = false;
}
if (Physics.Raycast(playerWeaponsManager.WeaponCamera.transform.position, cameraToMuzzle.normalized,
out RaycastHit hit, cameraToMuzzle.magnitude, HittableLayers, k_TriggerInteraction))
{
if (IsHitValid(hit))
{
OnHit(hit.point, hit.normal, hit.collider);
}
}
}
}
void Update()
{
// Move
transform.position += m_Velocity * Time.deltaTime;
if (InheritWeaponVelocity)
{
transform.position += m_ProjectileBase.InheritedMuzzleVelocity * Time.deltaTime;
}
// Drift towards trajectory override (this is so that projectiles can be centered
// with the camera center even though the actual weapon is offset)
if (m_HasTrajectoryOverride && m_ConsumedTrajectoryCorrectionVector.sqrMagnitude <
m_TrajectoryCorrectionVector.sqrMagnitude)
{
Vector3 correctionLeft = m_TrajectoryCorrectionVector - m_ConsumedTrajectoryCorrectionVector;
float distanceThisFrame = (Root.position - m_LastRootPosition).magnitude;
Vector3 correctionThisFrame =
(distanceThisFrame / TrajectoryCorrectionDistance) * m_TrajectoryCorrectionVector;
correctionThisFrame = Vector3.ClampMagnitude(correctionThisFrame, correctionLeft.magnitude);
m_ConsumedTrajectoryCorrectionVector += correctionThisFrame;
// Detect end of correction
if (m_ConsumedTrajectoryCorrectionVector.sqrMagnitude == m_TrajectoryCorrectionVector.sqrMagnitude)
{
m_HasTrajectoryOverride = false;
}
transform.position += correctionThisFrame;
}
// Orient towards velocity
transform.forward = m_Velocity.normalized;
// Gravity
if (GravityDownAcceleration > 0)
{
// add gravity to the projectile velocity for ballistic effect
m_Velocity += Vector3.down * GravityDownAcceleration * Time.deltaTime;
}
// Hit detection
{
RaycastHit closestHit = new RaycastHit();
closestHit.distance = Mathf.Infinity;
bool foundHit = false;
// Sphere cast
Vector3 displacementSinceLastFrame = Tip.position - m_LastRootPosition;
RaycastHit[] hits = Physics.SphereCastAll(m_LastRootPosition, Radius,
displacementSinceLastFrame.normalized, displacementSinceLastFrame.magnitude, HittableLayers,
k_TriggerInteraction);
foreach (var hit in hits)
{
if (IsHitValid(hit) && hit.distance < closestHit.distance)
{
foundHit = true;
closestHit = hit;
}
}
if (foundHit)
{
// Handle case of casting while already inside a collider
if (closestHit.distance <= 0f)
{
closestHit.point = Root.position;
closestHit.normal = -transform.forward;
}
OnHit(closestHit.point, closestHit.normal, closestHit.collider);
}
}
m_LastRootPosition = Root.position;
}
bool IsHitValid(RaycastHit hit)
{
// ignore hits with an ignore component
if (hit.collider.GetComponent<IgnoreHitDetection>())
{
return false;
}
// ignore hits with triggers that don't have a Damageable component
if (hit.collider.isTrigger && hit.collider.GetComponent<Damageable>() == null)
{
return false;
}
// ignore hits with specific ignored colliders (self colliders, by default)
if (m_IgnoredColliders != null && m_IgnoredColliders.Contains(hit.collider))
{
return false;
}
return true;
}
void OnHit(Vector3 point, Vector3 normal, Collider collider)
{
// damage
if (AreaOfDamage)
{
// area damage
AreaOfDamage.InflictDamageInArea(Damage, point, HittableLayers, k_TriggerInteraction,
m_ProjectileBase.Owner);
}
else
{
// point damage
Damageable damageable = collider.GetComponent<Damageable>();
if (damageable)
{
damageable.InflictDamage(Damage, false, m_ProjectileBase.Owner);
}
}
// impact vfx
if (ImpactVfx)
{
GameObject impactVfxInstance = Instantiate(ImpactVfx, point + (normal * ImpactVfxSpawnOffset),
Quaternion.LookRotation(normal));
if (ImpactVfxLifetime > 0)
{
Destroy(impactVfxInstance.gameObject, ImpactVfxLifetime);
}
}
// impact sfx
if (ImpactSfxClip)
{
AudioUtility.CreateSFX(ImpactSfxClip, point, AudioUtility.AudioGroups.Impact, 1f, 3f);
}
// Self Destruct
Destroy(this.gameObject);
}
void OnDrawGizmosSelected()
{
Gizmos.color = RadiusColor;
Gizmos.DrawSphere(transform.position, Radius);
}
}
}
TeleportPlayer
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
// Debug script, teleports the player across the map for faster testing
public class TeleportPlayer : MonoBehaviour
{
public KeyCode ActivateKey = KeyCode.F12;
PlayerCharacterController m_PlayerCharacterController;
void Awake()
{
m_PlayerCharacterController = FindObjectOfType<PlayerCharacterController>();
DebugUtility.HandleErrorIfNullFindObject<PlayerCharacterController, TeleportPlayer>(
m_PlayerCharacterController, this);
}
void Update()
{
if (Input.GetKeyDown(ActivateKey))
{
m_PlayerCharacterController.transform.SetPositionAndRotation(transform.position, transform.rotation);
Health playerHealth = m_PlayerCharacterController.GetComponent<Health>();
if (playerHealth)
{
playerHealth.Heal(999);
}
}
}
}
}
WeaponFuelCellHandler
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(WeaponController))]
public class WeaponFuelCellHandler : MonoBehaviour
{
[Tooltip("Retract All Fuel Cells Simultaneously")]
public bool SimultaneousFuelCellsUsage = false;
[Tooltip("List of GameObjects representing the fuel cells on the weapon")]
public GameObject[] FuelCells;
[Tooltip("Cell local position when used")]
public Vector3 FuelCellUsedPosition;
[Tooltip("Cell local position before use")]
public Vector3 FuelCellUnusedPosition = new Vector3(0f, -0.1f, 0f);
WeaponController m_Weapon;
bool[] m_FuelCellsCooled;
void Start()
{
m_Weapon = GetComponent<WeaponController>();
DebugUtility.HandleErrorIfNullGetComponent<WeaponController, WeaponFuelCellHandler>(m_Weapon, this,
gameObject);
m_FuelCellsCooled = new bool[FuelCells.Length];
for (int i = 0; i < m_FuelCellsCooled.Length; i++)
{
m_FuelCellsCooled[i] = true;
}
}
void Update()
{
if (SimultaneousFuelCellsUsage)
{
for (int i = 0; i < FuelCells.Length; i++)
{
FuelCells[i].transform.localPosition = Vector3.Lerp(FuelCellUsedPosition, FuelCellUnusedPosition,
m_Weapon.CurrentAmmoRatio);
}
}
else
{
// TODO: needs simplification
for (int i = 0; i < FuelCells.Length; i++)
{
float length = FuelCells.Length;
float lim1 = i / length;
float lim2 = (i + 1) / length;
float value = Mathf.InverseLerp(lim1, lim2, m_Weapon.CurrentAmmoRatio);
value = Mathf.Clamp01(value);
FuelCells[i].transform.localPosition =
Vector3.Lerp(FuelCellUsedPosition, FuelCellUnusedPosition, value);
}
}
}
}
}
WeaponPickup
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
public class WeaponPickup : Pickup
{
[Tooltip("The prefab for the weapon that will be added to the player on pickup")]
public WeaponController WeaponPrefab;
protected override void Start()
{
base.Start();
// Set all children layers to default (to prefent seeing weapons through meshes)
foreach (Transform t in GetComponentsInChildren<Transform>())
{
if (t != transform)
t.gameObject.layer = 0;
}
}
protected override void OnPicked(PlayerCharacterController byPlayer)
{
PlayerWeaponsManager playerWeaponsManager = byPlayer.GetComponent<PlayerWeaponsManager>();
if (playerWeaponsManager)
{
if (playerWeaponsManager.AddWeapon(WeaponPrefab))
{
// Handle auto-switching to weapon if no weapons currently
if (playerWeaponsManager.GetActiveWeapon() == null)
{
playerWeaponsManager.SwitchWeapon(true);
}
PlayPickupFeedback();
Destroy(gameObject);
}
}
}
}
}