581 lines
18 KiB
C#
581 lines
18 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("死亡音效")]
|
||
[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;
|
||
|
||
// 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()
|
||
{
|
||
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*GameManager.Ins.players.Count;
|
||
originHealth = enemyInfo.Hp*GameManager.Ins.players.Count;
|
||
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.ReflectVectorXOZ() - transform.position), Time.deltaTime * 10f);
|
||
}
|
||
}
|
||
|
||
private void LateUpdate()
|
||
{
|
||
if (GameLocal.Ins.place == Place.Yunnan_Lincang_Linxiang_Hengji_Dixia)
|
||
{
|
||
Vector3 pos = transform.position;
|
||
if (Physics.Raycast(pos + Vector3.up * 5f, Vector3.down, out RaycastHit hit, 20f, LayerMask.GetMask("Dixia")))
|
||
{
|
||
pos.y = hit.point.y;
|
||
transform.position = pos;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Vector3 pos = transform.position;
|
||
pos.y = 0f; // 或者设置为地面高度
|
||
transform.position = pos;
|
||
}
|
||
}
|
||
|
||
#region --- 动画播放(本地)辅助方法 ---
|
||
|
||
// 在本地播放行走(Animancer or Animator)
|
||
public void PlayWalkLocal()
|
||
{
|
||
if (isDead) return;
|
||
if (AnimatorComponent != null)
|
||
{
|
||
AnimatorComponent.CrossFade("Walk", 0.15f); // 假设存在名为 "Walk" 的状态,或使用 SetFloat 控制
|
||
}
|
||
}
|
||
|
||
// 在本地播放受击(根据 hitPoint 判断左右)
|
||
private void PlayHitLocal(Vector3 hitPoint)
|
||
{
|
||
if (isDead) return;
|
||
Vector3 local = transform.InverseTransformPoint(hitPoint);
|
||
string hitStr=local.x<0 ?"leftHit":"rightHit";
|
||
if (AnimatorComponent != null)
|
||
{
|
||
AnimatorComponent.SetTrigger(hitStr); // 回落:直接按名字播放(确保 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 (AnimatorComponent != null)
|
||
{
|
||
AnimatorComponent.SetBool("dead",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;
|
||
if(info is int i && i != -1)
|
||
GameManager.Ins.AddScore(i.ToString(), GameManager.Ins.EnemyInfos[type].Score);
|
||
RpcPlayDeath(hitPoint);
|
||
}
|
||
|
||
[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 风格兼容)
|
||
|
||
/// <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 权威)
|
||
AnimatorComponent.SetInteger("state",1);
|
||
ApplyAttackDamage();
|
||
}
|
||
public virtual void StopAttack()
|
||
{
|
||
if (!isServer) return; // 攻击判定/伤害应由 Server 执行(Host 权威)
|
||
AnimatorComponent.SetInteger("state",0);
|
||
}
|
||
|
||
private IEnumerator DoAttackCoroutine()
|
||
{
|
||
if (isDead) yield break;
|
||
isAttacking = true;
|
||
isAttacking = false;
|
||
}
|
||
|
||
public Transform attackPoint; // 攻击检测点
|
||
private float attackRange = 3f; // 攻击半径
|
||
private float attackAngle = 90f; // 扇形角度
|
||
public LayerMask playerLayer; // 玩家所在的层
|
||
|
||
/// <summary>
|
||
/// 在动画结束时调用(通过动画事件)
|
||
/// </summary>
|
||
public void ApplyAttackDamage()
|
||
{
|
||
Collider[] hits = Physics.OverlapSphere(attackPoint.position, attackRange, playerLayer);
|
||
|
||
foreach (Collider hit in hits)
|
||
{
|
||
// 判断是否在前方45度范围内
|
||
Vector3 dirToTarget = (hit.transform.position - attackPoint.position).normalized;
|
||
float angle = Vector3.Angle(attackPoint.forward, dirToTarget);
|
||
if (angle >= attackAngle ) // 扇形角度一半
|
||
{
|
||
Player player = hit.GetComponent<Player>();
|
||
if (player != null)
|
||
{
|
||
player.ApplyDamage(atk,null,transform);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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
|
||
}
|