Files

464 lines
18 KiB
C#
Raw Permalink 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 Data;
using Managers;
using UnityEngine;
namespace AI
{
public abstract class JobBase
{
public Entity.Entity entity;
protected int timeoutTicks = 150;
public bool Running => timeoutTicks > 0;
protected abstract void UpdateJob();
public virtual void StartJob(Entity.Entity target)
{
entity = target;
}
public bool Update()
{
if (!Running)
return false;
UpdateJob();
timeoutTicks--;
return true;
}
public virtual void StopJob()
{
timeoutTicks = 0;
}
}
public class WanderJob : JobBase
{
public override void StartJob(Entity.Entity target)
{
base.StartJob(target);
Vector3 move = new(Random.Range(-10, 10), Random.Range(-10, 10));
var targetPosition = entity.transform.position + move;
entity.SetTarget(targetPosition);
}
protected override void UpdateJob()
{
entity.TryMove();
}
override public void StopJob()
{
base.StopJob();
}
}
public class IdleJob : JobBase
{
override public void StartJob(Entity.Entity target)
{
base.StartJob(target);
timeoutTicks = 500;
}
protected override void UpdateJob()
{
}
}
public class MoveJob : JobBase
{
protected override void UpdateJob()
{
entity.TryMove();
}
}
public class AttackJob : JobBase
{
private Entity.Entity attackTarget;
// StartJob 方法:用于初始化任务,寻找初始攻击目标
override public void StartJob(Entity.Entity performerEntityContext) // 参数名更明确,通常是发起任务的实体
{
base.StartJob(performerEntityContext);
// 1. 任务执行者自身有效性检查
if (!entity)
{
StopJob(); // 调用StopJob来结束任务
return;
}
attackTarget = FindNewHostileTarget();
if (!attackTarget)
{
StopJob(); // 调用StopJob来结束任务
}
}
protected override void UpdateJob()
{
// 1. 任务执行者的基本检查
if (!entity || entity.IsDead)
{
StopJob();
return;
}
if (!attackTarget || attackTarget.IsDead)
{
attackTarget = FindNewHostileTarget(); // 尝试寻找新的攻击目标
if (!attackTarget)
{
StopJob();
return;
}
}
var weapon = entity.GetCurrentWeapon();
var attackRange = 0f;
if (weapon != null)
{
attackRange = weapon.Attributes.attackRange;
}
var distanceSq = (entity.Position - attackTarget.Position).sqrMagnitude;
var effectiveAttackRangeSq = attackRange * attackRange; // 将攻击范围也平方
entity.SetTarget(attackTarget.Position);
if (weapon != null && distanceSq <= effectiveAttackRangeSq)
{
entity.TryAttack();
}
else
{
entity.TryMove();
}
}
/// <summary>
/// 查找执行实体最近的敌对目标。
/// </summary>
/// <returns>找到的敌对实体如果没有则返回null。</returns>
private Entity.Entity FindNewHostileTarget()
{
if (!entity) return null;
return EntityManage.Instance.FindNearestEntityByRelation(
entity.currentDimensionId, // 搜索维度ID
entity.entityPrefab, // 执行实体的Prefab ID用于关系判断
Relation.Hostile)?.entity; // 寻找敌对关系的目标
}
}
public class AdvancedAttackJob : JobBase
{
private Entity.Entity attackTarget;
// 常量用于配置远程AI行为的风筝参数
private const float KITING_THRESHOLD_MULTIPLIER = 0.5f; // 当目标距离小于 (攻击范围 * 此乘数) 时,远程单位开始尝试远离
private const float KITING_BUFFER_DISTANCE = 5.0f; // 当远程单位远离时,目标点会是当前位置向反方向偏移此距离
// StartJob 方法:用于初始化任务,寻找初始攻击目标
override public void StartJob(Entity.Entity performerEntityContext)
{
base.StartJob(performerEntityContext);
if (entity == null)
{
StopJob();
return;
}
attackTarget = FindNewHostileTarget();
if (attackTarget == null)
{
StopJob();
}
}
protected override void UpdateJob()
{
// 1. 任务执行者的基本检查
if (entity == null || entity.IsDead)
{
StopJob();
return;
}
// 2. 攻击目标检查
if (attackTarget == null || attackTarget.IsDead)
{
attackTarget = FindNewHostileTarget(); // 尝试寻找新的攻击目标
if (attackTarget == null)
{
StopJob();
return;
}
}
// 获取武器和其属性
var weapon = entity.GetCurrentWeapon();
var attackRange = 0f;
var isRangedWeapon = false; // 标识是否为远程武器
if (weapon != null)
{
if (weapon.Attributes != null)
{
attackRange = weapon.Attributes.attackRange;
}
// 使用 WeaponType 来判断武器类型,更明确
isRangedWeapon = weapon.Type == WeaponType.Ranged;
}
var distanceSq = (entity.Position - attackTarget.Position).sqrMagnitude;
var effectiveAttackRangeSq = attackRange * attackRange; // 将攻击范围平方
// ---- 核心AI行为决策 ( AdvancedAttackJob 的智能之处) ----
if (isRangedWeapon)
{
// 远程单位的风筝Kiting逻辑
var kitingDistance = attackRange * KITING_THRESHOLD_MULTIPLIER;
var kitingThresholdSq = kitingDistance * kitingDistance; // 过近距离的平方
// 1. 如果目标过于接近 (小于风筝阈值),尝试远离
if (distanceSq < kitingThresholdSq)
{
// 计算一个远离目标的点作为移动目标
var directionAway = (entity.Position - attackTarget.Position).normalized;
var fleePosition = entity.Position + directionAway * KITING_BUFFER_DISTANCE;
entity.SetTarget(fleePosition); // 设置远离点为新的移动目标
entity.TryMove(); // 优先执行移动操作以拉开距离
// 在此状态下不进行攻击,专注于 reposition
}
// 2. 如果目标在最佳攻击范围内 (即在风筝阈值和有效攻击范围之间),则停止移动并攻击
else if (distanceSq <= effectiveAttackRangeSq)
{
entity.SetTarget(entity.Position); // 设定目标为当前位置,使其停止移动,专注于攻击
entity.TryAttack();
}
// 3. 如果目标太远 (超出有效攻击范围),则移动靠近目标
else
{
entity.SetTarget(attackTarget.Position); // 设置目标位置为移动目标
entity.TryMove();
}
}
else // 近战单位或没有武器的单位
{
entity.SetTarget(attackTarget.Position);
if (weapon != null && distanceSq <= effectiveAttackRangeSq)
{
entity.TryAttack();
}
else
{
entity.TryMove();
}
}
}
/// <summary>
/// 查找执行实体最近的敌对目标。
/// </summary>
/// <returns>找到的敌对实体如果没有则返回null。</returns>
private Entity.Entity FindNewHostileTarget()
{
if (!entity) return null;
return EntityManage.Instance.FindNearestEntityByRelation(
entity.currentDimensionId,
entity.entityPrefab,
Relation.Hostile)?.entity;
}
}
public class FleeJob : JobBase
{
// 逃跑时在远离方向上设置的目标点偏移距离。让AI尝试跑向更远的点。
private const float FLEE_BUFFER_DISTANCE = 10.0f;
// 远程单位在逃跑时,如果敌人距离已超出其攻击范围的多少倍,可以认为已经足够安全。
// 此时实体将停止紧急移动可以等待timeoutTicks结束或进行其他决策。
private const float RANGED_SAFE_DISTANCE_MULTIPLIER = 2.0f;
// 对于近战或无武器单位,敌人必须在这个距离内,才触发紧急逃跑。
// 超过这个距离,实体可能认为已经足够安全,可以停止紧急移动。
private const float MELEE_PERIL_DISTANCE = 15.0f;
private const float MELEE_PERIL_DISTANCE_SQUARED = MELEE_PERIL_DISTANCE * MELEE_PERIL_DISTANCE;
// **新增**:目标刷新间隔 (以Ticks计)
private const int TARGET_REFRESH_INTERVAL_TICKS = 30; // 每30帧Ticks刷新一次目标
// **新增**缓存的敌对目标避免频繁调用FindNearestHostileTarget
private Entity.Entity _cachedHostileTarget;
// **新增**:目标刷新计时器
private int _targetRefreshTimer;
// **新增**:侧走参数 (用于躲避远程攻击)
private const float EVASION_SIDE_STEP_STRENGTH = 0.5f; // 侧走分量强度,与主方向叠加,需要调整
private int _sideStepDirection = 1; // 1 for right, -1 for left
private int _sideStepTimer;
private const int EVASION_SIDE_STEP_INTERVAL_TICKS = 20; // 每20帧切换一次侧走方向
public FleeJob(int initialTimeoutTicks = 150)
{
timeoutTicks = initialTimeoutTicks;
_targetRefreshTimer = 0; // 初始化计时器让第一次UpdateJob立即刷新目标
_sideStepTimer = 0;
_sideStepDirection = Random.Range(0, 2) * 2 - 1; // 随机初始化左右方向
}
public override void StartJob(Entity.Entity performerEntityContext)
{
base.StartJob(performerEntityContext);
_cachedHostileTarget = null; // 任务开始时清空缓存,确保第一次更新会查找新目标
_targetRefreshTimer = 0; // 确保立即查找目标
_sideStepTimer = 0;
_sideStepDirection = Random.Range(0, 2) * 2 - 1; // 随机初始化左右方向
}
protected override void UpdateJob()
{
// 1. 任务执行者的基本检查
if (!entity || entity.IsDead)
{
StopJob();
return;
}
// 2. 目标刷新逻辑 (避免每帧都调用FindNearestHostileTarget)
_targetRefreshTimer--;
if (!_cachedHostileTarget || _cachedHostileTarget.IsDead || _targetRefreshTimer <= 0)
{
_cachedHostileTarget = FindNearestHostileTarget();
_targetRefreshTimer = TARGET_REFRESH_INTERVAL_TICKS; // 重置计时器
}
// 使用缓存的目标
var hostileTarget = _cachedHostileTarget;
if (!hostileTarget)
{
// 如果没有敌对目标可逃跑,则停止此工作。
StopJob();
return;
}
// 3. 计算与敌对目标的距离
var distanceSq = (entity.Position - hostileTarget.Position).sqrMagnitude;
// 4. 获取敌方目标武器信息,判断敌方是否为远程单位
var enemyWeapon = hostileTarget.GetCurrentWeapon();
var isEnemyRanged = enemyWeapon is { Type: WeaponType.Ranged };
// 获取自己的武器信息 (虽然逃跑主要看敌方武器,但自身安全距离判断需要)
var selfWeapon = entity.GetCurrentWeapon();
var isSelfRanged = selfWeapon is { Type: WeaponType.Ranged };
// 5. 计算一个远离目标的基础方向
var directionAway = (entity.Position - hostileTarget.Position).normalized;
var moveTargetDirection = directionAway; // 默认是纯远离方向
// 6. 如果敌方是远程单位,叠加侧走逻辑
if (isEnemyRanged)
{
_sideStepTimer--;
if (_sideStepTimer <= 0)
{
_sideStepDirection *= -1; // 切换侧走方向 (左或右)
_sideStepTimer = EVASION_SIDE_STEP_INTERVAL_TICKS;
}
// 计算侧走方向 (在X-Z平面上垂直于远离方向)
var perpendicularDirection = GetPerpendicularVectorXZ(directionAway) * _sideStepDirection;
// 侧走策略:在远离方向上叠加一个侧走分量
// 让侧走分量与远离分量结合,形成一个弧线或斜向移动
moveTargetDirection = (directionAway * (1 - EVASION_SIDE_STEP_STRENGTH) +
perpendicularDirection * EVASION_SIDE_STEP_STRENGTH).normalized;
// 或者更简单地moveTargetDirection = (directionAway + perpendicularDirection * EVASION_SIDE_STEP_STRENGTH).normalized;
// 需要根据实际效果调整EVASION_SIDE_STEP_STRENGTH的数值和结合方式。
}
// 7. 确定最终移动目标点
var fleePosition = entity.Position + moveTargetDirection * FLEE_BUFFER_DISTANCE;
// 8. 结合自己的武器类型,决定何时停止紧急移动 (和之前的逻辑类似)
if (isSelfRanged)
{
var selfAttackRange = 0f;
if (selfWeapon != null && selfWeapon.Attributes != null)
{
selfAttackRange = selfWeapon.Attributes.attackRange;
}
var safeDistanceSq = (selfAttackRange * RANGED_SAFE_DISTANCE_MULTIPLIER) *
(selfAttackRange * RANGED_SAFE_DISTANCE_MULTIPLIER);
// 如果目标在“安全距离”内,则实体需要继续远离
if (distanceSq < safeDistanceSq)
{
entity.SetTarget(fleePosition);
entity.TryMove();
}
else
{
// 已经达到足够的安全距离,此时实体可以停止紧急移动。
// FleeJob会继续运行直到timeoutTicks耗尽给予一定的缓冲时间。
entity.SetTarget(entity.Position); // 设定目标为当前位置,使其停止移动
}
}
else // 自己是近战单位或没有武器
{
// 只要目标在“危险距离”内,就一直向外逃跑
if (distanceSq < MELEE_PERIL_DISTANCE_SQUARED)
{
entity.SetTarget(fleePosition);
entity.TryMove();
}
else
{
// 已经拉开足够远的距离,停止紧急逃跑.
// 同样FleeJob会继续运行直到timeoutTicks耗尽。
entity.SetTarget(entity.Position); // 设定目标为当前位置,使其停止移动
}
}
}
override public void StopJob()
{
base.StopJob();
if (entity != null)
{
entity.SetTarget(entity.Position); // 确保实体停止任何正在进行的逃跑移动
}
_cachedHostileTarget = null; // 清除缓存目标以便下次StartJob时重新查找
}
/// <summary>
/// 查找执行实体最近的敌对目标。此方法可能耗时,应避免频繁调用。
/// </summary>
/// <returns>找到的敌对实体如果没有则返回null。</returns>
private Entity.Entity FindNearestHostileTarget()
{
if (entity == null) return null;
return EntityManage.Instance.FindNearestEntityByRelation(
entity.currentDimensionId, // 搜索维度ID
entity.entityPrefab, // 执行实体的Prefab ID用于关系判断
Relation.Hostile)?.entity; // 寻找敌对关系的目标
}
/// <summary>
/// 获取给定向量在X-Z平面上的垂直向量。
/// 假定Y轴是世界向上方向。
/// </summary>
/// <param name="direction">原始方向向量。</param>
/// <returns>在X-Z平面上垂直于原始向量的向量顺时针旋转90度。</returns>
private Vector3 GetPerpendicularVectorXZ(Vector3 direction)
{
// 创建一个只保留XZ分量的向量
var flatDirection = new Vector3(direction.x, 0, direction.z).normalized;
// 顺时针旋转90度 (x,z) => (z,-x)
return new Vector3(flatDirection.z, 0, -flatDirection.x);
}
}
}