1. 修复投射物闪避口径(使用 Value,与全局伤害逻辑一致)
  - 将 SimulationWorld 内投射物伤害计算改为直接复用 AIUtility.CalcDamageHP(...)。
  - 删除了 SimulationWorld 里那套重复且使用 dodgeStat.Percent 的私有计算函数,避免再次分叉。
2. 修复目标选择索引“重建过于激进”
  - 去掉了 Job Tick 末尾的“每帧强制 MarkDirty + Build”。
  - 改为在 Job 输出回写时,只有敌人 XZ 坐标发生变化才标记 dirty。
  - 空间索引构建条件改为“仅 dirty 时重建”,不再因跨帧自动重建。
This commit is contained in:
SepComet 2026-02-23 11:21:35 +08:00
parent 1a45b513f2
commit 688fefe848
5 changed files with 17 additions and 62 deletions

View File

@ -226,9 +226,6 @@ namespace Simulation
RecycleInactiveProjectiles(); RecycleInactiveProjectiles();
} }
} }
MarkEnemyTargetSpatialIndexDirty();
BuildEnemyTargetSpatialIndexIfNeeded();
} }
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context) private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)

View File

@ -619,9 +619,23 @@ namespace Simulation
private void ApplyJobOutputToSimulation() private void ApplyJobOutputToSimulation()
{ {
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length); int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
bool hasEnemyPositionChanged = false;
for (int i = 0; i < enemyCount; i++) for (int i = 0; i < enemyCount; i++)
{ {
_enemies[i] = ConvertToEnemySimData(_enemyJobOutputs[i]); EnemyJobOutputData output = _enemyJobOutputs[i];
if (!hasEnemyPositionChanged)
{
Vector3 currentPosition = _enemies[i].Position;
hasEnemyPositionChanged = currentPosition.x != output.Position.x ||
currentPosition.z != output.Position.z;
}
_enemies[i] = ConvertToEnemySimData(output);
}
if (hasEnemyPositionChanged)
{
MarkEnemyTargetSpatialIndexDirty();
} }
int projectileCount = Mathf.Min(_projectiles.Count, _projectileJobOutputs.Length); int projectileCount = Mathf.Min(_projectiles.Count, _projectileJobOutputs.Length);

View File

@ -453,7 +453,7 @@ namespace Simulation
return false; return false;
} }
damage = CalculateProjectileDamage(sourceImpact.AttackBase, sourceImpact.AttackStat, damage = AIUtility.CalcDamageHP(sourceImpact.AttackBase, sourceImpact.AttackStat,
targetImpact.DefenseStat, targetImpact.DefenseStat,
targetImpact.DodgeStat); targetImpact.DodgeStat);
shouldDispatchPresentation = true; shouldDispatchPresentation = true;
@ -537,36 +537,6 @@ namespace Simulation
return false; return false;
} }
private static int CalculateProjectileDamage(int attack, StatProperty attackStat, StatProperty defenseStat,
StatProperty dodgeStat)
{
if (dodgeStat != null)
{
if (UnityEngine.Random.value < Mathf.Clamp(dodgeStat.Percent, 0f, 0.9f))
{
return 0;
}
}
float damage = attack;
if (attackStat != null)
{
damage = (attack + attackStat.Value) * attackStat.Percent;
}
if (defenseStat != null)
{
damage = (damage - defenseStat.Value) / defenseStat.Percent;
}
if (damage < 1f)
{
return 1;
}
return Mathf.CeilToInt(damage);
}
private static bool TryGetTargetableEntity(int entityId, out TargetableObject target) private static bool TryGetTargetableEntity(int entityId, out TargetableObject target)
{ {
target = null; target = null;

View File

@ -8,7 +8,6 @@ namespace Simulation
{ {
private NativeParallelMultiHashMap<long, int> _enemyTargetBuckets; private NativeParallelMultiHashMap<long, int> _enemyTargetBuckets;
private bool _enemyTargetBucketsDirty = true; private bool _enemyTargetBucketsDirty = true;
private int _enemyTargetBucketsFrame = -1;
[SerializeField] private float _targetSelectionCellSize = 2f; [SerializeField] private float _targetSelectionCellSize = 2f;
@ -85,7 +84,6 @@ namespace Simulation
_enemyTargetBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent); _enemyTargetBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
_enemyTargetBucketsDirty = true; _enemyTargetBucketsDirty = true;
_enemyTargetBucketsFrame = -1;
} }
private void DisposeEnemyTargetSpatialIndex() private void DisposeEnemyTargetSpatialIndex()
@ -97,7 +95,6 @@ namespace Simulation
_enemyTargetBuckets = default; _enemyTargetBuckets = default;
_enemyTargetBucketsDirty = true; _enemyTargetBucketsDirty = true;
_enemyTargetBucketsFrame = -1;
} }
private void ClearEnemyTargetSpatialIndex() private void ClearEnemyTargetSpatialIndex()
@ -108,20 +105,18 @@ namespace Simulation
} }
_enemyTargetBucketsDirty = true; _enemyTargetBucketsDirty = true;
_enemyTargetBucketsFrame = -1;
} }
private void MarkEnemyTargetSpatialIndexDirty() private void MarkEnemyTargetSpatialIndexDirty()
{ {
_enemyTargetBucketsDirty = true; _enemyTargetBucketsDirty = true;
_enemyTargetBucketsFrame = -1;
} }
private void BuildEnemyTargetSpatialIndexIfNeeded() private void BuildEnemyTargetSpatialIndexIfNeeded()
{ {
InitializeEnemyTargetSpatialIndex(); InitializeEnemyTargetSpatialIndex();
if (!_enemyTargetBucketsDirty && _enemyTargetBucketsFrame == Time.frameCount) if (!_enemyTargetBucketsDirty)
{ {
return; return;
} }
@ -148,7 +143,6 @@ namespace Simulation
} }
_enemyTargetBucketsDirty = false; _enemyTargetBucketsDirty = false;
_enemyTargetBucketsFrame = Time.frameCount;
} }
private float GetTargetSelectionCellSize() private float GetTargetSelectionCellSize()

View File

@ -1,20 +0,0 @@
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours