fix(physics): search adjacent landblocks for collision objects
GetNearbyObjects now searches the player's landblock plus all 8 neighbors. Previously only searched one landblock, missing objects near landblock boundaries — which includes most trees/rocks since scenery is placed across the full streaming window. Also added diagnostic logging (will strip after verification). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2548146b5
commit
efd6a06c7d
2 changed files with 56 additions and 19 deletions
|
|
@ -109,36 +109,57 @@ public sealed class ShadowObjectRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all objects in cells that a sphere at worldPos with given radius overlaps.
|
/// Get all objects near a world position. Searches the given landblock plus
|
||||||
/// This is the main query used by the Transition system.
|
/// all 8 adjacent landblocks to handle objects near cell/landblock boundaries.
|
||||||
|
/// Within each landblock, queries only the cells the query sphere overlaps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void GetNearbyObjects(Vector3 worldPos, float queryRadius,
|
public void GetNearbyObjects(Vector3 worldPos, float queryRadius,
|
||||||
float worldOffsetX, float worldOffsetY, uint landblockId,
|
float worldOffsetX, float worldOffsetY, uint landblockId,
|
||||||
List<ShadowEntry> results)
|
List<ShadowEntry> results)
|
||||||
{
|
{
|
||||||
results.Clear();
|
results.Clear();
|
||||||
float localX = worldPos.X - worldOffsetX;
|
var seen = new HashSet<uint>();
|
||||||
float localY = worldPos.Y - worldOffsetY;
|
|
||||||
|
|
||||||
int minCx = Math.Max(0, (int)((localX - queryRadius) / 24f));
|
// Extract landblock X/Y from the ID.
|
||||||
int maxCx = Math.Min(7, (int)((localX + queryRadius) / 24f));
|
int lbX = (int)((landblockId >> 24) & 0xFF);
|
||||||
int minCy = Math.Max(0, (int)((localY - queryRadius) / 24f));
|
int lbY = (int)((landblockId >> 16) & 0xFF);
|
||||||
int maxCy = Math.Min(7, (int)((localY + queryRadius) / 24f));
|
|
||||||
|
|
||||||
uint lbPrefix = landblockId & 0xFFFF0000u;
|
// Search the player's landblock and all 8 neighbors.
|
||||||
var seen = new HashSet<uint>(); // avoid duplicates from overlapping cells
|
for (int dx = -1; dx <= 1; dx++)
|
||||||
|
|
||||||
for (int cx = minCx; cx <= maxCx; cx++)
|
|
||||||
{
|
{
|
||||||
for (int cy = minCy; cy <= maxCy; cy++)
|
for (int dy = -1; dy <= 1; dy++)
|
||||||
{
|
{
|
||||||
uint cellId = lbPrefix | (uint)(cx * 8 + cy + 1);
|
int nx = lbX + dx;
|
||||||
if (!_cells.TryGetValue(cellId, out var list)) continue;
|
int ny = lbY + dy;
|
||||||
|
if (nx < 0 || nx > 255 || ny < 0 || ny > 255) continue;
|
||||||
|
|
||||||
foreach (var entry in list)
|
uint neighborLb = ((uint)nx << 24) | ((uint)ny << 16) | 0xFFFFu;
|
||||||
|
uint nbPrefix = neighborLb & 0xFFFF0000u;
|
||||||
|
|
||||||
|
// Compute local position relative to this neighbor landblock.
|
||||||
|
float nbOffX = worldOffsetX + dx * 192f;
|
||||||
|
float nbOffY = worldOffsetY + dy * 192f;
|
||||||
|
float localX = worldPos.X - nbOffX;
|
||||||
|
float localY = worldPos.Y - nbOffY;
|
||||||
|
|
||||||
|
int minCx = Math.Max(0, (int)((localX - queryRadius) / 24f));
|
||||||
|
int maxCx = Math.Min(7, (int)((localX + queryRadius) / 24f));
|
||||||
|
int minCy = Math.Max(0, (int)((localY - queryRadius) / 24f));
|
||||||
|
int maxCy = Math.Min(7, (int)((localY + queryRadius) / 24f));
|
||||||
|
|
||||||
|
for (int cx = minCx; cx <= maxCx; cx++)
|
||||||
{
|
{
|
||||||
if (seen.Add(entry.EntityId))
|
for (int cy = minCy; cy <= maxCy; cy++)
|
||||||
results.Add(entry);
|
{
|
||||||
|
uint cellId = nbPrefix | (uint)(cx * 8 + cy + 1);
|
||||||
|
if (!_cells.TryGetValue(cellId, out var list)) continue;
|
||||||
|
|
||||||
|
foreach (var entry in list)
|
||||||
|
{
|
||||||
|
if (seen.Add(entry.EntityId))
|
||||||
|
results.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -611,6 +611,7 @@ public sealed class Transition
|
||||||
// Reused per-call to avoid per-step allocation; safe because Transition
|
// Reused per-call to avoid per-step allocation; safe because Transition
|
||||||
// is single-threaded per movement resolve.
|
// is single-threaded per movement resolve.
|
||||||
private readonly List<ShadowEntry> _nearbyObjs = new();
|
private readonly List<ShadowEntry> _nearbyObjs = new();
|
||||||
|
private int _debugMissCounter;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Query the ShadowObjectRegistry for nearby static objects and run
|
/// Query the ShadowObjectRegistry for nearby static objects and run
|
||||||
|
|
@ -646,6 +647,9 @@ public sealed class Transition
|
||||||
worldOffsetX, worldOffsetY, landblockId,
|
worldOffsetX, worldOffsetY, landblockId,
|
||||||
_nearbyObjs);
|
_nearbyObjs);
|
||||||
|
|
||||||
|
if (_debugMissCounter % 600 == 0)
|
||||||
|
Console.WriteLine($"QUERY: pos={currPos} lb=0x{landblockId:X8} off=({worldOffsetX:F0},{worldOffsetY:F0}) qr={queryRadius:F1} found={_nearbyObjs.Count} totalReg={engine.ShadowObjects.TotalRegistered}");
|
||||||
|
|
||||||
// Find the EARLIEST collision along the movement path.
|
// Find the EARLIEST collision along the movement path.
|
||||||
float bestT = float.MaxValue;
|
float bestT = float.MaxValue;
|
||||||
Vector3 bestNormal = Vector3.Zero;
|
Vector3 bestNormal = Vector3.Zero;
|
||||||
|
|
@ -750,7 +754,19 @@ public sealed class Transition
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestT >= float.MaxValue)
|
if (bestT >= float.MaxValue)
|
||||||
return TransitionState.OK; // no collision
|
{
|
||||||
|
// Debug: log nearby objects that didn't trigger collision
|
||||||
|
if (_debugMissCounter++ % 120 == 0)
|
||||||
|
{
|
||||||
|
foreach (var obj in _nearbyObjs)
|
||||||
|
{
|
||||||
|
float d = Vector3.Distance(checkPos, obj.Position);
|
||||||
|
if (d < 3f)
|
||||||
|
Console.WriteLine($"NEAR: obj {obj.EntityId:X8} gfx={obj.GfxObjId:X8} type={obj.CollisionType} dist={d:F2} r={obj.Radius:F2} cylH={obj.CylHeight:F2} pos={obj.Position}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TransitionState.OK;
|
||||||
|
}
|
||||||
|
|
||||||
// Already overlapping at the START of the step (bestT == 0 or very small).
|
// Already overlapping at the START of the step (bestT == 0 or very small).
|
||||||
// This happens when the player spawns inside an object or a previous
|
// This happens when the player spawns inside an object or a previous
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue