From 7c73c4100d522f535ecec3dc9cf03c8451219570 Mon Sep 17 00:00:00 2001 From: katboi01 Date: Wed, 30 Apr 2025 00:37:19 +0200 Subject: [PATCH] initial commit --- Characters/CharacterBase.cs | 71 +++++++++ Characters/Display/Display.cs | 36 +++++ Characters/EnemyController.cs | 144 +++++++++++++++++ Characters/PlayerController.cs | 278 +++++++++++++++++++++++++++++++++ Interfaces/IDamageable.cs | 6 + README.md | 6 + 6 files changed, 541 insertions(+) create mode 100644 Characters/CharacterBase.cs create mode 100644 Characters/Display/Display.cs create mode 100644 Characters/EnemyController.cs create mode 100644 Characters/PlayerController.cs create mode 100644 Interfaces/IDamageable.cs create mode 100644 README.md diff --git a/Characters/CharacterBase.cs b/Characters/CharacterBase.cs new file mode 100644 index 0000000..ed21d9d --- /dev/null +++ b/Characters/CharacterBase.cs @@ -0,0 +1,71 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class CharacterBase : MonoBehaviour, IDamageable { + + public int Health = 5; + public float Speed = 5; + public Display Display; + + [SerializeField] protected List Footsteps = new List(); + + protected float _footstepDelay = 0.5f; + protected float _footstepTimer = 0; + + protected CharacterController _cc; + protected AudioSource _audioSource; + + public bool IsAlive() { return Health > 0; } + + protected virtual void Awake() + { + _cc = GetComponent(); + _audioSource = GetComponent(); + } + + protected virtual void Start () { + + } + + protected virtual void Update () { + + } + + protected void PlayFootstep() + { + if (Footsteps.Count == 0) return; + if (!_audioSource.isPlaying) + { + if (_footstepTimer > 0) + { + _footstepTimer -= Time.deltaTime; + } + else + { + _audioSource.clip = Footsteps[Random.Range(0, Footsteps.Count)]; + _audioSource.Play(); + _footstepTimer = _footstepDelay; + } + } + } + + public virtual void TakeDamage(int dmg) + { + if(Health > 0) + { + Health -= dmg; + + if (Health <= 0) + { + OnDeath(); + } + } + } + + protected virtual void OnDeath() + { + //GameManager.Instance.PlaySound(DeathSound); + //Destroy(this.gameObject); + } +} diff --git a/Characters/Display/Display.cs b/Characters/Display/Display.cs new file mode 100644 index 0000000..f1eff7b --- /dev/null +++ b/Characters/Display/Display.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +public class Display : MonoBehaviour +{ + public bool Standalone = false; + + private void Update() + { + if (Standalone) + { + ManualUpdate(transform.forward); + } + } + + public virtual void Init() { } + + public virtual void ManualUpdate(Vector3 direction) { } + + public virtual void PlayState(string stateName, int layer = 0, float time = -1) { } + + public virtual void SetBool(string name, bool value) { } + + public virtual void SetFloat(string name, float value) { } + + public virtual void SetEvent(string name, bool value) { } + + public virtual void SetLayerWeight(int layer, float weight) { } + + public virtual Sprite GetSprite() { return null; } + + public virtual void ShowAfterImage(float timeOut) { } + + public virtual int GetAnimatorLayersCount() { return 0; } + public virtual AnimatorStateInfo GetCurrentAnimatorStateInfo(int layer = 0) { return new AnimatorStateInfo(); } + public virtual AnimatorStateInfo GetNextAnimatorStateInfo(int layer = 0) { return new AnimatorStateInfo(); } +} diff --git a/Characters/EnemyController.cs b/Characters/EnemyController.cs new file mode 100644 index 0000000..8ca6342 --- /dev/null +++ b/Characters/EnemyController.cs @@ -0,0 +1,144 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AI; +using UnityEngine.UI; + +[RequireComponent(typeof(NavMeshAgent), typeof(CharacterController))] +public class EnemyController : CharacterBase +{ + public enum AIState + { + Patrolling, + Chasing + } + + public AIState State; + public List PatrolPoints; + private NavMeshAgent _agent; + private Coroutine _attack; + private Coroutine _currentAction; + public LayerMask PlayerLayer; + public Image PlayerDetectIndicator; + + protected override void Awake() + { + base.Awake(); + _agent = GetComponent(); + _agent.speed = Speed; + } + + public bool IsAttacking() + { + return Display.GetCurrentAnimatorStateInfo(1).IsName("Attack"); + } + + public bool IsRecoil() + { + return Display.GetCurrentAnimatorStateInfo(0).IsName("Hit"); + } + + protected override void Update() + { + if (!IsAlive()) return; + if (GameManager.Instance == null) return; + + CharacterBase target = GameManager.Instance.Player; + if (_attack != null) return; + if (target == null) return; + + if (State == AIState.Patrolling) + { + if (_currentAction == null) + { + Debug.Log("Patrollin"); + _currentAction = StartCoroutine(SetNewPatrolPoint()); + } + else + { + var detectDistance = 5; + var detectAngle = 45; + var playerDir = target.transform.position - transform.position; + + if(Vector3.Angle(transform.forward, playerDir) <= detectAngle + && Physics.Raycast(transform.position + Vector3.up * 0.5f, playerDir, detectDistance, PlayerLayer.value)) + { + PlayerDetectIndicator.fillAmount += Time.deltaTime; + if(PlayerDetectIndicator.fillAmount >= 1) + { + State = AIState.Chasing; + } + } + else + { + PlayerDetectIndicator.fillAmount -= Time.deltaTime; + } + } + } + else if (State == AIState.Chasing) + { + _agent.SetDestination(target.transform.position); + var dist = Vector3.Distance(transform.position, target.transform.position); + + if (dist < 2.5f) + { + _attack = StartCoroutine(Attack((target.transform.position - transform.position).normalized)); + } + } + } + + private IEnumerator Attack(Vector3 moveDirection) + { + _agent.enabled = false; + _cc.enabled = true; + + Display.PlayState("Attack"); + float attackTime = 0; + + while(attackTime < 2) + { + _cc.SimpleMove(moveDirection * 5); + attackTime += Time.deltaTime; + yield return 0; + } + + _cc.enabled = false; + _agent.enabled = true; + _attack = null; + } + + private IEnumerator SetNewPatrolPoint() + { + var patrolPoint = PatrolPoints[Random.Range(0, PatrolPoints.Count)]; + var bounds = patrolPoint.bounds; + var point = new Vector3( + Random.Range(bounds.min.x, bounds.max.x), + Random.Range(bounds.min.y, bounds.max.y), + Random.Range(bounds.min.z, bounds.max.z) + ); + _agent.SetDestination(point); + float timeout = 10f; + while (!_agent.isStopped && timeout > 0) + { + timeout -= Time.deltaTime; + yield return 0; + } + yield return new WaitForSeconds(Random.Range(5f, 10f)); + _currentAction = null; + } + + public override void TakeDamage(int dmg) + { + base.TakeDamage(dmg); + if (IsAlive()) + { + Display.PlayState("Hit"); + } + } + + protected override void OnDeath() + { + Display.PlayState("Die"); + _cc.enabled = false; + } +} diff --git a/Characters/PlayerController.cs b/Characters/PlayerController.cs new file mode 100644 index 0000000..3e3bd48 --- /dev/null +++ b/Characters/PlayerController.cs @@ -0,0 +1,278 @@ +using System.Collections.Generic; +using System.Collections; +using UnityEngine; +using UnityEngine.AI; + +public class PlayerController : CharacterBase { + + public Display WildDisplay; + public Display3DFriend FriendDisplay; + [SerializeField] protected MeleeAttackCollider AttackCollider; + + [Header("Camera")] + public Camera Camera; + public Transform CameraTarget; + public float CameraDistanceMax; + public float CameraDistance; + public LayerMask CameraCollisionMask; + + [Header("Effects")] + [SerializeField] private ParticleSystem SandstarParticles; + [SerializeField] private ParticleSystem DeathParticles; + + private List _interactablesInRange = new List(); + + private NavMeshAgent _agent; + private DialogueController _currentDialogue; + private bool _lastAttackState = false; + + public bool IsAttacking() + { + if (Display.GetAnimatorLayersCount() < 1) return false; + return Display.GetCurrentAnimatorStateInfo(1).IsName("Attack"); + } + + public bool IsRecoil() + { + return Display.GetCurrentAnimatorStateInfo(0).IsName("Hit"); + } + + protected override void Awake() + { + base.Awake(); + _agent = GetComponent(); + } + + protected override void Start() + { + if(GameManager.Instance != null) + { + GameManager.Instance.Player = this; + } + } + + protected override void Update() + { +#if UNITY_EDITOR + if (Input.GetKeyDown(Controls.Debug_K)) + { + TakeDamage(9999); + } +#endif + + if (!IsAlive() || !_cc.enabled || _currentDialogue != null) return; + Vector3 input = new Vector3(Input.GetAxis(Controls.XAxis), 0, Input.GetAxis(Controls.YAxis)); + + bool isAttacking = IsAttacking(); + bool isRecoil = IsRecoil(); + + if (_lastAttackState != isAttacking && !isAttacking) + { + AttackEnd(); + } + + if (isRecoil) + { + if (isAttacking) + { + Display.SetEvent("InterruptAttack", true); + } + input = Vector3.zero; + } + + if (Input.GetKeyDown(Controls.Interact)) + { + if (TryInteract()) + { + return; + } + else if (!isAttacking) + { + AttackStart(); + } + } + + if (isAttacking) + { + input /= 2; + } + + _cc.SimpleMove(transform.forward * input.z * Speed); + Display.SetFloat("Input", input.z); + + if (input != Vector3.zero) + { + _cc.transform.Rotate(Vector3.up, input.x * 120 * Time.deltaTime); + } + + if (input.z != 0) + { + PlayFootstep(); + } + + _lastAttackState = isAttacking; + } + + void LateUpdate() + { + RaycastHit hit; + if (Physics.Raycast(CameraTarget.position, -CameraTarget.forward, out hit, CameraDistanceMax, CameraCollisionMask.value)) + { + CameraDistance = hit.distance; + } + else + { + CameraDistance = Mathf.Lerp(CameraDistance, CameraDistanceMax, 0.5f); + } + + Camera.transform.position = CameraTarget.position - CameraTarget.forward * CameraDistance; + } + + private bool TryInteract() + { + for (int i = _interactablesInRange.Count - 1; i>=0; i--) + { + if(_interactablesInRange[i] == null) + { + _interactablesInRange.RemoveAt(i); + } + } + + if (_interactablesInRange.Count == 0) + return false; + + if (!_interactablesInRange[0].Interact(this)) return false; + + Display.PlayState("Interact"); + return true; + } + + private void AttackStart() + { + Display.SetEvent("InterruptAttack", false); + AttackCollider.Toggle(true); + AttackCollider.OnHit.AddListener((target) => + { + if ((PlayerController)target == this) return; + target.TakeDamage(1); + }); + Display.SetLayerWeight(1, 1); + Display.PlayState("Attack", 1); + } + + private void AttackEnd() + { + if (AttackCollider.gameObject.activeSelf) + { + AttackCollider.Toggle(false); + AttackCollider.OnHit.RemoveAllListeners(); + } + Display.SetLayerWeight(1, 0); + } + + public void ToggleCharacterController(bool enabled) + { + _cc.enabled = enabled; + _interactablesInRange.Clear(); + } + + public void ToggleSandstarEmission(bool value) + { + var emission = SandstarParticles.emission; + emission.enabled = value; + } + + public override void TakeDamage(int dmg) + { + base.TakeDamage(dmg); + if (IsAlive()) + { + Display.PlayState("Hit"); + } + } + + protected override void OnDeath() + { + ToggleCharacterController(false); + Display.PlayState("Die"); + StartCoroutine(PlayDeathSequence(2)); + } + + private IEnumerator PlayDeathSequence(float delay) + { + yield return new WaitForSeconds(delay); + FriendDisplay.gameObject.SetActive(false); + DeathParticles.gameObject.SetActive(true); + + yield return new WaitForSeconds(0.5f); + transform.SetParent(null); + WildDisplay.gameObject.SetActive(true); + Display = WildDisplay; + yield return PathfindToEntrance(); + } + + private IEnumerator PathfindToEntrance() + { + var entrance = GameManager.Instance.CurrentExit; + if(entrance == null) { entrance = GameManager.Instance.Exits[0]; } + ToggleCharacterController(false); + + float timeOut = 2; //seconds + _agent.enabled = true; + _agent.SetDestination(entrance.GetComponent().GetExitLocator().position); + Display.SetFloat("Input", 1); + + do + { + timeOut -= Time.deltaTime; + yield return 0; + } + while ((_agent.remainingDistance > _agent.stoppingDistance) && timeOut > 0); + + Display.SetFloat("Input", 0); + Debug.Log("Pathfound"); + + if(string.IsNullOrEmpty(GameManager.Instance.PreviousExit.TargetScene)) + { + Debug.LogError("Previous scene not found! Using default"); + GameManager.ChangeSceneAsync(entrance.GetComponent().GetDefaultExit()); + } + else + { + GameManager.ChangeSceneAsync(GameManager.Instance.PreviousExit); + } + } + + public void SetDialogue(DialogueController controller) + { + Display.SetFloat("Input", 0); + _currentDialogue = controller; + } + + void OnControllerColliderHit(ControllerColliderHit hit) + { + var triggers = hit.gameObject.GetComponent(); + if (triggers != null && triggers.CollisionTags.Contains(this.tag)) + { + triggers.OnCollisionEnterAction.Invoke(); + } + } + + void OnTriggerEnter(Collider other) + { + var interactable = other.GetComponent(); + if (interactable != null) + { + _interactablesInRange.Add(interactable); + } + } + + void OnTriggerExit(Collider other) + { + var interactable = other.GetComponent(); + if (interactable != null && _interactablesInRange.Contains(interactable)) + { + _interactablesInRange.Remove(interactable); + } + } +} diff --git a/Interfaces/IDamageable.cs b/Interfaces/IDamageable.cs new file mode 100644 index 0000000..330beb6 --- /dev/null +++ b/Interfaces/IDamageable.cs @@ -0,0 +1,6 @@ + +public interface IDamageable +{ + void TakeDamage(int damage); + bool IsAlive(); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7223b4b --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +Sample character + enemy code extracted from a recent WIP 3rd person adventure game project. + +Contains: +- base class used by player, enemies and NPCs +- player character controller with tank controls, dynamic camera and environment interaction +- sample enemy with patrolling/attacking states \ No newline at end of file