2026-02-10 21:27:46 +00:00
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.Events;
|
|
|
|
|
using UnityEngine.AI;
|
|
|
|
|
|
|
|
|
|
public abstract class Character : MonoBehaviour
|
|
|
|
|
{
|
2026-02-11 17:30:49 +00:00
|
|
|
public enum WeaponType
|
|
|
|
|
{
|
|
|
|
|
None = 0,
|
|
|
|
|
Pistol = 1,
|
|
|
|
|
AssultRifle01 = 2,
|
|
|
|
|
AssultRifle02 = 3,
|
|
|
|
|
Shotgun = 4,
|
|
|
|
|
SniperRifle = 5,
|
|
|
|
|
Rifle = 6,
|
|
|
|
|
SubMachineGun = 7,
|
|
|
|
|
RPG = 8,
|
|
|
|
|
MiniGun = 9,
|
|
|
|
|
Grenades = 10,
|
|
|
|
|
Bow = 11,
|
|
|
|
|
Melee = 12
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public enum MeleeType
|
|
|
|
|
{
|
|
|
|
|
Stab = 0,
|
|
|
|
|
OneHanded = 1,
|
|
|
|
|
TwoHanded = 2
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 21:27:46 +00:00
|
|
|
[Header("Stats")]
|
2026-02-11 17:30:49 +00:00
|
|
|
[SerializeField]
|
|
|
|
|
protected int CurHp = 10;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
protected int MaxHp = 10;
|
2026-02-10 21:27:46 +00:00
|
|
|
|
|
|
|
|
[Header("Components")]
|
|
|
|
|
// Optional movement controller reference, assign in inspector if used
|
|
|
|
|
public NavMeshMovementController MovementController;
|
|
|
|
|
|
2026-02-11 17:30:49 +00:00
|
|
|
[SerializeField]
|
|
|
|
|
private Animator animator;
|
|
|
|
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private NavMeshAgent navMeshAgent;
|
|
|
|
|
|
|
|
|
|
[Header("Animation Settings")]
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private WeaponType weaponType = WeaponType.None;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private MeleeType meleeType = MeleeType.Stab;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private WeaponType attackWeaponType = WeaponType.Melee;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private MeleeType attackMeleeType = MeleeType.OneHanded;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private float attackHoldSeconds = 0.15f;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private bool restoreWeaponAfterAttack = true;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
[Tooltip("Idle animation index (Animation_int). 0 = normal idle.")]
|
|
|
|
|
private int idleAnimationIndex = 0;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private bool isGrounded = true;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private bool useRootMotion = true;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private float walkSpeedThreshold = 0.1f;
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private float runSpeedThreshold = 2.5f;
|
|
|
|
|
|
|
|
|
|
[Header("Animator Parameter Names")]
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramWeaponType = "WeaponType_int";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramMeleeType = "MeleeType_Int";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramIdleAnimation = "Animation_int";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramGrounded = "Grounded";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramRootMotion = "Static_b";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramShoot = "Shoot_b";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramHeadHorizontal = "Head_Horizontal_f";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramHeadVertical = "Head_Vertical_f";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramBodyHorizontal = "Body_Horizontal_f";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramBodyVertical = "Body_Vertical_f";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramDeath = "Death_b";
|
|
|
|
|
[SerializeField]
|
|
|
|
|
private string paramSpeed = "Speed_f";
|
|
|
|
|
|
2026-02-10 21:27:46 +00:00
|
|
|
protected Character target;
|
|
|
|
|
public event UnityAction onTakeDamage;
|
|
|
|
|
|
2026-02-11 17:30:49 +00:00
|
|
|
private WeaponType lastWeaponType;
|
|
|
|
|
private MeleeType lastMeleeType;
|
|
|
|
|
private Coroutine attackRoutine;
|
|
|
|
|
|
|
|
|
|
public int GetCurrentHP() => CurHp;
|
|
|
|
|
public int GetMaxHP() => MaxHp;
|
|
|
|
|
|
|
|
|
|
protected virtual void Awake()
|
|
|
|
|
{
|
|
|
|
|
if (animator == null)
|
|
|
|
|
{
|
|
|
|
|
animator = FindAnimatorWithParams();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (animator == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("Character: No Animator found in children. Assign one in the inspector.", gameObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (navMeshAgent == null)
|
|
|
|
|
{
|
|
|
|
|
navMeshAgent = GetComponent<NavMeshAgent>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ApplyStaticAnimatorParams();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void OnValidate()
|
|
|
|
|
{
|
|
|
|
|
if (animator == null)
|
|
|
|
|
{
|
|
|
|
|
animator = FindAnimatorWithParams();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (navMeshAgent == null)
|
|
|
|
|
{
|
|
|
|
|
navMeshAgent = GetComponent<NavMeshAgent>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void Update()
|
|
|
|
|
{
|
|
|
|
|
UpdateMovementAnimatorParams();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 21:27:46 +00:00
|
|
|
public void TakeDamage(int damageToTake)
|
|
|
|
|
{
|
2026-02-11 17:30:49 +00:00
|
|
|
if (damageToTake < 0) damageToTake = 0;
|
|
|
|
|
|
2026-02-10 21:27:46 +00:00
|
|
|
CurHp -= damageToTake;
|
|
|
|
|
onTakeDamage?.Invoke();
|
2026-02-11 17:30:49 +00:00
|
|
|
|
2026-02-10 21:27:46 +00:00
|
|
|
if (CurHp <= 0) Die();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public virtual void Die()
|
|
|
|
|
{
|
2026-02-11 17:30:49 +00:00
|
|
|
SetDeathAnimation();
|
2026-02-10 21:27:46 +00:00
|
|
|
Destroy(gameObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetTarget(Character t)
|
|
|
|
|
{
|
|
|
|
|
target = t;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Character GetTarget() => target;
|
2026-02-11 17:30:49 +00:00
|
|
|
|
|
|
|
|
public void SetWeaponType(WeaponType type)
|
|
|
|
|
{
|
|
|
|
|
weaponType = type;
|
|
|
|
|
ApplyStaticAnimatorParams();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetMeleeType(MeleeType type)
|
|
|
|
|
{
|
|
|
|
|
meleeType = type;
|
|
|
|
|
ApplyStaticAnimatorParams();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetIdleAnimation(int idleIndex)
|
|
|
|
|
{
|
|
|
|
|
idleAnimationIndex = Mathf.Max(0, idleIndex);
|
|
|
|
|
ApplyStaticAnimatorParams();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetGrounded(bool grounded)
|
|
|
|
|
{
|
|
|
|
|
isGrounded = grounded;
|
|
|
|
|
if (animator != null)
|
|
|
|
|
{
|
|
|
|
|
SetBoolIfExists(paramGrounded, isGrounded);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetUseRootMotion(bool enableRootMotion)
|
|
|
|
|
{
|
|
|
|
|
useRootMotion = enableRootMotion;
|
|
|
|
|
if (animator != null)
|
|
|
|
|
{
|
|
|
|
|
SetBoolIfExists(paramRootMotion, useRootMotion);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void TriggerAttack()
|
|
|
|
|
{
|
|
|
|
|
if (animator == null) return;
|
|
|
|
|
if (attackRoutine != null)
|
|
|
|
|
{
|
|
|
|
|
StopCoroutine(attackRoutine);
|
|
|
|
|
}
|
|
|
|
|
attackRoutine = StartCoroutine(AttackRoutine());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetHeadLook(float horizontal, float vertical)
|
|
|
|
|
{
|
|
|
|
|
if (animator == null) return;
|
|
|
|
|
SetFloatIfExists(paramHeadHorizontal, horizontal);
|
|
|
|
|
SetFloatIfExists(paramHeadVertical, vertical);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyStaticAnimatorParams()
|
|
|
|
|
{
|
|
|
|
|
if (animator == null) return;
|
|
|
|
|
|
|
|
|
|
SetIntIfExists(paramWeaponType, (int)weaponType);
|
|
|
|
|
SetIntIfExists(paramMeleeType, (int)meleeType);
|
|
|
|
|
SetIntIfExists(paramIdleAnimation, idleAnimationIndex);
|
|
|
|
|
SetBoolIfExists(paramGrounded, isGrounded);
|
|
|
|
|
SetBoolIfExists(paramRootMotion, useRootMotion);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateMovementAnimatorParams()
|
|
|
|
|
{
|
|
|
|
|
if (animator == null) return;
|
|
|
|
|
|
|
|
|
|
float speed = 0f;
|
|
|
|
|
if (navMeshAgent != null)
|
|
|
|
|
{
|
|
|
|
|
speed = navMeshAgent.velocity.magnitude;
|
|
|
|
|
|
|
|
|
|
if (speed < 0.01f && navMeshAgent.hasPath && !navMeshAgent.pathPending)
|
|
|
|
|
{
|
|
|
|
|
// Use desired velocity when actual velocity is near zero
|
|
|
|
|
speed = navMeshAgent.desiredVelocity.magnitude;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (MovementController != null)
|
|
|
|
|
{
|
|
|
|
|
speed = MovementController.GetSpeed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MovementState state = MovementState.Idle;
|
|
|
|
|
if (speed >= runSpeedThreshold)
|
|
|
|
|
{
|
|
|
|
|
state = MovementState.Run;
|
|
|
|
|
}
|
|
|
|
|
else if (speed >= walkSpeedThreshold)
|
|
|
|
|
{
|
|
|
|
|
state = MovementState.Walk;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector2 bodyParams = GetBodyParamsForState(weaponType, state);
|
|
|
|
|
SetFloatIfExists(paramBodyHorizontal, bodyParams.x);
|
|
|
|
|
SetFloatIfExists(paramBodyVertical, bodyParams.y);
|
|
|
|
|
SetFloatIfExists(paramSpeed, speed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Vector2 GetBodyParamsForState(WeaponType type, MovementState state)
|
|
|
|
|
{
|
|
|
|
|
// Defaults per Simple Fantasy ReadMe
|
|
|
|
|
if (type == WeaponType.None)
|
|
|
|
|
{
|
|
|
|
|
return Vector2.zero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type == WeaponType.Pistol)
|
|
|
|
|
{
|
|
|
|
|
if (state == MovementState.Run)
|
|
|
|
|
return new Vector2(0f, 0.2f);
|
|
|
|
|
|
|
|
|
|
return Vector2.zero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type == WeaponType.Grenades)
|
|
|
|
|
{
|
|
|
|
|
return Vector2.zero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All other weapons
|
|
|
|
|
if (state == MovementState.Run)
|
|
|
|
|
return new Vector2(0.3f, 0.6f);
|
|
|
|
|
|
|
|
|
|
return new Vector2(0f, 0.6f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetDeathAnimation()
|
|
|
|
|
{
|
|
|
|
|
if (animator == null) return;
|
|
|
|
|
SetBoolIfExists(paramDeath, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private System.Collections.IEnumerator ResetBoolNextFrame(string boolName)
|
|
|
|
|
{
|
|
|
|
|
SetBoolIfExists(boolName, true);
|
|
|
|
|
yield return null;
|
|
|
|
|
SetBoolIfExists(boolName, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private System.Collections.IEnumerator AttackRoutine()
|
|
|
|
|
{
|
|
|
|
|
lastWeaponType = weaponType;
|
|
|
|
|
lastMeleeType = meleeType;
|
|
|
|
|
|
|
|
|
|
weaponType = attackWeaponType;
|
|
|
|
|
meleeType = attackMeleeType;
|
|
|
|
|
ApplyStaticAnimatorParams();
|
|
|
|
|
|
|
|
|
|
SetBoolIfExists(paramShoot, true);
|
|
|
|
|
if (attackHoldSeconds > 0f)
|
|
|
|
|
{
|
|
|
|
|
yield return new WaitForSeconds(attackHoldSeconds);
|
|
|
|
|
}
|
|
|
|
|
SetBoolIfExists(paramShoot, false);
|
|
|
|
|
|
|
|
|
|
if (restoreWeaponAfterAttack)
|
|
|
|
|
{
|
|
|
|
|
weaponType = lastWeaponType;
|
|
|
|
|
meleeType = lastMeleeType;
|
|
|
|
|
ApplyStaticAnimatorParams();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attackRoutine = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool HasParam(string paramName, AnimatorControllerParameterType type)
|
|
|
|
|
{
|
|
|
|
|
if (animator == null || string.IsNullOrEmpty(paramName)) return false;
|
|
|
|
|
|
|
|
|
|
foreach (var param in animator.parameters)
|
|
|
|
|
{
|
|
|
|
|
if (param.name == paramName && param.type == type)
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool HasParam(Animator targetAnimator, string paramName, AnimatorControllerParameterType type)
|
|
|
|
|
{
|
|
|
|
|
if (targetAnimator == null || string.IsNullOrEmpty(paramName)) return false;
|
|
|
|
|
|
|
|
|
|
foreach (var param in targetAnimator.parameters)
|
|
|
|
|
{
|
|
|
|
|
if (param.name == paramName && param.type == type)
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Animator FindAnimatorWithParams()
|
|
|
|
|
{
|
|
|
|
|
var animators = GetComponentsInChildren<Animator>(true);
|
|
|
|
|
if (animators == null || animators.Length == 0) return null;
|
|
|
|
|
|
|
|
|
|
foreach (var anim in animators)
|
|
|
|
|
{
|
|
|
|
|
if (HasParam(anim, paramSpeed, AnimatorControllerParameterType.Float) ||
|
|
|
|
|
HasParam(anim, paramWeaponType, AnimatorControllerParameterType.Int))
|
|
|
|
|
{
|
|
|
|
|
return anim;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return animators[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetIntIfExists(string paramName, int value)
|
|
|
|
|
{
|
|
|
|
|
if (HasParam(paramName, AnimatorControllerParameterType.Int))
|
|
|
|
|
animator.SetInteger(paramName, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetFloatIfExists(string paramName, float value)
|
|
|
|
|
{
|
|
|
|
|
if (HasParam(paramName, AnimatorControllerParameterType.Float))
|
|
|
|
|
animator.SetFloat(paramName, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetBoolIfExists(string paramName, bool value)
|
|
|
|
|
{
|
|
|
|
|
if (HasParam(paramName, AnimatorControllerParameterType.Bool))
|
|
|
|
|
animator.SetBool(paramName, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private enum MovementState
|
|
|
|
|
{
|
|
|
|
|
Idle,
|
|
|
|
|
Walk,
|
|
|
|
|
Run
|
|
|
|
|
}
|
2026-02-10 21:27:46 +00:00
|
|
|
}
|