475 lines
13 KiB
C#
475 lines
13 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
public class MainCharacterCombat : MonoBehaviour
|
|
{
|
|
[Header("自动攻击")]
|
|
[Min(0f)]
|
|
public float attackRange = 8f;
|
|
|
|
[Min(0f)]
|
|
public float attackPower = 50f;
|
|
|
|
[Min(0f)]
|
|
public float attackInterval = 2f;
|
|
|
|
[Header("模型朝向修正")]
|
|
public float modelFacingOffset = 0f;
|
|
|
|
[Header("动画根运动")]
|
|
public bool useRootMotion = false;
|
|
|
|
[Header("法术球")]
|
|
public GameObject projectilePrefab;
|
|
public Transform attackPos;
|
|
|
|
[Header("召唤单位")]
|
|
public GameObject humanSoldierPrefab;
|
|
public GameObject archerPrefab;
|
|
public float summonDistance = 1.5f;
|
|
public float summonSideOffset = 0.8f;
|
|
public float summonSearchStep = 0.8f;
|
|
public int summonSearchCount = 5;
|
|
public float summonDepthSearchStep = 1f;
|
|
public int summonDepthSearchCount = 2;
|
|
public float summonAnimationTimeout = 2.5f;
|
|
|
|
[Min(0f)]
|
|
public float projectileSpeed = 12f;
|
|
|
|
[Min(0f)]
|
|
public float projectileLifetime = 5f;
|
|
|
|
private Animator animator;
|
|
private MainCharacterDebugMovement movement;
|
|
private GameObject target;
|
|
private float attackCooldown;
|
|
private float attackElapsed;
|
|
private bool attacking;
|
|
private bool attackEffectTriggered;
|
|
private GameObject pendingSummonPrefab;
|
|
private float summonElapsed;
|
|
private bool summonSpawned;
|
|
private bool summoning;
|
|
|
|
private void Start()
|
|
{
|
|
animator = GetComponent<Animator>();
|
|
movement = GetComponent<MainCharacterDebugMovement>();
|
|
|
|
if (animator != null)
|
|
{
|
|
animator.applyRootMotion = useRootMotion;
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (attackCooldown > 0f)
|
|
{
|
|
attackCooldown -= Time.deltaTime;
|
|
}
|
|
|
|
HandleSummonInput();
|
|
if (summoning)
|
|
{
|
|
summonElapsed += Time.deltaTime;
|
|
CheckSummonFinished();
|
|
return;
|
|
}
|
|
|
|
if (attacking)
|
|
{
|
|
attackElapsed += Time.deltaTime;
|
|
CheckAttackFinished();
|
|
return;
|
|
}
|
|
|
|
if (movement == null || movement.IsMoving || animator == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
target = FindNearestEnemy();
|
|
if (target == null || attackCooldown > 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FaceTarget(target);
|
|
SetAttackAnimationSpeed();
|
|
animator.SetInteger("speed", 0);
|
|
animator.ResetTrigger("attack");
|
|
animator.SetTrigger("attack");
|
|
attacking = true;
|
|
attackElapsed = 0f;
|
|
attackEffectTriggered = false;
|
|
}
|
|
|
|
public void OnAttackEffect()
|
|
{
|
|
if (attackEffectTriggered || !attacking || target == null || !target.activeInHierarchy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
attackEffectTriggered = true;
|
|
FireProjectile(target);
|
|
}
|
|
|
|
private void HandleSummonInput()
|
|
{
|
|
if (summoning || animator == null || movement == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Alpha2))
|
|
{
|
|
movement.StopMovement();
|
|
if (attacking)
|
|
{
|
|
FinishAttack();
|
|
}
|
|
}
|
|
|
|
if (Input.GetKeyDown(KeyCode.Alpha1))
|
|
{
|
|
BeginSummon(humanSoldierPrefab);
|
|
}
|
|
else if (Input.GetKeyDown(KeyCode.Alpha2))
|
|
{
|
|
BeginSummon(archerPrefab);
|
|
}
|
|
}
|
|
|
|
private void BeginSummon(GameObject prefab)
|
|
{
|
|
if (prefab == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
pendingSummonPrefab = prefab;
|
|
summonElapsed = 0f;
|
|
summonSpawned = false;
|
|
summoning = true;
|
|
animator.speed = 1f;
|
|
animator.SetInteger("speed", 0);
|
|
animator.ResetTrigger("skill");
|
|
animator.SetTrigger("skill");
|
|
}
|
|
|
|
public void OnSkillHit1()
|
|
{
|
|
if (!summoning || summonSpawned || pendingSummonPrefab == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3 direction = transform.forward;
|
|
direction.y = 0f;
|
|
direction.Normalize();
|
|
|
|
Vector3 spawnPosition;
|
|
if (!TryFindSummonPosition(direction, out spawnPosition))
|
|
{
|
|
Debug.LogWarning("主角附近没有可用的召唤位置,本次召唤取消");
|
|
FinishSummon();
|
|
return;
|
|
}
|
|
|
|
GameObject summoned = Instantiate(
|
|
pendingSummonPrefab,
|
|
spawnPosition,
|
|
Quaternion.LookRotation(direction, Vector3.up));
|
|
summoned.tag = "Friend";
|
|
summonSpawned = true;
|
|
pendingSummonPrefab = null;
|
|
Debug.Log("主角召唤单位:" + summoned.name + " position=" + spawnPosition);
|
|
}
|
|
|
|
private bool TryFindSummonPosition(Vector3 direction, out Vector3 position)
|
|
{
|
|
Vector3 side = Vector3.Cross(Vector3.up, direction).normalized;
|
|
Vector3[] directions = { direction, -direction };
|
|
|
|
foreach (Vector3 forward in directions)
|
|
{
|
|
for (int depthIndex = 0; depthIndex <= summonDepthSearchCount; depthIndex++)
|
|
{
|
|
float depthDistance = summonDistance + depthIndex * summonDepthSearchStep;
|
|
Vector3 center = transform.position + forward * depthDistance;
|
|
|
|
for (int sideIndex = 0; sideIndex <= summonSearchCount; sideIndex++)
|
|
{
|
|
if (sideIndex == 0 && TryUseSummonCandidate(center, out position))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
float sideDistance = summonSideOffset + (sideIndex - 1) * summonSearchStep;
|
|
if (sideIndex > 0)
|
|
{
|
|
if (TryUseSummonCandidate(center + side * sideDistance, out position)
|
|
|| TryUseSummonCandidate(center - side * sideDistance, out position))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
position = transform.position;
|
|
return false;
|
|
}
|
|
|
|
private bool TryUseSummonCandidate(Vector3 candidate, out Vector3 position)
|
|
{
|
|
candidate.y = transform.position.y;
|
|
if (IsInsideMovementBounds(candidate) && IsSummonPositionFree(candidate))
|
|
{
|
|
position = candidate;
|
|
return true;
|
|
}
|
|
|
|
position = transform.position;
|
|
return false;
|
|
}
|
|
|
|
private bool IsSummonPositionFree(Vector3 position)
|
|
{
|
|
const float radius = 0.55f;
|
|
const float height = 2f;
|
|
Vector3 center = position + Vector3.up * (height * 0.5f);
|
|
Collider[] hits = Physics.OverlapCapsule(
|
|
center - Vector3.up * (height * 0.5f - radius),
|
|
center + Vector3.up * (height * 0.5f - radius),
|
|
radius);
|
|
|
|
foreach (Collider hit in hits)
|
|
{
|
|
if (hit == null || hit.transform.root == transform.root)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (hit.gameObject.name == "BattlePlatform" || hit.transform.root.name == "BattlePlatform")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (hit.GetComponentInParent<MainCharacterDebugMovement>() != null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsInsideMovementBounds(Vector3 position)
|
|
{
|
|
return movement == null || (position.x >= movement.xLimits.x && position.x <= movement.xLimits.y
|
|
&& position.z >= movement.zLimits.x && position.z <= movement.zLimits.y);
|
|
}
|
|
|
|
private void CheckSummonFinished()
|
|
{
|
|
if (summonElapsed >= summonAnimationTimeout)
|
|
{
|
|
Debug.LogWarning("主角召唤动画超时,已解除召唤状态");
|
|
FinishSummon();
|
|
return;
|
|
}
|
|
|
|
AnimatorClipInfo[] clips = animator.GetCurrentAnimatorClipInfo(0);
|
|
foreach (AnimatorClipInfo clipInfo in clips)
|
|
{
|
|
string clipName = clipInfo.clip.name.ToLowerInvariant();
|
|
if ((clipName.Contains("skill") || clipName.Contains("召唤"))
|
|
&& animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1f)
|
|
{
|
|
FinishSummon();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FinishSummon()
|
|
{
|
|
summoning = false;
|
|
summonSpawned = false;
|
|
pendingSummonPrefab = null;
|
|
summonElapsed = 0f;
|
|
animator.speed = 1f;
|
|
animator.SetInteger("speed", 0);
|
|
}
|
|
|
|
private void CheckAttackFinished()
|
|
{
|
|
if (attackElapsed >= 2.4f)
|
|
{
|
|
FinishAttack();
|
|
return;
|
|
}
|
|
|
|
AnimatorClipInfo[] clips = animator.GetCurrentAnimatorClipInfo(0);
|
|
bool playingAttack = false;
|
|
foreach (AnimatorClipInfo clipInfo in clips)
|
|
{
|
|
string clipName = clipInfo.clip.name.ToLowerInvariant();
|
|
if (clipName.Contains("attack") || clipName.Contains("攻击") || clipName.Contains("普攻"))
|
|
{
|
|
playingAttack = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!playingAttack)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1f)
|
|
{
|
|
FinishAttack();
|
|
}
|
|
}
|
|
|
|
private void SetAttackAnimationSpeed()
|
|
{
|
|
float clipDuration = GetAttackAnimationDuration();
|
|
if (clipDuration > 0f && attackInterval > 0f)
|
|
{
|
|
animator.speed = clipDuration / attackInterval;
|
|
}
|
|
else
|
|
{
|
|
animator.speed = 1f;
|
|
}
|
|
}
|
|
|
|
private float GetAttackAnimationDuration()
|
|
{
|
|
if (animator == null || animator.runtimeAnimatorController == null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var overrideController = animator.runtimeAnimatorController as AnimatorOverrideController;
|
|
if (overrideController != null)
|
|
{
|
|
var overrides = new List<KeyValuePair<AnimationClip, AnimationClip>>();
|
|
overrideController.GetOverrides(overrides);
|
|
foreach (var pair in overrides)
|
|
{
|
|
if (IsAttackAnimation(pair.Key))
|
|
{
|
|
return pair.Value != null ? pair.Value.length : pair.Key.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var clip in animator.runtimeAnimatorController.animationClips)
|
|
{
|
|
if (IsAttackAnimation(clip))
|
|
{
|
|
return clip.length;
|
|
}
|
|
}
|
|
|
|
return 0f;
|
|
}
|
|
|
|
private bool IsAttackAnimation(AnimationClip clip)
|
|
{
|
|
if (clip == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string clipName = clip.name.ToLowerInvariant();
|
|
return clipName.Contains("attack")
|
|
|| clipName.Contains("攻击")
|
|
|| clipName.Contains("普攻");
|
|
}
|
|
|
|
private void FinishAttack()
|
|
{
|
|
attacking = false;
|
|
animator.speed = 1f;
|
|
animator.SetInteger("speed", 0);
|
|
}
|
|
|
|
private GameObject FindNearestEnemy()
|
|
{
|
|
GameObject nearest = null;
|
|
float nearestDistance = attackRange * attackRange;
|
|
|
|
foreach (GameObject enemy in GameObject.FindGameObjectsWithTag("Enemy"))
|
|
{
|
|
if (enemy == null || enemy.transform.root == transform.root)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
UnitBase enemyUnit = enemy.GetComponentInParent<UnitBase>();
|
|
if (enemyUnit == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float distance = (enemy.transform.position - transform.position).sqrMagnitude;
|
|
if (distance < nearestDistance)
|
|
{
|
|
nearest = enemy;
|
|
nearestDistance = distance;
|
|
}
|
|
}
|
|
|
|
return nearest;
|
|
}
|
|
|
|
private void FaceTarget(GameObject enemy)
|
|
{
|
|
Vector3 direction = enemy.transform.position - transform.position;
|
|
direction.y = 0f;
|
|
if (direction.sqrMagnitude > 0.001f)
|
|
{
|
|
Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
|
|
transform.rotation = Quaternion.Euler(
|
|
0f,
|
|
targetRotation.eulerAngles.y + modelFacingOffset,
|
|
0f);
|
|
}
|
|
}
|
|
|
|
private void FireProjectile(GameObject enemy)
|
|
{
|
|
if (projectilePrefab == null || attackPos == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameObject projectileObject = Instantiate(
|
|
projectilePrefab,
|
|
attackPos.position,
|
|
attackPos.rotation);
|
|
|
|
Projectile projectile = projectileObject.GetComponent<Projectile>();
|
|
if (projectile != null)
|
|
{
|
|
projectile.Initialize(
|
|
enemy,
|
|
attackPower,
|
|
gameObject,
|
|
projectileSpeed,
|
|
projectileLifetime,
|
|
false);
|
|
}
|
|
}
|
|
} |