Initial commit
This commit is contained in:
154
Assets/Scripts/EnemyAI.cs
Normal file
154
Assets/Scripts/EnemyAI.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Synty.AnimationBaseLocomotion.Samples;
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
[RequireComponent(typeof(EnemyRagdoll))]
|
||||
public class EnemyAI : MonoBehaviour
|
||||
{
|
||||
[Header("Stats")]
|
||||
public int health = 30;
|
||||
private bool isStunned = false;
|
||||
|
||||
[Header("Attacking")]
|
||||
public float attackRange = 2.5f;
|
||||
public float attackCooldown = 2.0f;
|
||||
public int attackDamage = 5;
|
||||
private float lastAttackTime;
|
||||
|
||||
|
||||
[Header("Blocking")]
|
||||
[Range(0f, 1f)]
|
||||
public float blockChance = 0.15f; // 15% chance to block by default
|
||||
|
||||
private Animator anim;
|
||||
private Rigidbody rb;
|
||||
private EnemyRagdoll ragdoll;
|
||||
private bool isDead = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
anim = GetComponent<Animator>();
|
||||
rb = GetComponent<Rigidbody>();
|
||||
ragdoll = GetComponent<EnemyRagdoll>();
|
||||
|
||||
rb.isKinematic = true;
|
||||
rb.useGravity = false;
|
||||
|
||||
EnemySwarmManager.Instance.RegisterEnemy(transform);
|
||||
anim.SetFloat("MoveSpeed", 3.5f);
|
||||
|
||||
// Add a random offset to the initial attack timer so the mob doesn't attack in perfectly unified sync
|
||||
lastAttackTime = Time.time + Random.Range(0f, attackCooldown);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (isDead || isStunned) return;
|
||||
// PlayerController.Instance comes from the core script setup
|
||||
if (SamplePlayerAnimationController.Instance != null)
|
||||
{
|
||||
Vector3 offset = SamplePlayerAnimationController.Instance.transform.position - transform.position;
|
||||
|
||||
// sqrMagnitude is significantly faster for the CPU than Vector3.Distance
|
||||
if (offset.sqrMagnitude <= attackRange * attackRange)
|
||||
{
|
||||
if (Time.time - lastAttackTime > attackCooldown)
|
||||
{
|
||||
PerformAttack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformAttack()
|
||||
{
|
||||
lastAttackTime = Time.time;
|
||||
|
||||
// Tells the animator to play the attack animation
|
||||
anim.SetTrigger("Attack");
|
||||
}
|
||||
|
||||
// --- TRIGGERED VIA ANIMATION EVENT ON THE ENEMY ATTACK CLIP ---
|
||||
public void ExecuteEnemyHit()
|
||||
{
|
||||
if (isDead || SamplePlayerAnimationController.Instance == null) return;
|
||||
|
||||
// Simple check to see if the player is still in range when the attack lands
|
||||
Vector3 offset = SamplePlayerAnimationController.Instance.transform.position - transform.position;
|
||||
if (offset.sqrMagnitude <= attackRange * attackRange)
|
||||
{
|
||||
SamplePlayerAnimationController.Instance.GetComponent<PlayerHealth>().TakeDamage(attackDamage);
|
||||
}
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage, Vector3 hitSource, float knockbackForce)
|
||||
{
|
||||
if (isDead) return;
|
||||
|
||||
// --- BLOCKING LOGIC ---
|
||||
// Check if the attack is coming from in front of the enemy
|
||||
Vector3 directionToHitSource = (hitSource - transform.position).normalized;
|
||||
float facingDotProduct = Vector3.Dot(transform.forward, directionToHitSource);
|
||||
|
||||
// A dot product > 0.3 means the hit is roughly within a 140-degree cone in front of the enemy
|
||||
if (facingDotProduct > 0.3f)
|
||||
{
|
||||
if (Random.value < blockChance)
|
||||
{
|
||||
anim.SetTrigger("Block");
|
||||
|
||||
// Optional visual flair for blocking
|
||||
// Instantiate(sparksPrefab, transform.position + Vector3.up, Quaternion.identity);
|
||||
|
||||
return; // Exit early so no damage is taken and no ragdoll occurs
|
||||
}
|
||||
}
|
||||
|
||||
// --- DAMAGE LOGIC ---
|
||||
health -= damage;
|
||||
|
||||
if (health <= 0)
|
||||
{
|
||||
Die(hitSource, knockbackForce);
|
||||
}
|
||||
else
|
||||
{
|
||||
anim.SetTrigger("Hit");
|
||||
}
|
||||
}
|
||||
|
||||
void Die(Vector3 hitSource, float knockbackForce)
|
||||
{
|
||||
isDead = true;
|
||||
|
||||
EnemySwarmManager.Instance.RemoveEnemy(transform);
|
||||
ragdoll.ActivateRagdoll(hitSource, knockbackForce);
|
||||
Destroy(gameObject, 3f);
|
||||
this.enabled = false;
|
||||
}
|
||||
public void ApplyStun(float duration)
|
||||
{
|
||||
if (isDead || isStunned) return;
|
||||
StartCoroutine(StunRoutine(duration));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator StunRoutine(float duration)
|
||||
{
|
||||
isStunned = true;
|
||||
anim.SetTrigger("Stun"); // Requires a "Stun" trigger in your Animator
|
||||
|
||||
// Detach from the Burst Compiler so they stop sliding toward you
|
||||
EnemySwarmManager.Instance.PauseEnemyMovement(transform);
|
||||
|
||||
yield return new WaitForSeconds(duration);
|
||||
|
||||
if (!isDead)
|
||||
{
|
||||
isStunned = false;
|
||||
anim.SetTrigger("Recover"); // Requires a "Recover" trigger in your Animator
|
||||
|
||||
// Reattach to the Burst Compiler so they resume the chase
|
||||
EnemySwarmManager.Instance.ResumeEnemyMovement(transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/EnemyAI.cs.meta
Normal file
2
Assets/Scripts/EnemyAI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b189faa88795b640b3dd93901d9334e
|
||||
101
Assets/Scripts/EnemyRagdoll.cs
Normal file
101
Assets/Scripts/EnemyRagdoll.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class EnemyRagdoll : MonoBehaviour
|
||||
{
|
||||
private Rigidbody[] allRigidbodies;
|
||||
private Collider[] allColliders;
|
||||
private Animator animator;
|
||||
|
||||
// The root components we use while alive (so we don't turn them off)
|
||||
private Collider mainCollider;
|
||||
private Rigidbody mainRigidbody;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
animator = GetComponent<Animator>();
|
||||
mainCollider = GetComponent<Collider>();
|
||||
mainRigidbody = GetComponent<Rigidbody>();
|
||||
|
||||
// Grab every bone physics component in the children
|
||||
allRigidbodies = GetComponentsInChildren<Rigidbody>();
|
||||
allColliders = GetComponentsInChildren<Collider>();
|
||||
|
||||
DeactivateRagdoll();
|
||||
}
|
||||
|
||||
public void DeactivateRagdoll()
|
||||
{
|
||||
// Turn off bone physics so the Animator can control the model
|
||||
foreach (Rigidbody rb in allRigidbodies)
|
||||
{
|
||||
if (rb != mainRigidbody)
|
||||
{
|
||||
rb.isKinematic = true;
|
||||
rb.useGravity = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Collider col in allColliders)
|
||||
{
|
||||
if (col != mainCollider)
|
||||
{
|
||||
col.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateRagdoll(Vector3 hitSource, float knockbackForce)
|
||||
{
|
||||
// 1. Turn off the main capsule collider so it doesn't interfere
|
||||
if (mainCollider != null) mainCollider.enabled = false;
|
||||
if (mainRigidbody != null) mainRigidbody.isKinematic = true;
|
||||
|
||||
// 2. Turn on all bone physics
|
||||
foreach (Collider col in allColliders)
|
||||
{
|
||||
if (col != mainCollider) col.enabled = true;
|
||||
}
|
||||
|
||||
foreach (Rigidbody rb in allRigidbodies)
|
||||
{
|
||||
if (rb != mainRigidbody)
|
||||
{
|
||||
rb.isKinematic = false;
|
||||
rb.useGravity = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Apply the Sauron Force directly to the Hips
|
||||
ApplyForceToHips(hitSource, knockbackForce);
|
||||
|
||||
// 4. Turn off the Animator LAST to let physics seamlessly take over
|
||||
animator.enabled = false;
|
||||
}
|
||||
private void ApplyForceToHips(Vector3 hitSource, float knockbackForce)
|
||||
{
|
||||
// Synty uses Humanoid rigs, so we can reliably find the Hips bone
|
||||
Transform hipsTransform = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
|
||||
if (hipsTransform != null)
|
||||
{
|
||||
Rigidbody hipsRb = hipsTransform.GetComponent<Rigidbody>();
|
||||
if (hipsRb != null)
|
||||
{
|
||||
// Calculate trajectory: away from player + up in the air
|
||||
Vector3 launchDirection = (hipsTransform.position - hitSource).normalized;
|
||||
launchDirection.y = 1.2f;
|
||||
|
||||
// THE FIX: Use VelocityChange so the heavy Ragdoll mass is ignored!
|
||||
hipsRb.AddForce(launchDirection * knockbackForce, ForceMode.VelocityChange);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("<color=red>Hips found, but it has no Rigidbody attached!</color>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("<color=red>Animator could not find the Hips bone!</color>");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/EnemyRagdoll.cs.meta
Normal file
2
Assets/Scripts/EnemyRagdoll.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d32c151f33aa0a243b87150e637f7da7
|
||||
237
Assets/Scripts/EnemySwarmManager.cs
Normal file
237
Assets/Scripts/EnemySwarmManager.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Jobs;
|
||||
using Unity.Jobs;
|
||||
using Unity.Burst;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Collections;
|
||||
using Synty.AnimationBaseLocomotion.Samples;
|
||||
|
||||
// --- WAVE DATA STRUCTURES ---
|
||||
[System.Serializable]
|
||||
public class WaveSpawnData
|
||||
{
|
||||
public GameObject enemyPrefab;
|
||||
public int count;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class WaveDefinition
|
||||
{
|
||||
public string waveName = "New Wave";
|
||||
public WaveSpawnData[] enemyGroups;
|
||||
}
|
||||
|
||||
public class EnemySwarmManager : MonoBehaviour
|
||||
{
|
||||
public static EnemySwarmManager Instance;
|
||||
|
||||
[Header("Swarm Settings")]
|
||||
public float enemySpeed = 3.5f;
|
||||
|
||||
[Header("Wave Management")]
|
||||
public WaveDefinition[] waves;
|
||||
public Transform[] spawnPoints;
|
||||
public float timeBetweenWaves = 5.0f;
|
||||
[Tooltip("Delay between individual enemy spawns to prevent lag spikes")]
|
||||
public float spawnStagger = 0.05f;
|
||||
|
||||
private int currentWaveIndex = 0;
|
||||
private int activeEnemyCount = 0;
|
||||
private bool isSpawning = false;
|
||||
private bool gameEnded = false;
|
||||
|
||||
// Burst Compiler variables
|
||||
private TransformAccessArray transformAccessArray;
|
||||
private NativeArray<float3> targetPositions;
|
||||
private JobHandle movementJobHandle;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
transformAccessArray = new TransformAccessArray(1000);
|
||||
targetPositions = new NativeArray<float3>(1, Allocator.Persistent);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (waves.Length > 0)
|
||||
{
|
||||
UIManager.Instance.UpdateWaveProgress(1, waves.Length);
|
||||
UIManager.Instance.UpdateEnemiesRemaining(0);
|
||||
StartCoroutine(WaveCooldownRoutine());
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("No waves defined in the Swarm Manager!");
|
||||
}
|
||||
}
|
||||
|
||||
// --- WAVE LOGIC ---
|
||||
|
||||
private IEnumerator WaveCooldownRoutine()
|
||||
{
|
||||
isSpawning = true; // Prevent accidental wave-clears during cooldown
|
||||
Debug.Log($"<color=cyan>Next wave starting in {timeBetweenWaves} seconds...</color>");
|
||||
|
||||
yield return new WaitForSeconds(timeBetweenWaves);
|
||||
|
||||
StartCoroutine(SpawnWaveRoutine(waves[currentWaveIndex]));
|
||||
}
|
||||
|
||||
private IEnumerator SpawnWaveRoutine(WaveDefinition wave)
|
||||
{
|
||||
Debug.Log($"<color=yellow>Starting {wave.waveName}</color>");
|
||||
isSpawning = true;
|
||||
|
||||
// Iterate through each type of enemy requested in this wave
|
||||
foreach (WaveSpawnData spawnGroup in wave.enemyGroups)
|
||||
{
|
||||
for (int i = 0; i < spawnGroup.count; i++)
|
||||
{
|
||||
// Pick a random spawn point from the array
|
||||
Transform randomSpawnPoint = spawnPoints[UnityEngine.Random.Range(0, spawnPoints.Length)];
|
||||
|
||||
// Add a tiny random offset so they don't spawn perfectly inside each other
|
||||
Vector3 spawnPos = randomSpawnPoint.position + new Vector3(
|
||||
UnityEngine.Random.Range(-2f, 2f),
|
||||
0f,
|
||||
UnityEngine.Random.Range(-2f, 2f)
|
||||
);
|
||||
|
||||
Instantiate(spawnGroup.enemyPrefab, spawnPos, randomSpawnPoint.rotation);
|
||||
|
||||
// Yield briefly to spread the Instantiation load across multiple frames
|
||||
yield return new WaitForSeconds(spawnStagger);
|
||||
}
|
||||
}
|
||||
|
||||
isSpawning = false;
|
||||
|
||||
// Failsafe: Check if the player somehow killed them all before the spawning even finished
|
||||
CheckWaveClear();
|
||||
}
|
||||
|
||||
public void RegisterEnemy(Transform enemyTransform)
|
||||
{
|
||||
movementJobHandle.Complete();
|
||||
transformAccessArray.Add(enemyTransform);
|
||||
|
||||
activeEnemyCount++;
|
||||
if (UIManager.Instance != null) UIManager.Instance.UpdateEnemiesRemaining(activeEnemyCount);
|
||||
}
|
||||
|
||||
public void RemoveEnemy(Transform enemyTransform)
|
||||
{
|
||||
movementJobHandle.Complete();
|
||||
for (int i = 0; i < transformAccessArray.length; i++)
|
||||
{
|
||||
if (transformAccessArray[i] == enemyTransform)
|
||||
{
|
||||
transformAccessArray.RemoveAtSwapBack(i);
|
||||
|
||||
activeEnemyCount--;
|
||||
if (UIManager.Instance != null) UIManager.Instance.UpdateEnemiesRemaining(activeEnemyCount);
|
||||
|
||||
CheckWaveClear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckWaveClear()
|
||||
{
|
||||
// Don't trigger a clear if we are currently spawning, or if the game is over
|
||||
if (isSpawning || gameEnded) return;
|
||||
|
||||
if (activeEnemyCount <= 0)
|
||||
{
|
||||
Debug.Log($"<color=green>Wave {currentWaveIndex + 1} Cleared!</color>");
|
||||
currentWaveIndex++;
|
||||
|
||||
if (currentWaveIndex < waves.Length)
|
||||
{
|
||||
// Update UI for the upcoming wave
|
||||
UIManager.Instance.UpdateWaveProgress(currentWaveIndex + 1, waves.Length);
|
||||
StartCoroutine(WaveCooldownRoutine());
|
||||
}
|
||||
else
|
||||
{
|
||||
gameEnded = true;
|
||||
Debug.Log("<color=gold>VICTORY! All waves cleared!</color>");
|
||||
// TODO: Trigger Victory UI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- BURST COMPILER MOVEMENT ---
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (SamplePlayerAnimationController.Instance == null || transformAccessArray.length == 0) return;
|
||||
|
||||
targetPositions[0] = SamplePlayerAnimationController.Instance.transform.position;
|
||||
|
||||
SwarmJob swarmJob = new SwarmJob
|
||||
{
|
||||
targetPos = targetPositions,
|
||||
speed = enemySpeed,
|
||||
dt = Time.deltaTime
|
||||
};
|
||||
|
||||
movementJobHandle = swarmJob.Schedule(transformAccessArray);
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
movementJobHandle.Complete();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
movementJobHandle.Complete();
|
||||
if (transformAccessArray.isCreated) transformAccessArray.Dispose();
|
||||
if (targetPositions.IsCreated) targetPositions.Dispose();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
struct SwarmJob : IJobParallelForTransform
|
||||
{
|
||||
[ReadOnly] public NativeArray<float3> targetPos;
|
||||
public float speed;
|
||||
public float dt;
|
||||
|
||||
public void Execute(int index, TransformAccess transform)
|
||||
{
|
||||
float3 currentPos = transform.position;
|
||||
float3 dir = targetPos[0] - currentPos;
|
||||
|
||||
if (math.lengthsq(dir) > 4.0f)
|
||||
{
|
||||
dir = math.normalize(dir);
|
||||
transform.position = currentPos + (dir * speed * dt);
|
||||
|
||||
quaternion targetRot = quaternion.LookRotationSafe(dir, math.up());
|
||||
transform.rotation = targetRot;
|
||||
}
|
||||
}
|
||||
}
|
||||
public void PauseEnemyMovement(Transform enemyTransform)
|
||||
{
|
||||
movementJobHandle.Complete();
|
||||
for (int i = 0; i < transformAccessArray.length; i++)
|
||||
{
|
||||
if (transformAccessArray[i] == enemyTransform)
|
||||
{
|
||||
transformAccessArray.RemoveAtSwapBack(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ResumeEnemyMovement(Transform enemyTransform)
|
||||
{
|
||||
movementJobHandle.Complete();
|
||||
transformAccessArray.Add(enemyTransform);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/EnemySwarmManager.cs.meta
Normal file
2
Assets/Scripts/EnemySwarmManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64c7522469f294044bc3fa07b42fe8fe
|
||||
84
Assets/Scripts/Pickup.cs
Normal file
84
Assets/Scripts/Pickup.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Synty.AnimationBaseLocomotion.Samples;
|
||||
using UnityEngine;
|
||||
|
||||
public class Pickup : MonoBehaviour
|
||||
{
|
||||
public enum PickupType { Health, Strength, StunBomb }
|
||||
|
||||
[Header("Pickup Settings")]
|
||||
public PickupType type;
|
||||
public float rotationSpeed = 90f; // Gives it that classic video game spin
|
||||
|
||||
[Header("Health Settings")]
|
||||
public int healAmount = 50;
|
||||
|
||||
[Header("Strength Settings")]
|
||||
public float strengthMultiplier = 2.0f; // Doubles damage and knockback
|
||||
public float strengthDuration = 10f;
|
||||
|
||||
[Header("Stun Settings")]
|
||||
public float stunRadius = 8f;
|
||||
public float stunDuration = 5f;
|
||||
public LayerMask enemyLayer;
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Slowly rotate the pickup to make it visually obvious
|
||||
transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// Make sure your Player Prefab has the "Player" Tag assigned at the very top of its Inspector!
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
ApplyPickupEffect(other.gameObject);
|
||||
|
||||
// Optional: Play a sound or spawn a particle effect here before destroying
|
||||
|
||||
Destroy(gameObject); // Remove the pickup from the world
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPickupEffect(GameObject player)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PickupType.Health:
|
||||
PlayerHealth health = player.GetComponent<PlayerHealth>();
|
||||
if (health != null) health.Heal(healAmount);
|
||||
break;
|
||||
|
||||
case PickupType.Strength:
|
||||
SyntyPlayerCombat combat = player.GetComponentInChildren<SyntyPlayerCombat>();
|
||||
if (combat != null) combat.ApplyStrengthBuff(strengthMultiplier, strengthDuration);
|
||||
break;
|
||||
|
||||
case PickupType.StunBomb:
|
||||
ExecuteStunBomb(transform.position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteStunBomb(Vector3 center)
|
||||
{
|
||||
Collider[] hitEnemies = Physics.OverlapSphere(center, stunRadius, enemyLayer);
|
||||
foreach (Collider enemyCollider in hitEnemies)
|
||||
{
|
||||
EnemyAI enemy = enemyCollider.GetComponentInParent<EnemyAI>();
|
||||
if (enemy != null)
|
||||
{
|
||||
enemy.ApplyStun(stunDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnDrawGizmosSelected()
|
||||
{
|
||||
if (type == PickupType.StunBomb)
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(transform.position, stunRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Pickup.cs.meta
Normal file
2
Assets/Scripts/Pickup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cb2e64f10285bd47afd23d4297f6f90
|
||||
41
Assets/Scripts/PlayerHealth.cs
Normal file
41
Assets/Scripts/PlayerHealth.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerHealth : MonoBehaviour
|
||||
{
|
||||
public int maxHealth = 100;
|
||||
private int currentHealth;
|
||||
|
||||
void Start()
|
||||
{
|
||||
currentHealth = maxHealth;
|
||||
// Force the UI to update on frame 1
|
||||
UIManager.Instance.UpdatePlayerHealth(currentHealth, maxHealth);
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
currentHealth -= damage;
|
||||
currentHealth = Mathf.Max(0, currentHealth); // Prevent negative health
|
||||
|
||||
UIManager.Instance.UpdatePlayerHealth(currentHealth, maxHealth);
|
||||
|
||||
if (currentHealth <= 0)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
Debug.Log("<color=red>Player has fallen! The horde wins.</color>");
|
||||
// TODO: Trigger player death animation and game over screen
|
||||
}
|
||||
public void Heal(int amount)
|
||||
{
|
||||
currentHealth += amount;
|
||||
currentHealth = Mathf.Min(currentHealth, maxHealth); // Prevent overhealing
|
||||
|
||||
UIManager.Instance.UpdatePlayerHealth(currentHealth, maxHealth);
|
||||
Debug.Log($"<color=green>Player Healed for {amount}!</color>");
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/PlayerHealth.cs.meta
Normal file
2
Assets/Scripts/PlayerHealth.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c125d7f669560234e82eb7c0ea36b04b
|
||||
53
Assets/Scripts/UIManager.cs
Normal file
53
Assets/Scripts/UIManager.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
using TMPro; // Required for TextMeshPro
|
||||
|
||||
public class UIManager : MonoBehaviour
|
||||
{
|
||||
public static UIManager Instance { get; private set; }
|
||||
|
||||
[Header("Player UI")]
|
||||
public TextMeshProUGUI healthText;
|
||||
|
||||
[Header("Wave UI")]
|
||||
public TextMeshProUGUI waveText;
|
||||
public TextMeshProUGUI enemiesLeftText;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Simple Singleton setup for the prototype
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePlayerHealth(int currentHealth, int maxHealth)
|
||||
{
|
||||
if (healthText != null)
|
||||
{
|
||||
healthText.text = $"HP: {currentHealth} / {maxHealth}";
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateWaveProgress(int currentWave, int totalWaves)
|
||||
{
|
||||
if (waveText != null)
|
||||
{
|
||||
// If totalWaves is 0, it means endless mode
|
||||
string totalString = totalWaves > 0 ? totalWaves.ToString() : "???";
|
||||
waveText.text = $"Wave: {currentWave} / {totalString}";
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateEnemiesRemaining(int enemiesAlive)
|
||||
{
|
||||
if (enemiesLeftText != null)
|
||||
{
|
||||
enemiesLeftText.text = $"Enemies Left: {enemiesAlive}";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UIManager.cs.meta
Normal file
2
Assets/Scripts/UIManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95eb56f5cbc49da4cab7d2276ddf63db
|
||||
Reference in New Issue
Block a user