initial commit
This commit is contained in:
58
Assets/Scripts/AmmoPickup.cs
Normal file
58
Assets/Scripts/AmmoPickup.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class AmmoPickup : PickupItem
|
||||
{
|
||||
[Header("Ammo Settings")]
|
||||
[SerializeField] private int ammoAmount = 30;
|
||||
[SerializeField] private string targetWeaponName = ""; // Leave blank for currently equipped weapon
|
||||
|
||||
protected override bool CanCollect(GameObject player)
|
||||
{
|
||||
var weaponsController = player.GetComponent<WeaponsController>();
|
||||
if (weaponsController == null)
|
||||
{
|
||||
weaponsController = player.GetComponentInChildren<WeaponsController>();
|
||||
}
|
||||
|
||||
if (weaponsController == null) return false;
|
||||
|
||||
Weapon activeWeapon = weaponsController.ActiveWeapon;
|
||||
if (activeWeapon == null) return false;
|
||||
|
||||
// If target is specified, check if that weapon can receive ammo
|
||||
if (!string.IsNullOrEmpty(targetWeaponName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Active weapon check: if infinite reserve and current mag full, skip pickup if desired
|
||||
if (activeWeapon.InfiniteReserve)
|
||||
{
|
||||
return activeWeapon.currentAmmo < activeWeapon.maxAmmo;
|
||||
}
|
||||
|
||||
return activeWeapon.CurrentReserveAmmo < activeWeapon.MaxReserveAmmo || activeWeapon.currentAmmo < activeWeapon.maxAmmo;
|
||||
}
|
||||
|
||||
protected override void ApplyPickupEffect(GameObject player)
|
||||
{
|
||||
var weaponsController = player.GetComponent<WeaponsController>();
|
||||
if (weaponsController == null)
|
||||
{
|
||||
weaponsController = player.GetComponentInChildren<WeaponsController>();
|
||||
}
|
||||
|
||||
if (weaponsController != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(targetWeaponName))
|
||||
{
|
||||
weaponsController.AddAmmoToActiveWeapon(ammoAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
weaponsController.AddAmmoToMatchingWeapon(targetWeaponName, ammoAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/AmmoPickup.cs.meta
Normal file
2
Assets/Scripts/AmmoPickup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c33fefe4f5bcd947a11a680b80a9633
|
||||
84
Assets/Scripts/AreaClearanceManager.cs
Normal file
84
Assets/Scripts/AreaClearanceManager.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class AreaClearanceManager : MonoBehaviour
|
||||
{
|
||||
public static AreaClearanceManager Instance { get; private set; }
|
||||
|
||||
[SerializeField] private LevelExit levelExit;
|
||||
private readonly HashSet<EnemyController> activeEnemies = new HashSet<EnemyController>();
|
||||
private bool areaCleared = false;
|
||||
|
||||
public int RemainingEnemies => activeEnemies.Count;
|
||||
public bool IsAreaCleared => areaCleared;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (levelExit != null)
|
||||
{
|
||||
levelExit.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterEnemy(EnemyController enemy)
|
||||
{
|
||||
if (enemy == null) return;
|
||||
|
||||
if (!activeEnemies.Contains(enemy))
|
||||
{
|
||||
activeEnemies.Add(enemy);
|
||||
UpdateUI();
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterEnemy(EnemyController enemy)
|
||||
{
|
||||
if (enemy == null) return;
|
||||
|
||||
if (activeEnemies.Contains(enemy))
|
||||
{
|
||||
activeEnemies.Remove(enemy);
|
||||
UpdateUI();
|
||||
|
||||
if (activeEnemies.Count == 0 && !areaCleared)
|
||||
{
|
||||
UnlockArea();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnlockArea()
|
||||
{
|
||||
areaCleared = true;
|
||||
if (levelExit != null)
|
||||
{
|
||||
levelExit.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
if (UIController.Instance != null)
|
||||
{
|
||||
UIController.Instance.ShowNotification("Area Cleared! Proceed to Exit.");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
if (UIController.Instance != null)
|
||||
{
|
||||
UIController.Instance.UpdateZombiesRemaining(activeEnemies.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/AreaClearanceManager.cs.meta
Normal file
2
Assets/Scripts/AreaClearanceManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70a30e5a795ddc948a0786fc868b6da0
|
||||
222
Assets/Scripts/EnemyController.cs
Normal file
222
Assets/Scripts/EnemyController.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
public class EnemyController : MonoBehaviour
|
||||
{
|
||||
public enum State { Idle, Chase, Attack, Dead }
|
||||
private State currentState = State.Idle;
|
||||
|
||||
[Header("Stats")]
|
||||
[SerializeField] private int maxHealth = 60;
|
||||
[SerializeField] private int attackDamage = 15;
|
||||
[SerializeField] private float attackRange = 1.8f;
|
||||
[SerializeField] private float attackCooldown = 1.2f;
|
||||
[SerializeField] private float detectionRadius = 18f;
|
||||
|
||||
[Header("Ranged Attack Settings (Optional)")]
|
||||
[SerializeField] private bool isRangedEnemy = false;
|
||||
[SerializeField] private GameObject rangedProjectilePrefab;
|
||||
[SerializeField] private Transform projectileSpawnPoint;
|
||||
[SerializeField] private float projectileSpeed = 25f;
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Animator animator;
|
||||
[SerializeField] private AudioSource audioSource;
|
||||
[SerializeField] private AudioClip attackSound;
|
||||
[SerializeField] private AudioClip hitSound;
|
||||
[SerializeField] private AudioClip deathSound;
|
||||
|
||||
private NavMeshAgent agent;
|
||||
private Transform playerTransform;
|
||||
private int currentHealth;
|
||||
private float nextAttackTime;
|
||||
|
||||
public State CurrentState => currentState;
|
||||
public int CurrentHealth => currentHealth;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
if (animator == null)
|
||||
{
|
||||
animator = GetComponentInChildren<Animator>();
|
||||
}
|
||||
if (audioSource == null)
|
||||
{
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
}
|
||||
currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||||
if (playerObj != null)
|
||||
{
|
||||
playerTransform = playerObj.transform;
|
||||
}
|
||||
|
||||
AreaClearanceManager.Instance?.RegisterEnemy(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (currentState == State.Dead) return;
|
||||
|
||||
// Re-acquire player reference if it was spawned dynamically
|
||||
if (playerTransform == null)
|
||||
{
|
||||
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||||
if (playerObj != null)
|
||||
{
|
||||
playerTransform = playerObj.transform;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
||||
|
||||
switch (currentState)
|
||||
{
|
||||
case State.Idle:
|
||||
if (distanceToPlayer <= detectionRadius)
|
||||
{
|
||||
currentState = State.Chase;
|
||||
}
|
||||
break;
|
||||
|
||||
case State.Chase:
|
||||
if (agent.isOnNavMesh)
|
||||
{
|
||||
agent.SetDestination(playerTransform.position);
|
||||
}
|
||||
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetFloat("Speed", agent.velocity.magnitude);
|
||||
}
|
||||
|
||||
if (distanceToPlayer <= attackRange)
|
||||
{
|
||||
currentState = State.Attack;
|
||||
if (agent.isOnNavMesh)
|
||||
{
|
||||
agent.ResetPath();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State.Attack:
|
||||
Vector3 lookTarget = new Vector3(playerTransform.position.x, transform.position.y, playerTransform.position.z);
|
||||
transform.LookAt(lookTarget);
|
||||
|
||||
if (Time.time >= nextAttackTime)
|
||||
{
|
||||
PerformAttack();
|
||||
nextAttackTime = Time.time + attackCooldown;
|
||||
}
|
||||
|
||||
if (distanceToPlayer > attackRange)
|
||||
{
|
||||
currentState = State.Chase;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformAttack()
|
||||
{
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetTrigger("Attack");
|
||||
}
|
||||
|
||||
if (attackSound != null && audioSource != null)
|
||||
{
|
||||
audioSource.PlayOneShot(attackSound);
|
||||
}
|
||||
|
||||
if (isRangedEnemy && rangedProjectilePrefab != null)
|
||||
{
|
||||
// Fire projectile towards player
|
||||
Vector3 spawnPos = projectileSpawnPoint != null ? projectileSpawnPoint.position : transform.position + Vector3.up * 1.2f + transform.forward * 0.5f;
|
||||
Vector3 aimDir = (playerTransform.position + Vector3.up * 1.0f - spawnPos).normalized;
|
||||
GameObject projObj = Instantiate(rangedProjectilePrefab, spawnPos, Quaternion.LookRotation(aimDir));
|
||||
if (projObj.TryGetComponent<Projectile>(out var proj))
|
||||
{
|
||||
proj.Initialize(attackDamage, projectileSpeed, "Enemy");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Direct melee strike
|
||||
if (PlayerHealthController.Instance != null)
|
||||
{
|
||||
PlayerHealthController.Instance.TakeDamage(attackDamage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
if (currentState == State.Dead) return;
|
||||
|
||||
currentHealth -= damage;
|
||||
|
||||
if (hitSound != null && audioSource != null)
|
||||
{
|
||||
audioSource.PlayOneShot(hitSound);
|
||||
}
|
||||
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetTrigger("Hit");
|
||||
}
|
||||
|
||||
// Auto-aggro when damaged if idle
|
||||
if (currentState == State.Idle)
|
||||
{
|
||||
currentState = State.Chase;
|
||||
}
|
||||
|
||||
if (currentHealth <= 0)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
currentState = State.Dead;
|
||||
|
||||
if (agent != null && agent.isOnNavMesh)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
}
|
||||
if (agent != null)
|
||||
{
|
||||
agent.enabled = false;
|
||||
}
|
||||
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetBool("IsDead", true);
|
||||
}
|
||||
|
||||
if (deathSound != null && audioSource != null)
|
||||
{
|
||||
audioSource.PlayOneShot(deathSound);
|
||||
}
|
||||
|
||||
Collider col = GetComponent<Collider>();
|
||||
if (col != null)
|
||||
{
|
||||
col.enabled = false;
|
||||
}
|
||||
|
||||
AreaClearanceManager.Instance?.UnregisterEnemy(this);
|
||||
Destroy(gameObject, 5f);
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/EnemyController.cs.meta
Normal file
2
Assets/Scripts/EnemyController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cae4326f08a20de49a7bbe8b42f160f8
|
||||
98
Assets/Scripts/EnemySpawner.cs
Normal file
98
Assets/Scripts/EnemySpawner.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class EnemySpawner : MonoBehaviour
|
||||
{
|
||||
[Header("Spawn Configuration")]
|
||||
[SerializeField] private GameObject[] enemyPrefabs;
|
||||
[SerializeField] private Transform[] spawnPoints;
|
||||
[SerializeField] private int totalEnemiesToSpawn = 10;
|
||||
[SerializeField] private float timeBetweenSpawns = 2.0f;
|
||||
[SerializeField] private int maxSimultaneousEnemies = 5;
|
||||
|
||||
[Header("Trigger Mode")]
|
||||
[SerializeField] private bool spawnOnStart = true;
|
||||
[SerializeField] private bool triggerOnPlayerEnter = false;
|
||||
|
||||
private int spawnedCount = 0;
|
||||
private bool isSpawning = false;
|
||||
private readonly List<EnemyController> livingEnemies = new List<EnemyController>();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (spawnOnStart)
|
||||
{
|
||||
StartSpawning();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (triggerOnPlayerEnter && !isSpawning && other.CompareTag("Player"))
|
||||
{
|
||||
StartSpawning();
|
||||
}
|
||||
}
|
||||
|
||||
public void StartSpawning()
|
||||
{
|
||||
if (!isSpawning)
|
||||
{
|
||||
isSpawning = true;
|
||||
StartCoroutine(SpawnRoutine());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SpawnRoutine()
|
||||
{
|
||||
while (spawnedCount < totalEnemiesToSpawn)
|
||||
{
|
||||
// Clean up any destroyed references
|
||||
livingEnemies.RemoveAll(e => e == null || e.CurrentState == EnemyController.State.Dead);
|
||||
|
||||
if (livingEnemies.Count < maxSimultaneousEnemies)
|
||||
{
|
||||
SpawnEnemy();
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(timeBetweenSpawns);
|
||||
}
|
||||
|
||||
isSpawning = false;
|
||||
}
|
||||
|
||||
private void SpawnEnemy()
|
||||
{
|
||||
if (enemyPrefabs == null || enemyPrefabs.Length == 0) return;
|
||||
if (spawnPoints == null || spawnPoints.Length == 0) return;
|
||||
|
||||
// Choose random prefab and random spawn point
|
||||
GameObject chosenPrefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Length)];
|
||||
Transform chosenPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
|
||||
|
||||
GameObject enemyObj = Instantiate(chosenPrefab, chosenPoint.position, chosenPoint.rotation);
|
||||
if (enemyObj.TryGetComponent<EnemyController>(out var enemy))
|
||||
{
|
||||
livingEnemies.Add(enemy);
|
||||
AreaClearanceManager.Instance?.RegisterEnemy(enemy);
|
||||
}
|
||||
|
||||
spawnedCount++;
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (spawnPoints == null) return;
|
||||
Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f);
|
||||
foreach (var point in spawnPoints)
|
||||
{
|
||||
if (point != null)
|
||||
{
|
||||
Gizmos.DrawWireSphere(point.position, 0.5f);
|
||||
Gizmos.DrawLine(point.position, point.position + point.forward * 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/EnemySpawner.cs.meta
Normal file
2
Assets/Scripts/EnemySpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4830dd96f9166e84abaa3e220046962d
|
||||
67
Assets/Scripts/HazardZone.cs
Normal file
67
Assets/Scripts/HazardZone.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class HazardZone : MonoBehaviour
|
||||
{
|
||||
[Header("Hazard Properties")]
|
||||
[SerializeField] private bool instantKill = false;
|
||||
[SerializeField] private int damagePerSecond = 20;
|
||||
[SerializeField] private float damageTickRate = 0.5f;
|
||||
[SerializeField] private bool damageEnemies = true;
|
||||
|
||||
[Header("Effects")]
|
||||
[SerializeField] private AudioClip burnOrPainSound;
|
||||
|
||||
private float nextDamageTime;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
GetComponent<Collider>().isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnTriggerStay(Collider other)
|
||||
{
|
||||
if (Time.time < nextDamageTime) return;
|
||||
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
if (PlayerHealthController.Instance != null)
|
||||
{
|
||||
if (instantKill)
|
||||
{
|
||||
PlayerHealthController.Instance.TakeDamage(PlayerHealthController.Instance.MaxHealth);
|
||||
}
|
||||
else
|
||||
{
|
||||
int damageToDeal = Mathf.RoundToInt(damagePerSecond * damageTickRate);
|
||||
PlayerHealthController.Instance.TakeDamage(Mathf.Max(1, damageToDeal));
|
||||
}
|
||||
|
||||
PlayPainSound();
|
||||
}
|
||||
nextDamageTime = Time.time + damageTickRate;
|
||||
}
|
||||
else if (damageEnemies && other.TryGetComponent<EnemyController>(out var enemy))
|
||||
{
|
||||
if (instantKill)
|
||||
{
|
||||
enemy.TakeDamage(9999);
|
||||
}
|
||||
else
|
||||
{
|
||||
int damageToDeal = Mathf.RoundToInt(damagePerSecond * damageTickRate);
|
||||
enemy.TakeDamage(Mathf.Max(1, damageToDeal));
|
||||
}
|
||||
nextDamageTime = Time.time + damageTickRate;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayPainSound()
|
||||
{
|
||||
if (burnOrPainSound != null)
|
||||
{
|
||||
AudioSource.PlayClipAtPoint(burnOrPainSound, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/HazardZone.cs.meta
Normal file
2
Assets/Scripts/HazardZone.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3e4ebac292f65540916c4d4801f13eb
|
||||
30
Assets/Scripts/HealthPickup.cs
Normal file
30
Assets/Scripts/HealthPickup.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class HealthPickup : PickupItem
|
||||
{
|
||||
[Header("Health Settings")]
|
||||
[SerializeField] private int healAmount = 25;
|
||||
[SerializeField] private bool onlyCollectWhenDamaged = true;
|
||||
|
||||
protected override bool CanCollect(GameObject player)
|
||||
{
|
||||
if (PlayerHealthController.Instance == null) return false;
|
||||
if (PlayerHealthController.Instance.IsDead) return false;
|
||||
|
||||
if (onlyCollectWhenDamaged)
|
||||
{
|
||||
return PlayerHealthController.Instance.CurrentHealth < PlayerHealthController.Instance.MaxHealth;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void ApplyPickupEffect(GameObject player)
|
||||
{
|
||||
if (PlayerHealthController.Instance != null)
|
||||
{
|
||||
PlayerHealthController.Instance.Heal(healAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/HealthPickup.cs.meta
Normal file
2
Assets/Scripts/HealthPickup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da073a924ec3dd04ca9f116b2c61751e
|
||||
75
Assets/Scripts/InteractiveDoor.cs
Normal file
75
Assets/Scripts/InteractiveDoor.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class InteractiveDoor : MonoBehaviour
|
||||
{
|
||||
[Header("Door Movement")]
|
||||
[SerializeField] private Vector3 openOffsetPosition = new Vector3(0, 3.5f, 0); // e.g. sliding up
|
||||
[SerializeField] private Vector3 openRotationEuler = Vector3.zero; // or swinging open
|
||||
[SerializeField] private float transitionSpeed = 3f;
|
||||
|
||||
[Header("Behavior")]
|
||||
[SerializeField] private bool openOnAreaCleared = true;
|
||||
[SerializeField] private bool openOnTriggerEnter = false;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] private AudioClip doorOpenSound;
|
||||
|
||||
private Vector3 closedPosition;
|
||||
private Quaternion closedRotation;
|
||||
private Vector3 targetPosition;
|
||||
private Quaternion targetRotation;
|
||||
private bool isOpen = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
closedPosition = transform.position;
|
||||
closedRotation = transform.rotation;
|
||||
targetPosition = closedPosition;
|
||||
targetRotation = closedRotation;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (openOnAreaCleared && !isOpen && AreaClearanceManager.Instance != null && AreaClearanceManager.Instance.IsAreaCleared)
|
||||
{
|
||||
OpenDoor();
|
||||
}
|
||||
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * transitionSpeed);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * transitionSpeed);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (openOnTriggerEnter && !isOpen && other.CompareTag("Player"))
|
||||
{
|
||||
OpenDoor();
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenDoor()
|
||||
{
|
||||
if (isOpen) return;
|
||||
isOpen = true;
|
||||
|
||||
targetPosition = closedPosition + transform.TransformDirection(openOffsetPosition);
|
||||
if (openRotationEuler != Vector3.zero)
|
||||
{
|
||||
targetRotation = closedRotation * Quaternion.Euler(openRotationEuler);
|
||||
}
|
||||
|
||||
if (doorOpenSound != null)
|
||||
{
|
||||
AudioSource.PlayClipAtPoint(doorOpenSound, transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseDoor()
|
||||
{
|
||||
isOpen = false;
|
||||
targetPosition = closedPosition;
|
||||
targetRotation = closedRotation;
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/InteractiveDoor.cs.meta
Normal file
2
Assets/Scripts/InteractiveDoor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5134a2e532152041a2d0a83157886d2
|
||||
38
Assets/Scripts/LevelExit.cs
Normal file
38
Assets/Scripts/LevelExit.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class LevelExit : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string nextSceneName = "Level 2";
|
||||
[SerializeField] private bool loadNextBuildIndex = false;
|
||||
|
||||
private bool isTransitioning = false;
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (isTransitioning) return;
|
||||
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
isTransitioning = true;
|
||||
if (loadNextBuildIndex)
|
||||
{
|
||||
int nextIndex = SceneManager.GetActiveScene().buildIndex + 1;
|
||||
if (nextIndex < SceneManager.sceneCountInBuildSettings)
|
||||
{
|
||||
SceneManager.LoadScene(nextIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("No more scenes in build settings. Reloading current scene.");
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(nextSceneName))
|
||||
{
|
||||
SceneManager.LoadScene(nextSceneName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/LevelExit.cs.meta
Normal file
2
Assets/Scripts/LevelExit.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9eb803030c01984c9268f49c1e261e4
|
||||
118
Assets/Scripts/PickupItem.cs
Normal file
118
Assets/Scripts/PickupItem.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public abstract class PickupItem : MonoBehaviour
|
||||
{
|
||||
[Header("Hover & Rotation Animation")]
|
||||
[SerializeField] private float rotationSpeed = 90f;
|
||||
[SerializeField] private float bobFrequency = 2f;
|
||||
[SerializeField] private float bobAmplitude = 0.2f;
|
||||
|
||||
[Header("Pickup Feedback")]
|
||||
[SerializeField] private GameObject pickupVFXPrefab;
|
||||
[SerializeField] private AudioClip pickupSFX;
|
||||
[SerializeField] private string pickupMessage = "Picked up item!";
|
||||
|
||||
[Header("Respawn Settings")]
|
||||
[SerializeField] private bool respawns = false;
|
||||
[SerializeField] private float respawnTime = 15f;
|
||||
|
||||
private Vector3 startPosition;
|
||||
private Collider itemCollider;
|
||||
private Renderer[] itemRenderers;
|
||||
private bool isCollected = false;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
itemCollider = GetComponent<Collider>();
|
||||
itemCollider.isTrigger = true;
|
||||
itemRenderers = GetComponentsInChildren<Renderer>();
|
||||
startPosition = transform.position;
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
if (isCollected) return;
|
||||
|
||||
// Rotate item
|
||||
transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime, Space.World);
|
||||
|
||||
// Bob up and down
|
||||
float newY = startPosition.y + Mathf.Sin(Time.time * bobFrequency) * bobAmplitude;
|
||||
transform.position = new Vector3(transform.position.x, newY, transform.position.z);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (isCollected) return;
|
||||
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
if (CanCollect(other.gameObject))
|
||||
{
|
||||
Collect(other.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool CanCollect(GameObject player)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract void ApplyPickupEffect(GameObject player);
|
||||
|
||||
private void Collect(GameObject player)
|
||||
{
|
||||
isCollected = true;
|
||||
ApplyPickupEffect(player);
|
||||
|
||||
// Play VFX
|
||||
if (pickupVFXPrefab != null)
|
||||
{
|
||||
GameObject vfx = Instantiate(pickupVFXPrefab, transform.position, Quaternion.identity);
|
||||
Destroy(vfx, 2.0f);
|
||||
}
|
||||
|
||||
// Play SFX
|
||||
if (pickupSFX != null)
|
||||
{
|
||||
AudioSource.PlayClipAtPoint(pickupSFX, transform.position);
|
||||
}
|
||||
|
||||
// Display toast notification on UI
|
||||
if (!string.IsNullOrEmpty(pickupMessage) && UIController.Instance != null)
|
||||
{
|
||||
UIController.Instance.ShowNotification(pickupMessage);
|
||||
}
|
||||
|
||||
if (respawns)
|
||||
{
|
||||
SetVisibility(false);
|
||||
Invoke(nameof(Respawn), respawnTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void Respawn()
|
||||
{
|
||||
isCollected = false;
|
||||
SetVisibility(true);
|
||||
}
|
||||
|
||||
private void SetVisibility(bool visible)
|
||||
{
|
||||
if (itemCollider != null) itemCollider.enabled = visible;
|
||||
if (itemRenderers != null)
|
||||
{
|
||||
foreach (var rend in itemRenderers)
|
||||
{
|
||||
rend.enabled = visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/PickupItem.cs.meta
Normal file
2
Assets/Scripts/PickupItem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04769e61ce15306468800708c4df0ad7
|
||||
84
Assets/Scripts/PlayerController.cs
Normal file
84
Assets/Scripts/PlayerController.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public class PlayerController : MonoBehaviour
|
||||
{
|
||||
[Header("Movement Settings")]
|
||||
[SerializeField] private float walkSpeed = 5.0f;
|
||||
[SerializeField] private float sprintSpeed = 8.5f;
|
||||
[SerializeField] private float gravity = -19.62f;
|
||||
[SerializeField] private float jumpHeight = 1.2f;
|
||||
|
||||
[Header("Look Settings")]
|
||||
[SerializeField] private Transform cameraHolder;
|
||||
[SerializeField] private float mouseSensitivity = 2.0f;
|
||||
[SerializeField] private float upDownLookLimit = 80.0f;
|
||||
|
||||
private CharacterController characterController;
|
||||
private Vector3 velocity;
|
||||
private Vector2 moveInput;
|
||||
private Vector2 lookInput;
|
||||
private float verticalRotation = 0f;
|
||||
private bool isSprinting;
|
||||
private bool jumpRequested;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
characterController = GetComponent<CharacterController>();
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleRotation();
|
||||
HandleMovement();
|
||||
}
|
||||
|
||||
public void OnMove(InputValue value) => moveInput = value.Get<Vector2>();
|
||||
public void OnLook(InputValue value) => lookInput = value.Get<Vector2>();
|
||||
public void OnSprint(InputValue value) => isSprinting = value.isPressed;
|
||||
public void OnJump(InputValue value)
|
||||
{
|
||||
if (value.isPressed && characterController.isGrounded)
|
||||
jumpRequested = true;
|
||||
}
|
||||
|
||||
private void HandleRotation()
|
||||
{
|
||||
if (cameraHolder == null) return;
|
||||
|
||||
float mouseX = lookInput.x * mouseSensitivity * 0.1f;
|
||||
float mouseY = lookInput.y * mouseSensitivity * 0.1f;
|
||||
|
||||
transform.Rotate(Vector3.up * mouseX);
|
||||
|
||||
verticalRotation -= mouseY;
|
||||
verticalRotation = Mathf.Clamp(verticalRotation, -upDownLookLimit, upDownLookLimit);
|
||||
cameraHolder.localRotation = Quaternion.Euler(verticalRotation, 0f, 0f);
|
||||
}
|
||||
|
||||
private void HandleMovement()
|
||||
{
|
||||
if (characterController.isGrounded && velocity.y < 0)
|
||||
{
|
||||
velocity.y = -2f;
|
||||
}
|
||||
|
||||
float currentSpeed = isSprinting ? sprintSpeed : walkSpeed;
|
||||
Vector3 moveDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
|
||||
characterController.Move(moveDirection * currentSpeed * Time.deltaTime);
|
||||
|
||||
if (jumpRequested)
|
||||
{
|
||||
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||
jumpRequested = false;
|
||||
}
|
||||
|
||||
velocity.y += gravity * Time.deltaTime;
|
||||
characterController.Move(velocity * Time.deltaTime);
|
||||
}
|
||||
|
||||
public void SetCameraHolder(Transform camHolder) => cameraHolder = camHolder;
|
||||
}
|
||||
2
Assets/Scripts/PlayerController.cs.meta
Normal file
2
Assets/Scripts/PlayerController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ffbc22d96e5fa743b1dc6e68796edce
|
||||
66
Assets/Scripts/PlayerHealthController.cs
Normal file
66
Assets/Scripts/PlayerHealthController.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerHealthController : MonoBehaviour
|
||||
{
|
||||
public static PlayerHealthController Instance { get; private set; }
|
||||
|
||||
[SerializeField] private int maxHealth = 100;
|
||||
private int currentHealth;
|
||||
|
||||
public event Action<int, int> OnHealthChanged;
|
||||
public event Action OnPlayerDied;
|
||||
|
||||
public int CurrentHealth => currentHealth;
|
||||
public int MaxHealth => maxHealth;
|
||||
public bool IsDead => currentHealth <= 0;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
currentHealth = maxHealth;
|
||||
OnHealthChanged?.Invoke(currentHealth, maxHealth);
|
||||
}
|
||||
|
||||
public void TakeDamage(int damageAmount)
|
||||
{
|
||||
if (currentHealth <= 0) return;
|
||||
|
||||
currentHealth = Mathf.Max(0, currentHealth - damageAmount);
|
||||
OnHealthChanged?.Invoke(currentHealth, maxHealth);
|
||||
|
||||
if (currentHealth <= 0)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
public void Heal(int healAmount)
|
||||
{
|
||||
if (currentHealth <= 0) return;
|
||||
|
||||
currentHealth = Mathf.Min(maxHealth, currentHealth + healAmount);
|
||||
OnHealthChanged?.Invoke(currentHealth, maxHealth);
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
OnPlayerDied?.Invoke();
|
||||
if (UIController.Instance != null)
|
||||
{
|
||||
UIController.Instance.ShowGameOverScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/PlayerHealthController.cs.meta
Normal file
2
Assets/Scripts/PlayerHealthController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1986d9aa7b4031343af66565767eccc7
|
||||
166
Assets/Scripts/Projectile.cs
Normal file
166
Assets/Scripts/Projectile.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Projectile : MonoBehaviour
|
||||
{
|
||||
[Header("Flight Settings")]
|
||||
[SerializeField] private float speed = 50f;
|
||||
[SerializeField] private float lifeTime = 5f;
|
||||
[SerializeField] private bool usePhysics = false;
|
||||
|
||||
[Header("Damage Settings")]
|
||||
[SerializeField] private int damage = 50;
|
||||
[SerializeField] private bool isExplosive = false;
|
||||
[SerializeField] private float explosionRadius = 4f;
|
||||
[SerializeField] private LayerMask damageableLayers = ~0;
|
||||
|
||||
[Header("Visual & Audio Effects")]
|
||||
[SerializeField] private GameObject impactEffectPrefab;
|
||||
[SerializeField] private AudioClip impactSound;
|
||||
|
||||
private Rigidbody rb;
|
||||
private string ownerTag = "Player";
|
||||
private bool hasImpacted = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rb = GetComponent<Rigidbody>();
|
||||
if (rb != null)
|
||||
{
|
||||
rb.useGravity = usePhysics;
|
||||
if (usePhysics)
|
||||
{
|
||||
rb.linearVelocity = transform.forward * speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Destroy(gameObject, lifeTime);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!usePhysics || rb == null)
|
||||
{
|
||||
// Manual kinematic translation with raycast sweep to prevent tunneling
|
||||
float step = speed * Time.deltaTime;
|
||||
Ray ray = new Ray(transform.position, transform.forward);
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, step, damageableLayers, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
if (hit.collider.CompareTag(ownerTag)) return;
|
||||
|
||||
transform.position = hit.point;
|
||||
OnHit(hit.collider, hit.point, hit.normal);
|
||||
return;
|
||||
}
|
||||
|
||||
transform.position += transform.forward * step;
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(int customDamage, float customSpeed, string shooterTag)
|
||||
{
|
||||
damage = customDamage;
|
||||
speed = customSpeed;
|
||||
ownerTag = shooterTag;
|
||||
|
||||
if (rb != null && usePhysics)
|
||||
{
|
||||
rb.linearVelocity = transform.forward * speed;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (hasImpacted || other.CompareTag(ownerTag)) return;
|
||||
HandleCollision(other, transform.position, -transform.forward);
|
||||
}
|
||||
|
||||
private void OnCollisionEnter(Collision collision)
|
||||
{
|
||||
if (hasImpacted || collision.collider.CompareTag(ownerTag)) return;
|
||||
|
||||
ContactPoint contact = collision.GetContact(0);
|
||||
HandleCollision(collision.collider, contact.point, contact.normal);
|
||||
}
|
||||
|
||||
private void OnHit(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal)
|
||||
{
|
||||
if (hasImpacted) return;
|
||||
HandleCollision(hitCollider, hitPoint, hitNormal);
|
||||
}
|
||||
|
||||
private void HandleCollision(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal)
|
||||
{
|
||||
hasImpacted = true;
|
||||
|
||||
if (isExplosive)
|
||||
{
|
||||
Explode(hitPoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Direct hit damage
|
||||
if (hitCollider.TryGetComponent<EnemyController>(out var enemy))
|
||||
{
|
||||
enemy.TakeDamage(damage);
|
||||
}
|
||||
else if (hitCollider.TryGetComponent<PlayerHealthController>(out var playerHealth))
|
||||
{
|
||||
playerHealth.TakeDamage(damage);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn visual impact effect
|
||||
if (impactEffectPrefab != null)
|
||||
{
|
||||
Quaternion rot = hitNormal != Vector3.zero ? Quaternion.LookRotation(hitNormal) : Quaternion.identity;
|
||||
GameObject impact = Instantiate(impactEffectPrefab, hitPoint, rot);
|
||||
Destroy(impact, 2.5f);
|
||||
}
|
||||
|
||||
// Play impact sound
|
||||
if (impactSound != null)
|
||||
{
|
||||
AudioSource.PlayClipAtPoint(impactSound, hitPoint);
|
||||
}
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void Explode(Vector3 center)
|
||||
{
|
||||
Collider[] hits = Physics.OverlapSphere(center, explosionRadius, damageableLayers);
|
||||
foreach (Collider col in hits)
|
||||
{
|
||||
if (col.CompareTag(ownerTag)) continue;
|
||||
|
||||
if (col.TryGetComponent<EnemyController>(out var enemy))
|
||||
{
|
||||
float distance = Vector3.Distance(center, col.transform.position);
|
||||
float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius));
|
||||
int finalDamage = Mathf.RoundToInt(damage * damageFalloff);
|
||||
enemy.TakeDamage(Mathf.Max(1, finalDamage));
|
||||
}
|
||||
else if (col.TryGetComponent<PlayerHealthController>(out var playerHealth))
|
||||
{
|
||||
float distance = Vector3.Distance(center, col.transform.position);
|
||||
float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius));
|
||||
int finalDamage = Mathf.RoundToInt(damage * damageFalloff);
|
||||
playerHealth.TakeDamage(Mathf.Max(1, finalDamage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (isExplosive)
|
||||
{
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, explosionRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Projectile.cs.meta
Normal file
2
Assets/Scripts/Projectile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f332dafbb7f75749b484d86aae04b60
|
||||
108
Assets/Scripts/UIController.cs
Normal file
108
Assets/Scripts/UIController.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class UIController : MonoBehaviour
|
||||
{
|
||||
public static UIController Instance { get; private set; }
|
||||
|
||||
[Header("HUD Elements")]
|
||||
[SerializeField] private Slider healthBar;
|
||||
[SerializeField] private TextMeshProUGUI ammoText;
|
||||
[SerializeField] private TextMeshProUGUI zombieCountText;
|
||||
[SerializeField] private TextMeshProUGUI notificationText;
|
||||
[SerializeField] private GameObject gameOverPanel;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (PlayerHealthController.Instance != null)
|
||||
{
|
||||
PlayerHealthController.Instance.OnHealthChanged += UpdateHealthBar;
|
||||
UpdateHealthBar(PlayerHealthController.Instance.CurrentHealth, PlayerHealthController.Instance.MaxHealth);
|
||||
}
|
||||
|
||||
if (gameOverPanel != null)
|
||||
{
|
||||
gameOverPanel.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (PlayerHealthController.Instance != null)
|
||||
{
|
||||
PlayerHealthController.Instance.OnHealthChanged -= UpdateHealthBar;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateHealthBar(int current, int max)
|
||||
{
|
||||
if (healthBar != null && max > 0)
|
||||
{
|
||||
healthBar.value = (float)current / max;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAmmoDisplay(int current, int max)
|
||||
{
|
||||
if (ammoText != null)
|
||||
{
|
||||
ammoText.text = $"{current} / {max}";
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateZombiesRemaining(int count)
|
||||
{
|
||||
if (zombieCountText != null)
|
||||
{
|
||||
zombieCountText.text = $"Zombies Left: {count}";
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowNotification(string message)
|
||||
{
|
||||
if (notificationText != null)
|
||||
{
|
||||
notificationText.text = message;
|
||||
CancelInvoke(nameof(ClearNotification));
|
||||
Invoke(nameof(ClearNotification), 4f);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearNotification()
|
||||
{
|
||||
if (notificationText != null)
|
||||
{
|
||||
notificationText.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowGameOverScreen()
|
||||
{
|
||||
if (gameOverPanel != null)
|
||||
{
|
||||
gameOverPanel.SetActive(true);
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartCurrentScene()
|
||||
{
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/UIController.cs.meta
Normal file
2
Assets/Scripts/UIController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 519f2304e430a5644a8ebb33249b6c38
|
||||
160
Assets/Scripts/Weapon.cs
Normal file
160
Assets/Scripts/Weapon.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class Weapon : MonoBehaviour
|
||||
{
|
||||
[Header("Weapon Stats")]
|
||||
public string weaponName = "Assault Rifle";
|
||||
public int damage = 25;
|
||||
public float fireRate = 0.12f;
|
||||
public float range = 100f;
|
||||
public int maxAmmo = 30;
|
||||
public int currentAmmo;
|
||||
public float reloadTime = 1.5f;
|
||||
|
||||
[Header("Effects")]
|
||||
[SerializeField] private ParticleSystem muzzleFlash;
|
||||
[SerializeField] private GameObject impactEffectPrefab;
|
||||
[SerializeField] private Transform muzzlePoint;
|
||||
|
||||
[Header("Ballistics & Projectile Support")]
|
||||
[SerializeField] private bool useProjectile = false;
|
||||
[SerializeField] private GameObject projectilePrefab;
|
||||
[SerializeField] private float projectileSpeed = 50f;
|
||||
|
||||
[Header("Ammo Reserves")]
|
||||
[SerializeField] private bool infiniteReserve = true;
|
||||
[SerializeField] private int currentReserveAmmo = 90;
|
||||
[SerializeField] private int maxReserveAmmo = 180;
|
||||
|
||||
[Header("Audio Effects (Optional)")]
|
||||
[SerializeField] private AudioSource audioSource;
|
||||
[SerializeField] private AudioClip fireSound;
|
||||
[SerializeField] private AudioClip reloadSound;
|
||||
[SerializeField] private AudioClip emptySound;
|
||||
|
||||
private float nextFireTime;
|
||||
private bool isReloading;
|
||||
|
||||
public bool IsReloading => isReloading;
|
||||
public bool InfiniteReserve => infiniteReserve;
|
||||
public int CurrentReserveAmmo => currentReserveAmmo;
|
||||
public int MaxReserveAmmo => maxReserveAmmo;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
currentAmmo = maxAmmo;
|
||||
if (audioSource == null)
|
||||
{
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanShoot() => Time.time >= nextFireTime && !isReloading && currentAmmo > 0;
|
||||
|
||||
public void Shoot(Camera playerCamera, LayerMask hitMask)
|
||||
{
|
||||
nextFireTime = Time.time + fireRate;
|
||||
currentAmmo--;
|
||||
|
||||
if (muzzleFlash) muzzleFlash.Play();
|
||||
|
||||
PlaySound(fireSound);
|
||||
|
||||
if (useProjectile && projectilePrefab != null)
|
||||
{
|
||||
FireProjectile(playerCamera);
|
||||
}
|
||||
else
|
||||
{
|
||||
FireHitscan(playerCamera, hitMask);
|
||||
}
|
||||
}
|
||||
|
||||
private void FireHitscan(Camera playerCamera, LayerMask hitMask)
|
||||
{
|
||||
Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, range, hitMask))
|
||||
{
|
||||
if (hit.collider.TryGetComponent<EnemyController>(out var enemy))
|
||||
{
|
||||
enemy.TakeDamage(damage);
|
||||
}
|
||||
|
||||
if (impactEffectPrefab)
|
||||
{
|
||||
GameObject impact = Instantiate(impactEffectPrefab, hit.point, Quaternion.LookRotation(hit.normal));
|
||||
Destroy(impact, 2.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FireProjectile(Camera playerCamera)
|
||||
{
|
||||
Vector3 spawnPosition = muzzlePoint != null ? muzzlePoint.position : playerCamera.transform.position + playerCamera.transform.forward * 0.5f;
|
||||
|
||||
// Calculate aim point by casting from crosshair
|
||||
Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
|
||||
Vector3 targetPoint;
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, range))
|
||||
{
|
||||
targetPoint = hit.point;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPoint = ray.GetPoint(range);
|
||||
}
|
||||
|
||||
Vector3 direction = (targetPoint - spawnPosition).normalized;
|
||||
Quaternion rotation = Quaternion.LookRotation(direction);
|
||||
|
||||
GameObject projectileObj = Instantiate(projectilePrefab, spawnPosition, rotation);
|
||||
if (projectileObj.TryGetComponent<Projectile>(out var projectile))
|
||||
{
|
||||
projectile.Initialize(damage, projectileSpeed, "Player");
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator ReloadRoutine()
|
||||
{
|
||||
if (isReloading || currentAmmo == maxAmmo) yield break;
|
||||
if (!infiniteReserve && currentReserveAmmo <= 0)
|
||||
{
|
||||
PlaySound(emptySound);
|
||||
yield break;
|
||||
}
|
||||
|
||||
isReloading = true;
|
||||
PlaySound(reloadSound);
|
||||
|
||||
yield return new WaitForSeconds(reloadTime);
|
||||
|
||||
int neededAmmo = maxAmmo - currentAmmo;
|
||||
if (infiniteReserve)
|
||||
{
|
||||
currentAmmo = maxAmmo;
|
||||
}
|
||||
else
|
||||
{
|
||||
int ammoToAdd = Mathf.Min(neededAmmo, currentReserveAmmo);
|
||||
currentAmmo += ammoToAdd;
|
||||
currentReserveAmmo -= ammoToAdd;
|
||||
}
|
||||
|
||||
isReloading = false;
|
||||
}
|
||||
|
||||
public void AddReserveAmmo(int amount)
|
||||
{
|
||||
currentReserveAmmo = Mathf.Clamp(currentReserveAmmo + amount, 0, maxReserveAmmo);
|
||||
}
|
||||
|
||||
private void PlaySound(AudioClip clip)
|
||||
{
|
||||
if (clip != null && audioSource != null)
|
||||
{
|
||||
audioSource.PlayOneShot(clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/Weapon.cs.meta
Normal file
2
Assets/Scripts/Weapon.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 834e0aba591c01943a67d3ebcb8ac841
|
||||
23
Assets/Scripts/WeaponPickup.cs
Normal file
23
Assets/Scripts/WeaponPickup.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class WeaponPickup : PickupItem
|
||||
{
|
||||
[Header("Weapon Unlock Settings")]
|
||||
[SerializeField] private int weaponSlotIndex = 1;
|
||||
|
||||
protected override bool CanCollect(GameObject player)
|
||||
{
|
||||
var weaponsController = player.GetComponent<WeaponsController>() ?? player.GetComponentInChildren<WeaponsController>();
|
||||
return weaponsController != null;
|
||||
}
|
||||
|
||||
protected override void ApplyPickupEffect(GameObject player)
|
||||
{
|
||||
var weaponsController = player.GetComponent<WeaponsController>() ?? player.GetComponentInChildren<WeaponsController>();
|
||||
if (weaponsController != null)
|
||||
{
|
||||
weaponsController.EquipWeapon(weaponSlotIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/WeaponPickup.cs.meta
Normal file
2
Assets/Scripts/WeaponPickup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: feb02c794cb1bf24ebd4b0b7c46f94e6
|
||||
126
Assets/Scripts/WeaponsController.cs
Normal file
126
Assets/Scripts/WeaponsController.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class WeaponsController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Weapon[] weaponSlots;
|
||||
[SerializeField] private Camera playerCamera;
|
||||
[SerializeField] private LayerMask hitMask;
|
||||
|
||||
private int activeWeaponIndex = 0;
|
||||
private bool isFiring;
|
||||
|
||||
public Weapon ActiveWeapon => (weaponSlots != null && activeWeaponIndex >= 0 && activeWeaponIndex < weaponSlots.Length) ? weaponSlots[activeWeaponIndex] : null;
|
||||
public int ActiveWeaponIndex => activeWeaponIndex;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (playerCamera == null)
|
||||
{
|
||||
playerCamera = Camera.main;
|
||||
}
|
||||
EquipWeapon(0);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (isFiring && ActiveWeapon != null && ActiveWeapon.CanShoot())
|
||||
{
|
||||
ActiveWeapon.Shoot(playerCamera, hitMask);
|
||||
UpdateAmmoUI();
|
||||
}
|
||||
}
|
||||
|
||||
// Input System Handlers (supporting both "Fire" and default "Attack" action names)
|
||||
public void OnFire(InputValue value) => isFiring = value.isPressed;
|
||||
public void OnAttack(InputValue value) => isFiring = value.isPressed;
|
||||
|
||||
public void OnReload(InputValue value)
|
||||
{
|
||||
if (value.isPressed && ActiveWeapon != null)
|
||||
{
|
||||
StartCoroutine(ActiveWeapon.ReloadRoutine());
|
||||
}
|
||||
}
|
||||
|
||||
public void OnNextWeapon(InputValue value)
|
||||
{
|
||||
if (value.isPressed) NextWeapon();
|
||||
}
|
||||
|
||||
public void OnNext(InputValue value)
|
||||
{
|
||||
if (value.isPressed) NextWeapon();
|
||||
}
|
||||
|
||||
public void OnPrevious(InputValue value)
|
||||
{
|
||||
if (value.isPressed) PreviousWeapon();
|
||||
}
|
||||
|
||||
public void NextWeapon()
|
||||
{
|
||||
if (weaponSlots == null || weaponSlots.Length <= 1) return;
|
||||
int next = (activeWeaponIndex + 1) % weaponSlots.Length;
|
||||
EquipWeapon(next);
|
||||
}
|
||||
|
||||
public void PreviousWeapon()
|
||||
{
|
||||
if (weaponSlots == null || weaponSlots.Length <= 1) return;
|
||||
int prev = (activeWeaponIndex - 1 + weaponSlots.Length) % weaponSlots.Length;
|
||||
EquipWeapon(prev);
|
||||
}
|
||||
|
||||
public void EquipWeapon(int index)
|
||||
{
|
||||
if (weaponSlots == null || weaponSlots.Length == 0) return;
|
||||
if (index < 0 || index >= weaponSlots.Length) return;
|
||||
|
||||
for (int i = 0; i < weaponSlots.Length; i++)
|
||||
{
|
||||
if (weaponSlots[i] != null)
|
||||
{
|
||||
weaponSlots[i].gameObject.SetActive(i == index);
|
||||
}
|
||||
}
|
||||
|
||||
activeWeaponIndex = index;
|
||||
UpdateAmmoUI();
|
||||
}
|
||||
|
||||
public bool AddAmmoToActiveWeapon(int amount)
|
||||
{
|
||||
if (ActiveWeapon != null)
|
||||
{
|
||||
ActiveWeapon.AddReserveAmmo(amount);
|
||||
UpdateAmmoUI();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool AddAmmoToMatchingWeapon(string weaponIdentifier, int amount)
|
||||
{
|
||||
if (weaponSlots == null) return false;
|
||||
foreach (var weapon in weaponSlots)
|
||||
{
|
||||
if (weapon != null && (string.IsNullOrEmpty(weaponIdentifier) || weapon.weaponName.ToLower().Contains(weaponIdentifier.ToLower())))
|
||||
{
|
||||
weapon.AddReserveAmmo(amount);
|
||||
UpdateAmmoUI();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void UpdateAmmoUI()
|
||||
{
|
||||
if (ActiveWeapon != null && UIController.Instance != null)
|
||||
{
|
||||
UIController.Instance.UpdateAmmoDisplay(ActiveWeapon.currentAmmo, ActiveWeapon.maxAmmo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/Scripts/WeaponsController.cs.meta
Normal file
2
Assets/Scripts/WeaponsController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58999ab6c071a3e40abcb6c42fb9537b
|
||||
Reference in New Issue
Block a user