Files
daluandou/Assets/Script/MainCharacterCombat.cs
T
huadexuan 69f33d8637 ```
feat(MainCharacterCombat): 完善主角召唤系统逻辑

- 调整召唤动画超时时间从2.5秒到4秒
- 在开始召唤时立即停止角色移动,调用ClearTarget()
- 将animator.SetTrigger改为animator.Play以确保动画正确播放
- 修改召唤位置查找逻辑,当找不到合适位置时在召唤师前方1.5米强制生成
- 更新边界检查逻辑,支持SummonerClickMovement的半场限制
- 添加idle动画播放确保状态正确切换
```
2026-09-16 23:06:48 +08:00

603 lines
17 KiB
C#

using System.Collections.Generic;
using UnityEngine;
public class MainCharacterCombat : MonoBehaviour
{
[Header("自动攻击")]
[Min(0f)]
public float attackRange = 8f;
[Min(0f)]
public float detectRange = 10f;
[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 CharacterData[] summonableCharacters = new CharacterData[5];
public float summonDistance = 1.5f;
public float summonSideOffset = 0.8f;
public float summonSearchStep = 0.8f;
public int summonSearchCount = 8; // 增加左右搜索次数:5→8
public float summonDepthSearchStep = 1f;
public int summonDepthSearchCount = 3; // 增加前后搜索次数:2→3
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 CharacterData pendingSummonCharacterData;
private PlayerCharacterData pendingSummonPlayerData;
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 (animator == null)
{
return;
}
// 巡逻中也能发现敌人;发现后停止巡逻,没有敌人则恢复巡逻
target = FindNearestEnemy();
if (target == null)
{
if (movement != null)
{
movement.ClearChaseTarget();
movement.SetPatrolEnabled(true);
}
return;
}
if (movement != null)
{
movement.SetPatrolEnabled(false);
// 寻敌范围内但超出攻击范围:主动走近敌人再攻击(受基地栓绳限制)
float targetDistance = GetHorizontalDistance(target.transform.position);
if (targetDistance > attackRange)
{
movement.SetChaseTarget(target.transform.position);
return;
}
movement.ClearChaseTarget();
}
if (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 (Input.GetKeyDown(KeyCode.Alpha1))
{
RequestSummon(1);
}
else if (Input.GetKeyDown(KeyCode.Alpha2))
{
RequestSummon(2);
}
}
public bool RequestSummon(int slot)
{
if (summoning || animator == null)
{
return false;
}
int index = slot - 1;
if (index < 0 || index >= summonableCharacters.Length)
{
return false;
}
CharacterData characterData = summonableCharacters[index];
if (characterData == null || characterData.unitPrefab == null)
{
return false;
}
if (ManaSystem.Instance == null || !ManaSystem.Instance.TryConsumeMana(characterData.manaCost))
{
return false;
}
PlayerCharacterData playerData = PlayerDataManager.Instance != null
? PlayerDataManager.Instance.GetDeckCharacter(index)
: null;
if (playerData == null)
{
playerData = new PlayerCharacterData(characterData.characterId, 1, 0);
}
if (movement != null)
{
movement.SetPatrolEnabled(false);
}
if (attacking)
{
FinishAttack();
}
BeginSummon(characterData.unitPrefab, characterData, playerData);
return true;
}
// 召唤师对决模式:点卡片按 CharacterData 直接召唤(能量消耗在 RequestSummon 同路径处理)。
public bool RequestSummonByData(CharacterData characterData)
{
if (summoning || animator == null || characterData == null || characterData.unitPrefab == null)
{
return false;
}
int index = System.Array.IndexOf(summonableCharacters, characterData);
if (index < 0)
{
return false;
}
return RequestSummon(index + 1);
}
private void BeginSummon(GameObject prefab, CharacterData characterData, PlayerCharacterData playerData)
{
if (prefab == null)
{
return;
}
// 立即停止移动
SummonerClickMovement clickMovement = GetComponent<SummonerClickMovement>();
if (clickMovement != null)
{
clickMovement.ClearTarget();
}
pendingSummonPrefab = prefab;
pendingSummonCharacterData = characterData;
pendingSummonPlayerData = playerData;
summonElapsed = 0f;
summonSpawned = false;
summoning = true;
animator.speed = 1f;
animator.SetInteger("speed", 0);
animator.ResetTrigger("skill");
animator.Play("skill", 0, 0f);
}
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))
{
// 找不到合适位置时,直接在召唤师前方1.5米处生成
spawnPosition = transform.position + direction * 1.5f;
spawnPosition.y = transform.position.y;
}
GameObject summoned = Instantiate(
pendingSummonPrefab,
spawnPosition,
Quaternion.LookRotation(direction, Vector3.up));
summoned.tag = "Friend";
UnitBase unitBase = summoned.GetComponent<UnitBase>();
if (unitBase != null && pendingSummonCharacterData != null && pendingSummonPlayerData != null)
{
float finalHealth = pendingSummonPlayerData.GetMaxHealth(pendingSummonCharacterData);
float finalAttack = pendingSummonPlayerData.GetAttackPower(pendingSummonCharacterData);
float finalSpeed = pendingSummonPlayerData.GetMoveSpeed(pendingSummonCharacterData);
unitBase.InitializeFromPlayerData(finalHealth, finalAttack, finalSpeed);
Debug.Log($"主角召唤单位:{summoned.name} Lv.{pendingSummonPlayerData.level} HP={finalHealth} ATK={finalAttack} position={spawnPosition}");
}
else
{
Debug.Log("主角召唤单位:" + summoned.name + " position=" + spawnPosition);
}
summonSpawned = true;
pendingSummonPrefab = null;
pendingSummonCharacterData = null;
pendingSummonPlayerData = null;
}
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;
// 只检查碰撞,不检查边界(召唤师移动范围已由 SummonerClickMovement 限制)
if (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)
{
// 召唤师对决模式:检查新的 SummonerClickMovement 半场限制
SummonerClickMovement summonerMovement = GetComponent<SummonerClickMovement>();
if (summonerMovement != null)
{
// 使用召唤师的半场范围
return position.x >= summonerMovement.minX && position.x <= summonerMovement.maxX
&& position.z >= summonerMovement.minZ && position.z <= summonerMovement.maxZ;
}
// 旧版建筑对攻模式:使用栓绳限制
if (movement != null)
{
return movement.IsWithinLeash(position);
}
// 无移动组件时不限制
return true;
}
private void CheckSummonFinished()
{
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
if (state.IsName("skill") && state.normalizedTime >= 1f)
{
FinishSummon();
return;
}
// skill 状态意外被其他动画打断时,才使用超时兜底,避免永久锁定召唤。
if (summonElapsed >= summonAnimationTimeout)
{
Debug.LogWarning("主角召唤动画超时,已解除召唤状态");
FinishSummon();
}
}
private void FinishSummon()
{
summoning = false;
summonSpawned = false;
pendingSummonPrefab = null;
summonElapsed = 0f;
animator.speed = 1f;
animator.SetInteger("speed", 0);
animator.Play("idle", 0, 0f);
}
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 float GetHorizontalDistance(Vector3 position)
{
Vector3 offset = position - transform.position;
offset.y = 0f;
return offset.magnitude;
}
private GameObject FindNearestEnemy()
{
// 索敌范围使用较小的 detectRange,主角不主动远离基地寻找敌人
GameObject nearest = null;
float nearestDistance = detectRange * detectRange;
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);
}
}
}