Upload files to "/"

This commit is contained in:
katboi01 2025-04-30 06:35:31 +08:00
commit d071106868
6 changed files with 541 additions and 0 deletions

71
CharacterBase.cs Normal file
View File

@ -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<AudioClip> Footsteps = new List<AudioClip>();
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<CharacterController>();
_audioSource = GetComponent<AudioSource>();
}
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);
}
}

36
Display.cs Normal file
View File

@ -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(); }
}

144
EnemyController.cs Normal file
View File

@ -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<Collider> PatrolPoints;
private NavMeshAgent _agent;
private Coroutine _attack;
private Coroutine _currentAction;
public LayerMask PlayerLayer;
public Image PlayerDetectIndicator;
protected override void Awake()
{
base.Awake();
_agent = GetComponent<NavMeshAgent>();
_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;
}
}

6
IDamageable.cs Normal file
View File

@ -0,0 +1,6 @@

public interface IDamageable
{
void TakeDamage(int damage);
bool IsAlive();
}

278
PlayerController.cs Normal file
View File

@ -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<Interactable> _interactablesInRange = new List<Interactable>();
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<NavMeshAgent>();
}
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<ILocationChange>().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<ILocationChange>().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<CollisionTriggers>();
if (triggers != null && triggers.CollisionTags.Contains(this.tag))
{
triggers.OnCollisionEnterAction.Invoke();
}
}
void OnTriggerEnter(Collider other)
{
var interactable = other.GetComponent<Interactable>();
if (interactable != null)
{
_interactablesInRange.Add(interactable);
}
}
void OnTriggerExit(Collider other)
{
var interactable = other.GetComponent<Interactable>();
if (interactable != null && _interactablesInRange.Contains(interactable))
{
_interactablesInRange.Remove(interactable);
}
}
}

6
README.md Normal file
View File

@ -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