From d2772cdbdb2f4b69293a8a5d8378a0d31be41887 Mon Sep 17 00:00:00 2001 From: "SDQHOME\\caleb" Date: Fri, 17 Apr 2026 10:38:43 +0100 Subject: [PATCH] ai and player nd health scripts --- Assets/Scripts/AIBehaviour.cs | 298 +++++++++++++++++++++++++++ Assets/Scripts/AIBehaviour.cs.meta | 2 + Assets/Scripts/Barrier.cs | 82 ++++++++ Assets/Scripts/Barrier.cs.meta | 2 + Assets/Scripts/PlayerHealth.cs | 49 +++++ Assets/Scripts/PlayerHealth.cs.meta | 2 + Assets/Settings/Mobile_RPAsset.asset | 14 +- 7 files changed, 446 insertions(+), 3 deletions(-) create mode 100644 Assets/Scripts/AIBehaviour.cs create mode 100644 Assets/Scripts/AIBehaviour.cs.meta create mode 100644 Assets/Scripts/Barrier.cs create mode 100644 Assets/Scripts/Barrier.cs.meta create mode 100644 Assets/Scripts/PlayerHealth.cs create mode 100644 Assets/Scripts/PlayerHealth.cs.meta diff --git a/Assets/Scripts/AIBehaviour.cs b/Assets/Scripts/AIBehaviour.cs new file mode 100644 index 0000000..b182b89 --- /dev/null +++ b/Assets/Scripts/AIBehaviour.cs @@ -0,0 +1,298 @@ +using UnityEngine; +using UnityEngine.AI; + +/// +/// Call of Duty Zombies-style enemy AI. +/// States: Seeking (chase player) → BreakingBarrier (hammer a blocked door/window) → Attacking (melee the player) +/// +/// Setup requirements: +/// - NavMeshAgent on this GameObject +/// - Player GameObject tagged "Player" with a PlayerHealth component +/// - Barrier objects on a layer assigned to barrierLayer, with a Barrier component + NavMeshObstacle (carving on) +/// +[RequireComponent(typeof(NavMeshAgent))] +public class AIBehaviour : MonoBehaviour +{ + public enum ZombieState { Seeking, BreakingBarrier, Attacking, Dead } + + // ------------------------------------------------------------------------- + // Inspector + // ------------------------------------------------------------------------- + [Header("Movement")] + public float moveSpeed = 3.5f; + public float acceleration = 12f; + + [Header("Attack — Player")] + public float attackRange = 1.8f; + public float attackDamage = 15f; + [Tooltip("Attacks per second")] public float attackRate = 1f; + + [Header("Attack — Barrier")] + public float barrierDetectRange = 1.6f; + public float barrierDamage = 25f; + [Tooltip("Barrier hits per second")] public float barrierAttackRate = 1.5f; + public LayerMask barrierLayer; + + [Header("Health")] + public float maxHealth = 100f; + + [Header("Animation (optional)")] + public Animator animator; + + // ------------------------------------------------------------------------- + // Private state + // ------------------------------------------------------------------------- + private NavMeshAgent _agent; + private Transform _player; + private PlayerHealth _playerHealth; + private ZombieState _state = ZombieState.Seeking; + private float _health; + private float _attackTimer; + private float _barrierTimer; + private Barrier _targetBarrier; + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + void Awake() + { + _agent = GetComponent(); + _agent.speed = moveSpeed; + _agent.acceleration = acceleration; + _agent.stoppingDistance = attackRange * 0.85f; + _agent.autoBraking = true; + _health = maxHealth; + } + + void Start() + { + GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); + if (playerObj != null) + { + _player = playerObj.transform; + _playerHealth = playerObj.GetComponent(); + } + else + { + Debug.LogWarning("[ZombieAI] No GameObject tagged 'Player' found."); + } + } + + void Update() + { + if (_state == ZombieState.Dead || _player == null) return; + + _attackTimer -= Time.deltaTime; + _barrierTimer -= Time.deltaTime; + + switch (_state) + { + case ZombieState.Seeking: UpdateSeeking(); break; + case ZombieState.BreakingBarrier: UpdateBreakingBarrier(); break; + case ZombieState.Attacking: UpdateAttacking(); break; + } + + UpdateAnimator(); + } + + // ------------------------------------------------------------------------- + // Seeking — navigate toward the player, detect barriers blocking the path + // ------------------------------------------------------------------------- + void UpdateSeeking() + { + float dist = Vector3.Distance(transform.position, _player.position); + + // Close enough to swing + if (dist <= attackRange) + { + _agent.ResetPath(); + TransitionTo(ZombieState.Attacking); + return; + } + + // Keep chasing + _agent.SetDestination(_player.position); + + // If the NavMesh path can't reach the player, a barrier may be blocking + if (_agent.pathStatus == NavMeshPathStatus.PathPartial || + _agent.pathStatus == NavMeshPathStatus.PathInvalid) + { + Barrier b = ScanForBarrierAhead(); + if (b != null) + { + _targetBarrier = b; + _agent.ResetPath(); + TransitionTo(ZombieState.BreakingBarrier); + return; + } + } + + // Also do a short spherecast while moving in case the path is briefly valid + // but a barrier is right in front of us (tight corridor) + if (_agent.velocity.magnitude > 0.2f) + { + Barrier bAhead = ScanForBarrierAhead(); + if (bAhead != null) + { + _targetBarrier = bAhead; + _agent.ResetPath(); + TransitionTo(ZombieState.BreakingBarrier); + } + } + } + + // ------------------------------------------------------------------------- + // Breaking barrier — face it and hammer until it falls + // ------------------------------------------------------------------------- + void UpdateBreakingBarrier() + { + // Barrier was destroyed (by us or another zombie) + if (_targetBarrier == null || _targetBarrier.IsDestroyed) + { + _targetBarrier = null; + TransitionTo(ZombieState.Seeking); + return; + } + + // Face the barrier + FaceTarget(_targetBarrier.transform.position); + + // Hit on cooldown + if (_barrierTimer <= 0f) + { + _targetBarrier.TakeDamage(barrierDamage); + _barrierTimer = 1f / barrierAttackRate; + TriggerAnimatorTrigger("BarrierHit"); + } + } + + // ------------------------------------------------------------------------- + // Attacking — melee the player + // ------------------------------------------------------------------------- + void UpdateAttacking() + { + float dist = Vector3.Distance(transform.position, _player.position); + + // Player escaped melee range — resume chase + if (dist > attackRange + 0.6f) + { + TransitionTo(ZombieState.Seeking); + return; + } + + FaceTarget(_player.position); + + if (_attackTimer <= 0f) + { + PlayerHealth ph = _player.GetComponent(); + if (ph != null) ph.TakeDamage(attackDamage); + _attackTimer = 1f / attackRate; + TriggerAnimatorTrigger("Attack"); + } + } + + // ------------------------------------------------------------------------- + // Public API — take damage / die + // ------------------------------------------------------------------------- + public void TakeDamage(float damage) + { + if (_state == ZombieState.Dead) return; + _health -= damage; + if (_health <= 0f) Die(); + } + + void Die() + { + TransitionTo(ZombieState.Dead); + _agent.ResetPath(); + _agent.enabled = false; + TriggerAnimatorTrigger("Die"); + Destroy(gameObject, 3f); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /// Spherecast in the direction of movement to find a Barrier collider. + Barrier ScanForBarrierAhead() + { + Vector3 origin = transform.position + Vector3.up * 0.8f; + Vector3 dir = _agent.desiredVelocity.magnitude > 0.1f + ? _agent.desiredVelocity.normalized + : transform.forward; + + if (Physics.SphereCast(origin, 0.45f, dir, out RaycastHit hit, + barrierDetectRange, barrierLayer, + QueryTriggerInteraction.Ignore)) + { + Barrier b = hit.collider.GetComponentInParent(); + if (b != null && !b.IsDestroyed) + return b; + } + return null; + } + + void FaceTarget(Vector3 targetPos) + { + Vector3 dir = (targetPos - transform.position); + dir.y = 0f; + if (dir.sqrMagnitude < 0.001f) return; + Quaternion target = Quaternion.LookRotation(dir); + transform.rotation = Quaternion.Slerp(transform.rotation, target, 10f * Time.deltaTime); + } + + void TransitionTo(ZombieState next) + { + _state = next; + } + + // ------------------------------------------------------------------------- + // Animator bridge — all parameters are optional; missing params are ignored + // ------------------------------------------------------------------------- + void UpdateAnimator() + { + if (animator == null) return; + SetAnimFloat("Speed", _agent.velocity.magnitude); + SetAnimBool("IsAttacking", _state == ZombieState.Attacking); + SetAnimBool("IsBreakingBarrier",_state == ZombieState.BreakingBarrier); + SetAnimBool("IsDead", _state == ZombieState.Dead); + } + + void SetAnimFloat(string name, float value) + { + if (animator.HasParameterByName(name)) animator.SetFloat(name, value); + } + + void SetAnimBool(string name, bool value) + { + if (animator.HasParameterByName(name)) animator.SetBool(name, value); + } + + void TriggerAnimatorTrigger(string name) + { + if (animator != null && animator.HasParameterByName(name)) + animator.SetTrigger(name); + } + +#if UNITY_EDITOR + void OnDrawGizmosSelected() + { + Gizmos.color = Color.red; + Gizmos.DrawWireSphere(transform.position, attackRange); + Gizmos.color = Color.yellow; + Gizmos.DrawWireSphere(transform.position, barrierDetectRange); + } +#endif +} + +/// Extension to safely check Animator parameter existence +public static class AnimatorExtensions +{ + public static bool HasParameterByName(this Animator animator, string name) + { + foreach (AnimatorControllerParameter p in animator.parameters) + if (p.name == name) return true; + return false; + } +} diff --git a/Assets/Scripts/AIBehaviour.cs.meta b/Assets/Scripts/AIBehaviour.cs.meta new file mode 100644 index 0000000..6523321 --- /dev/null +++ b/Assets/Scripts/AIBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1dace61880c5f3c4ebca0fd5711cc442 \ No newline at end of file diff --git a/Assets/Scripts/Barrier.cs b/Assets/Scripts/Barrier.cs new file mode 100644 index 0000000..c18c65a --- /dev/null +++ b/Assets/Scripts/Barrier.cs @@ -0,0 +1,82 @@ +using UnityEngine; +using UnityEngine.AI; + +/// +/// Attach to any door/window barrier the zombie should break through. +/// +/// Setup: +/// - Add a NavMeshObstacle component (Carve = true) so the NavMesh treats it as blocked. +/// - Assign the barrier's layer to match AIBehaviour.barrierLayer. +/// - Optionally populate the 'boards' array with child visuals that get hidden as health drops. +/// +public class Barrier : MonoBehaviour +{ + [Header("Health")] + public float maxHealth = 100f; + + [Header("Board Visuals (optional)")] + [Tooltip("Child objects representing planks/boards. They are hidden progressively as health drops.")] + public GameObject[] boards; + + public bool IsDestroyed { get; private set; } + + private float _health; + private NavMeshObstacle _obstacle; + + void Awake() + { + _health = maxHealth; + _obstacle = GetComponent(); + } + + public void TakeDamage(float damage) + { + if (IsDestroyed) return; + + _health -= damage; + UpdateBoards(); + + if (_health <= 0f) + Break(); + } + + void UpdateBoards() + { + if (boards == null || boards.Length == 0) return; + + // Show boards proportional to remaining health + int boardsToShow = Mathf.CeilToInt((_health / maxHealth) * boards.Length); + for (int i = 0; i < boards.Length; i++) + { + if (boards[i] != null) + boards[i].SetActive(i < boardsToShow); + } + } + + void Break() + { + IsDestroyed = true; + + // Disable the NavMesh obstacle so pathfinding opens back up + if (_obstacle != null) + _obstacle.enabled = false; + + // Disable colliders so zombies walk through + foreach (Collider col in GetComponentsInChildren()) + col.enabled = false; + + // Hide visuals + foreach (GameObject board in boards) + if (board != null) board.SetActive(false); + + Destroy(gameObject, 0.1f); + } + +#if UNITY_EDITOR + void OnDrawGizmosSelected() + { + Gizmos.color = new Color(1f, 0.5f, 0f, 0.5f); + Gizmos.DrawWireCube(transform.position, transform.localScale); + } +#endif +} diff --git a/Assets/Scripts/Barrier.cs.meta b/Assets/Scripts/Barrier.cs.meta new file mode 100644 index 0000000..c11db32 --- /dev/null +++ b/Assets/Scripts/Barrier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: db8fa53b3e3928e42878084d32a9d6b6 \ No newline at end of file diff --git a/Assets/Scripts/PlayerHealth.cs b/Assets/Scripts/PlayerHealth.cs new file mode 100644 index 0000000..284ad71 --- /dev/null +++ b/Assets/Scripts/PlayerHealth.cs @@ -0,0 +1,49 @@ +using UnityEngine; +using UnityEngine.Events; + +/// +/// Simple player health component. Attach to the Player GameObject. +/// +public class PlayerHealth : MonoBehaviour +{ + [Header("Health")] + public float maxHealth = 100f; + + [Header("Events")] + public UnityEvent onDeath; + public UnityEvent onHealthChanged; // passes current health + + public float CurrentHealth { get; private set; } + public bool IsDead { get; private set; } + + void Awake() + { + CurrentHealth = maxHealth; + } + + public void TakeDamage(float damage) + { + if (IsDead) return; + + CurrentHealth = Mathf.Max(0f, CurrentHealth - damage); + onHealthChanged?.Invoke(CurrentHealth); + + if (CurrentHealth <= 0f) + Die(); + } + + public void Heal(float amount) + { + if (IsDead) return; + + CurrentHealth = Mathf.Min(maxHealth, CurrentHealth + amount); + onHealthChanged?.Invoke(CurrentHealth); + } + + void Die() + { + IsDead = true; + onDeath?.Invoke(); + Debug.Log("[PlayerHealth] Player died."); + } +} diff --git a/Assets/Scripts/PlayerHealth.cs.meta b/Assets/Scripts/PlayerHealth.cs.meta new file mode 100644 index 0000000..89c621d --- /dev/null +++ b/Assets/Scripts/PlayerHealth.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4004eaf09fe63cb48bd59da9575615ff \ No newline at end of file diff --git a/Assets/Settings/Mobile_RPAsset.asset b/Assets/Settings/Mobile_RPAsset.asset index 7ceffe7..fedee07 100644 --- a/Assets/Settings/Mobile_RPAsset.asset +++ b/Assets/Settings/Mobile_RPAsset.asset @@ -12,8 +12,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3} m_Name: Mobile_RPAsset m_EditorClassIdentifier: - k_AssetVersion: 12 - k_AssetPreviousVersion: 12 + k_AssetVersion: 13 + k_AssetPreviousVersion: 13 m_RendererType: 1 m_RendererData: {fileID: 0} m_RendererDataList: @@ -53,6 +53,7 @@ MonoBehaviour: m_AdditionalLightsShadowResolutionTierHigh: 1024 m_ReflectionProbeBlending: 1 m_ReflectionProbeBoxProjection: 1 + m_ReflectionProbeAtlas: 1 m_ShadowDistance: 50 m_ShadowCascadeCount: 1 m_Cascade2Split: 0.25 @@ -78,11 +79,11 @@ MonoBehaviour: m_UseAdaptivePerformance: 1 m_ColorGradingMode: 0 m_ColorGradingLutSize: 32 + m_AllowPostProcessAlphaOutput: 0 m_UseFastSRGBLinearConversion: 1 m_SupportDataDrivenLensFlare: 1 m_SupportScreenSpaceLensFlare: 1 m_GPUResidentDrawerMode: 0 - m_UseLegacyLightmaps: 0 m_SmallMeshScreenPercentage: 0 m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0 m_ShadowType: 1 @@ -109,6 +110,7 @@ MonoBehaviour: m_PrefilterDebugKeywords: 1 m_PrefilterWriteRenderingLayers: 1 m_PrefilterHDROutput: 1 + m_PrefilterAlphaOutput: 0 m_PrefilterSSAODepthNormals: 1 m_PrefilterSSAOSourceDepthLow: 1 m_PrefilterSSAOSourceDepthMedium: 0 @@ -126,8 +128,14 @@ MonoBehaviour: m_PrefilterSoftShadowsQualityHigh: 1 m_PrefilterSoftShadows: 0 m_PrefilterScreenCoord: 1 + m_PrefilterScreenSpaceIrradiance: 0 m_PrefilterNativeRenderPass: 1 m_PrefilterUseLegacyLightmaps: 0 + m_PrefilterBicubicLightmapSampling: 0 + m_PrefilterReflectionProbeRotation: 0 + m_PrefilterReflectionProbeBlending: 0 + m_PrefilterReflectionProbeBoxProjection: 0 + m_PrefilterReflectionProbeAtlas: 0 m_ShaderVariantLogLevel: 0 m_ShadowCascades: 0 m_Textures: