备份,准备做双端
This commit is contained in:
@@ -771,9 +771,17 @@ public class EffectSystem : MonoBehaviour
|
||||
// - AdjacentAllies -> use AllyCombatant.slotIndex (from source) and teamUIController helpers
|
||||
// - CurrentEnemies -> specificTarget if provided else all objects with tag "Enemy"
|
||||
// - AllEntities -> find all enemies + allies
|
||||
// Reusable gathering buffer for ResolveTargets. Safe to reuse because it never escapes
|
||||
// this method (the deduped result is copied into a fresh `uniq` list that IS returned)
|
||||
// and the gathering phase never re-enters ResolveTargets. The returned list cannot share
|
||||
// this buffer: ApplyEffect's switch can re-enter EffectSystem.ApplyEffect (e.g. via
|
||||
// ModifyHP -> skill triggers), which would clobber a shared return buffer mid-iteration.
|
||||
private readonly List<GameObject> _resolveGatherBuffer = new List<GameObject>(8);
|
||||
|
||||
private List<GameObject> ResolveTargets(Selector selector, GameObject source, GameObject specificTarget)
|
||||
{
|
||||
List<GameObject> list = new List<GameObject>();
|
||||
List<GameObject> list = _resolveGatherBuffer;
|
||||
list.Clear();
|
||||
|
||||
// prefer teamUIController when available for authoritative ally/enemy objects
|
||||
var ui = teamUIController.Instance != null ? teamUIController.Instance : SceneObjectLookupCache.FindAny<teamUIController>();
|
||||
@@ -810,13 +818,13 @@ public class EffectSystem : MonoBehaviour
|
||||
if (go != null && !list.Contains(go)) list.Add(go);
|
||||
}
|
||||
if (source != null)
|
||||
list.RemoveAll(g => g == null || g == source || g.gameObject == source.gameObject);
|
||||
RemoveSelfFromList(list, source);
|
||||
}
|
||||
else
|
||||
{
|
||||
list.AddRange(FindAllAllies());
|
||||
if (source != null)
|
||||
list.RemoveAll(g => g == null || g == source || g.gameObject == source.gameObject);
|
||||
RemoveSelfFromList(list, source);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -910,33 +918,59 @@ public class EffectSystem : MonoBehaviour
|
||||
break;
|
||||
}
|
||||
|
||||
// remove nulls and duplicates
|
||||
list.RemoveAll(x => x == null);
|
||||
var uniq = new List<GameObject>();
|
||||
foreach (var g in list)
|
||||
if (!uniq.Contains(g)) uniq.Add(g);
|
||||
|
||||
// Filter out dead or empty combatants so they won't receive further effects (damage/score/etc.).
|
||||
// This prevents "dead units still get hit / still gain score" edge cases.
|
||||
uniq.RemoveAll(go =>
|
||||
// Build the deduped, alive-only result. `uniq` is a fresh list because it escapes
|
||||
// (ApplyEffect can re-enter EffectSystem, which would clobber a shared buffer). The
|
||||
// filtering is done inline (no RemoveAll closures) to avoid per-call lambda allocs on
|
||||
// this per-effect/per-damage-tick hot path.
|
||||
var uniq = new List<GameObject>(list.Count);
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (go == null) return true;
|
||||
var ally = go.GetComponent<AllyCombatant>();
|
||||
if (ally != null) return ally.IsDead || ally.maxHP == 0; // Skip dead or empty allies
|
||||
var enemy = go.GetComponent<EnemyCombatant>();
|
||||
if (enemy != null) return enemy.IsDead || enemy.currentHP <= 0;
|
||||
return false;
|
||||
});
|
||||
GameObject g = list[i];
|
||||
if (g == null) continue;
|
||||
if (uniq.Contains(g)) continue;
|
||||
// Skip dead or empty combatants so they won't receive further effects
|
||||
// (prevents "dead units still get hit / still gain score" edge cases).
|
||||
var ally = g.GetComponent<AllyCombatant>();
|
||||
if (ally != null)
|
||||
{
|
||||
if (ally.IsDead || ally.maxHP == 0) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var enemy = g.GetComponent<EnemyCombatant>();
|
||||
if (enemy != null && (enemy.IsDead || enemy.currentHP <= 0)) continue;
|
||||
}
|
||||
uniq.Add(g);
|
||||
}
|
||||
|
||||
// Debug: log resolved targets for diagnosis (guarded to avoid log spam in normal gameplay).
|
||||
if (uniq.Count == 0)
|
||||
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} returned 0 targets (source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
|
||||
else
|
||||
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
|
||||
// Debug: log resolved targets for diagnosis. Guarded by the flag directly so the
|
||||
// per-target ConvertAll + string.Join (allocating) never runs in normal gameplay —
|
||||
// ResolveTargets is on the per-effect/per-damage-tick hot path.
|
||||
if (GameConfig.verboseLogs)
|
||||
{
|
||||
if (uniq.Count == 0)
|
||||
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} returned 0 targets (source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
|
||||
else
|
||||
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
|
||||
}
|
||||
|
||||
return uniq;
|
||||
}
|
||||
|
||||
// Allocation-free removal of `source` (and nulls) from a gathered target list, replacing
|
||||
// a per-call closure-capturing RemoveAll on the ResolveTargets hot path.
|
||||
private static void RemoveSelfFromList(List<GameObject> list, GameObject source)
|
||||
{
|
||||
for (int i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
GameObject g = list[i];
|
||||
if (g == null || g == source || g.gameObject == source.gameObject)
|
||||
{
|
||||
list.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<GameObject> FindAllAllies()
|
||||
{
|
||||
if (_cachedAlliesFrame == Time.frameCount) return _cachedAllies;
|
||||
|
||||
Reference in New Issue
Block a user