根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:
``` docs(readme): 更新文档说明 - 添加项目使用指南 - 完善API接口说明 - 修正错误的配置示例 ```
This commit is contained in:
1 parent
a74d1e62d7
commit
8d58c2223c
167 files changed
+1119
-15679
No files matched your search
@@ -0,0 +1,680 @@
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XHsWs3+oBXnsjqsMON2GVR71rCVLDHfEJ8faf2+LZIF5Arn4oiYdFQc=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,244 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 主角自动巡逻移动:不再支持点击地面移动(避免与拖动屏幕、建筑放置的输入冲突)。
|
||||
/// 没有敌人时在己方主建筑周围小范围来回走动,所有移动被限制在基地附近(栓绳范围)。
|
||||
/// 战斗组件发现敌人后调用 SetPatrolEnabled(false) 停止巡逻进行攻击。
|
||||
/// </summary>
|
||||
public class MainCharacterDebugMovement : MonoBehaviour
|
||||
{
|
||||
[Header("巡逻移动")]
|
||||
[Min(0f)]
|
||||
public float moveSpeed = 4f;
|
||||
|
||||
[Min(0f)]
|
||||
public float patrolStopDistance = 0.15f;
|
||||
|
||||
[Min(0f)]
|
||||
public float patrolPause = 1.5f;
|
||||
|
||||
[Header("巡逻区域(以主建筑为锚点)")]
|
||||
[Min(0f)]
|
||||
public float patrolRadius = 5f;
|
||||
|
||||
[Min(0f)]
|
||||
public float leashRadius = 8f;
|
||||
|
||||
public string friendlyBuildingTag = "Friend";
|
||||
|
||||
private Animator animator;
|
||||
private Transform baseAnchor;
|
||||
private Vector3 fallbackAnchor;
|
||||
private Vector3 patrolTarget;
|
||||
private Vector3 chaseTarget;
|
||||
private bool hasPatrolTarget;
|
||||
private bool hasChaseTarget;
|
||||
private float pauseTimer;
|
||||
private bool patrolEnabled = true;
|
||||
|
||||
public bool IsMoving { get; private set; }
|
||||
|
||||
private Vector3 AnchorPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector3 position = baseAnchor != null ? baseAnchor.position : fallbackAnchor;
|
||||
position.y = transform.position.y;
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
animator = GetComponent<Animator>();
|
||||
fallbackAnchor = transform.position;
|
||||
baseAnchor = FindMainBase();
|
||||
SetWalking(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!patrolEnabled)
|
||||
{
|
||||
UpdateChase();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pauseTimer > 0f)
|
||||
{
|
||||
pauseTimer -= Time.deltaTime;
|
||||
IsMoving = false;
|
||||
SetWalking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPatrolTarget)
|
||||
{
|
||||
patrolTarget = GetNextPatrolPoint();
|
||||
hasPatrolTarget = true;
|
||||
}
|
||||
|
||||
Vector3 toTarget = patrolTarget - transform.position;
|
||||
toTarget.y = 0f;
|
||||
|
||||
if (toTarget.magnitude <= patrolStopDistance)
|
||||
{
|
||||
hasPatrolTarget = false;
|
||||
pauseTimer = patrolPause;
|
||||
IsMoving = false;
|
||||
SetWalking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Move(toTarget.normalized);
|
||||
}
|
||||
|
||||
private void UpdateChase()
|
||||
{
|
||||
if (!hasChaseTarget)
|
||||
{
|
||||
IsMoving = false;
|
||||
SetWalking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 toTarget = chaseTarget - transform.position;
|
||||
toTarget.y = 0f;
|
||||
|
||||
if (toTarget.magnitude <= patrolStopDistance)
|
||||
{
|
||||
IsMoving = false;
|
||||
SetWalking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 追击同样受栓绳限制,不会完全离开基地
|
||||
Move(toTarget.normalized);
|
||||
}
|
||||
|
||||
private Transform FindMainBase()
|
||||
{
|
||||
Transform firstBuilding = null;
|
||||
foreach (GameObject candidate in GameObject.FindGameObjectsWithTag(friendlyBuildingTag))
|
||||
{
|
||||
if (candidate == null || candidate.transform.root == transform.root)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate.GetComponentInParent<BuildingHealth>() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 主建筑按名称优先匹配;找不到时退回第一个己方建筑
|
||||
if (candidate.name.Contains("主要") || candidate.name.Contains("基地"))
|
||||
{
|
||||
return candidate.transform;
|
||||
}
|
||||
|
||||
if (firstBuilding == null)
|
||||
{
|
||||
firstBuilding = candidate.transform;
|
||||
}
|
||||
}
|
||||
|
||||
return firstBuilding;
|
||||
}
|
||||
|
||||
private Vector3 GetNextPatrolPoint()
|
||||
{
|
||||
Vector2 offset = Random.insideUnitCircle * patrolRadius;
|
||||
return ClampToLeash(AnchorPosition + new Vector3(offset.x, 0f, offset.y));
|
||||
}
|
||||
|
||||
private Vector3 ClampToLeash(Vector3 position)
|
||||
{
|
||||
Vector3 offset = position - AnchorPosition;
|
||||
offset.y = 0f;
|
||||
if (offset.magnitude > leashRadius)
|
||||
{
|
||||
offset = offset.normalized * leashRadius;
|
||||
}
|
||||
|
||||
return AnchorPosition + offset;
|
||||
}
|
||||
|
||||
public bool IsWithinLeash(Vector3 position)
|
||||
{
|
||||
Vector3 offset = position - AnchorPosition;
|
||||
offset.y = 0f;
|
||||
return offset.magnitude <= leashRadius;
|
||||
}
|
||||
|
||||
public void SetPatrolEnabled(bool enabled)
|
||||
{
|
||||
if (patrolEnabled == enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
patrolEnabled = enabled;
|
||||
if (!enabled)
|
||||
{
|
||||
hasPatrolTarget = false;
|
||||
pauseTimer = 0f;
|
||||
IsMoving = false;
|
||||
SetWalking(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
hasChaseTarget = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetChaseTarget(Vector3 position)
|
||||
{
|
||||
chaseTarget = position;
|
||||
chaseTarget.y = transform.position.y;
|
||||
hasChaseTarget = true;
|
||||
}
|
||||
|
||||
public void ClearChaseTarget()
|
||||
{
|
||||
hasChaseTarget = false;
|
||||
}
|
||||
|
||||
public void StopMovement()
|
||||
{
|
||||
hasPatrolTarget = false;
|
||||
hasChaseTarget = false;
|
||||
IsMoving = false;
|
||||
SetWalking(false);
|
||||
}
|
||||
|
||||
private void Move(Vector3 direction)
|
||||
{
|
||||
direction.y = 0f;
|
||||
if (direction.sqrMagnitude <= 0.001f)
|
||||
{
|
||||
IsMoving = false;
|
||||
SetWalking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
IsMoving = true;
|
||||
SetWalking(true);
|
||||
transform.position += direction * moveSpeed * Time.deltaTime;
|
||||
|
||||
// 栓绳:任何情况下都不能完全离开基地
|
||||
Vector3 clamped = ClampToLeash(transform.position);
|
||||
clamped.y = transform.position.y;
|
||||
transform.position = clamped;
|
||||
|
||||
transform.rotation = Quaternion.LookRotation(direction, Vector3.up);
|
||||
}
|
||||
|
||||
private void SetWalking(bool walking)
|
||||
{
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetInteger("speed", walking ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XXxM4S+rUHpTbCwQ4LVSXHFBDX+gn7XX5k/AX9VRTY82fi78RLmn7+w=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,157 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
/// <summary>
|
||||
/// 召唤师对决:召唤师点地移动操控(直线移动,无寻路)。
|
||||
/// 点击己方区域地面 → 直线走到目标点(clamp 在己方带内)。
|
||||
/// 移动时不攻击;站定后自动攻击射程内目标。
|
||||
/// </summary>
|
||||
public class SummonerClickMovement : MonoBehaviour
|
||||
{
|
||||
[Header("己方半场 clamp(世界坐标)")]
|
||||
public float minX = -9f;
|
||||
public float maxX = 9f;
|
||||
public float minZ = -29f;
|
||||
public float maxZ = -11f;
|
||||
|
||||
[Header("移动速度")]
|
||||
public float moveSpeed = 5f;
|
||||
public float stopDistance = 0.2f;
|
||||
|
||||
[Header("地面射线")]
|
||||
public LayerMask groundMask = ~0;
|
||||
|
||||
private Camera mainCamera;
|
||||
private MainCharacterCombat combat;
|
||||
private Animator animator;
|
||||
private Vector3 targetPosition;
|
||||
private bool hasTarget;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
combat = GetComponent<MainCharacterCombat>();
|
||||
animator = GetComponent<Animator>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleClickToMove();
|
||||
HandleMovement();
|
||||
}
|
||||
|
||||
// 点地移动:点击地面即设置目标点(EventSystem 判定不在 UI 上时才生效)。
|
||||
private void HandleClickToMove()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(0) && !IsPointerOverUI())
|
||||
{
|
||||
if (RaycastGround(out Vector3 groundPoint))
|
||||
{
|
||||
Vector3 clamped = ClampToOwnHalf(groundPoint);
|
||||
MoveTo(clamped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行直线移动
|
||||
private void HandleMovement()
|
||||
{
|
||||
if (!hasTarget) return;
|
||||
|
||||
Vector3 currentPos = transform.position;
|
||||
Vector3 direction = targetPosition - currentPos;
|
||||
direction.y = 0f;
|
||||
float distance = direction.magnitude;
|
||||
|
||||
// 到达目标
|
||||
if (distance <= stopDistance)
|
||||
{
|
||||
ClearTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
// 移动
|
||||
direction.Normalize();
|
||||
transform.position += direction * moveSpeed * Time.deltaTime;
|
||||
|
||||
// 朝向移动方向
|
||||
if (direction.sqrMagnitude > 0.001f)
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(direction, Vector3.up);
|
||||
}
|
||||
|
||||
// 播放走路动画
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetInteger("speed", 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 外部入口:把召唤师移动到某点(clamp 后设置目标)。
|
||||
public void MoveTo(Vector3 worldPosition)
|
||||
{
|
||||
Vector3 clamped = ClampToOwnHalf(worldPosition);
|
||||
targetPosition = clamped;
|
||||
targetPosition.y = transform.position.y; // 保持 Y 不变
|
||||
hasTarget = true;
|
||||
|
||||
// 移动中暂停攻击(防风筝规则)
|
||||
if (combat != null)
|
||||
{
|
||||
combat.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearTarget()
|
||||
{
|
||||
hasTarget = false;
|
||||
|
||||
// 停止动画
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetInteger("speed", 0);
|
||||
}
|
||||
|
||||
// 站定后恢复自动攻击
|
||||
if (combat != null)
|
||||
{
|
||||
combat.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 半场 clamp:X 与 Z 均限制在己方带内。
|
||||
private Vector3 ClampToOwnHalf(Vector3 position)
|
||||
{
|
||||
return new Vector3(
|
||||
Mathf.Clamp(position.x, minX, maxX),
|
||||
position.y,
|
||||
Mathf.Clamp(position.z, minZ, maxZ));
|
||||
}
|
||||
|
||||
private bool RaycastGround(out Vector3 point)
|
||||
{
|
||||
point = Vector3.zero;
|
||||
if (mainCamera == null)
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
if (mainCamera == null) return false;
|
||||
}
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 200f, groundMask.value))
|
||||
{
|
||||
point = hit.point;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsPointerOverUI()
|
||||
{
|
||||
return EventSystem.current != null && EventSystem.current.IsPointerOverGameObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WnsdsnurBXu73oetl8PpBFS15JbWYOToViLHQjuwwWbjfK6jglVS9pk=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Wysctir/AS85coFpOAVgITeURgK9Mr8QfSzBF8YJITfrH+Iw1gVKFtc=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 单位阵营标记:用阵营 ID 替代 Unity Tag 判断敌我关系。
|
||||
/// 支持 PVP:客户端只需设置 GameManager.LocalPlayerTeamId,所有单位的敌我判断自动正确。
|
||||
/// </summary>
|
||||
public class UnitTeam : MonoBehaviour
|
||||
{
|
||||
[Header("阵营 ID")]
|
||||
[Tooltip("0 = 玩家1阵营,1 = 玩家2阵营,2+ = 其他阵营")]
|
||||
public int teamId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 判断目标是否为敌人(不同阵营)
|
||||
/// </summary>
|
||||
public bool IsEnemy(GameObject other)
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
var otherTeam = other.GetComponent<UnitTeam>();
|
||||
if (otherTeam == null) return false;
|
||||
|
||||
return otherTeam.teamId != this.teamId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断目标是否为友军(相同阵营)
|
||||
/// </summary>
|
||||
public bool IsFriendly(GameObject other)
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
var otherTeam = other.GetComponent<UnitTeam>();
|
||||
if (otherTeam == null) return false;
|
||||
|
||||
return otherTeam.teamId == this.teamId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否属于本地玩家阵营
|
||||
/// </summary>
|
||||
public bool IsLocalPlayerTeam()
|
||||
{
|
||||
if (GameManager.Instance == null) return teamId == 0;
|
||||
return teamId == GameManager.Instance.LocalPlayerTeamId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: W3sY53n4U38uqE52iT9T3SGCXa4N1Lq1D+yZzpdpF2bStp+zdGJU0pg=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user