653 lines
20 KiB
C#
653 lines
20 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using DragonLi.Core;
|
||
using Mirror;
|
||
using Pathfinding;
|
||
using Pathfinding.RVO;
|
||
using UnityEngine;
|
||
using BehaviorDesigner.Runtime;
|
||
using BehaviorDesigner.Runtime.Tasks;
|
||
using Animancer; // Animancer 支持
|
||
using DarkTonic.MasterAudio;
|
||
using DragonLi.Frame;
|
||
|
||
public enum EnemyState
|
||
{
|
||
Borning = 0,
|
||
Charge = 1,
|
||
Search = 2,
|
||
Run = 3,
|
||
Atk = 4,
|
||
Die = 99,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将原来的射手 Enemy 改造成丧尸近战 Enemy(继承 Agent)
|
||
/// - 使用 Animancer 播放动画(优先);若无 Animancer 则回落到 AnimatorComponent
|
||
/// - Host(isServer)为权威:伤害与死亡在服务器上处理,并通过 ClientRpc 通知客户端播放视觉动画
|
||
/// - 支持 IAstarAI / AIPath / RVOController 的移动接口(SetDestination 等),以供 BehaviorDesigner 调用
|
||
/// </summary>
|
||
public class Enemy : Agent
|
||
{
|
||
// A* / RVO
|
||
private IAstarAI _ai;
|
||
public IAstarAI ai
|
||
{
|
||
get
|
||
{
|
||
if (_ai == null) _ai = GetComponent<IAstarAI>();
|
||
return _ai;
|
||
}
|
||
}
|
||
|
||
private AIPath _aiPath;
|
||
public AIPath aiPath
|
||
{
|
||
get
|
||
{
|
||
if (_aiPath == null) _aiPath = GetComponent<AIPath>();
|
||
return _aiPath;
|
||
}
|
||
}
|
||
|
||
private BehaviorTree _behaviorTree;
|
||
public BehaviorTree behaviorTree
|
||
{
|
||
get
|
||
{
|
||
if (_behaviorTree == null) _behaviorTree = GetComponent<BehaviorTree>();
|
||
return _behaviorTree;
|
||
}
|
||
}
|
||
|
||
private RVOController _rvoController;
|
||
public RVOController rvoController
|
||
{
|
||
get
|
||
{
|
||
if (_rvoController == null) _rvoController = GetComponent<RVOController>();
|
||
return _rvoController;
|
||
}
|
||
}
|
||
|
||
private Collider _collider;
|
||
public Collider selfCollider
|
||
{
|
||
get
|
||
{
|
||
if (_collider == null) _collider = GetComponent<Collider>();
|
||
return _collider;
|
||
}
|
||
}
|
||
|
||
public Transform weakness;
|
||
|
||
[Header("Animancer (优先) / Animator (回落)")]
|
||
public AnimancerComponent AnimancerComponent; // 请在 Inspector 赋值或让脚本自动获取
|
||
// AnimatorComponent 在 Agent 中已有(AnimatorComponent 字段),保留为回落
|
||
|
||
[Header("动画片段 - 请在 Inspector 填充")]
|
||
public AnimationClip introClip; // 开场(非循环)
|
||
public AnimationClip walkClip; // 行走(Loop)
|
||
public AnimationClip attackClip; // 攻击
|
||
public AnimationClip hitLeftClip; // 受击 左
|
||
public AnimationClip hitRightClip; // 受击 右
|
||
public AnimationClip deathClip; // 死亡
|
||
|
||
[Header("死亡音效")]
|
||
[SoundGroup]
|
||
public string dieSound;
|
||
|
||
public GameObject explosion_prefab;
|
||
|
||
[Header("编号")]
|
||
[SyncVar]
|
||
public int id = 0;
|
||
|
||
[Header("怪物类型")]
|
||
[SyncVar]
|
||
public EnemyType type;
|
||
|
||
[Header("当前状态")]
|
||
public EnemyState state;
|
||
|
||
[Header("等级")]
|
||
[SyncVar]
|
||
public int lvl = 0;
|
||
|
||
[Header("速度")]
|
||
[SyncVar]
|
||
public float speed = 0;
|
||
|
||
[Header("攻击力")]
|
||
[SyncVar]
|
||
public float atk = 0;
|
||
|
||
[Header("特殊攻击1攻击力")]
|
||
[SyncVar]
|
||
public float SpecialAtk1 = 0;
|
||
|
||
[Header("特殊攻击2攻击力")]
|
||
[SyncVar]
|
||
public float SpecialAtk2 = 0;
|
||
|
||
[Header("目标引用 (通常由 BT 设置)")]
|
||
public GameObject target = null;
|
||
|
||
// 行为状态(本地/服务器)
|
||
private bool updateAnimatorSpeedValue = false;
|
||
private bool updateAnimatorSpeedFValue = false;
|
||
private bool updateAnimatorSpeedRValue = false;
|
||
|
||
// 动画播放缓存(Animancer 状态)
|
||
private AnimancerState walkState;
|
||
|
||
// AI / 行为标志(server 上驱动)
|
||
private bool introPlayed = false;
|
||
private bool isAttacking = false;
|
||
private bool isDead = false;
|
||
|
||
// RVO 速度参数(可在 Inspector 调整)
|
||
[Header("RVO 参数 (若使用 RVOController)")]
|
||
public float desiredSpeed = 1.2f;
|
||
public float maxSpeed = 1.5f;
|
||
|
||
void Awake()
|
||
{
|
||
// 自动获取 AnimancerComponent(如果没有手动赋值)
|
||
if (AnimancerComponent == null)
|
||
AnimancerComponent = GetComponent<AnimancerComponent>();
|
||
|
||
InitAnimator();
|
||
}
|
||
|
||
void InitAnimator()
|
||
{
|
||
// 检测 Animator 参数,以便按需设置 speed / speedf / speedr
|
||
if (AnimatorComponent == null) return;
|
||
AnimatorControllerParameter[] parameters = AnimatorComponent.parameters;
|
||
foreach (AnimatorControllerParameter val in parameters)
|
||
{
|
||
if (val.type == AnimatorControllerParameterType.Float)
|
||
{
|
||
switch (val.name)
|
||
{
|
||
case "speed":
|
||
updateAnimatorSpeedValue = true;
|
||
break;
|
||
case "speedr":
|
||
updateAnimatorSpeedRValue = true;
|
||
break;
|
||
case "speedf":
|
||
updateAnimatorSpeedFValue = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
[Server]
|
||
public virtual void OnSpawn(int id, EnemyType type, int lvl)
|
||
{
|
||
base.OnSpawn();
|
||
this.id = id;
|
||
this.type = type;
|
||
state = EnemyState.Borning;
|
||
this.lvl = lvl;
|
||
EnemyInfo enemyInfo = GameManager.Ins.EnemyInfos[type];
|
||
speed = enemyInfo.Speed;
|
||
atk = enemyInfo.Atk;
|
||
health = enemyInfo.Hp;
|
||
originHealth = enemyInfo.Hp;
|
||
aiPath.enabled = true;
|
||
aiPath.maxSpeed = speed;
|
||
GameManager.Ins.CreateEnemyUI(this);
|
||
// 初始设置(server)
|
||
if (isServer)
|
||
{
|
||
state = EnemyState.Borning;
|
||
if (aiPath != null) aiPath.enabled = true;
|
||
if (rvoController != null) rvoController.enabled = true;
|
||
if (selfCollider != null) selfCollider.enabled = true;
|
||
if (behaviorTree != null) behaviorTree.enabled = true; // 仅 Host(Server)上运行 BT
|
||
// 初始血量已在 Agent.OnSpawn 中设置为 originHealth / health
|
||
}
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
OnUpdate();
|
||
}
|
||
|
||
public override void OnUpdate()
|
||
{
|
||
// 只有 Server/Host 驱动移动(rvo / ai),其它客户端只做视觉播放(由 Rpc 通知)
|
||
if (isServer && !isDead)
|
||
{
|
||
// 该方法在 Agent.Update 中由 Server 调用(以前你的实现)
|
||
// 这里我们保持相同更新:更新动画参数 & 旋转等(只在 Server 上操作 AI/移动)
|
||
if (aiPath != null && aiPath.enabled)
|
||
{
|
||
UpdateAnimatorValues(aiPath.velocity);
|
||
}
|
||
else if (RigidbodyComponent)
|
||
{
|
||
UpdateAnimatorValues(RigidbodyComponent.velocity);
|
||
}
|
||
UpdateRotation();
|
||
}
|
||
}
|
||
|
||
protected void UpdateAnimatorValues(Vector3 vel)
|
||
{
|
||
// if (AnimatorComponent != null)
|
||
// {
|
||
// if (updateAnimatorSpeedValue)
|
||
// {
|
||
// AnimatorComponent.SetFloat("speed", vel.magnitude);
|
||
// }
|
||
// Vector3 val;
|
||
// if (updateAnimatorSpeedFValue)
|
||
// {
|
||
// val = Vector3.Project(vel, transform.forward);
|
||
// float num = vel.magnitude > 0 ? val.magnitude / (vel.magnitude * (Vector3.Angle(vel, transform.forward) > 90f ? -1f : 1f)) : 0f;
|
||
// AnimatorComponent.SetFloat("speedf", num);
|
||
// }
|
||
// if (updateAnimatorSpeedRValue)
|
||
// {
|
||
// val = Vector3.Project(vel, transform.right);
|
||
// float num2 = vel.magnitude > 0 ? val.magnitude / (vel.magnitude * (Vector3.Angle(vel, transform.right) > 90f ? -1f : 1f)) : 0f;
|
||
// AnimatorComponent.SetFloat("speedr", num2);
|
||
// }
|
||
// }
|
||
|
||
// 如果使用 Animancer 且你希望通过脚本控制 walkClip 的播放,则在 UpdateLocalVisuals 中处理(下文)
|
||
}
|
||
|
||
public virtual void UpdateRotation()
|
||
{
|
||
if (target != null)
|
||
{
|
||
transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(target.transform.position - transform.position), Time.deltaTime * 10f);
|
||
}
|
||
}
|
||
|
||
private void LateUpdate()
|
||
{
|
||
Vector3 pos = transform.position;
|
||
pos.y = 0f; // 或者设置为地面高度
|
||
transform.position = pos;
|
||
}
|
||
|
||
#region --- 动画播放(本地)辅助方法 ---
|
||
|
||
// 在本地播放行走(Animancer or Animator)
|
||
public void PlayWalkLocal()
|
||
{
|
||
if (isDead) return;
|
||
if (AnimancerComponent != null && walkClip != null)
|
||
{
|
||
if (walkState == null)
|
||
{
|
||
Debug.LogError("移动");
|
||
walkState = AnimancerComponent.Play(walkClip);
|
||
}
|
||
else
|
||
{
|
||
walkState.Play();
|
||
}
|
||
|
||
}
|
||
else if (AnimatorComponent != null)
|
||
{
|
||
AnimatorComponent.CrossFade("Walk", 0.15f); // 假设存在名为 "Walk" 的状态,或使用 SetFloat 控制
|
||
}
|
||
}
|
||
|
||
// 在本地播放受击(根据 hitPoint 判断左右)
|
||
private void PlayHitLocal(Vector3 hitPoint)
|
||
{
|
||
if (isDead) return;
|
||
Vector3 local = transform.InverseTransformPoint(hitPoint);
|
||
AnimationClip clip = local.x < 0 ? hitLeftClip : hitRightClip;
|
||
if (clip == null) return;
|
||
|
||
if (AnimancerComponent != null)
|
||
{
|
||
var s = AnimancerComponent.Play(clip);
|
||
// 示例绑定本地结束回调(如果需要)
|
||
s.Events(this).OnEnd = () =>
|
||
{
|
||
// 受击动画结束时的本地处理(通常不需要通知服务器)
|
||
};
|
||
}
|
||
else if (AnimatorComponent != null)
|
||
{
|
||
AnimatorComponent.Play(clip.name); // 回落:直接按名字播放(确保 Animator 有对应 state)
|
||
}
|
||
}
|
||
|
||
// 在本地播放死亡动画
|
||
private void PlayDeathLocal(Vector3 hitPoint)
|
||
{
|
||
if (isDead) return;
|
||
isDead = true;
|
||
|
||
// 停用碰撞与 BT(本地表现)
|
||
if (selfCollider != null) selfCollider.enabled = false;
|
||
if (behaviorTree != null) behaviorTree.enabled = false;
|
||
if (aiPath != null) aiPath.enabled = false;
|
||
if (rvoController != null) rvoController.enabled = false;
|
||
|
||
if (AnimancerComponent != null && deathClip != null)
|
||
{
|
||
var s = AnimancerComponent.Play(deathClip);
|
||
s.Events(this).OnEnd = () =>
|
||
{
|
||
NetworkServer.Destroy(gameObject);
|
||
if(isServer)
|
||
GameManager.Ins.DeleteEnemy(id);
|
||
// client 本地死亡结束后的处理(通常由 server 决定是否销毁)
|
||
// 这里不直接销毁网络对象,server 会在需要时 Destroy
|
||
};
|
||
// 播放死亡音效(客户端本地)
|
||
GameManager.Ins.PlaySound3DRPC(dieSound,transform,true);
|
||
}
|
||
else
|
||
{
|
||
if (AnimatorComponent != null)
|
||
{
|
||
AnimatorComponent.SetBool("dead", true);
|
||
GameManager.Ins.PlaySound3DRPC(dieSound,transform,true);
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region --- Server authoritative: 伤害与广播受击/死亡 ---
|
||
|
||
/// <summary>
|
||
/// 当 Server 调用 ApplyDamage -> 会触发 OnReceiveDamage 在 Server 上执行
|
||
/// 我们在这里播放受击视觉(通过 Rpc 广播给客户端)
|
||
/// 返回的 float 仍然是实际要减少的生命值(可在这里应用韧性/抗性计算)
|
||
/// </summary>
|
||
public override float OnReceiveDamage(float value, object info, Transform _sender)
|
||
{
|
||
// info 可以携带 hitPoint,如果是 Vector3 则按命中点处理;否则可按 _sender 确定方向
|
||
Vector3 hitPoint = transform.position;
|
||
if (info is Vector3)
|
||
{
|
||
hitPoint = (Vector3)info;
|
||
}
|
||
else if (_sender != null)
|
||
{
|
||
// 近似命中点为攻击者位置
|
||
hitPoint = _sender.position;
|
||
}
|
||
|
||
// Server 上播放受击(广播给所有客户端)
|
||
RpcPlayHit(hitPoint);
|
||
|
||
// 伤害计算:默认返回原始值;若你需要韧性(toughness)生效,可在这里调整
|
||
// 例如:value = Mathf.Max(0f, value - toughness);
|
||
return value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Server -> Rpc: 通知所有客户端播放受击视觉
|
||
/// </summary>
|
||
[ClientRpc]
|
||
private void RpcPlayHit(Vector3 hitPoint)
|
||
{
|
||
PlayHitLocal(hitPoint);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 覆盖 Die(Agent.Die),Server 上调用后广播死亡给客户端并做 server 清理
|
||
/// </summary>
|
||
public override void Die(object info, Transform _sender)
|
||
{
|
||
// 服务端处理(设置状态、移除 UI 等)
|
||
GameManager.Ins.DeleteEnemyUI(id);
|
||
state = EnemyState.Die;
|
||
|
||
// 禁用交互组件(server 端)
|
||
if (selfCollider != null) selfCollider.enabled = false;
|
||
if (behaviorTree != null) behaviorTree.enabled = false;
|
||
if (aiPath != null) aiPath.enabled = false;
|
||
if (rvoController != null) rvoController.enabled = false;
|
||
|
||
// 在 server 上播放死亡并通知客户端
|
||
Vector3 hitPoint = transform.position;
|
||
if (info is Vector3) hitPoint = (Vector3)info;
|
||
RpcPlayDeath(hitPoint);
|
||
|
||
// 播放死亡音效(在客户端通过 RpcPlayDeath 会触发本地 PlaySound)
|
||
// Server 端额外处理:生成爆炸/掉落/计分等
|
||
PlayServerDeathEffects();
|
||
|
||
// 最终 server 可决定是否销毁网络对象
|
||
if (/*你想直接销毁*/ false)
|
||
{
|
||
NetworkServer.Destroy(gameObject);
|
||
}
|
||
else
|
||
{
|
||
// 保持对象存在,直到 server 另外决定销毁
|
||
}
|
||
}
|
||
|
||
[ClientRpc]
|
||
private void RpcPlayDeath(Vector3 hitPoint)
|
||
{
|
||
PlayDeathLocal(hitPoint);
|
||
}
|
||
|
||
// Server 上死亡后的额外处理(掉落、爆炸、声音等)
|
||
[Server]
|
||
private void PlayServerDeathEffects()
|
||
{
|
||
// 生成爆炸示例(如果需要)
|
||
if (explosion_prefab != null)
|
||
{
|
||
GameObject explosion = Instantiate(explosion_prefab);
|
||
explosion.transform.position = transform.position;
|
||
NetworkServer.Spawn(explosion);
|
||
// 延迟销毁可能需要协程或任务系统
|
||
CoroutineTaskManager.Instance.WaitSecondTodo(() =>
|
||
{
|
||
if (explosion != null) NetworkServer.Destroy(explosion);
|
||
}, 2f);
|
||
}
|
||
|
||
// 播放死亡音效通过 Rpc(客户端会播放)
|
||
RpcPlaySound(dieSound);
|
||
}
|
||
|
||
[ClientRpc]
|
||
private void RpcPlaySound(string sound)
|
||
{
|
||
if (!string.IsNullOrEmpty(sound))
|
||
MasterAudio.PlaySound3DAtVector3(sound, transform.position);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region --- 对外接口:Behavior Tree / Actions 使用 ---
|
||
|
||
// BehaviorDesigner Action 可直接调用这些方法(与你现有 Actions 风格兼容)
|
||
|
||
public void PlayIntro()
|
||
{
|
||
if (introPlayed) return;
|
||
StartCoroutine(PlayIntroCoroutine());
|
||
}
|
||
|
||
private IEnumerator PlayIntroCoroutine()
|
||
{
|
||
if (introClip != null)
|
||
{
|
||
if (AnimancerComponent != null)
|
||
{
|
||
var s = AnimancerComponent.Play(introClip);
|
||
bool finished = false;
|
||
s.Events(this).OnEnd = () => finished = true;
|
||
yield return new WaitUntil(() => finished);
|
||
}
|
||
else if (AnimatorComponent != null)
|
||
{
|
||
AnimatorComponent.Play(introClip.name);
|
||
// 若要等待 Animator 的结束,需要使用 AnimatorStateInfo.length(不可靠),这里不等待
|
||
yield return null;
|
||
}
|
||
}
|
||
introPlayed = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置目标点(NavMesh / RVO 通用)
|
||
/// </summary>
|
||
public void SetDestination(Vector3 pos)
|
||
{
|
||
if (ai != null)
|
||
{
|
||
// IAstarAI 通常实现 destination via AIPath or Seeker; use ai.destination if available
|
||
// 若 ai 实现的是 IAstarAI,则可以尝试 cast to RichAI / AIPath 处理
|
||
var rich = ai as IAstarAI;
|
||
try
|
||
{
|
||
ai.destination = pos;
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
if (aiPath != null)
|
||
{
|
||
aiPath.maxSpeed = speed;
|
||
aiPath.enabled = true;
|
||
// AIPath/Seeker 会寻路到 target (assuming Call to seeker handled by BT)
|
||
}
|
||
|
||
if (rvoController != null)
|
||
{
|
||
rvoController.locked = false;
|
||
rvoController.SetTarget(pos, desiredSpeed, maxSpeed, pos);
|
||
}
|
||
}
|
||
|
||
public void SetDestinationToTarget(Transform t)
|
||
{
|
||
if (t == null) return;
|
||
SetDestination(t.position);
|
||
}
|
||
|
||
public void MoveForwardDistance(float distance)
|
||
{
|
||
Vector3 targetPos = transform.position + transform.forward * distance;
|
||
SetDestination(targetPos);
|
||
}
|
||
|
||
public void MoveLocalBackward(float distance)
|
||
{
|
||
Vector3 targetPos = transform.position + transform.TransformDirection(new Vector3(0, 0, -1)) * distance;
|
||
SetDestination(targetPos);
|
||
}
|
||
|
||
public void StartChase()
|
||
{
|
||
if (ai != null) ai.canMove = true;
|
||
if (aiPath != null) aiPath.enabled = true;
|
||
if (rvoController != null) rvoController.locked = false;
|
||
}
|
||
|
||
public void StopChase()
|
||
{
|
||
if (ai != null) ai.canMove = false;
|
||
if (aiPath != null) aiPath.enabled = false;
|
||
if (rvoController != null) { rvoController.locked = true; rvoController.ForceSetVelocity(Vector3.zero); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发起一次攻击(播放攻击动画,Server 上触发伤害判定/事件)
|
||
/// - BehaviorTree 在 host 上执行时应调用 DoAttack(),并在动画的事件帧调用真正的伤害函数(或在 DoAttackCoroutine 中等待到事件再次调用)
|
||
/// </summary>
|
||
public virtual void DoAttack()
|
||
{
|
||
if (!isServer) return; // 攻击判定/伤害应由 Server 执行(Host 权威)
|
||
StartCoroutine(DoAttackCoroutine());
|
||
}
|
||
|
||
private IEnumerator DoAttackCoroutine()
|
||
{
|
||
if (isDead) yield break;
|
||
isAttacking = true;
|
||
|
||
if (attackClip != null && AnimancerComponent != null)
|
||
{
|
||
var s = AnimancerComponent.Play(attackClip);
|
||
bool finished = false;
|
||
// 你可以在动画的关键帧添加事件并调用本脚本的 ServerDealDamage() —— 或者在这里等待结束再处理
|
||
s.Events(this).OnEnd = () => finished = true;
|
||
yield return new WaitUntil(() => finished);
|
||
}
|
||
else if (AnimatorComponent != null && attackClip != null)
|
||
{
|
||
AnimatorComponent.Play(attackClip.name);
|
||
yield return new WaitForSeconds(0.7f);
|
||
}
|
||
else
|
||
{
|
||
yield return new WaitForSeconds(0.7f);
|
||
}
|
||
|
||
isAttacking = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Server 端在合适时机调用(例如动画事件帧)执行伤害判定
|
||
/// 你可以在 Animancer 的动画事件里调用这个方法(服务器上的 host 会执行)
|
||
/// </summary>
|
||
[Server]
|
||
public void ServerDealDamageToTarget(float damage)
|
||
{
|
||
if (!isServer) return;
|
||
if (target == null) return;
|
||
|
||
IDamagable dam = target.GetComponent<IDamagable>();
|
||
if (dam != null)
|
||
{
|
||
// info 可传命中点
|
||
Vector3 hitPoint = target.transform.position;
|
||
// 调用目标的 ApplyDamage(目标可能是 Player,服务器应执行)
|
||
// 假设目标的 ApplyDamage 签名为 ApplyDamage(float, object, Transform)
|
||
var targetAgent = target.GetComponent<Agent>();
|
||
if (targetAgent != null)
|
||
{
|
||
targetAgent.ApplyDamage(damage, hitPoint, transform);
|
||
}
|
||
else
|
||
{
|
||
// 如果目标不是 Agent,尝试用 IDamagable 接口
|
||
dam.ApplyDamage(damage, hitPoint, transform);
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Debug Gizmos
|
||
void OnDrawGizmosSelected()
|
||
{
|
||
Gizmos.color = Color.red;
|
||
Gizmos.DrawWireSphere(transform.position, 1.5f); // 可替换为攻击范围的字段
|
||
if (target != null)
|
||
{
|
||
Gizmos.color = Color.yellow;
|
||
Gizmos.DrawLine(transform.position, target.transform.position);
|
||
}
|
||
}
|
||
#endregion
|
||
}
|