feat(physics): cell-based ShadowObject collision
Register static entities into terrain cells during streaming. Transition system queries nearby objects and runs BSP collision. Player can no longer walk through trees and buildings. - ShadowObjectRegistry: 24m×24m cell index, Register/Deregister/ RemoveLandblock/GetNearbyObjects matching retail AC's approach - PhysicsEngine: ShadowObjects property + DataCache wiring point; RemoveLandblock now also clears shadow objects; TryGetLandblockContext helper lets Transition resolve landblock id+offset for a world pos - Transition.FindObjCollisions: queries registry, broad-phase sphere test, narrow-phase BSPQuery.SphereIntersectsPoly in object-local space, returns Slid on hit to redirect movement along the surface - GameWindow.ApplyLoadedTerrainLocked: registers each static entity after physics BSP data is cached; selects radius from BSP bounding sphere or Setup.Radius; wires PhysicsDataCache into engine on OnLoad - 16 new ShadowObjectRegistry unit tests, all 361 tests green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
246713e2cc
commit
e2f0c8580e
5 changed files with 541 additions and 2 deletions
180
tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs
Normal file
180
tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ShadowObjectRegistry"/> — Task 7 cell-based
|
||||
/// spatial index for object collision.
|
||||
/// </summary>
|
||||
public class ShadowObjectRegistryTests
|
||||
{
|
||||
private const uint LbId = 0xA9B40000u; // landblock prefix used throughout
|
||||
private const float OffX = 0f;
|
||||
private const float OffY = 0f;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Register / TotalRegistered
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Register_SingleEntity_TotalRegisteredIsOne()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
||||
Assert.Equal(1, reg.TotalRegistered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_SameEntityTwice_NoDuplicate()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
||||
reg.Register(1u, 0x01000001u, new Vector3(13f, 12f, 50f), 1f, OffX, OffY, LbId); // re-register (position update)
|
||||
Assert.Equal(1, reg.TotalRegistered);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GetObjectsInCell
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetObjectsInCell_EntityInCenter_ReturnedInExpectedCell()
|
||||
{
|
||||
// Entity at local (12, 12) = cell (0,0) = cellId prefix | 1.
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(42u, 0x01000002u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
||||
|
||||
uint cellId = LbId | 1u; // cx=0, cy=0 → 0*8+0+1 = 1
|
||||
var objs = reg.GetObjectsInCell(cellId);
|
||||
Assert.Single(objs);
|
||||
Assert.Equal(42u, objs[0].EntityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetObjectsInCell_EntitySpanning2Cells_RegisteredInBoth()
|
||||
{
|
||||
// Entity at local (24, 12) with radius=2 spans cells cx=0 and cx=1 in X.
|
||||
// Cell 0,0 = prefix|1; cell 1,0 = prefix|(1*8+0+1)=prefix|9.
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(7u, 0x01000003u, new Vector3(24f, 12f, 50f), 2f, OffX, OffY, LbId);
|
||||
|
||||
uint cell00 = LbId | 1u; // cx=0, cy=0
|
||||
uint cell10 = LbId | 9u; // cx=1, cy=0
|
||||
|
||||
Assert.Contains(reg.GetObjectsInCell(cell00), e => e.EntityId == 7u);
|
||||
Assert.Contains(reg.GetObjectsInCell(cell10), e => e.EntityId == 7u);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Deregister
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Deregister_RemovesFromAllCells()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(5u, 0x01000004u, new Vector3(24f, 12f, 50f), 2f, OffX, OffY, LbId);
|
||||
reg.Deregister(5u);
|
||||
|
||||
Assert.Equal(0, reg.TotalRegistered);
|
||||
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
|
||||
Assert.Empty(reg.GetObjectsInCell(LbId | 9u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deregister_NonexistentEntity_NoThrow()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
// Should not throw.
|
||||
reg.Deregister(999u);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// RemoveLandblock
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_ClearsAllEntitiesForThatBlock()
|
||||
{
|
||||
const uint otherLb = 0xAAAA0000u;
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
||||
// Entity 2 lives in otherLb whose world origin is at X=192. Place it at
|
||||
// world X=204 so localX = 204-192 = 12, which maps to cell (0,0) of otherLb.
|
||||
reg.Register(2u, 0x01000002u, new Vector3(204f, 12f, 50f), 1f, 192f, 0f, otherLb);
|
||||
|
||||
reg.RemoveLandblock(LbId);
|
||||
|
||||
Assert.Equal(1, reg.TotalRegistered); // entity 2 (otherLb) survives
|
||||
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GetNearbyObjects
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void GetNearbyObjects_QueryCoversEntity_ReturnsIt()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(10u, 0x01000005u, new Vector3(30f, 30f, 50f), 1f, OffX, OffY, LbId);
|
||||
|
||||
var results = new List<ShadowEntry>();
|
||||
reg.GetNearbyObjects(new Vector3(30f, 30f, 50f), 5f, OffX, OffY, LbId, results);
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Equal(10u, results[0].EntityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNearbyObjects_QueryFarFromEntity_ReturnsEmpty()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
// Entity at local (12, 12) — cell (0,0).
|
||||
reg.Register(11u, 0x01000006u, new Vector3(12f, 12f, 50f), 1f, OffX, OffY, LbId);
|
||||
|
||||
var results = new List<ShadowEntry>();
|
||||
// Query at local (180, 180) — cell (7,7) — far away.
|
||||
reg.GetNearbyObjects(new Vector3(180f, 180f, 50f), 5f, OffX, OffY, LbId, results);
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNearbyObjects_EntityInMultipleCells_ReturnedOnce()
|
||||
{
|
||||
// Entity spans cells 0,0 and 1,0 (local X≈24, radius=2).
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(20u, 0x01000007u, new Vector3(24f, 12f, 50f), 2f, OffX, OffY, LbId);
|
||||
|
||||
var results = new List<ShadowEntry>();
|
||||
// Large query covers both cells; entity must appear exactly once.
|
||||
reg.GetNearbyObjects(new Vector3(24f, 12f, 50f), 10f, OffX, OffY, LbId, results);
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Equal(20u, results[0].EntityId);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Cell ID formula
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0f, 0f, 1u)] // cell (0,0) → index 0*8+0+1 = 1
|
||||
[InlineData(48f, 0f, 17u)] // cell (2,0) → 2*8+0+1 = 17
|
||||
[InlineData(0f, 48f, 3u)] // cell (0,2) → 0*8+2+1 = 3
|
||||
[InlineData(168f, 168f, 64u)] // cell (7,7) → 7*8+7+1 = 64
|
||||
public void GetObjectsInCell_CellIdFormula_Correct(float lx, float ly, uint expectedLow)
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
// Small radius so entity sits in exactly one cell.
|
||||
reg.Register(99u, 0x01000008u, new Vector3(lx + 0.5f, ly + 0.5f, 50f), 0.1f, OffX, OffY, LbId);
|
||||
|
||||
uint cellId = LbId | expectedLow;
|
||||
var objs = reg.GetObjectsInCell(cellId);
|
||||
Assert.Contains(objs, e => e.EntityId == 99u);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue