备份,准备做双端

This commit is contained in:
FloatGaming
2026-07-30 23:15:58 +08:00
parent add675e45d
commit ec4ec53545
88 changed files with 4050 additions and 872 deletions
+58 -24
View File
@@ -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;