Files
daluandou/Assets/Scripts/角色脚本/MainCharacterCombat.cs
T
huadexuan 23c559d9f7 ```
feat(gameplay): 提升单位检测范围并添加法师属性

- 将所有单位的检测范围从默认值提升至100
- 为法师预制体添加护盾、等级等新属性
- 为法师添加暴击、吸血、伤害减免等战斗属性
- 更换法师弹丸预设并调整相关参数
- 添加锚点交战Z线控制功能
- 更新玩家数据中配备的召唤师技能
- 移除多余的纹理配置文件
```
2026-10-07 23:59:17 +08:00

680 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(StatusEffectController))]
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 StatusEffectController statusEffectController;
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;
public bool IsStunned => statusEffectController != null && statusEffectController.IsStunned;
private void Start()
{
animator = GetComponent<Animator>();
movement = GetComponent<MainCharacterDebugMovement>();
statusEffectController = GetComponent<StatusEffectController>();
if (statusEffectController == null)
{
statusEffectController = gameObject.AddComponent<StatusEffectController>();
}
if (animator != null)
{
animator.applyRootMotion = useRootMotion;
}
}
private void Update()
{
// 眩晕时停止所有行动
if (IsStunned)
{
if (animator != null)
{
animator.SetInteger("speed", 0);
}
return;
}
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;
}
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();
clickMovement.enabled = false; // 禁用移动脚本,避免它设置 speed 参数
}
pendingSummonPrefab = prefab;
pendingSummonCharacterData = characterData;
pendingSummonPlayerData = playerData;
summonElapsed = 0f;
summonSpawned = false;
summoning = true;
animator.speed = 1f;
// 强制重置动画状态,避免状态过渡冲突
animator.SetInteger("speed", 0);
animator.ResetTrigger("skill");
animator.ResetTrigger("attack");
// 直接强制播放 skill 动画,绕过状态机的所有过渡条件
// Play() 会立即切换,不管当前状态是什么
animator.Play("skill", 0, 0f);
Debug.Log("强制播放 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))
{
// 找不到合适位置时,直接在召唤师前方1.5米处生成
spawnPosition = transform.position + direction * 1.5f;
spawnPosition.y = transform.position.y;
}
GameObject summoned = Instantiate(
pendingSummonPrefab,
spawnPosition,
Quaternion.LookRotation(direction, Vector3.up));
// 召唤的单位继承召唤师的阵营(不再硬编码为 Friend)
UnitTeam summonerTeam = GetComponent<UnitTeam>();
UnitTeam summonedTeam = summoned.GetComponent<UnitTeam>();
if (summonerTeam != null && summonedTeam != null)
{
summonedTeam.teamId = summonerTeam.teamId;
// 根据阵营设置 tag(向后兼容旧系统)
summoned.tag = summonerTeam.teamId == 0 ? "Friend" : "Enemy";
}
else
{
// 降级:没有 UnitTeam 时保持原逻辑
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, pendingSummonPlayerData.level);
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);
// 正常完成:skill 动画播放完毕
if (state.IsName("skill") && state.normalizedTime >= 1f)
{
FinishSummon();
return;
}
// 超时检测:添加调试信息
if (summonElapsed >= summonAnimationTimeout)
{
// 输出当前动画状态,帮助诊断问题
string currentState = "unknown";
if (state.IsName("skill")) currentState = "skill";
else if (state.IsName("idle")) currentState = "idle";
else if (state.IsName("run")) currentState = "run";
else if (state.IsName("attack")) currentState = "attack";
Debug.LogWarning($"主角召唤动画超时,已解除召唤状态。当前动画状态: {currentState}, normalizedTime: {state.normalizedTime:F2}");
FinishSummon();
}
}
private void FinishSummon()
{
summoning = false;
summonSpawned = false;
pendingSummonPrefab = null;
summonElapsed = 0f;
animator.speed = 1f;
animator.SetInteger("speed", 0);
animator.Play("idle", 0, 0f);
// 重新启用移动脚本
SummonerClickMovement clickMovement = GetComponent<SummonerClickMovement>();
if (clickMovement != null)
{
clickMovement.enabled = true;
}
Debug.Log("召唤完成,恢复移动脚本");
}
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,主角不主动远离基地寻找敌人
UnitTeam myTeam = GetComponent<UnitTeam>();
if (myTeam == null)
{
return null;
}
GameObject nearest = null;
float nearestDistance = detectRange * detectRange;
// 查找所有带 UnitTeam 的对象(包括单位和召唤师)
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
foreach (UnitTeam targetTeam in allTeams)
{
GameObject enemy = targetTeam.gameObject;
if (enemy == null || enemy == gameObject || enemy.transform.root == transform.root)
{
continue;
}
// 检查是否是敌人(使用阵营 ID)
if (!myTeam.IsEnemy(enemy))
{
continue;
}
// 检查是否有 UnitBase 或 WorldHealthBar(召唤师)
UnitBase enemyUnit = enemy.GetComponentInParent<UnitBase>();
WorldHealthBar enemyHealth = enemy.GetComponentInParent<WorldHealthBar>();
if (enemyUnit == null && enemyHealth == 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);
}
}
}