initial commit
This commit is contained in:
commit
7c73c4100d
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(); }
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
public interface IDamageable
|
||||||
|
{
|
||||||
|
void TakeDamage(int damage);
|
||||||
|
bool IsAlive();
|
||||||
|
}
|
|
@ -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
|
Loading…
Reference in New Issue