This commit is contained in:
Alex38Lyon
2025-06-03 12:00:47 +02:00
parent ed8041abcd
commit 878ea46cac
1300 changed files with 527178 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dfbed033b9b1c0d4984f232a7c86567e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+141
View File
@@ -0,0 +1,141 @@
using System.Linq;
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.AI
{
public class DetectionModule : MonoBehaviour
{
[Tooltip("The point representing the source of target-detection raycasts for the enemy AI")]
public Transform DetectionSourcePoint;
[Tooltip("The max distance at which the enemy can see targets")]
public float DetectionRange = 20f;
[Tooltip("The max distance at which the enemy can attack its target")]
public float AttackRange = 10f;
[Tooltip("Time before an enemy abandons a known target that it can't see anymore")]
public float KnownTargetTimeout = 4f;
[Tooltip("Optional animator for OnShoot animations")]
public Animator Animator;
public UnityAction onDetectedTarget;
public UnityAction onLostTarget;
public GameObject KnownDetectedTarget { get; private set; }
public bool IsTargetInAttackRange { get; private set; }
public bool IsSeeingTarget { get; private set; }
public bool HadKnownTarget { get; private set; }
protected float TimeLastSeenTarget = Mathf.NegativeInfinity;
ActorsManager m_ActorsManager;
const string k_AnimAttackParameter = "Attack";
const string k_AnimOnDamagedParameter = "OnDamaged";
protected virtual void Start()
{
m_ActorsManager = FindAnyObjectByType<ActorsManager>();
DebugUtility.HandleErrorIfNullFindObject<ActorsManager, DetectionModule>(m_ActorsManager, this);
}
public virtual void HandleTargetDetection(Actor actor, Collider[] selfColliders)
{
// Handle known target detection timeout
if (KnownDetectedTarget && !IsSeeingTarget && (Time.time - TimeLastSeenTarget) > KnownTargetTimeout)
{
KnownDetectedTarget = null;
}
// Find the closest visible hostile actor
float sqrDetectionRange = DetectionRange * DetectionRange;
IsSeeingTarget = false;
float closestSqrDistance = Mathf.Infinity;
foreach (Actor otherActor in m_ActorsManager.Actors)
{
if (otherActor.Affiliation != actor.Affiliation)
{
float sqrDistance = (otherActor.transform.position - DetectionSourcePoint.position).sqrMagnitude;
if (sqrDistance < sqrDetectionRange && sqrDistance < closestSqrDistance)
{
// Check for obstructions
RaycastHit[] hits = Physics.RaycastAll(DetectionSourcePoint.position,
(otherActor.AimPoint.position - DetectionSourcePoint.position).normalized, DetectionRange,
-1, QueryTriggerInteraction.Ignore);
RaycastHit closestValidHit = new RaycastHit();
closestValidHit.distance = Mathf.Infinity;
bool foundValidHit = false;
foreach (var hit in hits)
{
if (!selfColliders.Contains(hit.collider) && hit.distance < closestValidHit.distance)
{
closestValidHit = hit;
foundValidHit = true;
}
}
if (foundValidHit)
{
Actor hitActor = closestValidHit.collider.GetComponentInParent<Actor>();
if (hitActor == otherActor)
{
IsSeeingTarget = true;
closestSqrDistance = sqrDistance;
TimeLastSeenTarget = Time.time;
KnownDetectedTarget = otherActor.AimPoint.gameObject;
}
}
}
}
}
IsTargetInAttackRange = KnownDetectedTarget != null &&
Vector3.Distance(transform.position, KnownDetectedTarget.transform.position) <=
AttackRange;
// Detection events
if (!HadKnownTarget &&
KnownDetectedTarget != null)
{
OnDetect();
}
if (HadKnownTarget &&
KnownDetectedTarget == null)
{
OnLostTarget();
}
// Remember if we already knew a target (for next frame)
HadKnownTarget = KnownDetectedTarget != null;
}
public virtual void OnLostTarget() => onLostTarget?.Invoke();
public virtual void OnDetect() => onDetectedTarget?.Invoke();
public virtual void OnDamaged(GameObject damageSource)
{
TimeLastSeenTarget = Time.time;
KnownDetectedTarget = damageSource;
if (Animator)
{
Animator.SetTrigger(k_AnimOnDamagedParameter);
}
}
public virtual void OnAttack()
{
if (Animator)
{
Animator.SetTrigger(k_AnimAttackParameter);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 927ff0b4cb9a5dd4fb83c45166cffc50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+490
View File
@@ -0,0 +1,490 @@
using System.Collections.Generic;
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Events;
namespace Unity.FPS.AI
{
[RequireComponent(typeof(Health), typeof(Actor), typeof(NavMeshAgent))]
public class EnemyController : MonoBehaviour
{
[System.Serializable]
public struct RendererIndexData
{
public Renderer Renderer;
public int MaterialIndex;
public RendererIndexData(Renderer renderer, int index)
{
Renderer = renderer;
MaterialIndex = index;
}
}
[Header("Parameters")]
[Tooltip("The Y height at which the enemy will be automatically killed (if it falls off of the level)")]
public float SelfDestructYHeight = -20f;
[Tooltip("The distance at which the enemy considers that it has reached its current path destination point")]
public float PathReachingRadius = 2f;
[Tooltip("The speed at which the enemy rotates")]
public float OrientationSpeed = 10f;
[Tooltip("Delay after death where the GameObject is destroyed (to allow for animation)")]
public float DeathDuration = 0f;
[Header("Weapons Parameters")] [Tooltip("Allow weapon swapping for this enemy")]
public bool SwapToNextWeapon = false;
[Tooltip("Time delay between a weapon swap and the next attack")]
public float DelayAfterWeaponSwap = 0f;
[Header("Eye color")] [Tooltip("Material for the eye color")]
public Material EyeColorMaterial;
[Tooltip("The default color of the bot's eye")] [ColorUsageAttribute(true, true)]
public Color DefaultEyeColor;
[Tooltip("The attack color of the bot's eye")] [ColorUsageAttribute(true, true)]
public Color AttackEyeColor;
[Header("Flash on hit")] [Tooltip("The material used for the body of the hoverbot")]
public Material BodyMaterial;
[Tooltip("The gradient representing the color of the flash on hit")] [GradientUsageAttribute(true)]
public Gradient OnHitBodyGradient;
[Tooltip("The duration of the flash on hit")]
public float FlashOnHitDuration = 0.5f;
[Header("Sounds")] [Tooltip("Sound played when recieving damages")]
public AudioClip DamageTick;
[Header("VFX")] [Tooltip("The VFX prefab spawned when the enemy dies")]
public GameObject DeathVfx;
[Tooltip("The point at which the death VFX is spawned")]
public Transform DeathVfxSpawnPoint;
[Header("Loot")] [Tooltip("The object this enemy can drop when dying")]
public GameObject LootPrefab;
[Tooltip("The chance the object has to drop")] [Range(0, 1)]
public float DropRate = 1f;
[Header("Debug Display")] [Tooltip("Color of the sphere gizmo representing the path reaching range")]
public Color PathReachingRangeColor = Color.yellow;
[Tooltip("Color of the sphere gizmo representing the attack range")]
public Color AttackRangeColor = Color.red;
[Tooltip("Color of the sphere gizmo representing the detection range")]
public Color DetectionRangeColor = Color.blue;
public UnityAction onAttack;
public UnityAction onDetectedTarget;
public UnityAction onLostTarget;
public UnityAction onDamaged;
List<RendererIndexData> m_BodyRenderers = new List<RendererIndexData>();
MaterialPropertyBlock m_BodyFlashMaterialPropertyBlock;
float m_LastTimeDamaged = float.NegativeInfinity;
RendererIndexData m_EyeRendererData;
MaterialPropertyBlock m_EyeColorMaterialPropertyBlock;
public PatrolPath PatrolPath { get; set; }
public GameObject KnownDetectedTarget => DetectionModule.KnownDetectedTarget;
public bool IsTargetInAttackRange => DetectionModule.IsTargetInAttackRange;
public bool IsSeeingTarget => DetectionModule.IsSeeingTarget;
public bool HadKnownTarget => DetectionModule.HadKnownTarget;
public NavMeshAgent NavMeshAgent { get; private set; }
public DetectionModule DetectionModule { get; private set; }
int m_PathDestinationNodeIndex;
EnemyManager m_EnemyManager;
ActorsManager m_ActorsManager;
Health m_Health;
Actor m_Actor;
Collider[] m_SelfColliders;
GameFlowManager m_GameFlowManager;
bool m_WasDamagedThisFrame;
float m_LastTimeWeaponSwapped = Mathf.NegativeInfinity;
int m_CurrentWeaponIndex;
WeaponController m_CurrentWeapon;
WeaponController[] m_Weapons;
NavigationModule m_NavigationModule;
void Start()
{
m_EnemyManager = FindAnyObjectByType<EnemyManager>();
DebugUtility.HandleErrorIfNullFindObject<EnemyManager, EnemyController>(m_EnemyManager, this);
m_ActorsManager = FindAnyObjectByType<ActorsManager>();
DebugUtility.HandleErrorIfNullFindObject<ActorsManager, EnemyController>(m_ActorsManager, this);
m_EnemyManager.RegisterEnemy(this);
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, EnemyController>(m_Health, this, gameObject);
m_Actor = GetComponent<Actor>();
DebugUtility.HandleErrorIfNullGetComponent<Actor, EnemyController>(m_Actor, this, gameObject);
NavMeshAgent = GetComponent<NavMeshAgent>();
m_SelfColliders = GetComponentsInChildren<Collider>();
m_GameFlowManager = FindAnyObjectByType<GameFlowManager>();
DebugUtility.HandleErrorIfNullFindObject<GameFlowManager, EnemyController>(m_GameFlowManager, this);
// Subscribe to damage & death actions
m_Health.OnDie += OnDie;
m_Health.OnDamaged += OnDamaged;
// Find and initialize all weapons
FindAndInitializeAllWeapons();
var weapon = GetCurrentWeapon();
weapon.ShowWeapon(true);
var detectionModules = GetComponentsInChildren<DetectionModule>();
DebugUtility.HandleErrorIfNoComponentFound<DetectionModule, EnemyController>(detectionModules.Length, this,
gameObject);
DebugUtility.HandleWarningIfDuplicateObjects<DetectionModule, EnemyController>(detectionModules.Length,
this, gameObject);
// Initialize detection module
DetectionModule = detectionModules[0];
DetectionModule.onDetectedTarget += OnDetectedTarget;
DetectionModule.onLostTarget += OnLostTarget;
onAttack += DetectionModule.OnAttack;
var navigationModules = GetComponentsInChildren<NavigationModule>();
DebugUtility.HandleWarningIfDuplicateObjects<DetectionModule, EnemyController>(detectionModules.Length,
this, gameObject);
// Override navmesh agent data
if (navigationModules.Length > 0)
{
m_NavigationModule = navigationModules[0];
NavMeshAgent.speed = m_NavigationModule.MoveSpeed;
NavMeshAgent.angularSpeed = m_NavigationModule.AngularSpeed;
NavMeshAgent.acceleration = m_NavigationModule.Acceleration;
}
foreach (var renderer in GetComponentsInChildren<Renderer>(true))
{
for (int i = 0; i < renderer.sharedMaterials.Length; i++)
{
if (renderer.sharedMaterials[i] == EyeColorMaterial)
{
m_EyeRendererData = new RendererIndexData(renderer, i);
}
if (renderer.sharedMaterials[i] == BodyMaterial)
{
m_BodyRenderers.Add(new RendererIndexData(renderer, i));
}
}
}
m_BodyFlashMaterialPropertyBlock = new MaterialPropertyBlock();
// Check if we have an eye renderer for this enemy
if (m_EyeRendererData.Renderer != null)
{
m_EyeColorMaterialPropertyBlock = new MaterialPropertyBlock();
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", DefaultEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);
}
}
void Update()
{
EnsureIsWithinLevelBounds();
DetectionModule.HandleTargetDetection(m_Actor, m_SelfColliders);
Color currentColor = OnHitBodyGradient.Evaluate((Time.time - m_LastTimeDamaged) / FlashOnHitDuration);
m_BodyFlashMaterialPropertyBlock.SetColor("_EmissionColor", currentColor);
foreach (var data in m_BodyRenderers)
{
data.Renderer.SetPropertyBlock(m_BodyFlashMaterialPropertyBlock, data.MaterialIndex);
}
m_WasDamagedThisFrame = false;
}
void EnsureIsWithinLevelBounds()
{
// at every frame, this tests for conditions to kill the enemy
if (transform.position.y < SelfDestructYHeight)
{
Destroy(gameObject);
return;
}
}
void OnLostTarget()
{
onLostTarget.Invoke();
// Set the eye attack color and property block if the eye renderer is set
if (m_EyeRendererData.Renderer != null)
{
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", DefaultEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);
}
}
void OnDetectedTarget()
{
onDetectedTarget.Invoke();
// Set the eye default color and property block if the eye renderer is set
if (m_EyeRendererData.Renderer != null)
{
m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", AttackEyeColor);
m_EyeRendererData.Renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock,
m_EyeRendererData.MaterialIndex);
}
}
public void OrientTowards(Vector3 lookPosition)
{
Vector3 lookDirection = Vector3.ProjectOnPlane(lookPosition - transform.position, Vector3.up).normalized;
if (lookDirection.sqrMagnitude != 0f)
{
Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
transform.rotation =
Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * OrientationSpeed);
}
}
bool IsPathValid()
{
return PatrolPath && PatrolPath.PathNodes.Count > 0;
}
public void ResetPathDestination()
{
m_PathDestinationNodeIndex = 0;
}
public void SetPathDestinationToClosestNode()
{
if (IsPathValid())
{
int closestPathNodeIndex = 0;
for (int i = 0; i < PatrolPath.PathNodes.Count; i++)
{
float distanceToPathNode = PatrolPath.GetDistanceToNode(transform.position, i);
if (distanceToPathNode < PatrolPath.GetDistanceToNode(transform.position, closestPathNodeIndex))
{
closestPathNodeIndex = i;
}
}
m_PathDestinationNodeIndex = closestPathNodeIndex;
}
else
{
m_PathDestinationNodeIndex = 0;
}
}
public Vector3 GetDestinationOnPath()
{
if (IsPathValid())
{
return PatrolPath.GetPositionOfPathNode(m_PathDestinationNodeIndex);
}
else
{
return transform.position;
}
}
public void SetNavDestination(Vector3 destination)
{
if (NavMeshAgent)
{
NavMeshAgent.SetDestination(destination);
}
}
public void UpdatePathDestination(bool inverseOrder = false)
{
if (IsPathValid())
{
// Check if reached the path destination
if ((transform.position - GetDestinationOnPath()).magnitude <= PathReachingRadius)
{
// increment path destination index
m_PathDestinationNodeIndex =
inverseOrder ? (m_PathDestinationNodeIndex - 1) : (m_PathDestinationNodeIndex + 1);
if (m_PathDestinationNodeIndex < 0)
{
m_PathDestinationNodeIndex += PatrolPath.PathNodes.Count;
}
if (m_PathDestinationNodeIndex >= PatrolPath.PathNodes.Count)
{
m_PathDestinationNodeIndex -= PatrolPath.PathNodes.Count;
}
}
}
}
void OnDamaged(float damage, GameObject damageSource)
{
// test if the damage source is the player
if (damageSource && !damageSource.GetComponent<EnemyController>())
{
// pursue the player
DetectionModule.OnDamaged(damageSource);
onDamaged?.Invoke();
m_LastTimeDamaged = Time.time;
// play the damage tick sound
if (DamageTick && !m_WasDamagedThisFrame)
AudioUtility.CreateSFX(DamageTick, transform.position, AudioUtility.AudioGroups.DamageTick, 0f);
m_WasDamagedThisFrame = true;
}
}
void OnDie()
{
// spawn a particle system when dying
var vfx = Instantiate(DeathVfx, DeathVfxSpawnPoint.position, Quaternion.identity);
Destroy(vfx, 5f);
// tells the game flow manager to handle the enemy destuction
m_EnemyManager.UnregisterEnemy(this);
// loot an object
if (TryDropItem())
{
Instantiate(LootPrefab, transform.position, Quaternion.identity);
}
// this will call the OnDestroy function
Destroy(gameObject, DeathDuration);
}
void OnDrawGizmosSelected()
{
// Path reaching range
Gizmos.color = PathReachingRangeColor;
Gizmos.DrawWireSphere(transform.position, PathReachingRadius);
if (DetectionModule != null)
{
// Detection range
Gizmos.color = DetectionRangeColor;
Gizmos.DrawWireSphere(transform.position, DetectionModule.DetectionRange);
// Attack range
Gizmos.color = AttackRangeColor;
Gizmos.DrawWireSphere(transform.position, DetectionModule.AttackRange);
}
}
public void OrientWeaponsTowards(Vector3 lookPosition)
{
for (int i = 0; i < m_Weapons.Length; i++)
{
// orient weapon towards player
Vector3 weaponForward = (lookPosition - m_Weapons[i].WeaponRoot.transform.position).normalized;
m_Weapons[i].transform.forward = weaponForward;
}
}
public bool TryAtack(Vector3 enemyPosition)
{
if (m_GameFlowManager.GameIsEnding)
return false;
OrientWeaponsTowards(enemyPosition);
if ((m_LastTimeWeaponSwapped + DelayAfterWeaponSwap) >= Time.time)
return false;
// Shoot the weapon
bool didFire = GetCurrentWeapon().HandleShootInputs(false, true, false);
if (didFire && onAttack != null)
{
onAttack.Invoke();
if (SwapToNextWeapon && m_Weapons.Length > 1)
{
int nextWeaponIndex = (m_CurrentWeaponIndex + 1) % m_Weapons.Length;
SetCurrentWeapon(nextWeaponIndex);
}
}
return didFire;
}
public bool TryDropItem()
{
if (DropRate == 0 || LootPrefab == null)
return false;
else if (DropRate == 1)
return true;
else
return (Random.value <= DropRate);
}
void FindAndInitializeAllWeapons()
{
// Check if we already found and initialized the weapons
if (m_Weapons == null)
{
m_Weapons = GetComponentsInChildren<WeaponController>();
DebugUtility.HandleErrorIfNoComponentFound<WeaponController, EnemyController>(m_Weapons.Length, this,
gameObject);
for (int i = 0; i < m_Weapons.Length; i++)
{
m_Weapons[i].Owner = gameObject;
}
}
}
public WeaponController GetCurrentWeapon()
{
FindAndInitializeAllWeapons();
// Check if no weapon is currently selected
if (m_CurrentWeapon == null)
{
// Set the first weapon of the weapons list as the current weapon
SetCurrentWeapon(0);
}
DebugUtility.HandleErrorIfNullGetComponent<WeaponController, EnemyController>(m_CurrentWeapon, this,
gameObject);
return m_CurrentWeapon;
}
void SetCurrentWeapon(int index)
{
m_CurrentWeaponIndex = index;
m_CurrentWeapon = m_Weapons[m_CurrentWeaponIndex];
if (SwapToNextWeapon)
{
m_LastTimeWeaponSwapped = Time.time;
}
else
{
m_LastTimeWeaponSwapped = Mathf.NegativeInfinity;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3de939d976a5bb0419ccd8dbd0b6836b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+38
View File
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
public class EnemyManager : MonoBehaviour
{
public List<EnemyController> Enemies { get; private set; }
public int NumberOfEnemiesTotal { get; private set; }
public int NumberOfEnemiesRemaining => Enemies.Count;
void Awake()
{
Enemies = new List<EnemyController>();
}
public void RegisterEnemy(EnemyController enemy)
{
Enemies.Add(enemy);
NumberOfEnemiesTotal++;
}
public void UnregisterEnemy(EnemyController enemyKilled)
{
int enemiesRemainingNotification = NumberOfEnemiesRemaining - 1;
EnemyKillEvent evt = Events.EnemyKillEvent;
evt.Enemy = enemyKilled.gameObject;
evt.RemainingEnemyCount = enemiesRemainingNotification;
EventManager.Broadcast(evt);
// removes the enemy from the list, so that we can keep track of how many are left on the map
Enemies.Remove(enemyKilled);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 888c7d8f8f69c6b42a086589b6f01bb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+185
View File
@@ -0,0 +1,185 @@
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
[RequireComponent(typeof(EnemyController))]
public class EnemyMobile : MonoBehaviour
{
public enum AIState
{
Patrol,
Follow,
Attack,
}
public Animator Animator;
[Tooltip("Fraction of the enemy's attack range at which it will stop moving towards target while attacking")]
[Range(0f, 1f)]
public float AttackStopDistanceRatio = 0.5f;
[Tooltip("The random hit damage effects")]
public ParticleSystem[] RandomHitSparks;
public ParticleSystem[] OnDetectVfx;
public AudioClip OnDetectSfx;
[Header("Sound")] public AudioClip MovementSound;
public MinMaxFloat PitchDistortionMovementSpeed;
public AIState AiState { get; private set; }
EnemyController m_EnemyController;
AudioSource m_AudioSource;
const string k_AnimMoveSpeedParameter = "MoveSpeed";
const string k_AnimAttackParameter = "Attack";
const string k_AnimAlertedParameter = "Alerted";
const string k_AnimOnDamagedParameter = "OnDamaged";
void Start()
{
m_EnemyController = GetComponent<EnemyController>();
DebugUtility.HandleErrorIfNullGetComponent<EnemyController, EnemyMobile>(m_EnemyController, this,
gameObject);
m_EnemyController.onAttack += OnAttack;
m_EnemyController.onDetectedTarget += OnDetectedTarget;
m_EnemyController.onLostTarget += OnLostTarget;
m_EnemyController.SetPathDestinationToClosestNode();
m_EnemyController.onDamaged += OnDamaged;
// Start patrolling
AiState = AIState.Patrol;
// adding a audio source to play the movement sound on it
m_AudioSource = GetComponent<AudioSource>();
DebugUtility.HandleErrorIfNullGetComponent<AudioSource, EnemyMobile>(m_AudioSource, this, gameObject);
m_AudioSource.clip = MovementSound;
m_AudioSource.Play();
}
void Update()
{
UpdateAiStateTransitions();
UpdateCurrentAiState();
float moveSpeed = m_EnemyController.NavMeshAgent.velocity.magnitude;
// Update animator speed parameter
Animator.SetFloat(k_AnimMoveSpeedParameter, moveSpeed);
// changing the pitch of the movement sound depending on the movement speed
m_AudioSource.pitch = Mathf.Lerp(PitchDistortionMovementSpeed.Min, PitchDistortionMovementSpeed.Max,
moveSpeed / m_EnemyController.NavMeshAgent.speed);
}
void UpdateAiStateTransitions()
{
// Handle transitions
switch (AiState)
{
case AIState.Follow:
// Transition to attack when there is a line of sight to the target
if (m_EnemyController.IsSeeingTarget && m_EnemyController.IsTargetInAttackRange)
{
AiState = AIState.Attack;
m_EnemyController.SetNavDestination(transform.position);
}
break;
case AIState.Attack:
// Transition to follow when no longer a target in attack range
if (!m_EnemyController.IsTargetInAttackRange)
{
AiState = AIState.Follow;
}
break;
}
}
void UpdateCurrentAiState()
{
// Handle logic
switch (AiState)
{
case AIState.Patrol:
m_EnemyController.UpdatePathDestination();
// m_EnemyController.SetNavDestination(m_EnemyController.GetDestinationOnPath());
break;
case AIState.Follow:
m_EnemyController.SetNavDestination(m_EnemyController.KnownDetectedTarget.transform.position);
m_EnemyController.OrientTowards(m_EnemyController.KnownDetectedTarget.transform.position);
m_EnemyController.OrientWeaponsTowards(m_EnemyController.KnownDetectedTarget.transform.position);
break;
case AIState.Attack:
if (Vector3.Distance(m_EnemyController.KnownDetectedTarget.transform.position,
m_EnemyController.DetectionModule.DetectionSourcePoint.position)
>= (AttackStopDistanceRatio * m_EnemyController.DetectionModule.AttackRange))
{
m_EnemyController.SetNavDestination(m_EnemyController.KnownDetectedTarget.transform.position);
}
else
{
m_EnemyController.SetNavDestination(transform.position);
}
m_EnemyController.OrientTowards(m_EnemyController.KnownDetectedTarget.transform.position);
m_EnemyController.TryAtack(m_EnemyController.KnownDetectedTarget.transform.position);
break;
}
}
void OnAttack()
{
Animator.SetTrigger(k_AnimAttackParameter);
}
void OnDetectedTarget()
{
if (AiState == AIState.Patrol)
{
AiState = AIState.Follow;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Play();
}
if (OnDetectSfx)
{
AudioUtility.CreateSFX(OnDetectSfx, transform.position, AudioUtility.AudioGroups.EnemyDetection, 1f);
}
Animator.SetBool(k_AnimAlertedParameter, true);
}
void OnLostTarget()
{
if (AiState == AIState.Follow || AiState == AIState.Attack)
{
AiState = AIState.Patrol;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Stop();
}
Animator.SetBool(k_AnimAlertedParameter, false);
}
void OnDamaged()
{
if (RandomHitSparks.Length > 0)
{
int n = Random.Range(0, RandomHitSparks.Length - 1);
RandomHitSparks[n].Play();
}
Animator.SetTrigger(k_AnimOnDamagedParameter);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1857b2df8de24b24190ce9e067d2e1e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+170
View File
@@ -0,0 +1,170 @@
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
[RequireComponent(typeof(EnemyController))]
public class EnemyTurret : MonoBehaviour
{
public enum AIState
{
Idle,
Attack,
}
public Transform TurretPivot;
public Transform TurretAimPoint;
public Animator Animator;
public float AimRotationSharpness = 5f;
public float LookAtRotationSharpness = 2.5f;
public float DetectionFireDelay = 1f;
public float AimingTransitionBlendTime = 1f;
[Tooltip("The random hit damage effects")]
public ParticleSystem[] RandomHitSparks;
public ParticleSystem[] OnDetectVfx;
public AudioClip OnDetectSfx;
public AIState AiState { get; private set; }
EnemyController m_EnemyController;
Health m_Health;
Quaternion m_RotationWeaponForwardToPivot;
float m_TimeStartedDetection;
float m_TimeLostDetection;
Quaternion m_PreviousPivotAimingRotation;
Quaternion m_PivotAimingRotation;
const string k_AnimOnDamagedParameter = "OnDamaged";
const string k_AnimIsActiveParameter = "IsActive";
void Start()
{
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, EnemyTurret>(m_Health, this, gameObject);
m_Health.OnDamaged += OnDamaged;
m_EnemyController = GetComponent<EnemyController>();
DebugUtility.HandleErrorIfNullGetComponent<EnemyController, EnemyTurret>(m_EnemyController, this,
gameObject);
m_EnemyController.onDetectedTarget += OnDetectedTarget;
m_EnemyController.onLostTarget += OnLostTarget;
// Remember the rotation offset between the pivot's forward and the weapon's forward
m_RotationWeaponForwardToPivot =
Quaternion.Inverse(m_EnemyController.GetCurrentWeapon().WeaponMuzzle.rotation) * TurretPivot.rotation;
// Start with idle
AiState = AIState.Idle;
m_TimeStartedDetection = Mathf.NegativeInfinity;
m_PreviousPivotAimingRotation = TurretPivot.rotation;
}
void Update()
{
UpdateCurrentAiState();
}
void LateUpdate()
{
UpdateTurretAiming();
}
void UpdateCurrentAiState()
{
// Handle logic
switch (AiState)
{
case AIState.Attack:
bool mustShoot = Time.time > m_TimeStartedDetection + DetectionFireDelay;
// Calculate the desired rotation of our turret (aim at target)
Vector3 directionToTarget =
(m_EnemyController.KnownDetectedTarget.transform.position - TurretAimPoint.position).normalized;
Quaternion offsettedTargetRotation =
Quaternion.LookRotation(directionToTarget) * m_RotationWeaponForwardToPivot;
m_PivotAimingRotation = Quaternion.Slerp(m_PreviousPivotAimingRotation, offsettedTargetRotation,
(mustShoot ? AimRotationSharpness : LookAtRotationSharpness) * Time.deltaTime);
// shoot
if (mustShoot)
{
Vector3 correctedDirectionToTarget =
(m_PivotAimingRotation * Quaternion.Inverse(m_RotationWeaponForwardToPivot)) *
Vector3.forward;
m_EnemyController.TryAtack(TurretAimPoint.position + correctedDirectionToTarget);
}
break;
}
}
void UpdateTurretAiming()
{
switch (AiState)
{
case AIState.Attack:
TurretPivot.rotation = m_PivotAimingRotation;
break;
default:
// Use the turret rotation of the animation
TurretPivot.rotation = Quaternion.Slerp(m_PivotAimingRotation, TurretPivot.rotation,
(Time.time - m_TimeLostDetection) / AimingTransitionBlendTime);
break;
}
m_PreviousPivotAimingRotation = TurretPivot.rotation;
}
void OnDamaged(float dmg, GameObject source)
{
if (RandomHitSparks.Length > 0)
{
int n = Random.Range(0, RandomHitSparks.Length - 1);
RandomHitSparks[n].Play();
}
Animator.SetTrigger(k_AnimOnDamagedParameter);
}
void OnDetectedTarget()
{
if (AiState == AIState.Idle)
{
AiState = AIState.Attack;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Play();
}
if (OnDetectSfx)
{
AudioUtility.CreateSFX(OnDetectSfx, transform.position, AudioUtility.AudioGroups.EnemyDetection, 1f);
}
Animator.SetBool(k_AnimIsActiveParameter, true);
m_TimeStartedDetection = Time.time;
}
void OnLostTarget()
{
if (AiState == AIState.Attack)
{
AiState = AIState.Idle;
}
for (int i = 0; i < OnDetectVfx.Length; i++)
{
OnDetectVfx[i].Stop();
}
Animator.SetBool(k_AnimIsActiveParameter, false);
m_TimeLostDetection = Time.time;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4cb5371dba09ee46806cbe0c2aa2c9c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+30
View File
@@ -0,0 +1,30 @@
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.AI
{
public class FollowPlayer : MonoBehaviour
{
Transform m_PlayerTransform;
Vector3 m_OriginalOffset;
void Start()
{
ActorsManager actorsManager = FindAnyObjectByType<ActorsManager>();
if (actorsManager != null)
m_PlayerTransform = actorsManager.Player.transform;
else
{
enabled = false;
return;
}
m_OriginalOffset = transform.position - m_PlayerTransform.position;
}
void LateUpdate()
{
transform.position = m_PlayerTransform.position + m_OriginalOffset;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3cb6842226810914cb7e746a8f872d68
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+18
View File
@@ -0,0 +1,18 @@
using UnityEngine;
namespace Unity.FPS.AI
{
// Component used to override values on start from the NavmeshAgent component in order to change
// how the agent is moving
public class NavigationModule : MonoBehaviour
{
[Header("Parameters")] [Tooltip("The maximum speed at which the enemy is moving (in world units per second).")]
public float MoveSpeed = 0f;
[Tooltip("The maximum speed at which the enemy is rotating (degrees per second).")]
public float AngularSpeed = 0f;
[Tooltip("The acceleration to reach the maximum speed (in world units per second squared).")]
public float Acceleration = 0f;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22d6b672a3f9d27468cfdd79b2119c7e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+59
View File
@@ -0,0 +1,59 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.AI
{
public class PatrolPath : MonoBehaviour
{
[Tooltip("Enemies that will be assigned to this path on Start")]
public List<EnemyController> EnemiesToAssign = new List<EnemyController>();
[Tooltip("The Nodes making up the path")]
public List<Transform> PathNodes = new List<Transform>();
void Start()
{
foreach (var enemy in EnemiesToAssign)
{
enemy.PatrolPath = this;
}
}
public float GetDistanceToNode(Vector3 origin, int destinationNodeIndex)
{
if (destinationNodeIndex < 0 || destinationNodeIndex >= PathNodes.Count ||
PathNodes[destinationNodeIndex] == null)
{
return -1f;
}
return (PathNodes[destinationNodeIndex].position - origin).magnitude;
}
public Vector3 GetPositionOfPathNode(int nodeIndex)
{
if (nodeIndex < 0 || nodeIndex >= PathNodes.Count || PathNodes[nodeIndex] == null)
{
return Vector3.zero;
}
return PathNodes[nodeIndex].position;
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
for (int i = 0; i < PathNodes.Count; i++)
{
int nextIndex = i + 1;
if (nextIndex >= PathNodes.Count)
{
nextIndex -= PathNodes.Count;
}
Gizmos.DrawLine(PathNodes[i].position, PathNodes[nextIndex].position);
Gizmos.DrawSphere(PathNodes[i].position, 0.1f);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2ec2f49cb5488d0459ff53ecc714acb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -0,0 +1,15 @@
{
"name": "fps.AI",
"references": [
"GUID:9c5543eabb73a6249ac07b2c065ee8b4"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e9dc24b3d0971d64a9e95b6986d97af5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 25ff9b5709276df40a3b4650a7247ced
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+386
View File
@@ -0,0 +1,386 @@
using System.Collections.Generic;
using Unity.FPS.AI;
using Unity.FPS.Game;
using Unity.FPS.Gameplay;
using UnityEditor;
using UnityEngine;
namespace Unity.FPS.EditorExt
{
public class MiniProfiler : EditorWindow
{
class BoundsAndCount
{
public Bounds Bounds;
public int Count;
}
class CellData
{
public Bounds Bounds;
public int Count;
public float Ratio;
public Color Color;
}
Vector2 m_ScrollPos;
bool m_MustRepaint = false;
bool m_MustLaunchHeatmapNextFrame = false;
bool m_HeatmapIsCalculating = false;
float m_CellTransparency = 0.9f;
float m_CellThreshold = 0f;
string m_LevelAnalysisString = "";
List<string> m_SuggestionStrings = new List<string>();
static List<CellData> s_CellDatas = new List<CellData>();
const float k_CellSize = 10;
const string k_NewLine = "\n";
const string k_HeaderSeparator = "==============================";
// Add menu item named "My Window" to the Window menu
[MenuItem("Tools/MiniProfiler")]
public static void ShowWindow()
{
//Show existing window instance. If one doesn't exist, make one.
EditorWindow.GetWindow(typeof(MiniProfiler));
}
void OnEnable()
{
#if UNITY_2019_1_OR_NEWER
SceneView.duringSceneGui -= OnSceneGUI;
SceneView.duringSceneGui += OnSceneGUI;
#elif UNITY_2018_1_OR_NEWER
SceneView.onSceneGUIDelegate -= OnSceneGUI;
SceneView.onSceneGUIDelegate += OnSceneGUI;
#endif
}
void OnGUI()
{
m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos, false, false);
GUILayout.Space(20);
EditorGUILayout.LabelField("Performance Tips");
DisplayTips();
GUILayout.Space(20);
EditorGUILayout.LabelField("Level Analysis");
if (Application.isPlaying)
{
EditorGUILayout.HelpBox("You must exit Play mode for this feature to be available",
MessageType.Warning);
}
else
{
if (GUILayout.Button("Analyze"))
{
AnalyzeLevel();
}
if (m_LevelAnalysisString != null && m_LevelAnalysisString != "")
{
EditorGUILayout.HelpBox(m_LevelAnalysisString, MessageType.None);
}
if (m_SuggestionStrings.Count > 0)
{
EditorGUILayout.LabelField("Suggestions");
foreach (var s in m_SuggestionStrings)
{
EditorGUILayout.HelpBox(s, MessageType.Warning);
}
}
if (GUILayout.Button("Clear Analysis"))
{
ClearAnalysis();
m_MustRepaint = true;
}
}
GUILayout.Space(20);
EditorGUILayout.LabelField("Polygon count Heatmap");
if (Application.isPlaying)
{
EditorGUILayout.HelpBox("You must exit Play mode for this feature to be available",
MessageType.Warning);
}
else
{
if (m_MustLaunchHeatmapNextFrame)
{
DoPolycountMap();
m_CellTransparency = 0.9f;
m_CellThreshold = 0f;
m_MustLaunchHeatmapNextFrame = false;
m_MustRepaint = true;
}
if (GUILayout.Button("Build Heatmap"))
{
m_MustLaunchHeatmapNextFrame = true;
m_HeatmapIsCalculating = true;
}
if (s_CellDatas.Count > 0)
{
float prevAlpha = m_CellTransparency;
m_CellTransparency = EditorGUILayout.Slider("Cell Transparency", m_CellTransparency, 0f, 1f);
if (m_CellTransparency != prevAlpha)
{
m_MustRepaint = true;
}
float prevTreshold = m_CellThreshold;
m_CellThreshold = EditorGUILayout.Slider("Cell Display Threshold", m_CellThreshold, 0f, 1f);
if (m_CellThreshold != prevTreshold)
{
m_MustRepaint = true;
}
}
if (GUILayout.Button("Clear Heatmap"))
{
m_MustRepaint = true;
s_CellDatas.Clear();
}
}
EditorGUILayout.EndScrollView();
if (m_MustRepaint)
{
EditorWindow.GetWindow<SceneView>().Repaint();
m_MustRepaint = false;
}
if (m_HeatmapIsCalculating)
EditorUtility.DisplayProgressBar("Polygon Count Heatmap", "Calculations in progress", 0.99f);
}
void OnSceneGUI(SceneView sceneView)
{
// Draw heatmap
foreach (CellData c in s_CellDatas)
{
if (c.Ratio >= m_CellThreshold && c.Count > 0)
{
Color col = c.Color;
col.a = 1f - m_CellTransparency;
Handles.color = col;
Handles.CubeHandleCap(0, c.Bounds.center, Quaternion.identity, c.Bounds.extents.x * 2f,
EventType.Repaint);
}
}
}
void ClearAnalysis()
{
m_LevelAnalysisString = "";
m_SuggestionStrings.Clear();
}
void DisplayTips()
{
EditorGUILayout.HelpBox(
"All of your meshes that will never move (floor/wall meshes, for examples) should be placed as children of the \"Level\" GameObject in the scene. This is because the \"Mesh Combiner\" script on that object will take care of combining all meshes under it on game start, and this reduces the cost of rendering them. It is more efficient to render one big mesh than lots of small meshes, even when the number of polygons is the same.",
MessageType.None);
EditorGUILayout.HelpBox(
"Every light added to the level will have a performance cost. If you do add more lights to the level, consider making them not cast any shadows to reduce the performance impact. However, be aware that in WebGL there is a limit of 4 lights to be drawn on screen at the same time",
MessageType.None);
EditorGUILayout.HelpBox("Transparent objects are more expensive for performance than opaque objects",
MessageType.None);
EditorGUILayout.HelpBox(
"Animated 3D models (known as \"Skinned Meshes\") are more expensive for performance than regular meshes",
MessageType.None);
EditorGUILayout.HelpBox(
"Having a lot of enemies in the level could impact performance, due to their AI logic",
MessageType.None);
EditorGUILayout.HelpBox("Adding rigidbodies (physics objects) to the level could impact performance",
MessageType.None);
EditorGUILayout.HelpBox(
"Open the Profiler window from the top menu bar (Window > Analysis > Profiler) to see in-depth information about your game's performance while you are playing",
MessageType.None);
}
void AnalyzeLevel()
{
ClearAnalysis();
EditorStyles.textArea.wordWrap = true;
MeshCombiner mainMeshCombiner = FindAnyObjectByType<MeshCombiner>();
// Analyze
MeshFilter[] meshFilters = FindObjectsByType<MeshFilter>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
SkinnedMeshRenderer[] skinnedMeshes = FindObjectsByType<SkinnedMeshRenderer>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
int skinnedMeshesCount = skinnedMeshes.Length;
int meshCount = meshFilters.Length;
int nonCombinedMeshCount = 0;
int polyCount = 0;
foreach (MeshFilter mf in meshFilters)
{
if (!mf.sharedMesh)
continue;
polyCount += mf.sharedMesh.triangles.Length / 3;
bool willBeCombined = false;
if (mainMeshCombiner)
{
foreach (GameObject combineParent in mainMeshCombiner.CombineParents)
{
if (mf.transform.IsChildOf(combineParent.transform))
{
willBeCombined = true;
}
}
}
if (!willBeCombined)
{
if (!(mf.GetComponentInParent<PlayerCharacterController>() ||
mf.GetComponentInParent<EnemyController>() ||
mf.GetComponentInParent<Pickup>() ||
mf.GetComponentInParent<Objective>()))
{
nonCombinedMeshCount++;
}
}
}
foreach (SkinnedMeshRenderer sm in skinnedMeshes)
{
polyCount += sm.sharedMesh.triangles.Length / 3;
}
int rigidbodiesCount = 0;
foreach (var r in FindObjectsByType<Rigidbody>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
if (!r.isKinematic)
{
rigidbodiesCount++;
}
}
int lightsCount = FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).Length;
int enemyCount = FindObjectsByType<EnemyController>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).Length;
// Level analysis
m_LevelAnalysisString += "- Meshes count: " + meshCount;
m_LevelAnalysisString += k_NewLine;
m_LevelAnalysisString += "- Animated models (SkinnedMeshes) count: " + skinnedMeshesCount;
m_LevelAnalysisString += k_NewLine;
m_LevelAnalysisString += "- Polygon count: " + polyCount;
m_LevelAnalysisString += k_NewLine;
m_LevelAnalysisString += "- Physics objects (rigidbodies) count: " + rigidbodiesCount;
m_LevelAnalysisString += k_NewLine;
m_LevelAnalysisString += "- Lights count: " + lightsCount;
m_LevelAnalysisString += k_NewLine;
m_LevelAnalysisString += "- Enemy count: " + enemyCount;
// Suggestions
if (nonCombinedMeshCount > 50)
{
m_SuggestionStrings.Add(nonCombinedMeshCount +
" meshes in the scene are not setup to be combined on game start. Make sure that all the meshes " +
"that will never move, change, or be removed during play are under the \"Level\" gameObject in the scene, so they can be combined for greater performance. \n \n" +
"Note that it is always normal to have a few meshes that will not be combined, such as pickups, player meshes, enemy meshes, etc....");
}
}
void DoPolycountMap()
{
s_CellDatas.Clear();
List<BoundsAndCount> meshBoundsAndCount = new List<BoundsAndCount>();
Bounds levelBounds = new Bounds();
Renderer[] allRenderers = FindObjectsByType<Renderer>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
// Get level bounds and list of bounds & polycount
for (int i = 0; i < allRenderers.Length; i++)
{
Renderer r = allRenderers[i];
if (r.gameObject.GetComponent<IgnoreHeatMap>())
continue;
levelBounds.Encapsulate(r.bounds);
MeshRenderer mr = (r as MeshRenderer);
if (mr)
{
MeshFilter mf = r.GetComponent<MeshFilter>();
if (mf && mf.sharedMesh != null)
{
BoundsAndCount b = new BoundsAndCount();
b.Bounds = r.bounds;
b.Count = mf.sharedMesh.triangles.Length / 3;
meshBoundsAndCount.Add(b);
}
}
else
{
SkinnedMeshRenderer smr = (r as SkinnedMeshRenderer);
if (smr)
{
if (smr.sharedMesh != null)
{
BoundsAndCount b = new BoundsAndCount();
b.Bounds = r.bounds;
b.Count = smr.sharedMesh.triangles.Length / 3;
meshBoundsAndCount.Add(b);
}
}
}
}
Vector3 boundsBottomCorner = levelBounds.center - levelBounds.extents;
Vector3Int gridResolution = new Vector3Int(Mathf.CeilToInt((levelBounds.extents.x * 2f) / k_CellSize),
Mathf.CeilToInt((levelBounds.extents.y * 2f) / k_CellSize),
Mathf.CeilToInt((levelBounds.extents.z * 2f) / k_CellSize));
int highestCount = 0;
for (int x = 0; x < gridResolution.x; x++)
{
for (int y = 0; y < gridResolution.y; y++)
{
for (int z = 0; z < gridResolution.z; z++)
{
CellData cellData = new CellData();
Vector3 cellCenter = boundsBottomCorner + (new Vector3(x, y, z) * k_CellSize) +
(Vector3.one * k_CellSize * 0.5f);
cellData.Bounds = new Bounds(cellCenter, Vector3.one * k_CellSize);
for (int i = 0; i < meshBoundsAndCount.Count; i++)
{
if (cellData.Bounds.Intersects(meshBoundsAndCount[i].Bounds))
{
cellData.Count += meshBoundsAndCount[i].Count;
}
}
if (cellData.Count > highestCount)
{
highestCount = cellData.Count;
}
s_CellDatas.Add(cellData);
}
}
}
for (int i = 0; i < s_CellDatas.Count; i++)
{
s_CellDatas[i].Ratio = (float) s_CellDatas[i].Count / (float) highestCount;
Color col = Color.Lerp(Color.green, Color.red, s_CellDatas[i].Ratio);
s_CellDatas[i].Color = col;
}
m_HeatmapIsCalculating = false;
EditorUtility.ClearProgressBar();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b5425b2a870ac14f8c96f11c641db95
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using Unity.FPS.Game;
using UnityEngine;
using UnityEditor;
namespace Unity.FPS.EditorExt
{
[CustomEditor(typeof(PrefabReplacer))]
public class PrefabReplacerEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (GUILayout.Button("Replace"))
{
Replace((target as PrefabReplacer));
}
}
public void Replace(PrefabReplacer replacer)
{
List<GameObject> allPrefabObjectsInScene = new List<GameObject>();
foreach (Transform t in GameObject.FindObjectsByType<Transform>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
if (PrefabUtility.IsAnyPrefabInstanceRoot(t.gameObject))
{
allPrefabObjectsInScene.Add(t.gameObject);
}
}
foreach (GameObject go in allPrefabObjectsInScene)
{
GameObject instanceSource = PrefabUtility.GetCorrespondingObjectFromSource(go);
foreach (var replacement in replacer.Replacements)
{
GameObject source = replacer.SwitchOrder ? replacement.TargetPrefab : replacement.SourcePrefab;
GameObject target = replacer.SwitchOrder ? replacement.SourcePrefab : replacement.TargetPrefab;
if (instanceSource == source)
{
// Create the instance
GameObject instance = PrefabUtility.InstantiatePrefab(target) as GameObject;
instance.transform.SetParent(go.transform.parent);
instance.transform.position = go.transform.position;
instance.transform.rotation = go.transform.rotation;
instance.transform.localScale = go.transform.localScale;
Undo.RegisterCreatedObjectUndo(instance, "prefab replace");
Undo.DestroyObjectImmediate(go);
}
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a6c78584504d3a4ebb6211df75433d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,88 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
namespace Unity.FPS.EditorExt
{
// Simple example of stripping of a debug build configuration
class ShaderBuildStripping : IPreprocessShaders
{
List<ShaderKeyword> m_ExcludedKeywords;
public ShaderBuildStripping()
{
#if MANUAL_SHADER_STRIPPING
m_ExcludedKeywords = new List<ShaderKeyword>
{
new ShaderKeyword("DEBUG"),
// ifdef
new ShaderKeyword("UNITY_GATHER_SUPPORTED"),
new ShaderKeyword("UNITY_POSTFX_SSR"),
new ShaderKeyword("DISTORT"),
new ShaderKeyword("BLUR_HIGH_QUALITY"),
new ShaderKeyword("UNITY_CAN_COMPILE_TESSELLATION"),
new ShaderKeyword("ENABLE_WIND"),
new ShaderKeyword("WIND_EFFECT_FROND_RIPPLE_ADJUST_LIGHTING"),
new ShaderKeyword("LOD_FADE_CROSSFADE"),
new ShaderKeyword("DYNAMICLIGHTMAP_ON"),
new ShaderKeyword("EDITOR_VISUALIZATION"),
new ShaderKeyword("UNITY_INSTANCING_ENABLED"),
new ShaderKeyword("STEREO_MULTIVIEW_ON"),
new ShaderKeyword("STEREO_INSTANCING_ON"),
new ShaderKeyword("SOFTPARTICLES_ON"),
new ShaderKeyword("PIXELSNAP_ON"),
new ShaderKeyword("SHADER_API_D3D11"),
// if defined()
new ShaderKeyword("SHADER_API_VULKAN"),
new ShaderKeyword("UNITY_SINGLE_PASS_STEREO"),
new ShaderKeyword("FOG_LINEAR"),
new ShaderKeyword("FOG_EXP"),
new ShaderKeyword("FOG_EXP2"),
new ShaderKeyword("UNITY_PASS_DEFERRED"),
new ShaderKeyword("LIGHTMAP_ON"),
new ShaderKeyword("_PARALLAXMAP"),
new ShaderKeyword("SHADOWS_SCREEN"),
};
#endif
}
// Multiple callback may be implemented.
// The first one executed is the one where callbackOrder is returning the smallest number.
public int callbackOrder
{
get { return 0; }
}
public void OnProcessShader(
Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
{
#if MANUAL_SHADER_STRIPPING
// In development, don't strip debug variants
if (EditorUserBuildSettings.development)
return;
for (int i = 0; i < shaderCompilerData.Count; ++i)
{
bool mustStrip = false;
foreach (var kw in m_ExcludedKeywords)
{
if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(kw))
{
mustStrip = true;
break;
}
}
if (mustStrip)
{
shaderCompilerData.RemoveAt(i);
--i;
}
}
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dac7d39682c10044695d8cda9fe08678
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
using Unity.FPS.UI;
using UnityEditor;
using UnityEngine;
namespace Unity.FPS.EditorExt
{
// The Editor for the UITable component to add an Update button
[CustomEditor(typeof(UITable), true)]
public class UITableEditor : Editor
{
public override void OnInspectorGUI()
{
UITable myTarget = (UITable)target;
DrawDefaultInspector();
if (GUILayout.Button("Update"))
{
myTarget.UpdateTable(null);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6ad043c45f6e794cae57e90893e61fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
{
"name": "fps.Editor",
"references": [
"GUID:9c5543eabb73a6249ac07b2c065ee8b4",
"GUID:27b7e9efd224f064990a97ed99e4456f",
"GUID:e9dc24b3d0971d64a9e95b6986d97af5",
"GUID:269903ea973e2a64ca0fb91ee07d3625"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bd05a56a26480e646a63a781f822fc20
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ca85cc8b78b3c1d469a63fe657be17e5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+38
View File
@@ -0,0 +1,38 @@
using UnityEngine;
namespace Unity.FPS.Game
{
// This class contains general information describing an actor (player or enemies).
// It is mostly used for AI detection logic and determining if an actor is friend or foe
public class Actor : MonoBehaviour
{
[Tooltip("Represents the affiliation (or team) of the actor. Actors of the same affiliation are friendly to each other")]
public int Affiliation;
[Tooltip("Represents point where other actors will aim when they attack this actor")]
public Transform AimPoint;
ActorsManager m_ActorsManager;
void Start()
{
m_ActorsManager = GameObject.FindFirstObjectByType<ActorsManager>();
DebugUtility.HandleErrorIfNullFindObject<ActorsManager, Actor>(m_ActorsManager, this);
// Register as an actor
if (!m_ActorsManager.Actors.Contains(this))
{
m_ActorsManager.Actors.Add(this);
}
}
void OnDestroy()
{
// Unregister as an actor
if (m_ActorsManager)
{
m_ActorsManager.Actors.Remove(this);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81b4c4e7c5d6cd34bb04cb46517b4e23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+77
View File
@@ -0,0 +1,77 @@
using UnityEngine;
using UnityEngine.Audio;
namespace Unity.FPS.Game
{
public class AudioUtility
{
static AudioManager s_AudioManager;
public enum AudioGroups
{
DamageTick,
Impact,
EnemyDetection,
Pickup,
WeaponShoot,
WeaponOverheat,
WeaponChargeBuildup,
WeaponChargeLoop,
HUDVictory,
HUDObjective,
EnemyAttack
}
public static void CreateSFX(AudioClip clip, Vector3 position, AudioGroups audioGroup, float spatialBlend,
float rolloffDistanceMin = 1f)
{
GameObject impactSfxInstance = new GameObject();
impactSfxInstance.transform.position = position;
AudioSource source = impactSfxInstance.AddComponent<AudioSource>();
source.clip = clip;
source.spatialBlend = spatialBlend;
source.minDistance = rolloffDistanceMin;
source.Play();
source.outputAudioMixerGroup = GetAudioGroup(audioGroup);
TimedSelfDestruct timedSelfDestruct = impactSfxInstance.AddComponent<TimedSelfDestruct>();
timedSelfDestruct.LifeTime = clip.length;
}
public static AudioMixerGroup GetAudioGroup(AudioGroups group)
{
if (s_AudioManager == null)
s_AudioManager = Object.FindFirstObjectByType<AudioManager>();
var groups = s_AudioManager.FindMatchingGroups(group.ToString());
if (groups.Length > 0)
return groups[0];
Debug.LogWarning("Didn't find audio group for " + group.ToString());
return null;
}
public static void SetMasterVolume(float value)
{
if (s_AudioManager == null)
s_AudioManager = Object.FindFirstObjectByType<AudioManager>();
if (value <= 0)
value = 0.001f;
float valueInDb = Mathf.Log10(value) * 20;
s_AudioManager.SetFloat("MasterVolume", valueInDb);
}
public static float GetMasterVolume()
{
if (s_AudioManager == null)
s_AudioManager = Object.FindFirstObjectByType<AudioManager>();
s_AudioManager.GetFloat("MasterVolume", out var valueInDb);
return Mathf.Pow(10f, valueInDb / 20.0f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4940d922b729e64091b72854e008d47
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,15 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class ConstantRotation : MonoBehaviour
{
[Tooltip("Rotation angle per second")] public float RotatingSpeed = 360f;
void Update()
{
// Handle rotating
transform.Rotate(Vector3.up, RotatingSpeed * Time.deltaTime, Space.Self);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c63e3394714c5af4180408522b392eb2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+57
View File
@@ -0,0 +1,57 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public static class DebugUtility
{
public static void HandleErrorIfNullGetComponent<TO, TS>(Component component, Component source,
GameObject onObject)
{
#if UNITY_EDITOR
if (component == null)
{
Debug.LogError("Error: Component of type " + typeof(TS) + " on GameObject " + source.gameObject.name +
" expected to find a component of type " + typeof(TO) + " on GameObject " +
onObject.name + ", but none were found.");
}
#endif
}
public static void HandleErrorIfNullFindObject<TO, TS>(UnityEngine.Object obj, Component source)
{
#if UNITY_EDITOR
if (obj == null)
{
Debug.LogError("Error: Component of type " + typeof(TS) + " on GameObject " + source.gameObject.name +
" expected to find an object of type " + typeof(TO) +
" in the scene, but none were found.");
}
#endif
}
public static void HandleErrorIfNoComponentFound<TO, TS>(int count, Component source, GameObject onObject)
{
#if UNITY_EDITOR
if (count == 0)
{
Debug.LogError("Error: Component of type " + typeof(TS) + " on GameObject " + source.gameObject.name +
" expected to find at least one component of type " + typeof(TO) + " on GameObject " +
onObject.name + ", but none were found.");
}
#endif
}
public static void HandleWarningIfDuplicateObjects<TO, TS>(int count, Component source, GameObject onObject)
{
#if UNITY_EDITOR
if (count > 1)
{
Debug.LogWarning("Warning: Component of type " + typeof(TS) + " on GameObject " +
source.gameObject.name +
" expected to find only one component of type " + typeof(TO) + " on GameObject " +
onObject.name + ", but several were found. First one found will be selected.");
}
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 47c7fd761b0457944805cf20ac23649d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+66
View File
@@ -0,0 +1,66 @@
using UnityEngine;
namespace Unity.FPS.Game
{
// The Game Events used across the Game.
// Anytime there is a need for a new event, it should be added here.
public static class Events
{
public static ObjectiveUpdateEvent ObjectiveUpdateEvent = new ObjectiveUpdateEvent();
public static AllObjectivesCompletedEvent AllObjectivesCompletedEvent = new AllObjectivesCompletedEvent();
public static GameOverEvent GameOverEvent = new GameOverEvent();
public static PlayerDeathEvent PlayerDeathEvent = new PlayerDeathEvent();
public static EnemyKillEvent EnemyKillEvent = new EnemyKillEvent();
public static PickupEvent PickupEvent = new PickupEvent();
public static AmmoPickupEvent AmmoPickupEvent = new AmmoPickupEvent();
public static DamageEvent DamageEvent = new DamageEvent();
public static DisplayMessageEvent DisplayMessageEvent = new DisplayMessageEvent();
}
public class ObjectiveUpdateEvent : GameEvent
{
public Objective Objective;
public string DescriptionText;
public string CounterText;
public bool IsComplete;
public string NotificationText;
}
public class AllObjectivesCompletedEvent : GameEvent { }
public class GameOverEvent : GameEvent
{
public bool Win;
}
public class PlayerDeathEvent : GameEvent { }
public class EnemyKillEvent : GameEvent
{
public GameObject Enemy;
public int RemainingEnemyCount;
}
public class PickupEvent : GameEvent
{
public GameObject Pickup;
}
public class AmmoPickupEvent : GameEvent
{
public WeaponController Weapon;
}
public class DamageEvent : GameEvent
{
public GameObject Sender;
public float DamageValue;
}
public class DisplayMessageEvent : GameEvent
{
public string Message;
public float DelayBeforeDisplay;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1ebb8b94dd6b9f4292a7a4a2996fed1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+29
View File
@@ -0,0 +1,29 @@
namespace Unity.FPS.Game
{
public class GameConstants
{
// all the constant string used across the game
public const string k_AxisNameVertical = "Vertical";
public const string k_AxisNameHorizontal = "Horizontal";
public const string k_MouseAxisNameVertical = "Mouse Y";
public const string k_MouseAxisNameHorizontal = "Mouse X";
public const string k_AxisNameJoystickLookVertical = "Look Y";
public const string k_AxisNameJoystickLookHorizontal = "Look X";
public const string k_ButtonNameAim = "Aim";
public const string k_ButtonNameFire = "Fire";
public const string k_ButtonNameSprint = "Sprint";
public const string k_ButtonNameJump = "Jump";
public const string k_ButtonNameCrouch = "Crouch";
public const string k_ButtonNameGamepadFire = "Gamepad Fire";
public const string k_ButtonNameGamepadAim = "Gamepad Aim";
public const string k_ButtonNameSwitchWeapon = "Mouse ScrollWheel";
public const string k_ButtonNameGamepadSwitchWeapon = "Gamepad Switch";
public const string k_ButtonNameNextWeapon = "NextWeapon";
public const string k_ButtonNamePauseMenu = "Pause Menu";
public const string k_ButtonNameSubmit = "Submit";
public const string k_ButtonNameCancel = "Cancel";
public const string k_ButtonReload = "Reload";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45a026fab99db8b4fac1af90f1ece0b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class IgnoreHeatMap : MonoBehaviour
{
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 853b31967a79acb49808dd17790b1c49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class IgnoreHitDetection : MonoBehaviour
{
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7579dbb2d10d4b34bbe05adb3f8014cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ed1f07e0262531d47b1d996b95d657b8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.Game
{
public class ActorsManager : MonoBehaviour
{
public List<Actor> Actors { get; private set; }
public GameObject Player { get; private set; }
public void SetPlayer(GameObject player) => Player = player;
void Awake()
{
Actors = new List<Actor>();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf10145f6bd13744c8fd10972a92ac61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
using UnityEngine;
using UnityEngine.Audio;
namespace Unity.FPS.Game
{
public class AudioManager : MonoBehaviour
{
public AudioMixer[] AudioMixers;
public AudioMixerGroup[] FindMatchingGroups(string subPath)
{
for (int i = 0; i < AudioMixers.Length; i++)
{
AudioMixerGroup[] results = AudioMixers[i].FindMatchingGroups(subPath);
if (results != null && results.Length != 0)
{
return results;
}
}
return null;
}
public void SetFloat(string name, float value)
{
for (int i = 0; i < AudioMixers.Length; i++)
{
if (AudioMixers[i] != null)
{
AudioMixers[i].SetFloat(name, value);
}
}
}
public void GetFloat(string name, out float value)
{
value = 0f;
for (int i = 0; i < AudioMixers.Length; i++)
{
if (AudioMixers[i] != null)
{
AudioMixers[i].GetFloat(name, out value);
break;
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d54177cab788734b8ecb4674c6e5328
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
namespace Unity.FPS.Game
{
public class GameEvent
{
}
// A simple Event System that can be used for remote systems communication
public static class EventManager
{
static readonly Dictionary<Type, Action<GameEvent>> s_Events = new Dictionary<Type, Action<GameEvent>>();
static readonly Dictionary<Delegate, Action<GameEvent>> s_EventLookups =
new Dictionary<Delegate, Action<GameEvent>>();
public static void AddListener<T>(Action<T> evt) where T : GameEvent
{
if (!s_EventLookups.ContainsKey(evt))
{
Action<GameEvent> newAction = (e) => evt((T) e);
s_EventLookups[evt] = newAction;
if (s_Events.TryGetValue(typeof(T), out Action<GameEvent> internalAction))
s_Events[typeof(T)] = internalAction += newAction;
else
s_Events[typeof(T)] = newAction;
}
}
public static void RemoveListener<T>(Action<T> evt) where T : GameEvent
{
if (s_EventLookups.TryGetValue(evt, out var action))
{
if (s_Events.TryGetValue(typeof(T), out var tempAction))
{
tempAction -= action;
if (tempAction == null)
s_Events.Remove(typeof(T));
else
s_Events[typeof(T)] = tempAction;
}
s_EventLookups.Remove(evt);
}
}
public static void Broadcast(GameEvent evt)
{
if (s_Events.TryGetValue(evt.GetType(), out var action))
action.Invoke(evt);
}
public static void Clear()
{
s_Events.Clear();
s_EventLookups.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 30e5751c33b241646926d24ab481e1f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Unity.FPS.Game
{
public class GameFlowManager : MonoBehaviour
{
[Header("Parameters")] [Tooltip("Duration of the fade-to-black at the end of the game")]
public float EndSceneLoadDelay = 3f;
[Tooltip("The canvas group of the fade-to-black screen")]
public CanvasGroup EndGameFadeCanvasGroup;
[Header("Win")] [Tooltip("This string has to be the name of the scene you want to load when winning")]
public string WinSceneName = "WinScene";
[Tooltip("Duration of delay before the fade-to-black, if winning")]
public float DelayBeforeFadeToBlack = 4f;
[Tooltip("Win game message")]
public string WinGameMessage;
[Tooltip("Duration of delay before the win message")]
public float DelayBeforeWinMessage = 2f;
[Tooltip("Sound played on win")] public AudioClip VictorySound;
[Header("Lose")] [Tooltip("This string has to be the name of the scene you want to load when losing")]
public string LoseSceneName = "LoseScene";
public bool GameIsEnding { get; private set; }
float m_TimeLoadEndGameScene;
string m_SceneToLoad;
void Awake()
{
EventManager.AddListener<AllObjectivesCompletedEvent>(OnAllObjectivesCompleted);
EventManager.AddListener<PlayerDeathEvent>(OnPlayerDeath);
}
void Start()
{
AudioUtility.SetMasterVolume(1);
}
void Update()
{
if (GameIsEnding)
{
float timeRatio = 1 - (m_TimeLoadEndGameScene - Time.time) / EndSceneLoadDelay;
EndGameFadeCanvasGroup.alpha = timeRatio;
AudioUtility.SetMasterVolume(1 - timeRatio);
// See if it's time to load the end scene (after the delay)
if (Time.time >= m_TimeLoadEndGameScene)
{
SceneManager.LoadScene(m_SceneToLoad);
GameIsEnding = false;
}
}
}
void OnAllObjectivesCompleted(AllObjectivesCompletedEvent evt) => EndGame(true);
void OnPlayerDeath(PlayerDeathEvent evt) => EndGame(false);
void EndGame(bool win)
{
// unlocks the cursor before leaving the scene, to be able to click buttons
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
// Remember that we need to load the appropriate end scene after a delay
GameIsEnding = true;
EndGameFadeCanvasGroup.gameObject.SetActive(true);
if (win)
{
m_SceneToLoad = WinSceneName;
m_TimeLoadEndGameScene = Time.time + EndSceneLoadDelay + DelayBeforeFadeToBlack;
// play a sound on win
var audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = VictorySound;
audioSource.playOnAwake = false;
audioSource.outputAudioMixerGroup = AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.HUDVictory);
audioSource.PlayScheduled(AudioSettings.dspTime + DelayBeforeWinMessage);
// create a game message
//var message = Instantiate(WinGameMessagePrefab).GetComponent<DisplayMessage>();
//if (message)
//{
// message.delayBeforeShowing = delayBeforeWinMessage;
// message.GetComponent<Transform>().SetAsLastSibling();
//}
DisplayMessageEvent displayMessage = Events.DisplayMessageEvent;
displayMessage.Message = WinGameMessage;
displayMessage.DelayBeforeDisplay = DelayBeforeWinMessage;
EventManager.Broadcast(displayMessage);
}
else
{
m_SceneToLoad = LoseSceneName;
m_TimeLoadEndGameScene = Time.time + EndSceneLoadDelay;
}
}
void OnDestroy()
{
EventManager.RemoveListener<AllObjectivesCompletedEvent>(OnAllObjectivesCompleted);
EventManager.RemoveListener<PlayerDeathEvent>(OnPlayerDeath);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e1fea5cb3ddbc249bd0181e6766db68
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.Game
{
public class ObjectiveManager : MonoBehaviour
{
List<Objective> m_Objectives = new List<Objective>();
bool m_ObjectivesCompleted = false;
void Awake()
{
Objective.OnObjectiveCreated += RegisterObjective;
}
void RegisterObjective(Objective objective) => m_Objectives.Add(objective);
void Update()
{
if (m_Objectives.Count == 0 || m_ObjectivesCompleted)
return;
for (int i = 0; i < m_Objectives.Count; i++)
{
// pass every objectives to check if they have been completed
if (m_Objectives[i].IsBlocking())
{
// break the loop as soon as we find one uncompleted objective
return;
}
}
m_ObjectivesCompleted = true;
EventManager.Broadcast(Events.AllObjectivesCompletedEvent);
}
void OnDestroy()
{
Objective.OnObjectiveCreated -= RegisterObjective;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5174b5e414847cf4a9dca6e207326c8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,183 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.ProBuilder;
using UnityEngine.Rendering;
namespace Unity.FPS.Game
{
public static class MeshCombineUtility
{
public class RenderBatchData
{
public class MeshAndTrs
{
public Mesh Mesh;
public Matrix4x4 Trs;
public MeshAndTrs(Mesh m, Matrix4x4 t)
{
Mesh = m;
Trs = t;
}
}
public Material Material;
public int SubmeshIndex = 0;
public ShadowCastingMode ShadowMode;
public bool ReceiveShadows;
public MotionVectorGenerationMode MotionVectors;
public List<MeshAndTrs> MeshesWithTrs = new List<MeshAndTrs>();
}
public enum RendererDisposeMethod
{
DestroyGameObject,
DestroyRendererAndFilter,
DisableGameObject,
DisableRenderer,
}
public static void Combine(List<MeshRenderer> renderers, RendererDisposeMethod disposeMethod,
string newObjectName)
{
int renderersCount = renderers.Count;
List<RenderBatchData> renderBatches = new List<RenderBatchData>();
// Build render batches for all unique material + submeshIndex combinations
for (int i = 0; i < renderersCount; i++)
{
MeshRenderer meshRenderer = renderers[i];
if (meshRenderer == null)
continue;
MeshFilter meshFilter = meshRenderer.GetComponent<MeshFilter>();
if (meshFilter == null)
continue;
Mesh mesh = meshFilter.sharedMesh;
if (mesh == null)
continue;
Transform t = meshRenderer.GetComponent<Transform>();
Material[] materials = meshRenderer.sharedMaterials;
for (int s = 0; s < mesh.subMeshCount; s++)
{
if (materials[s] == null)
continue;
int batchIndex = GetExistingRenderBatch(renderBatches, materials[s], meshRenderer, s);
if (batchIndex >= 0)
{
renderBatches[batchIndex].MeshesWithTrs
.Add(new RenderBatchData.MeshAndTrs(mesh,
Matrix4x4.TRS(t.position, t.rotation, t.lossyScale)));
}
else
{
RenderBatchData newBatchData = new RenderBatchData();
newBatchData.Material = materials[s];
newBatchData.SubmeshIndex = s;
newBatchData.ShadowMode = meshRenderer.shadowCastingMode;
newBatchData.ReceiveShadows = meshRenderer.receiveShadows;
newBatchData.MeshesWithTrs.Add(new RenderBatchData.MeshAndTrs(mesh,
Matrix4x4.TRS(t.position, t.rotation, t.lossyScale)));
renderBatches.Add(newBatchData);
}
}
// Destroy probuilder component if present
ProBuilderMesh pbm = meshRenderer.GetComponent<ProBuilderMesh>();
if (pbm)
{
GameObject.Destroy(pbm);
}
switch (disposeMethod)
{
case RendererDisposeMethod.DestroyGameObject:
if (Application.isPlaying)
{
GameObject.Destroy(meshRenderer.gameObject);
}
else
{
GameObject.DestroyImmediate(meshRenderer.gameObject);
}
break;
case RendererDisposeMethod.DestroyRendererAndFilter:
if (Application.isPlaying)
{
GameObject.Destroy(meshRenderer);
GameObject.Destroy(meshFilter);
}
else
{
GameObject.DestroyImmediate(meshRenderer);
GameObject.DestroyImmediate(meshFilter);
}
break;
case RendererDisposeMethod.DisableGameObject:
meshRenderer.gameObject.SetActive(false);
break;
case RendererDisposeMethod.DisableRenderer:
meshRenderer.enabled = false;
break;
}
}
// Combine each unique render batch
for (int i = 0; i < renderBatches.Count; i++)
{
RenderBatchData rbd = renderBatches[i];
Mesh newMesh = new Mesh();
newMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
CombineInstance[] combineInstances = new CombineInstance[rbd.MeshesWithTrs.Count];
for (int j = 0; j < rbd.MeshesWithTrs.Count; j++)
{
combineInstances[j].subMeshIndex = rbd.SubmeshIndex;
combineInstances[j].mesh = rbd.MeshesWithTrs[j].Mesh;
combineInstances[j].transform = rbd.MeshesWithTrs[j].Trs;
}
// Create mesh
newMesh.CombineMeshes(combineInstances);
newMesh.RecalculateBounds();
// Create the gameObject
GameObject combinedObject = new GameObject(newObjectName);
MeshFilter mf = combinedObject.AddComponent<MeshFilter>();
mf.sharedMesh = newMesh;
MeshRenderer mr = combinedObject.AddComponent<MeshRenderer>();
mr.sharedMaterial = rbd.Material;
mr.shadowCastingMode = rbd.ShadowMode;
}
}
static int GetExistingRenderBatch(List<RenderBatchData> renderBatches, Material mat, MeshRenderer ren, int submeshIndex)
{
for (int i = 0; i < renderBatches.Count; i++)
{
RenderBatchData data = renderBatches[i];
if (data.Material == mat &&
data.SubmeshIndex == submeshIndex &&
data.ShadowMode == ren.shadowCastingMode &&
data.ReceiveShadows == ren.receiveShadows)
{
return i;
}
}
return -1;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 288522e658b17b44c984ef72bffcda11
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+111
View File
@@ -0,0 +1,111 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.Game
{
public class MeshCombiner : MonoBehaviour
{
public List<GameObject> CombineParents = new List<GameObject>();
[Header("Grid parameters")] public bool UseGrid = false;
public Vector3 GridCenter;
public Vector3 GridExtents = new Vector3(10, 10, 10);
public Vector3Int GridResolution = new Vector3Int(2, 2, 2);
public Color GridPreviewColor = Color.green;
void Start()
{
Combine();
}
public void Combine()
{
List<MeshRenderer> validRenderers = new List<MeshRenderer>();
foreach (GameObject combineParent in CombineParents)
{
validRenderers.AddRange(combineParent.GetComponentsInChildren<MeshRenderer>());
}
if (UseGrid)
{
for (int i = 0; i < GetGridCellCount(); i++)
{
if (GetGridCellBounds(i, out Bounds bounds))
{
CombineAllInBounds(bounds, validRenderers);
}
}
}
else
{
MeshCombineUtility.Combine(validRenderers,
MeshCombineUtility.RendererDisposeMethod.DestroyRendererAndFilter, "Level_Combined");
}
}
void CombineAllInBounds(Bounds bounds, List<MeshRenderer> validRenderers)
{
List<MeshRenderer> renderersForThisCell = new List<MeshRenderer>();
for (int i = validRenderers.Count - 1; i >= 0; i--)
{
MeshRenderer m = validRenderers[i];
if (bounds.Intersects(m.bounds))
{
renderersForThisCell.Add(m);
validRenderers.Remove(m);
}
}
if (renderersForThisCell.Count > 0)
{
MeshCombineUtility.Combine(renderersForThisCell,
MeshCombineUtility.RendererDisposeMethod.DestroyRendererAndFilter, "Level_Combined");
}
}
int GetGridCellCount()
{
return GridResolution.x * GridResolution.y * GridResolution.z;
}
public bool GetGridCellBounds(int index, out Bounds bounds)
{
bounds = default;
if (index < 0 || index >= GetGridCellCount())
return false;
int xCoord = index / (GridResolution.y * GridResolution.z);
int yCoord = (index / GridResolution.z) % GridResolution.y;
int zCoord = index % GridResolution.z;
Vector3 gridBottomCorner = GridCenter - (GridExtents * 0.5f);
Vector3 cellSize = new Vector3(GridExtents.x / (float) GridResolution.x,
GridExtents.y / (float) GridResolution.y, GridExtents.z / (float) GridResolution.z);
Vector3 cellCenter = gridBottomCorner + (new Vector3((xCoord * cellSize.x) + (cellSize.x * 0.5f),
(yCoord * cellSize.y) + (cellSize.y * 0.5f),
(zCoord * cellSize.z) + (cellSize.z * 0.5f)));
bounds.center = cellCenter;
bounds.size = cellSize;
return true;
}
void OnDrawGizmosSelected()
{
if (UseGrid)
{
Gizmos.color = GridPreviewColor;
for (int i = 0; i < GetGridCellCount(); i++)
{
if (GetGridCellBounds(i, out Bounds bounds))
{
Gizmos.DrawWireCube(bounds.center, bounds.size);
}
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eec9f03de17857a48ac47c6ae8577551
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using UnityEngine;
namespace Unity.FPS.Game
{
[System.Serializable]
public struct MinMaxFloat
{
public float Min;
public float Max;
public float GetValueFromRatio(float ratio)
{
return Mathf.Lerp(Min, Max, ratio);
}
}
[System.Serializable]
public struct MinMaxColor
{
[ColorUsage(true, true)] public Color Min;
[ColorUsage(true, true)] public Color Max;
public Color GetValueFromRatio(float ratio)
{
return Color.Lerp(Min, Max, ratio);
}
}
[System.Serializable]
public struct MinMaxVector3
{
public Vector3 Min;
public Vector3 Max;
public Vector3 GetValueFromRatio(float ratio)
{
return Vector3.Lerp(Min, Max, ratio);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9861723f7fe90784fa94a969e322fcd2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+18
View File
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.Game
{
public class PrefabReplacer : MonoBehaviour
{
[System.Serializable]
public struct ReplacementDefinition
{
public GameObject SourcePrefab;
public GameObject TargetPrefab;
}
public bool SwitchOrder;
public List<ReplacementDefinition> Replacements = new List<ReplacementDefinition>();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 55cca505d6267e94fbd3796f4f059ec8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
namespace Unity.FPS.Game
{
[ExecuteInEditMode]
public class PrefabReplacerOnInstance : MonoBehaviour
{
public GameObject TargetPrefab;
void Awake()
{
#if UNITY_EDITOR
List<GameObject> allPrefabObjectsInScene = new List<GameObject>();
foreach (Transform t in GameObject.FindObjectsByType<Transform>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
if (PrefabUtility.IsAnyPrefabInstanceRoot(t.gameObject))
{
allPrefabObjectsInScene.Add(t.gameObject);
}
}
foreach (GameObject go in allPrefabObjectsInScene)
{
GameObject instanceSource = PrefabUtility.GetCorrespondingObjectFromSource(go);
if (instanceSource == TargetPrefab)
{
transform.SetParent(go.transform.parent);
transform.position = go.transform.position;
transform.rotation = go.transform.rotation;
transform.localScale = go.transform.localScale;
// Undo.Register
Undo.DestroyObjectImmediate(go);
Debug.Log("Replaced prefab in scene");
DestroyImmediate(this);
break;
}
}
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 66a57e166e75d154ab24c723fe6e96f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8202695dcc7a5e1418d444571d7800cb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.Game
{
public class DamageArea : MonoBehaviour
{
[Tooltip("Area of damage when the projectile hits something")]
public float AreaOfEffectDistance = 5f;
[Tooltip("Damage multiplier over distance for area of effect")]
public AnimationCurve DamageRatioOverDistance;
[Header("Debug")] [Tooltip("Color of the area of effect radius")]
public Color AreaOfEffectColor = Color.red * 0.5f;
public void InflictDamageInArea(float damage, Vector3 center, LayerMask layers,
QueryTriggerInteraction interaction, GameObject owner)
{
Dictionary<Health, Damageable> uniqueDamagedHealths = new Dictionary<Health, Damageable>();
// Create a collection of unique health components that would be damaged in the area of effect (in order to avoid damaging a same entity multiple times)
Collider[] affectedColliders = Physics.OverlapSphere(center, AreaOfEffectDistance, layers, interaction);
foreach (var coll in affectedColliders)
{
Damageable damageable = coll.GetComponent<Damageable>();
if (damageable)
{
Health health = damageable.GetComponentInParent<Health>();
if (health && !uniqueDamagedHealths.ContainsKey(health))
{
uniqueDamagedHealths.Add(health, damageable);
}
}
}
// Apply damages with distance falloff
foreach (Damageable uniqueDamageable in uniqueDamagedHealths.Values)
{
float distance = Vector3.Distance(uniqueDamageable.transform.position, transform.position);
uniqueDamageable.InflictDamage(
damage * DamageRatioOverDistance.Evaluate(distance / AreaOfEffectDistance), true, owner);
}
}
void OnDrawGizmosSelected()
{
Gizmos.color = AreaOfEffectColor;
Gizmos.DrawSphere(transform.position, AreaOfEffectDistance);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0244d6b9e98532469c7be0732490a61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class Damageable : MonoBehaviour
{
[Tooltip("Multiplier to apply to the received damage")]
public float DamageMultiplier = 1f;
[Range(0, 1)] [Tooltip("Multiplier to apply to self damage")]
public float SensibilityToSelfdamage = 0.5f;
public Health Health { get; private set; }
void Awake()
{
// find the health component either at the same level, or higher in the hierarchy
Health = GetComponent<Health>();
if (!Health)
{
Health = GetComponentInParent<Health>();
}
}
public void InflictDamage(float damage, bool isExplosionDamage, GameObject damageSource)
{
if (Health)
{
var totalDamage = damage;
// skip the crit multiplier if it's from an explosion
if (!isExplosionDamage)
{
totalDamage *= DamageMultiplier;
}
// potentially reduce damages if inflicted by self
if (Health.gameObject == damageSource)
{
totalDamage *= SensibilityToSelfdamage;
}
// apply the damages
Health.TakeDamage(totalDamage, damageSource);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f9b0ce1bb69c96b49a7ac1098a7f5796
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class Destructable : MonoBehaviour
{
Health m_Health;
void Start()
{
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, Destructable>(m_Health, this, gameObject);
// Subscribe to damage & death actions
m_Health.OnDie += OnDie;
m_Health.OnDamaged += OnDamaged;
}
void OnDamaged(float damage, GameObject damageSource)
{
// TODO: damage reaction
}
void OnDie()
{
// this will call the OnDestroy function
Destroy(gameObject);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f0914267284bd7468f8ac89253e1557
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+87
View File
@@ -0,0 +1,87 @@
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Game
{
public class Health : MonoBehaviour
{
[Tooltip("Maximum amount of health")] public float MaxHealth = 10f;
[Tooltip("Health ratio at which the critical health vignette starts appearing")]
public float CriticalHealthRatio = 0.3f;
public UnityAction<float, GameObject> OnDamaged;
public UnityAction<float> OnHealed;
public UnityAction OnDie;
public float CurrentHealth { get; set; }
public bool Invincible { get; set; }
public bool CanPickup() => CurrentHealth < MaxHealth;
public float GetRatio() => CurrentHealth / MaxHealth;
public bool IsCritical() => GetRatio() <= CriticalHealthRatio;
bool m_IsDead;
void Start()
{
CurrentHealth = MaxHealth;
}
public void Heal(float healAmount)
{
float healthBefore = CurrentHealth;
CurrentHealth += healAmount;
CurrentHealth = Mathf.Clamp(CurrentHealth, 0f, MaxHealth);
// call OnHeal action
float trueHealAmount = CurrentHealth - healthBefore;
if (trueHealAmount > 0f)
{
OnHealed?.Invoke(trueHealAmount);
}
}
public void TakeDamage(float damage, GameObject damageSource)
{
if (Invincible)
return;
float healthBefore = CurrentHealth;
CurrentHealth -= damage;
CurrentHealth = Mathf.Clamp(CurrentHealth, 0f, MaxHealth);
// call OnDamage action
float trueDamageAmount = healthBefore - CurrentHealth;
if (trueDamageAmount > 0f)
{
OnDamaged?.Invoke(trueDamageAmount, damageSource);
}
HandleDeath();
}
public void Kill()
{
CurrentHealth = 0f;
// call OnDamage action
OnDamaged?.Invoke(MaxHealth, null);
HandleDeath();
}
void HandleDeath()
{
if (m_IsDead)
return;
// call OnDie action
if (CurrentHealth <= 0f)
{
m_IsDead = true;
OnDie?.Invoke();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e27364eefd6dd0341ba72a3bfc0d9727
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,62 @@
using System;
using UnityEngine;
namespace Unity.FPS.Game
{
public abstract class Objective : MonoBehaviour
{
[Tooltip("Name of the objective that will be shown on screen")]
public string Title;
[Tooltip("Short text explaining the objective that will be shown on screen")]
public string Description;
[Tooltip("Whether the objective is required to win or not")]
public bool IsOptional;
[Tooltip("Delay before the objective becomes visible")]
public float DelayVisible;
public bool IsCompleted { get; private set; }
public bool IsBlocking() => !(IsOptional || IsCompleted);
public static event Action<Objective> OnObjectiveCreated;
public static event Action<Objective> OnObjectiveCompleted;
protected virtual void Start()
{
OnObjectiveCreated?.Invoke(this);
DisplayMessageEvent displayMessage = Events.DisplayMessageEvent;
displayMessage.Message = Title;
displayMessage.DelayBeforeDisplay = 0.0f;
EventManager.Broadcast(displayMessage);
}
public void UpdateObjective(string descriptionText, string counterText, string notificationText)
{
ObjectiveUpdateEvent evt = Events.ObjectiveUpdateEvent;
evt.Objective = this;
evt.DescriptionText = descriptionText;
evt.CounterText = counterText;
evt.NotificationText = notificationText;
evt.IsComplete = IsCompleted;
EventManager.Broadcast(evt);
}
public void CompleteObjective(string descriptionText, string counterText, string notificationText)
{
IsCompleted = true;
ObjectiveUpdateEvent evt = Events.ObjectiveUpdateEvent;
evt.Objective = this;
evt.DescriptionText = descriptionText;
evt.CounterText = counterText;
evt.NotificationText = notificationText;
evt.IsComplete = IsCompleted;
EventManager.Broadcast(evt);
OnObjectiveCompleted?.Invoke(this);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7535462baa2ed0d469f708cbe145b720
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Game
{
public abstract class ProjectileBase : MonoBehaviour
{
public GameObject Owner { get; private set; }
public Vector3 InitialPosition { get; private set; }
public Vector3 InitialDirection { get; private set; }
public Vector3 InheritedMuzzleVelocity { get; private set; }
public float InitialCharge { get; private set; }
public UnityAction OnShoot;
public void Shoot(WeaponController controller)
{
Owner = controller.Owner;
InitialPosition = transform.position;
InitialDirection = transform.forward;
InheritedMuzzleVelocity = controller.MuzzleWorldVelocity;
InitialCharge = controller.CurrentCharge;
OnShoot?.Invoke();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1962d3d25050a4a44819b089007baf39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,502 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Game
{
public enum WeaponShootType
{
Manual,
Automatic,
Charge,
}
[System.Serializable]
public struct CrosshairData
{
[Tooltip("The image that will be used for this weapon's crosshair")]
public Sprite CrosshairSprite;
[Tooltip("The size of the crosshair image")]
public int CrosshairSize;
[Tooltip("The color of the crosshair image")]
public Color CrosshairColor;
}
[RequireComponent(typeof(AudioSource))]
public class WeaponController : MonoBehaviour
{
[Header("Information")] [Tooltip("The name that will be displayed in the UI for this weapon")]
public string WeaponName;
[Tooltip("The image that will be displayed in the UI for this weapon")]
public Sprite WeaponIcon;
[Tooltip("Default data for the crosshair")]
public CrosshairData CrosshairDataDefault;
[Tooltip("Data for the crosshair when targeting an enemy")]
public CrosshairData CrosshairDataTargetInSight;
[Header("Internal References")]
[Tooltip("The root object for the weapon, this is what will be deactivated when the weapon isn't active")]
public GameObject WeaponRoot;
[Tooltip("Tip of the weapon, where the projectiles are shot")]
public Transform WeaponMuzzle;
[Header("Shoot Parameters")] [Tooltip("The type of weapon wil affect how it shoots")]
public WeaponShootType ShootType;
[Tooltip("The projectile prefab")] public ProjectileBase ProjectilePrefab;
[Tooltip("Minimum duration between two shots")]
public float DelayBetweenShots = 0.5f;
[Tooltip("Angle for the cone in which the bullets will be shot randomly (0 means no spread at all)")]
public float BulletSpreadAngle = 0f;
[Tooltip("Amount of bullets per shot")]
public int BulletsPerShot = 1;
[Tooltip("Force that will push back the weapon after each shot")] [Range(0f, 2f)]
public float RecoilForce = 1;
[Tooltip("Ratio of the default FOV that this weapon applies while aiming")] [Range(0f, 1f)]
public float AimZoomRatio = 1f;
[Tooltip("Translation to apply to weapon arm when aiming with this weapon")]
public Vector3 AimOffset;
[Header("Ammo Parameters")]
[Tooltip("Should the player manually reload")]
public bool AutomaticReload = true;
[Tooltip("Has physical clip on the weapon and ammo shells are ejected when firing")]
public bool HasPhysicalBullets = false;
[Tooltip("Number of bullets in a clip")]
public int ClipSize = 30;
[Tooltip("Bullet Shell Casing")]
public GameObject ShellCasing;
[Tooltip("Weapon Ejection Port for physical ammo")]
public Transform EjectionPort;
[Tooltip("Force applied on the shell")]
[Range(0.0f, 5.0f)] public float ShellCasingEjectionForce = 2.0f;
[Tooltip("Maximum number of shell that can be spawned before reuse")]
[Range(1, 30)] public int ShellPoolSize = 1;
[Tooltip("Amount of ammo reloaded per second")]
public float AmmoReloadRate = 1f;
[Tooltip("Delay after the last shot before starting to reload")]
public float AmmoReloadDelay = 2f;
[Tooltip("Maximum amount of ammo in the gun")]
public int MaxAmmo = 8;
[Header("Charging parameters (charging weapons only)")]
[Tooltip("Trigger a shot when maximum charge is reached")]
public bool AutomaticReleaseOnCharged;
[Tooltip("Duration to reach maximum charge")]
public float MaxChargeDuration = 2f;
[Tooltip("Initial ammo used when starting to charge")]
public float AmmoUsedOnStartCharge = 1f;
[Tooltip("Additional ammo used when charge reaches its maximum")]
public float AmmoUsageRateWhileCharging = 1f;
[Header("Audio & Visual")]
[Tooltip("Optional weapon animator for OnShoot animations")]
public Animator WeaponAnimator;
[Tooltip("Prefab of the muzzle flash")]
public GameObject MuzzleFlashPrefab;
[Tooltip("Unparent the muzzle flash instance on spawn")]
public bool UnparentMuzzleFlash;
[Tooltip("sound played when shooting")]
public AudioClip ShootSfx;
[Tooltip("Sound played when changing to this weapon")]
public AudioClip ChangeWeaponSfx;
[Tooltip("Continuous Shooting Sound")] public bool UseContinuousShootSound = false;
public AudioClip ContinuousShootStartSfx;
public AudioClip ContinuousShootLoopSfx;
public AudioClip ContinuousShootEndSfx;
AudioSource m_ContinuousShootAudioSource = null;
bool m_WantsToShoot = false;
public UnityAction OnShoot;
public event Action OnShootProcessed;
int m_CarriedPhysicalBullets;
float m_CurrentAmmo;
float m_LastTimeShot = Mathf.NegativeInfinity;
public float LastChargeTriggerTimestamp { get; private set; }
Vector3 m_LastMuzzlePosition;
public GameObject Owner { get; set; }
public GameObject SourcePrefab { get; set; }
public bool IsCharging { get; private set; }
public float CurrentAmmoRatio { get; private set; }
public bool IsWeaponActive { get; private set; }
public bool IsCooling { get; private set; }
public float CurrentCharge { get; private set; }
public Vector3 MuzzleWorldVelocity { get; private set; }
public float GetAmmoNeededToShoot() =>
(ShootType != WeaponShootType.Charge ? 1f : Mathf.Max(1f, AmmoUsedOnStartCharge)) /
(MaxAmmo * BulletsPerShot);
public int GetCarriedPhysicalBullets() => m_CarriedPhysicalBullets;
public int GetCurrentAmmo() => Mathf.FloorToInt(m_CurrentAmmo);
AudioSource m_ShootAudioSource;
public bool IsReloading { get; private set; }
const string k_AnimAttackParameter = "Attack";
private Queue<Rigidbody> m_PhysicalAmmoPool;
void Awake()
{
m_CurrentAmmo = MaxAmmo;
m_CarriedPhysicalBullets = HasPhysicalBullets ? ClipSize : 0;
m_LastMuzzlePosition = WeaponMuzzle.position;
m_ShootAudioSource = GetComponent<AudioSource>();
DebugUtility.HandleErrorIfNullGetComponent<AudioSource, WeaponController>(m_ShootAudioSource, this,
gameObject);
if (UseContinuousShootSound)
{
m_ContinuousShootAudioSource = gameObject.AddComponent<AudioSource>();
m_ContinuousShootAudioSource.playOnAwake = false;
m_ContinuousShootAudioSource.clip = ContinuousShootLoopSfx;
m_ContinuousShootAudioSource.outputAudioMixerGroup =
AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponShoot);
m_ContinuousShootAudioSource.loop = true;
}
if (HasPhysicalBullets)
{
m_PhysicalAmmoPool = new Queue<Rigidbody>(ShellPoolSize);
for (int i = 0; i < ShellPoolSize; i++)
{
GameObject shell = Instantiate(ShellCasing, transform);
shell.SetActive(false);
m_PhysicalAmmoPool.Enqueue(shell.GetComponent<Rigidbody>());
}
}
}
public void AddCarriablePhysicalBullets(int count) => m_CarriedPhysicalBullets = Mathf.Max(m_CarriedPhysicalBullets + count, MaxAmmo);
void ShootShell()
{
Rigidbody nextShell = m_PhysicalAmmoPool.Dequeue();
nextShell.transform.position = EjectionPort.transform.position;
nextShell.transform.rotation = EjectionPort.transform.rotation;
nextShell.gameObject.SetActive(true);
nextShell.transform.SetParent(null);
nextShell.collisionDetectionMode = CollisionDetectionMode.Continuous;
nextShell.AddForce(nextShell.transform.up * ShellCasingEjectionForce, ForceMode.Impulse);
m_PhysicalAmmoPool.Enqueue(nextShell);
}
void PlaySFX(AudioClip sfx) => AudioUtility.CreateSFX(sfx, transform.position, AudioUtility.AudioGroups.WeaponShoot, 0.0f);
void Reload()
{
if (m_CarriedPhysicalBullets > 0)
{
m_CurrentAmmo = Mathf.Min(m_CarriedPhysicalBullets, ClipSize);
}
IsReloading = false;
}
public void StartReloadAnimation()
{
if (m_CurrentAmmo < m_CarriedPhysicalBullets)
{
GetComponent<Animator>().SetTrigger("Reload");
IsReloading = true;
}
}
void Update()
{
UpdateAmmo();
UpdateCharge();
UpdateContinuousShootSound();
if (Time.deltaTime > 0)
{
MuzzleWorldVelocity = (WeaponMuzzle.position - m_LastMuzzlePosition) / Time.deltaTime;
m_LastMuzzlePosition = WeaponMuzzle.position;
}
}
void UpdateAmmo()
{
if (AutomaticReload && m_LastTimeShot + AmmoReloadDelay < Time.time && m_CurrentAmmo < MaxAmmo && !IsCharging)
{
// reloads weapon over time
m_CurrentAmmo += AmmoReloadRate * Time.deltaTime;
// limits ammo to max value
m_CurrentAmmo = Mathf.Clamp(m_CurrentAmmo, 0, MaxAmmo);
IsCooling = true;
}
else
{
IsCooling = false;
}
if (MaxAmmo == Mathf.Infinity)
{
CurrentAmmoRatio = 1f;
}
else
{
CurrentAmmoRatio = m_CurrentAmmo / MaxAmmo;
}
}
void UpdateCharge()
{
if (IsCharging)
{
if (CurrentCharge < 1f)
{
float chargeLeft = 1f - CurrentCharge;
// Calculate how much charge ratio to add this frame
float chargeAdded = 0f;
if (MaxChargeDuration <= 0f)
{
chargeAdded = chargeLeft;
}
else
{
chargeAdded = (1f / MaxChargeDuration) * Time.deltaTime;
}
chargeAdded = Mathf.Clamp(chargeAdded, 0f, chargeLeft);
// See if we can actually add this charge
float ammoThisChargeWouldRequire = chargeAdded * AmmoUsageRateWhileCharging;
if (ammoThisChargeWouldRequire <= m_CurrentAmmo)
{
// Use ammo based on charge added
UseAmmo(ammoThisChargeWouldRequire);
// set current charge ratio
CurrentCharge = Mathf.Clamp01(CurrentCharge + chargeAdded);
}
}
}
}
void UpdateContinuousShootSound()
{
if (UseContinuousShootSound)
{
if (m_WantsToShoot && m_CurrentAmmo >= 1f)
{
if (!m_ContinuousShootAudioSource.isPlaying)
{
m_ShootAudioSource.PlayOneShot(ShootSfx);
m_ShootAudioSource.PlayOneShot(ContinuousShootStartSfx);
m_ContinuousShootAudioSource.Play();
}
}
else if (m_ContinuousShootAudioSource.isPlaying)
{
m_ShootAudioSource.PlayOneShot(ContinuousShootEndSfx);
m_ContinuousShootAudioSource.Stop();
}
}
}
public void ShowWeapon(bool show)
{
WeaponRoot.SetActive(show);
if (show && ChangeWeaponSfx)
{
// m_ShootAudioSource.PlayOneShot(ChangeWeaponSfx);
}
IsWeaponActive = show;
}
public void UseAmmo(float amount)
{
m_CurrentAmmo = Mathf.Clamp(m_CurrentAmmo - amount, 0f, MaxAmmo);
m_CarriedPhysicalBullets -= Mathf.RoundToInt(amount);
m_CarriedPhysicalBullets = Mathf.Clamp(m_CarriedPhysicalBullets, 0, MaxAmmo);
m_LastTimeShot = Time.time;
}
public bool HandleShootInputs(bool inputDown, bool inputHeld, bool inputUp)
{
m_WantsToShoot = inputDown || inputHeld;
switch (ShootType)
{
case WeaponShootType.Manual:
if (inputDown)
{
return TryShoot();
}
return false;
case WeaponShootType.Automatic:
if (inputHeld)
{
return TryShoot();
}
return false;
case WeaponShootType.Charge:
if (inputHeld)
{
TryBeginCharge();
}
// Check if we released charge or if the weapon shoot autmatically when it's fully charged
if (inputUp || (AutomaticReleaseOnCharged && CurrentCharge >= 1f))
{
return TryReleaseCharge();
}
return false;
default:
return false;
}
}
bool TryShoot()
{
if (m_CurrentAmmo >= 1f
&& m_LastTimeShot + DelayBetweenShots < Time.time)
{
HandleShoot();
m_CurrentAmmo -= 1f;
return true;
}
return false;
}
bool TryBeginCharge()
{
if (!IsCharging
&& m_CurrentAmmo >= AmmoUsedOnStartCharge
&& Mathf.FloorToInt((m_CurrentAmmo - AmmoUsedOnStartCharge) * BulletsPerShot) > 0
&& m_LastTimeShot + DelayBetweenShots < Time.time)
{
UseAmmo(AmmoUsedOnStartCharge);
LastChargeTriggerTimestamp = Time.time;
IsCharging = true;
return true;
}
return false;
}
bool TryReleaseCharge()
{
if (IsCharging)
{
HandleShoot();
CurrentCharge = 0f;
IsCharging = false;
return true;
}
return false;
}
void HandleShoot()
{
int bulletsPerShotFinal = ShootType == WeaponShootType.Charge
? Mathf.CeilToInt(CurrentCharge * BulletsPerShot)
: BulletsPerShot;
// spawn all bullets with random direction
for (int i = 0; i < bulletsPerShotFinal; i++)
{
Vector3 shotDirection = GetShotDirectionWithinSpread(WeaponMuzzle);
ProjectileBase newProjectile = Instantiate(ProjectilePrefab, WeaponMuzzle.position,
Quaternion.LookRotation(shotDirection));
newProjectile.Shoot(this);
}
// muzzle flash
if (MuzzleFlashPrefab != null)
{
GameObject muzzleFlashInstance = Instantiate(MuzzleFlashPrefab, WeaponMuzzle.position,
WeaponMuzzle.rotation, WeaponMuzzle.transform);
// Unparent the muzzleFlashInstance
if (UnparentMuzzleFlash)
{
muzzleFlashInstance.transform.SetParent(null);
}
Destroy(muzzleFlashInstance, 2f);
}
if (HasPhysicalBullets)
{
ShootShell();
m_CarriedPhysicalBullets--;
}
m_LastTimeShot = Time.time;
// play shoot SFX
if (ShootSfx && !UseContinuousShootSound)
{
m_ShootAudioSource.PlayOneShot(ShootSfx);
}
// Trigger attack animation if there is any
if (WeaponAnimator)
{
WeaponAnimator.SetTrigger(k_AnimAttackParameter);
}
OnShoot?.Invoke();
OnShootProcessed?.Invoke();
}
public Vector3 GetShotDirectionWithinSpread(Transform shootTransform)
{
float spreadAngleRatio = BulletSpreadAngle / 180f;
Vector3 spreadWorldDirection = Vector3.Slerp(shootTransform.forward, UnityEngine.Random.insideUnitSphere,
spreadAngleRatio);
return spreadWorldDirection;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ad90ba0b1477764da373f5ec01a8cb5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class TimedSelfDestruct : MonoBehaviour
{
public float LifeTime = 1f;
float m_SpawnTime;
void Awake()
{
m_SpawnTime = Time.time;
}
void Update()
{
if (Time.time > m_SpawnTime + LifeTime)
{
Destroy(gameObject);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09df4c6380233d34397a4a7506903302
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -0,0 +1,15 @@
{
"name": "fps.Game",
"references": [
"GUID:1826c0224c0d048b88112c79bbb0cd85"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9c5543eabb73a6249ac07b2c065ee8b4
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d442422ab15c9024e9edea7b5d39e133
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+34
View File
@@ -0,0 +1,34 @@
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);
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4bb683ec2991b784fa1d1fea214ca53b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,45 @@
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));
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da2b0de22b1558e4caa11aa3696e87fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,159 @@
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();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b92ab16a4151e294a9e6f5a025221be2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
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);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 828abdfaec1bdff4dbda622331d65ba5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+154
View File
@@ -0,0 +1,154 @@
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 48f22a420493bcf4a8ca3e259797484f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
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);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd5fd1bc5d131a34791bbd68d41d568f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More