update
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfbed033b9b1c0d4984f232a7c86567e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1857b2df8de24b24190ce9e067d2e1e4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4cb5371dba09ee46806cbe0c2aa2c9c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ec2f49cb5488d0459ff53ecc714acb7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "fps.AI",
|
||||
"references": [
|
||||
"GUID:9c5543eabb73a6249ac07b2c065ee8b4"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9dc24b3d0971d64a9e95b6986d97af5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25ff9b5709276df40a3b4650a7247ced
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca85cc8b78b3c1d469a63fe657be17e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81b4c4e7c5d6cd34bb04cb46517b4e23
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1ebb8b94dd6b9f4292a7a4a2996fed1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d442422ab15c9024e9edea7b5d39e133
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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
Reference in New Issue
Block a user