update
This commit is contained in:
@@ -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:
|
||||
Reference in New Issue
Block a user