using System; using System.Collections.Generic; using Cielonos.MainGame.Characters; using UnityEngine; namespace Cielonos.MainGame { public partial class CombatManager { public partial class EnemySubmodule { /// /// 索敌评分结果。保留各分项,方便调试面板和后续权重调参。 /// 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(); /// /// 索敌评分配置。这里控制距离、输入方向、镜头朝向、粘滞目标、锁定目标等评分权重。 /// public TargetingScoreConfig TargetingConfig => _targetingConfig; /// /// 记录最近命中的目标,用于让连续攻击在短时间内更倾向同一个敌人。 /// public void SetLastHitTarget(CharacterBase target) { _lastHitTarget = target; _lastHitTime = Time.time; } /// /// 创建一条索敌查询链。默认从玩家位置出发,候选范围为全部活跃敌人。 /// public TargetingQuery Query(TargetingScorePreset preset = TargetingScorePreset.Default) { return new TargetingQuery(this).UsePreset(preset); } /// /// 创建指定半径内的索敌查询链。 /// public TargetingQuery Query(float radius, TargetingScorePreset preset = TargetingScorePreset.Default) { return new TargetingQuery(this).WithinRadius(radius).UsePreset(preset); } /// /// 创建基于已有候选列表的索敌查询链。 /// public TargetingQuery Query(List candidates, TargetingScorePreset preset = TargetingScorePreset.Default) { return new TargetingQuery(this).FromCandidates(candidates).UsePreset(preset); } private List ScoreEnemies(List candidates, Vector3 origin, TargetingScoreConfig config = null) { if (candidates == null || candidates.Count == 0) { return new List(); } 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 results = new List(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 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; } /// /// 链式索敌查询。调用侧应尽量通过这套 API 表达索敌意图: /// CombatManager.EnemySm.Query(4f).PreferDisruptable().Best(); /// Query 会缓存候选列表和评分结果,仅建议在一次决策中临时使用,不要跨帧保存为字段。 /// public sealed class TargetingQuery { private readonly EnemySubmodule _enemySm; private readonly List> _filters = new List>(); private readonly List _scoreModifiers = new List(); private readonly List _excludedTargets = new List(); private Transform _originTransform; private Vector3 _originPosition; private bool _hasOriginPosition; private float _radius = float.MaxValue; private SortingType _sortingType = SortingType.Nearest; private TargetingScoreConfig _scoreConfig; private List _sourceCandidates; private List _cachedCandidates; private List _cachedScores; private Vector3 _cachedOrigin; private bool _hasCachedOrigin; private int _cachedFrame = -1; private int _cachedTargetingVersion = -1; public TargetingQuery(EnemySubmodule enemySm) { _enemySm = enemySm; } /// /// 设置索敌原点为某个 Transform。评分和半径都会使用它的当前位置。 /// public TargetingQuery From(Transform origin) { _originTransform = origin; _hasOriginPosition = false; Invalidate(); return this; } /// /// 设置索敌原点为固定世界坐标,适合爆炸、弹体、攻击区域等非角色来源。 /// public TargetingQuery From(Vector3 origin) { _originPosition = origin; _originTransform = null; _hasOriginPosition = true; Invalidate(); return this; } /// /// 限制候选目标半径。 /// public TargetingQuery WithinRadius(float radius) { _radius = radius; Invalidate(); return this; } /// /// 使用外部已经筛好的候选列表继续索敌。 /// public TargetingQuery FromCandidates(List candidates) { _sourceCandidates = candidates != null ? new List(candidates) : null; Invalidate(); return this; } /// /// 设置候选列表的基础排序,主要用于 Candidates/Nearest。 /// public TargetingQuery SortBy(SortingType sortingType) { _sortingType = sortingType; Invalidate(); return this; } public TargetingQuery UsePreset(TargetingScorePreset preset) { _scoreConfig = TargetingScoreConfig.CreatePreset(preset); InvalidateScores(); return this; } /// /// 从候选目标中排除单个敌人。 /// public TargetingQuery Exclude(Enemy target) { if (target != null && !_excludedTargets.Contains(target)) { _excludedTargets.Add(target); Invalidate(); } return this; } /// /// 从候选目标中排除一组敌人。 /// public TargetingQuery Exclude(IEnumerable targets) { if (targets == null) { return this; } foreach (Enemy target in targets) { if (target != null && !_excludedTargets.Contains(target)) { _excludedTargets.Add(target); } } Invalidate(); return this; } /// /// 硬过滤候选目标。不符合条件的敌人会直接从 Candidates/Scores/Best 中移除。 /// public TargetingQuery Where(Predicate match) { if (match == null) { return this; } _filters.Add(match); Invalidate(); return this; } /// /// 只保留存活敌人。默认候选列表已做该过滤,显式调用可增强复杂查询的语义。 /// public TargetingQuery OnlyAlive() { return Where(enemy => enemy != null && !enemy.statusSm.isDead); } /// /// 只保留屏幕内可见的敌人。 /// public TargetingQuery OnlyInScreen() { return Where(IsInScreen); } /// /// 只保留当前索敌系统认为可见的敌人。目前等同于屏幕内目标,后续可扩展遮挡判定。 /// public TargetingQuery OnlyVisible() { return OnlyInScreen(); } /// /// 只保留可被指定 Breakthrough 类型打断的敌人。 /// public TargetingQuery OnlyDisruptable(Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption) { return Where(enemy => _enemySm.IsDisruptable(enemy, breakthroughType)); } /// /// 对符合条件的目标施加额外评分。offset 是固定加分,amplifier 是按基础分放大。 /// public TargetingQuery Prefer(Predicate match, float offset = 1f, float amplifier = 0f) { if (match == null) { return this; } _scoreModifiers.Add(new ScoreModifier(match, amplifier, offset)); InvalidateScores(); return this; } /// /// 提高可被 Breakthrough 打断目标的优先级。 /// public TargetingQuery PreferDisruptable(float offset = 1f, Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption, float amplifier = 0f) { return Prefer(enemy => _enemySm.IsDisruptable(enemy, breakthroughType), offset, amplifier); } /// /// 锁定目标优先的终端查询:如果锁定目标在本次候选范围内则直接返回,否则返回 Best()。 /// public Enemy LockonFirst() { Enemy lockTarget = _enemySm.GetValidLockonTarget(Origin, _radius, ResolveCandidates()); return lockTarget != null ? lockTarget : Best(); } /// /// 返回当前查询的候选敌人列表。列表按 SortBy 指定规则排序,默认最近优先。 /// public List Candidates() { return new List(ResolveCandidates()); } /// /// 返回完整评分结果,按 totalScore 从高到低排序。 /// public List Scores() { return new List(ResolveScores()); } /// /// 返回综合评分最高的敌人。 /// public Enemy Best() { return ResolveScores().BestEnemy(); } /// /// 返回综合评分最高的前 count 个敌人。 /// public List Best(int count) { return ResolveScores().BestEnemies(count); } /// /// 返回距离原点最近的敌人,不进行综合评分。 /// public Enemy Nearest() { List candidates = ResolveCandidates(); return candidates.Count > 0 ? candidates[0] : null; } /// /// 返回距离原点最近的前 count 个敌人,不进行综合评分。 /// public List Nearest(int count) { List candidates = ResolveCandidates(); List result = new List(Mathf.Min(count, candidates.Count)); for (int i = 0; i < candidates.Count && i < count; i++) { result.Add(candidates[i]); } return result; } /// /// 当前候选列表中是否存在可被 Breakthrough 打断的敌人。 /// public bool AnyDisruptable(Breakthrough.Type breakthroughType = Breakthrough.Type.Disruption) { List 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 ResolveCandidates() { InvalidateIfStale(); if (_cachedCandidates != null) { return _cachedCandidates; } Vector3 origin = Origin; List 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 ResolveScores() { InvalidateIfStale(); if (_cachedScores != null) { return _cachedScores; } List 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 FilterSourceCandidates(Vector3 origin) { List candidates = new List(_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 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 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 match; public readonly float amplifier; public readonly float offset; public ScoreModifier(Predicate match, float amplifier, float offset) { this.match = match; this.amplifier = amplifier; this.offset = offset; } } } } } public static class TargetingSystemExtensions { public static List ApplyScoreModifier( this List scores, List 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 ApplyScoreModifier( this List scores, Predicate 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 scores) { return scores.Count > 0 ? scores[0].target : null; } public static List BestEnemies(this List scores, int count) { List result = new List(Mathf.Min(count, scores.Count)); for (int i = 0; i < scores.Count && i < count; i++) { result.Add(scores[i].target); } return result; } } }