Files
Cielonos/Assets/Scripts/MainGame/Managers/CombatManager/EnemySubmodule/TargetingSystem.cs
SoulliesOfficial 39b43680a9 爆更
2026-07-18 03:16:20 -04:00

749 lines
29 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;
using System.Collections.Generic;
using Cielonos.MainGame.Characters;
using UnityEngine;
namespace Cielonos.MainGame
{
public partial class CombatManager
{
public partial class EnemySubmodule
{
/// <summary>
/// 索敌评分结果。保留各分项,方便调试面板和后续权重调参。
/// </summary>
public struct TargetingScore
{
public Enemy target;
public float totalScore;
public float baseScore;
public float bonusScore;
public float distanceScore;
public float inputDirScore;
public float cameraFacingScore;
public float stickyScore;
public float lockOnScore;
}
private CharacterBase _lastHitTarget;
private float _lastHitTime;
private readonly TargetingScoreConfig _targetingConfig = new();
/// <summary>
/// 索敌评分配置。这里控制距离、输入方向、镜头朝向、粘滞目标、锁定目标等评分权重。
/// </summary>
public TargetingScoreConfig TargetingConfig => _targetingConfig;
/// <summary>
/// 记录最近命中的目标,用于让连续攻击在短时间内更倾向同一个敌人。
/// </summary>
public void SetLastHitTarget(CharacterBase target)
{
_lastHitTarget = target;
_lastHitTime = Time.time;
}
/// <summary>
/// 创建一条索敌查询链。默认从玩家位置出发,候选范围为全部活跃敌人。
/// </summary>
public TargetingQuery Query(TargetingScorePreset preset = TargetingScorePreset.Default)
{
return new TargetingQuery(this).UsePreset(preset);
}
/// <summary>
/// 创建指定半径内的索敌查询链。
/// </summary>
public TargetingQuery Query(float radius, TargetingScorePreset preset = TargetingScorePreset.Default)
{
return new TargetingQuery(this).WithinRadius(radius).UsePreset(preset);
}
/// <summary>
/// 创建基于已有候选列表的索敌查询链。
/// </summary>
public TargetingQuery Query(List<Enemy> candidates, TargetingScorePreset preset = TargetingScorePreset.Default)
{
return new TargetingQuery(this).FromCandidates(candidates).UsePreset(preset);
}
private List<TargetingScore> ScoreEnemies(List<Enemy> candidates, Vector3 origin, TargetingScoreConfig config = null)
{
if (candidates == null || candidates.Count == 0)
{
return new List<TargetingScore>();
}
config ??= _targetingConfig;
Camera camera = MainGameManager.Player.viewSc.playerCamera;
Vector2 moveInput = MainGameManager.Player.inputSc.Move;
bool hasInput = MainGameManager.Player.inputSc.IsMoving;
// 将移动输入转成世界方向;有输入时,摇杆方向会成为强索敌意图。
Vector3 inputWorldDir = Vector3.zero;
if (hasInput)
{
float cameraYaw = camera.transform.eulerAngles.y;
inputWorldDir = Quaternion.Euler(0f, cameraYaw, 0f) *
new Vector3(moveInput.x, 0f, moveInput.y).normalized;
}
Vector3 cameraForward = camera.transform.forward;
cameraForward.y = 0f;
cameraForward.Normalize();
CharacterBase lockTarget = MainGameManager.Player.viewSc.lockTargetModule.isLocking
? MainGameManager.Player.viewSc.lockTargetModule.lockTarget
: null;
// 最近命中目标会随时间衰减,避免攻击链在多敌人中频繁抖动换目标。
float stickyFactor = 0f;
if (_lastHitTarget != null && !_lastHitTarget.statusSm.isDead)
{
float elapsed = Time.time - _lastHitTime;
stickyFactor = Mathf.Clamp01(1f - elapsed / config.stickyDecayTime);
}
float totalWeight = config.TotalWeight;
if (totalWeight <= 0f)
{
totalWeight = 1f;
}
float maxDist = 0f;
for (int i = 0; i < candidates.Count; i++)
{
Enemy enemy = candidates[i];
if (enemy == null || enemy.statusSm.isDead) continue;
float dist = GetSurfaceDistance(origin, enemy);
if (dist > maxDist)
{
maxDist = dist;
}
}
if (maxDist <= 0f)
{
maxDist = 1f;
}
float inputCosThreshold = Mathf.Cos(config.inputConeHalfAngle * Mathf.Deg2Rad);
float cameraCosThreshold = Mathf.Cos(config.cameraConeHalfAngle * Mathf.Deg2Rad);
List<TargetingScore> results = new List<TargetingScore>(candidates.Count);
for (int i = 0; i < candidates.Count; i++)
{
Enemy enemy = candidates[i];
if (enemy == null || enemy.statusSm.isDead) continue;
Vector3 toEnemy = enemy.transform.position - origin;
toEnemy.y = 0f;
float centerDistance = toEnemy.magnitude;
float distance = GetSurfaceDistance(origin, enemy);
Vector3 toEnemyDir = centerDistance > 0.01f ? toEnemy / centerDistance : Vector3.zero;
TargetingScore score = new TargetingScore { target = enemy };
// 1. 距离分:越近越高。
score.distanceScore = 1f - Mathf.Clamp01(distance / maxDist);
// 2. 输入方向分:有移动输入时,优先选玩家指向的敌人;无输入时不惩罚任何目标。
if (hasInput && toEnemyDir.sqrMagnitude > 0f)
{
float dot = Vector3.Dot(inputWorldDir, toEnemyDir);
score.inputDirScore = dot >= inputCosThreshold
? Mathf.InverseLerp(inputCosThreshold, 1f, dot)
: 0f;
}
else
{
score.inputDirScore = 1f;
}
// 3. 镜头朝向分:用于辅助无输入或弱输入时的自然索敌。
if (cameraForward.sqrMagnitude > 0f && toEnemyDir.sqrMagnitude > 0f)
{
float dot = Vector3.Dot(cameraForward, toEnemyDir);
score.cameraFacingScore = dot >= cameraCosThreshold
? Mathf.InverseLerp(cameraCosThreshold, 1f, dot)
: 0f;
}
else
{
score.cameraFacingScore = 0f;
}
// 4. 粘滞分:短时间内维持攻击目标,减少目标跳变。
score.stickyScore = enemy == _lastHitTarget ? stickyFactor : 0f;
// 5. 锁定分:锁定目标在常规评分中获得额外倾向,但不会强制覆盖所有查询。
score.lockOnScore = lockTarget != null && enemy == lockTarget ? 1f : 0f;
score.baseScore =
(config.distanceWeight * score.distanceScore
+ config.inputDirectionWeight * score.inputDirScore
+ config.cameraFacingWeight * score.cameraFacingScore
+ config.stickyWeight * score.stickyScore
+ config.lockOnWeight * score.lockOnScore)
/ totalWeight;
score.bonusScore = 0f;
score.totalScore = score.baseScore;
results.Add(score);
}
results.Sort((a, b) => b.totalScore.CompareTo(a.totalScore));
return results;
}
private Enemy GetValidLockonTarget(Vector3 origin, float radius, List<Enemy> candidates)
{
var lockModule = MainGameManager.Player.viewSc.lockTargetModule;
if (!lockModule.isLocking || lockModule.lockTarget == null)
{
return null;
}
Enemy lockTarget = lockModule.lockTarget;
if (candidates != null && !candidates.Contains(lockTarget))
{
return null;
}
return IsEnemyInRadius(lockTarget, origin, radius) ? lockTarget : null;
}
/// <summary>
/// 链式索敌查询。调用侧应尽量通过这套 API 表达索敌意图:
/// CombatManager.EnemySm.Query(4f).PreferDisruptable().Best();
/// Query 会缓存候选列表和评分结果,仅建议在一次决策中临时使用,不要跨帧保存为字段。
/// </summary>
public sealed class TargetingQuery
{
private readonly EnemySubmodule _enemySm;
private readonly List<Predicate<Enemy>> _filters = new List<Predicate<Enemy>>();
private readonly List<ScoreModifier> _scoreModifiers = new List<ScoreModifier>();
private readonly List<Enemy> _excludedTargets = new List<Enemy>();
private Transform _originTransform;
private Vector3 _originPosition;
private bool _hasOriginPosition;
private float _radius = float.MaxValue;
private SortingType _sortingType = SortingType.Nearest;
private TargetingScoreConfig _scoreConfig;
private List<Enemy> _sourceCandidates;
private List<Enemy> _cachedCandidates;
private List<TargetingScore> _cachedScores;
private Vector3 _cachedOrigin;
private bool _hasCachedOrigin;
private int _cachedFrame = -1;
private int _cachedTargetingVersion = -1;
public TargetingQuery(EnemySubmodule enemySm)
{
_enemySm = enemySm;
}
/// <summary>
/// 设置索敌原点为某个 Transform。评分和半径都会使用它的当前位置。
/// </summary>
public TargetingQuery From(Transform origin)
{
_originTransform = origin;
_hasOriginPosition = false;
Invalidate();
return this;
}
/// <summary>
/// 设置索敌原点为固定世界坐标,适合爆炸、弹体、攻击区域等非角色来源。
/// </summary>
public TargetingQuery From(Vector3 origin)
{
_originPosition = origin;
_originTransform = null;
_hasOriginPosition = true;
Invalidate();
return this;
}
/// <summary>
/// 限制候选目标半径。
/// </summary>
public TargetingQuery WithinRadius(float radius)
{
_radius = radius;
Invalidate();
return this;
}
/// <summary>
/// 使用外部已经筛好的候选列表继续索敌。
/// </summary>
public TargetingQuery FromCandidates(List<Enemy> candidates)
{
_sourceCandidates = candidates != null ? new List<Enemy>(candidates) : null;
Invalidate();
return this;
}
/// <summary>
/// 设置候选列表的基础排序,主要用于 Candidates/Nearest。
/// </summary>
public TargetingQuery SortBy(SortingType sortingType)
{
_sortingType = sortingType;
Invalidate();
return this;
}
public TargetingQuery UsePreset(TargetingScorePreset preset)
{
_scoreConfig = TargetingScoreConfig.CreatePreset(preset);
InvalidateScores();
return this;
}
/// <summary>
/// 从候选目标中排除单个敌人。
/// </summary>
public TargetingQuery Exclude(Enemy target)
{
if (target != null && !_excludedTargets.Contains(target))
{
_excludedTargets.Add(target);
Invalidate();
}
return this;
}
/// <summary>
/// 从候选目标中排除一组敌人。
/// </summary>
public TargetingQuery Exclude(IEnumerable<Enemy> targets)
{
if (targets == null)
{
return this;
}
foreach (Enemy target in targets)
{
if (target != null && !_excludedTargets.Contains(target))
{
_excludedTargets.Add(target);
}
}
Invalidate();
return this;
}
/// <summary>
/// 硬过滤候选目标。不符合条件的敌人会直接从 Candidates/Scores/Best 中移除。
/// </summary>
public TargetingQuery Where(Predicate<Enemy> match)
{
if (match == null)
{
return this;
}
_filters.Add(match);
Invalidate();
return this;
}
/// <summary>
/// 只保留存活敌人。默认候选列表已做该过滤,显式调用可增强复杂查询的语义。
/// </summary>
public TargetingQuery OnlyAlive()
{
return Where(enemy => enemy != null && !enemy.statusSm.isDead);
}
/// <summary>
/// 只保留屏幕内可见的敌人。
/// </summary>
public TargetingQuery OnlyInScreen()
{
return Where(IsInScreen);
}
/// <summary>
/// 只保留当前索敌系统认为可见的敌人。目前等同于屏幕内目标,后续可扩展遮挡判定。
/// </summary>
public TargetingQuery OnlyVisible()
{
return OnlyInScreen();
}
/// <summary>
/// 只保留可被指定 Breakthrough 类型打断的敌人。
/// </summary>
public TargetingQuery OnlyDisruptable(Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption)
{
return Where(enemy => _enemySm.IsDisruptable(enemy, breakthroughType));
}
/// <summary>
/// 对符合条件的目标施加额外评分。offset 是固定加分amplifier 是按基础分放大。
/// </summary>
public TargetingQuery Prefer(Predicate<Enemy> match, float offset = 1f, float amplifier = 0f)
{
if (match == null)
{
return this;
}
_scoreModifiers.Add(new ScoreModifier(match, amplifier, offset));
InvalidateScores();
return this;
}
/// <summary>
/// 提高可被 Breakthrough 打断目标的优先级。
/// </summary>
public TargetingQuery PreferDisruptable(float offset = 1f,
Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption, float amplifier = 0f)
{
return Prefer(enemy => _enemySm.IsDisruptable(enemy, breakthroughType), offset, amplifier);
}
/// <summary>
/// 锁定目标优先的终端查询:如果锁定目标在本次候选范围内则直接返回,否则返回 Best()。
/// </summary>
public Enemy LockonFirst()
{
Enemy lockTarget = _enemySm.GetValidLockonTarget(Origin, _radius, ResolveCandidates());
return lockTarget != null ? lockTarget : Best();
}
/// <summary>
/// 返回当前查询的候选敌人列表。列表按 SortBy 指定规则排序,默认最近优先。
/// </summary>
public List<Enemy> Candidates()
{
return new List<Enemy>(ResolveCandidates());
}
/// <summary>
/// 返回完整评分结果,按 totalScore 从高到低排序。
/// </summary>
public List<TargetingScore> Scores()
{
return new List<TargetingScore>(ResolveScores());
}
/// <summary>
/// 返回综合评分最高的敌人。
/// </summary>
public Enemy Best()
{
return ResolveScores().BestEnemy();
}
/// <summary>
/// 返回综合评分最高的前 count 个敌人。
/// </summary>
public List<Enemy> Best(int count)
{
return ResolveScores().BestEnemies(count);
}
/// <summary>
/// 返回距离原点最近的敌人,不进行综合评分。
/// </summary>
public Enemy Nearest()
{
List<Enemy> candidates = ResolveCandidates();
return candidates.Count > 0 ? candidates[0] : null;
}
/// <summary>
/// 返回距离原点最近的前 count 个敌人,不进行综合评分。
/// </summary>
public List<Enemy> Nearest(int count)
{
List<Enemy> candidates = ResolveCandidates();
List<Enemy> result = new List<Enemy>(Mathf.Min(count, candidates.Count));
for (int i = 0; i < candidates.Count && i < count; i++)
{
result.Add(candidates[i]);
}
return result;
}
/// <summary>
/// 当前候选列表中是否存在可被 Breakthrough 打断的敌人。
/// </summary>
public bool AnyDisruptable(Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption)
{
List<Enemy> candidates = ResolveCandidates();
for (int i = 0; i < candidates.Count; i++)
{
if (_enemySm.IsDisruptable(candidates[i], breakthroughType))
{
return true;
}
}
return false;
}
private Vector3 Origin
{
get
{
if (_hasOriginPosition)
{
return _originPosition;
}
return _originTransform != null ? _originTransform.position : MainGameManager.Player.transform.position;
}
}
private List<Enemy> ResolveCandidates()
{
InvalidateIfStale();
if (_cachedCandidates != null)
{
return _cachedCandidates;
}
Vector3 origin = Origin;
List<Enemy> candidates = _sourceCandidates == null
? _enemySm.CollectEnemiesInRadius(origin, _radius, _sortingType)
: FilterSourceCandidates(origin);
if (_excludedTargets.Count > 0)
{
for (int i = candidates.Count - 1; i >= 0; i--)
{
if (_excludedTargets.Contains(candidates[i]))
{
candidates.RemoveAt(i);
}
}
}
ApplyFilters(candidates);
SortCandidates(candidates, origin);
_cachedCandidates = candidates;
_cachedOrigin = origin;
_hasCachedOrigin = true;
_cachedFrame = Time.frameCount;
_cachedTargetingVersion = _enemySm.TargetingVersion;
return _cachedCandidates;
}
private List<TargetingScore> ResolveScores()
{
InvalidateIfStale();
if (_cachedScores != null)
{
return _cachedScores;
}
List<TargetingScore> scores = _enemySm.ScoreEnemies(ResolveCandidates(), Origin, _scoreConfig);
foreach (ScoreModifier modifier in _scoreModifiers)
{
scores.ApplyScoreModifier(modifier.match, modifier.amplifier, modifier.offset);
}
_cachedScores = scores;
_cachedOrigin = Origin;
_hasCachedOrigin = true;
_cachedFrame = Time.frameCount;
_cachedTargetingVersion = _enemySm.TargetingVersion;
return _cachedScores;
}
private List<Enemy> FilterSourceCandidates(Vector3 origin)
{
List<Enemy> candidates = new List<Enemy>(_sourceCandidates.Count);
for (int i = 0; i < _sourceCandidates.Count; i++)
{
Enemy enemy = _sourceCandidates[i];
if (enemy == null || enemy.statusSm.isDead)
{
continue;
}
if (_radius < float.MaxValue && !_enemySm.IsEnemyInRadius(enemy, origin, _radius))
{
continue;
}
candidates.Add(enemy);
}
return candidates;
}
private void ApplyFilters(List<Enemy> candidates)
{
if (_filters.Count == 0)
{
return;
}
for (int i = candidates.Count - 1; i >= 0; i--)
{
Enemy enemy = candidates[i];
for (int j = 0; j < _filters.Count; j++)
{
if (!_filters[j](enemy))
{
candidates.RemoveAt(i);
break;
}
}
}
}
private void SortCandidates(List<Enemy> candidates, Vector3 origin)
{
if (_sortingType == SortingType.Nearest)
{
candidates.Sort((a, b) =>
_enemySm.GetSurfaceDistance(origin, a)
.CompareTo(_enemySm.GetSurfaceDistance(origin, b)));
}
else if (_sortingType == SortingType.Farthest)
{
candidates.Sort((a, b) =>
_enemySm.GetSurfaceDistance(origin, b)
.CompareTo(_enemySm.GetSurfaceDistance(origin, a)));
}
}
private static bool IsInScreen(Enemy enemy)
{
if (enemy == null || enemy.statusSm.isDead)
{
return false;
}
Vector3 screenPos = MainGameManager.Player.viewSc.playerCamera.WorldToScreenPoint(enemy.CenterPoint.position);
return screenPos.z > 0f &&
screenPos.x > 0f && screenPos.x < Screen.width &&
screenPos.y > 0f && screenPos.y < Screen.height;
}
private void Invalidate()
{
_cachedCandidates = null;
_cachedScores = null;
_hasCachedOrigin = false;
_cachedFrame = -1;
_cachedTargetingVersion = -1;
}
private void InvalidateScores()
{
_cachedScores = null;
}
private void InvalidateIfStale()
{
if (_cachedFrame < 0)
{
return;
}
if (_cachedFrame == Time.frameCount &&
_cachedTargetingVersion == _enemySm.TargetingVersion &&
(!_hasCachedOrigin || _cachedOrigin == Origin))
{
return;
}
_cachedCandidates = null;
_cachedScores = null;
_hasCachedOrigin = false;
_cachedFrame = -1;
_cachedTargetingVersion = -1;
}
private readonly struct ScoreModifier
{
public readonly Predicate<Enemy> match;
public readonly float amplifier;
public readonly float offset;
public ScoreModifier(Predicate<Enemy> match, float amplifier, float offset)
{
this.match = match;
this.amplifier = amplifier;
this.offset = offset;
}
}
}
}
}
public static class TargetingSystemExtensions
{
public static List<CombatManager.EnemySubmodule.TargetingScore> ApplyScoreModifier(
this List<CombatManager.EnemySubmodule.TargetingScore> scores,
List<Enemy> boostTargets, float amplifier, float offset = 0)
{
return scores.ApplyScoreModifier(Predicate, amplifier, offset);
bool Predicate(Enemy target)
{
return boostTargets != null && boostTargets.Contains(target);
}
}
public static List<CombatManager.EnemySubmodule.TargetingScore> ApplyScoreModifier(
this List<CombatManager.EnemySubmodule.TargetingScore> scores,
Predicate<Enemy> match, float amplifier, float offset = 0)
{
bool changed = false;
for (int i = 0; i < scores.Count; i++)
{
if (!match(scores[i].target)) continue;
CombatManager.EnemySubmodule.TargetingScore s = scores[i];
s.bonusScore += s.baseScore * amplifier + offset;
s.totalScore = s.baseScore + s.bonusScore;
scores[i] = s;
changed = true;
}
if (changed)
{
scores.Sort((a, b) => b.totalScore.CompareTo(a.totalScore));
}
return scores;
}
public static Enemy BestEnemy(this List<CombatManager.EnemySubmodule.TargetingScore> scores)
{
return scores.Count > 0 ? scores[0].target : null;
}
public static List<Enemy> BestEnemies(this List<CombatManager.EnemySubmodule.TargetingScore> scores, int count)
{
List<Enemy> result = new List<Enemy>(Mathf.Min(count, scores.Count));
for (int i = 0; i < scores.Count && i < count; i++)
{
result.Add(scores[i].target);
}
return result;
}
}
}