2021년 4학년 1학기 기업연계프로젝트2 컴퓨터소프트웨어공학과 <원광투어팀> 팀장 : 송유진 팀원 : 김나영, 이경희, 한유진
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

500 lines
17 KiB

5 years ago
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.Events;
  5. namespace Unity.FPS.Game
  6. {
  7. public enum WeaponShootType
  8. {
  9. Manual,
  10. Automatic,
  11. Charge,
  12. }
  13. [System.Serializable]
  14. public struct CrosshairData
  15. {
  16. [Tooltip("The image that will be used for this weapon's crosshair")]
  17. public Sprite CrosshairSprite;
  18. [Tooltip("The size of the crosshair image")]
  19. public int CrosshairSize;
  20. [Tooltip("The color of the crosshair image")]
  21. public Color CrosshairColor;
  22. }
  23. [RequireComponent(typeof(AudioSource))]
  24. public class WeaponController : MonoBehaviour
  25. {
  26. [Header("Information")] [Tooltip("The name that will be displayed in the UI for this weapon")]
  27. public string WeaponName;
  28. [Tooltip("The image that will be displayed in the UI for this weapon")]
  29. public Sprite WeaponIcon;
  30. [Tooltip("Default data for the crosshair")]
  31. public CrosshairData CrosshairDataDefault;
  32. [Tooltip("Data for the crosshair when targeting an enemy")]
  33. public CrosshairData CrosshairDataTargetInSight;
  34. [Header("Internal References")]
  35. [Tooltip("The root object for the weapon, this is what will be deactivated when the weapon isn't active")]
  36. public GameObject WeaponRoot;
  37. [Tooltip("Tip of the weapon, where the projectiles are shot")]
  38. public Transform WeaponMuzzle;
  39. [Header("Shoot Parameters")] [Tooltip("The type of weapon wil affect how it shoots")]
  40. public WeaponShootType ShootType;
  41. [Tooltip("The projectile prefab")] public ProjectileBase ProjectilePrefab;
  42. [Tooltip("Minimum duration between two shots")]
  43. public float DelayBetweenShots = 0.5f;
  44. [Tooltip("Angle for the cone in which the bullets will be shot randomly (0 means no spread at all)")]
  45. public float BulletSpreadAngle = 0f;
  46. [Tooltip("Amount of bullets per shot")]
  47. public int BulletsPerShot = 1;
  48. [Tooltip("Force that will push back the weapon after each shot")] [Range(0f, 2f)]
  49. public float RecoilForce = 1;
  50. [Tooltip("Ratio of the default FOV that this weapon applies while aiming")] [Range(0f, 1f)]
  51. public float AimZoomRatio = 1f;
  52. [Tooltip("Translation to apply to weapon arm when aiming with this weapon")]
  53. public Vector3 AimOffset;
  54. [Header("Ammo Parameters")]
  55. [Tooltip("Should the player manually reload")]
  56. public bool AutomaticReload = true;
  57. [Tooltip("Has physical clip on the weapon and ammo shells are ejected when firing")]
  58. public bool HasPhysicalBullets = false;
  59. [Tooltip("Number of bullets in a clip")]
  60. public int ClipSize = 30;
  61. [Tooltip("Bullet Shell Casing")]
  62. public GameObject ShellCasing;
  63. [Tooltip("Weapon Ejection Port for physical ammo")]
  64. public Transform EjectionPort;
  65. [Tooltip("Force applied on the shell")]
  66. [Range(0.0f, 5.0f)] public float ShellCasingEjectionForce = 2.0f;
  67. [Tooltip("Maximum number of shell that can be spawned before reuse")]
  68. [Range(1, 30)] public int ShellPoolSize = 1;
  69. [Tooltip("Amount of ammo reloaded per second")]
  70. public float AmmoReloadRate = 1f;
  71. [Tooltip("Delay after the last shot before starting to reload")]
  72. public float AmmoReloadDelay = 2f;
  73. [Tooltip("Maximum amount of ammo in the gun")]
  74. public int MaxAmmo = 8;
  75. [Header("Charging parameters (charging weapons only)")]
  76. [Tooltip("Trigger a shot when maximum charge is reached")]
  77. public bool AutomaticReleaseOnCharged;
  78. [Tooltip("Duration to reach maximum charge")]
  79. public float MaxChargeDuration = 2f;
  80. [Tooltip("Initial ammo used when starting to charge")]
  81. public float AmmoUsedOnStartCharge = 1f;
  82. [Tooltip("Additional ammo used when charge reaches its maximum")]
  83. public float AmmoUsageRateWhileCharging = 1f;
  84. [Header("Audio & Visual")]
  85. [Tooltip("Optional weapon animator for OnShoot animations")]
  86. public Animator WeaponAnimator;
  87. [Tooltip("Prefab of the muzzle flash")]
  88. public GameObject MuzzleFlashPrefab;
  89. [Tooltip("Unparent the muzzle flash instance on spawn")]
  90. public bool UnparentMuzzleFlash;
  91. [Tooltip("sound played when shooting")]
  92. public AudioClip ShootSfx;
  93. [Tooltip("Sound played when changing to this weapon")]
  94. public AudioClip ChangeWeaponSfx;
  95. [Tooltip("Continuous Shooting Sound")] public bool UseContinuousShootSound = false;
  96. public AudioClip ContinuousShootStartSfx;
  97. public AudioClip ContinuousShootLoopSfx;
  98. public AudioClip ContinuousShootEndSfx;
  99. AudioSource m_ContinuousShootAudioSource = null;
  100. bool m_WantsToShoot = false;
  101. public UnityAction OnShoot;
  102. public event Action OnShootProcessed;
  103. int m_CarriedPhysicalBullets;
  104. float m_CurrentAmmo;
  105. float m_LastTimeShot = Mathf.NegativeInfinity;
  106. public float LastChargeTriggerTimestamp { get; private set; }
  107. Vector3 m_LastMuzzlePosition;
  108. public GameObject Owner { get; set; }
  109. public GameObject SourcePrefab { get; set; }
  110. public bool IsCharging { get; private set; }
  111. public float CurrentAmmoRatio { get; private set; }
  112. public bool IsWeaponActive { get; private set; }
  113. public bool IsCooling { get; private set; }
  114. public float CurrentCharge { get; private set; }
  115. public Vector3 MuzzleWorldVelocity { get; private set; }
  116. public float GetAmmoNeededToShoot() =>
  117. (ShootType != WeaponShootType.Charge ? 1f : Mathf.Max(1f, AmmoUsedOnStartCharge)) /
  118. (MaxAmmo * BulletsPerShot);
  119. public int GetCarriedPhysicalBullets() => m_CarriedPhysicalBullets;
  120. AudioSource m_ShootAudioSource;
  121. public bool IsReloading { get; private set; }
  122. const string k_AnimAttackParameter = "Attack";
  123. private Queue<Rigidbody> m_PhysicalAmmoPool;
  124. void Awake()
  125. {
  126. m_CurrentAmmo = MaxAmmo;
  127. m_CarriedPhysicalBullets = ClipSize;
  128. m_LastMuzzlePosition = WeaponMuzzle.position;
  129. m_ShootAudioSource = GetComponent<AudioSource>();
  130. DebugUtility.HandleErrorIfNullGetComponent<AudioSource, WeaponController>(m_ShootAudioSource, this,
  131. gameObject);
  132. if (UseContinuousShootSound)
  133. {
  134. m_ContinuousShootAudioSource = gameObject.AddComponent<AudioSource>();
  135. m_ContinuousShootAudioSource.playOnAwake = false;
  136. m_ContinuousShootAudioSource.clip = ContinuousShootLoopSfx;
  137. m_ContinuousShootAudioSource.outputAudioMixerGroup =
  138. AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponShoot);
  139. m_ContinuousShootAudioSource.loop = true;
  140. }
  141. if (HasPhysicalBullets)
  142. {
  143. m_PhysicalAmmoPool = new Queue<Rigidbody>(ShellPoolSize);
  144. for (int i = 0; i < ShellPoolSize; i++)
  145. {
  146. GameObject shell = Instantiate(ShellCasing, transform);
  147. shell.SetActive(false);
  148. m_PhysicalAmmoPool.Enqueue(shell.GetComponent<Rigidbody>());
  149. }
  150. }
  151. }
  152. public void AddCarriablePhysicalBullets(int count) => m_CarriedPhysicalBullets = Mathf.Max(m_CarriedPhysicalBullets + count, MaxAmmo);
  153. void ShootShell()
  154. {
  155. Rigidbody nextShell = m_PhysicalAmmoPool.Dequeue();
  156. nextShell.transform.position = EjectionPort.transform.position;
  157. nextShell.transform.rotation = EjectionPort.transform.rotation;
  158. nextShell.gameObject.SetActive(true);
  159. nextShell.transform.SetParent(null);
  160. nextShell.collisionDetectionMode = CollisionDetectionMode.Continuous;
  161. nextShell.AddForce(nextShell.transform.up * ShellCasingEjectionForce, ForceMode.Impulse);
  162. m_PhysicalAmmoPool.Enqueue(nextShell);
  163. }
  164. void PlaySFX(AudioClip sfx) => AudioUtility.CreateSFX(sfx, transform.position, AudioUtility.AudioGroups.WeaponShoot, 0.0f);
  165. void Reload()
  166. {
  167. if (m_CarriedPhysicalBullets > 0)
  168. {
  169. m_CurrentAmmo = Mathf.Min(m_CarriedPhysicalBullets, ClipSize);
  170. }
  171. IsReloading = false;
  172. }
  173. public void StartReloadAnimation()
  174. {
  175. if (m_CurrentAmmo < m_CarriedPhysicalBullets)
  176. {
  177. GetComponent<Animator>().SetTrigger("Reload");
  178. IsReloading = true;
  179. }
  180. }
  181. void Update()
  182. {
  183. UpdateAmmo();
  184. UpdateCharge();
  185. UpdateContinuousShootSound();
  186. if (Time.deltaTime > 0)
  187. {
  188. MuzzleWorldVelocity = (WeaponMuzzle.position - m_LastMuzzlePosition) / Time.deltaTime;
  189. m_LastMuzzlePosition = WeaponMuzzle.position;
  190. }
  191. }
  192. void UpdateAmmo()
  193. {
  194. if (AutomaticReload && m_LastTimeShot + AmmoReloadDelay < Time.time && m_CurrentAmmo < MaxAmmo && !IsCharging)
  195. {
  196. // reloads weapon over time
  197. m_CurrentAmmo += AmmoReloadRate * Time.deltaTime;
  198. // limits ammo to max value
  199. m_CurrentAmmo = Mathf.Clamp(m_CurrentAmmo, 0, MaxAmmo);
  200. IsCooling = true;
  201. }
  202. else
  203. {
  204. IsCooling = false;
  205. }
  206. if (MaxAmmo == Mathf.Infinity)
  207. {
  208. CurrentAmmoRatio = 1f;
  209. }
  210. else
  211. {
  212. CurrentAmmoRatio = m_CurrentAmmo / MaxAmmo;
  213. }
  214. }
  215. void UpdateCharge()
  216. {
  217. if (IsCharging)
  218. {
  219. if (CurrentCharge < 1f)
  220. {
  221. float chargeLeft = 1f - CurrentCharge;
  222. // Calculate how much charge ratio to add this frame
  223. float chargeAdded = 0f;
  224. if (MaxChargeDuration <= 0f)
  225. {
  226. chargeAdded = chargeLeft;
  227. }
  228. else
  229. {
  230. chargeAdded = (1f / MaxChargeDuration) * Time.deltaTime;
  231. }
  232. chargeAdded = Mathf.Clamp(chargeAdded, 0f, chargeLeft);
  233. // See if we can actually add this charge
  234. float ammoThisChargeWouldRequire = chargeAdded * AmmoUsageRateWhileCharging;
  235. if (ammoThisChargeWouldRequire <= m_CurrentAmmo)
  236. {
  237. // Use ammo based on charge added
  238. UseAmmo(ammoThisChargeWouldRequire);
  239. // set current charge ratio
  240. CurrentCharge = Mathf.Clamp01(CurrentCharge + chargeAdded);
  241. }
  242. }
  243. }
  244. }
  245. void UpdateContinuousShootSound()
  246. {
  247. if (UseContinuousShootSound)
  248. {
  249. if (m_WantsToShoot && m_CurrentAmmo >= 1f)
  250. {
  251. if (!m_ContinuousShootAudioSource.isPlaying)
  252. {
  253. m_ShootAudioSource.PlayOneShot(ShootSfx);
  254. m_ShootAudioSource.PlayOneShot(ContinuousShootStartSfx);
  255. m_ContinuousShootAudioSource.Play();
  256. }
  257. }
  258. else if (m_ContinuousShootAudioSource.isPlaying)
  259. {
  260. m_ShootAudioSource.PlayOneShot(ContinuousShootEndSfx);
  261. m_ContinuousShootAudioSource.Stop();
  262. }
  263. }
  264. }
  265. public void ShowWeapon(bool show)
  266. {
  267. WeaponRoot.SetActive(show);
  268. if (show && ChangeWeaponSfx)
  269. {
  270. m_ShootAudioSource.PlayOneShot(ChangeWeaponSfx);
  271. }
  272. IsWeaponActive = show;
  273. }
  274. public void UseAmmo(float amount)
  275. {
  276. m_CurrentAmmo = Mathf.Clamp(m_CurrentAmmo - amount, 0f, MaxAmmo);
  277. m_CarriedPhysicalBullets -= Mathf.RoundToInt(amount);
  278. m_CarriedPhysicalBullets = Mathf.Clamp(m_CarriedPhysicalBullets, 0, MaxAmmo);
  279. m_LastTimeShot = Time.time;
  280. }
  281. public bool HandleShootInputs(bool inputDown, bool inputHeld, bool inputUp)
  282. {
  283. m_WantsToShoot = inputDown || inputHeld;
  284. switch (ShootType)
  285. {
  286. case WeaponShootType.Manual:
  287. if (inputDown)
  288. {
  289. return TryShoot();
  290. }
  291. return false;
  292. case WeaponShootType.Automatic:
  293. if (inputHeld)
  294. {
  295. return TryShoot();
  296. }
  297. return false;
  298. case WeaponShootType.Charge:
  299. if (inputHeld)
  300. {
  301. TryBeginCharge();
  302. }
  303. // Check if we released charge or if the weapon shoot autmatically when it's fully charged
  304. if (inputUp || (AutomaticReleaseOnCharged && CurrentCharge >= 1f))
  305. {
  306. return TryReleaseCharge();
  307. }
  308. return false;
  309. default:
  310. return false;
  311. }
  312. }
  313. bool TryShoot()
  314. {
  315. if (m_CurrentAmmo >= 1f
  316. && m_LastTimeShot + DelayBetweenShots < Time.time)
  317. {
  318. HandleShoot();
  319. m_CurrentAmmo -= 1f;
  320. return true;
  321. }
  322. return false;
  323. }
  324. bool TryBeginCharge()
  325. {
  326. if (!IsCharging
  327. && m_CurrentAmmo >= AmmoUsedOnStartCharge
  328. && Mathf.FloorToInt((m_CurrentAmmo - AmmoUsedOnStartCharge) * BulletsPerShot) > 0
  329. && m_LastTimeShot + DelayBetweenShots < Time.time)
  330. {
  331. UseAmmo(AmmoUsedOnStartCharge);
  332. LastChargeTriggerTimestamp = Time.time;
  333. IsCharging = true;
  334. return true;
  335. }
  336. return false;
  337. }
  338. bool TryReleaseCharge()
  339. {
  340. if (IsCharging)
  341. {
  342. HandleShoot();
  343. CurrentCharge = 0f;
  344. IsCharging = false;
  345. return true;
  346. }
  347. return false;
  348. }
  349. void HandleShoot()
  350. {
  351. int bulletsPerShotFinal = ShootType == WeaponShootType.Charge
  352. ? Mathf.CeilToInt(CurrentCharge * BulletsPerShot)
  353. : BulletsPerShot;
  354. // spawn all bullets with random direction
  355. for (int i = 0; i < bulletsPerShotFinal; i++)
  356. {
  357. Vector3 shotDirection = GetShotDirectionWithinSpread(WeaponMuzzle);
  358. ProjectileBase newProjectile = Instantiate(ProjectilePrefab, WeaponMuzzle.position,
  359. Quaternion.LookRotation(shotDirection));
  360. newProjectile.Shoot(this);
  361. }
  362. // muzzle flash
  363. if (MuzzleFlashPrefab != null)
  364. {
  365. GameObject muzzleFlashInstance = Instantiate(MuzzleFlashPrefab, WeaponMuzzle.position,
  366. WeaponMuzzle.rotation, WeaponMuzzle.transform);
  367. // Unparent the muzzleFlashInstance
  368. if (UnparentMuzzleFlash)
  369. {
  370. muzzleFlashInstance.transform.SetParent(null);
  371. }
  372. Destroy(muzzleFlashInstance, 2f);
  373. }
  374. if (HasPhysicalBullets)
  375. {
  376. ShootShell();
  377. m_CarriedPhysicalBullets--;
  378. }
  379. m_LastTimeShot = Time.time;
  380. // play shoot SFX
  381. if (ShootSfx && !UseContinuousShootSound)
  382. {
  383. m_ShootAudioSource.PlayOneShot(ShootSfx);
  384. }
  385. // Trigger attack animation if there is any
  386. if (WeaponAnimator)
  387. {
  388. WeaponAnimator.SetTrigger(k_AnimAttackParameter);
  389. }
  390. OnShoot?.Invoke();
  391. OnShootProcessed?.Invoke();
  392. }
  393. public Vector3 GetShotDirectionWithinSpread(Transform shootTransform)
  394. {
  395. float spreadAngleRatio = BulletSpreadAngle / 180f;
  396. Vector3 spreadWorldDirection = Vector3.Slerp(shootTransform.forward, UnityEngine.Random.insideUnitSphere,
  397. spreadAngleRatio);
  398. return spreadWorldDirection;
  399. }
  400. }
  401. }