Accurate grid bounds (#2027)

* Fright night

* Shitty bounds working

* No more fixture leak

* Optimise TryFindGridAt

Should be O(1) now instead of the previous O(n) for number of fixtures

* ambush

* Merge stuffies

* Merge to master

* Fixes I guess

* Fix client sync

* Fix grid deserialization

* Jank test

* Fix deferral shitfuckery

* Optimise and remove

* Fixes

* Just werk dam u

* Optimisations

* Bunch of fixes

* FINALLY IT'S DONE

* Fixes

* Fix

* Comment
This commit is contained in:
metalgearsloth
2021-09-20 21:07:31 +10:00
committed by GitHub
parent ac21e24f33
commit 68576ace72
27 changed files with 414 additions and 427 deletions

View File

@@ -176,12 +176,18 @@ namespace Robust.Client.Debugging
var worldBox = physBody.GetWorldAABB();
if (worldBox.IsEmpty()) continue;
var pTransform = physBody.GetTransform();
foreach (var fixture in physBody.Fixtures)
{
var shape = fixture.Shape;
var sleepPercent = physBody.Awake ? 0.0f : 1.0f;
shape.DebugDraw(drawing, transform.WorldMatrix, in viewport, sleepPercent);
drawing.SetTransform(in Matrix3.Identity);
var aabb = shape.ComputeAABB(pTransform, 0);
worldHandle.DrawRect(aabb, Color.Blue, false);
}
foreach (var joint in physBody.Joints)

View File

@@ -64,19 +64,21 @@ namespace Robust.Client.GameObjects
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
{
var mapGrid = (IMapGridInternal) grid;
var gridEnt = _entityManager.GetEntity(grid.GridEntityId);
var worldPos = gridEnt.Transform.WorldPosition;
var worldRot = gridEnt.Transform.WorldRotation;
if (!_entityManager.ComponentManager.TryGetComponent<PhysicsComponent>(gridEnt.Uid, out var body)) continue;
foreach (var chunk in mapGrid.GetMapChunks(viewport))
var transform = body.GetTransform();
foreach (var fixture in body.Fixtures)
{
var chunkBounds = chunk.CalcWorldBounds(worldPos, worldRot);
var aabb = chunkBounds.CalcBoundingBox();
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
var aabb = fixture.Shape.ComputeAABB(transform, i);
args.WorldHandle.DrawRect(chunkBounds, Color.Green.WithAlpha(0.2f), true);
args.WorldHandle.DrawRect(aabb, Color.Red, false);
args.WorldHandle.DrawRect(aabb, Color.Green.WithAlpha(0.2f));
args.WorldHandle.DrawRect(aabb, Color.Red.WithAlpha(0.5f), false);
}
}
}
}

View File

@@ -47,11 +47,16 @@ namespace Robust.Server.Map
// Seemed easier than having this method on GridFixtureSystem
if (!TryGetGrid(chunk.GridId, out var grid) ||
!ComponentManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? body) ||
chunk.Fixture == null) return;
chunk.Fixtures.Count == 0) return;
// TODO: Like MapManager injecting this is a PITA so need to work out an easy way to do it.
// Maybe just add like a PostInject method that gets called way later?
EntitySystem.Get<SharedBroadphaseSystem>().DestroyFixture(body, chunk.Fixture);
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
foreach (var fixture in chunk.Fixtures)
{
broadphaseSystem.DestroyFixture(body, fixture);
}
}
public GameStateMapData? GetStateData(GameTick fromTick)

View File

@@ -11,6 +11,8 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
@@ -443,11 +445,47 @@ namespace Robust.Server.Maps
/// </summary>
private void ApplyGridFixtures()
{
var compManager = IoCManager.Resolve<IComponentManager>();
var entManager = IoCManager.Resolve<IEntityManager>();
var gridFixtures = EntitySystem.Get<GridFixtureSystem>();
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
foreach (var gridId in _mapManager.GetAllGrids())
foreach (var grid in Grids)
{
gridFixtures.ProcessGrid(gridId.Index);
var gridInternal = (IMapGridInternal) grid;
var body = compManager.EnsureComponent<PhysicsComponent>(entManager.GetEntity(grid.GridEntityId));
gridFixtures.ProcessGrid(gridInternal);
// Need to go through and double-check we don't have any hanging-on fixtures that
// no longer apply (e.g. due to an update in GridFixtureSystem)
var toRemove = new List<Fixture>();
foreach (var fixture in body.Fixtures)
{
var found = false;
foreach (var (_, chunk) in gridInternal.GetMapChunks())
{
foreach (var cFixture in chunk.Fixtures)
{
if (!cFixture.Equals(fixture)) continue;
found = true;
break;
}
if (found) break;
}
if (!found)
{
toRemove.Add(fixture);
}
}
foreach (var fixture in toRemove)
{
broadphaseSystem.DestroyFixture(fixture);
}
}
}

View File

@@ -140,7 +140,6 @@ namespace Robust.Server.Maps
}
chunk.SuppressCollisionRegeneration = false;
chunk.RegenerateCollision();
mapMan.SuppressOnTileChanged = false;
}
}

View File

@@ -1,6 +1,5 @@
using Robust.Server.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
namespace Robust.Server.Physics
{

View File

@@ -1,14 +1,4 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
namespace Robust.Server.Physics
{
@@ -17,156 +7,5 @@ namespace Robust.Server.Physics
/// </summary>
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
// We'll defer gridfixture updates during deserialization because MapLoader is interesting
private Dictionary<GridId, HashSet<MapChunk>> _queuedFixtureUpdates = new();
public override void Initialize()
{
base.Initialize();
UpdatesBefore.Add(typeof(SharedBroadphaseSystem));
SubscribeLocalEvent<RegenerateChunkCollisionEvent>(HandleCollisionRegenerate);
}
/// <summary>
/// Queue the chunk to generate (if cooldown > 0) or immediately process it.
/// </summary>
/// <param name="ev"></param>
private void HandleCollisionRegenerate(RegenerateChunkCollisionEvent ev)
{
// TODO: Probably shouldn't do this but MapLoader can lead to a lot of ordering nonsense hence we
// need to defer for it to ensure the fixtures get attached to the chunks properly.
if (!EntityManager.EntityExists(_mapManager.GetGrid(ev.Chunk.GridId).GridEntityId))
{
if (!_queuedFixtureUpdates.TryGetValue(ev.Chunk.GridId, out var chunks))
{
chunks = new HashSet<MapChunk>();
_queuedFixtureUpdates[ev.Chunk.GridId] = chunks;
}
chunks.Add(ev.Chunk);
return;
}
RegenerateCollision(ev.Chunk);
}
public void ProcessGrid(GridId gridId)
{
if (!_queuedFixtureUpdates.TryGetValue(gridId, out var chunks))
{
return;
}
if (!_mapManager.GridExists(gridId))
{
_queuedFixtureUpdates.Remove(gridId);
return;
}
foreach (var chunk in chunks)
{
RegenerateCollision(chunk);
}
_queuedFixtureUpdates.Remove(gridId);
}
private void RegenerateCollision(MapChunk chunk)
{
if (!_mapManager.TryGetGrid(chunk.GridId, out var grid) ||
!EntityManager.TryGetEntity(grid.GridEntityId, out var gridEnt)) return;
DebugTools.Assert(chunk.ValidTiles > 0);
// Currently this is gonna be hella simple.
if (!gridEnt.TryGetComponent(out PhysicsComponent? physicsComponent))
{
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEnt} that doesn't have {nameof(physicsComponent)}");
return;
}
// TODO: Lots of stuff here etc etc, make changes to mapgridchunk.
var bounds = chunk.CalcLocalBounds();
// So something goes on with the chunk's internal bounds caching where if there's no data the bound is 0 or something?
if (bounds.IsEmpty()) return;
var origin = chunk.Indices * chunk.ChunkSize;
bounds = bounds.Translated(origin);
// So we store a reference to the fixture on the chunk because it's easier to cross-reference it.
// This is because when we get multiple fixtures per chunk there's no easy way to tell which the old one
// corresponds with.
// We also ideally want to avoid re-creating the fixture every time a tile changes and pushing that data
// to the client hence we diff it.
// Additionally, we need to handle map deserialization where content may have stored its own data
// on the grid (e.g. mass) which we want to preserve.
var oldFixture = chunk.Fixture;
var polyShape = new PolygonShape();
Span<Vector2> vertices = stackalloc Vector2[4];
vertices[0] = bounds.BottomLeft;
vertices[1] = bounds.BottomRight;
vertices[2] = bounds.TopRight;
vertices[3] = bounds.TopLeft;
polyShape.SetVertices(vertices);
var newFixture = new Fixture(
polyShape,
MapGridHelpers.CollisionGroup,
MapGridHelpers.CollisionGroup,
true) {ID = GetChunkId(chunk),
Body = physicsComponent};
// Check if we have an existing fixture on MapGrid
var existingFixture = physicsComponent.GetFixture(newFixture.ID);
var same = true;
// Some fucky shit but we gotta handle map deserialization.
if (existingFixture is {Shape: PolygonShape poly})
{
var newPoly = (PolygonShape) newFixture.Shape;
if (!poly.EqualsApprox(newPoly))
same = false;
}
else
{
same = false;
}
// TODO: Chunk will likely need multiple fixtures but future sloth problem lmao idiot
if (same)
{
// If we're deserializing map this can occur so just update it.
if (oldFixture == null && existingFixture != null)
{
chunk.Fixture = existingFixture;
existingFixture.CollisionMask = MapGridHelpers.CollisionGroup;
existingFixture.CollisionLayer = MapGridHelpers.CollisionGroup;
}
return;
}
if (oldFixture != null)
_broadphase.DestroyFixture(physicsComponent, oldFixture);
_broadphase.CreateFixture(physicsComponent, newFixture);
chunk.Fixture = newFixture;
EntityManager.EventBus.RaiseLocalEvent(gridEnt.Uid,new GridFixtureChangeEvent {OldFixture = oldFixture, NewFixture = newFixture});
}
}
public sealed class GridFixtureChangeEvent : EntityEventArgs
{
public Fixture? OldFixture { get; init; }
public Fixture? NewFixture { get; init; }
}
}

View File

@@ -22,6 +22,8 @@ namespace Robust.Shared.Maths
public readonly int Height => Math.Abs(Top - Bottom);
public readonly Vector2i Size => new(Width, Height);
public readonly int Area => Width * Height;
public Box2i(Vector2i bottomLeft, Vector2i topRight)
{
Unsafe.SkipInit(out this);

View File

@@ -454,6 +454,13 @@ namespace Robust.Shared
* PHYSICS
*/
// Grid fixtures
/// <summary>
/// I'ma be real with you: the only reason this exists is to get tests working.
/// </summary>
public static readonly CVarDef<bool> GenerateGridFixtures =
CVarDef.Create("physics.grid_fixtures", true, CVar.REPLICATED);
// - Contacts
public static readonly CVarDef<int> ContactMultithreadThreshold =
CVarDef.Create("physics.contact_multithread_threshold", 32);

View File

@@ -1,15 +1,12 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Internal.TypeSystem;
using Robust.Shared.Exceptions;
using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
@@ -377,6 +374,17 @@ namespace Robust.Shared.GameObjects
&& netSet.ContainsKey(netId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T EnsureComponent<T>(IEntity entity) where T : Component, new()
{
if (TryGetComponent<T>(entity.Uid, out var component))
{
return component;
}
return AddComponent<T>(entity);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetComponent<T>(EntityUid uid)

View File

@@ -71,6 +71,7 @@ namespace Robust.Shared.GameObjects
private set
{
if (_gridId.Equals(value)) return;
_gridId = value;
foreach (var transformComponent in Children)
{

View File

@@ -121,6 +121,8 @@ namespace Robust.Shared.GameObjects
/// <returns>True if the entity has a component with the given network ID, otherwise false.</returns>
bool HasComponent(EntityUid uid, ushort netId);
T EnsureComponent<T>(IEntity entity) where T : Component, new();
/// <summary>
/// Returns the component of a specific type.
/// </summary>

View File

@@ -1,12 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects
{
public abstract class SharedGridFixtureSystem : EntitySystem
{
internal string GetChunkId(MapChunk chunk)
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
private bool _enabled;
public override void Initialize()
{
return $"grid_chunk-{chunk.Indices.X}-{chunk.Indices.Y}";
base.Initialize();
UpdatesBefore.Add(typeof(SharedBroadphaseSystem));
IoCManager.Resolve<IConfigurationManager>().OnValueChanged(CVars.GenerateGridFixtures, SetEnabled, true);
}
public override void Shutdown()
{
base.Shutdown();
IoCManager.Resolve<IConfigurationManager>().UnsubValueChanged(CVars.GenerateGridFixtures, SetEnabled);
}
private void SetEnabled(bool value) => _enabled = value;
internal void ProcessGrid(IMapGridInternal gridInternal)
{
// Just in case there's any deleted we'll ToArray
foreach (var (_, chunk) in gridInternal.GetMapChunks().ToArray())
{
chunk.RegenerateCollision();
}
}
internal void RegenerateCollision(MapChunk chunk, List<Box2i> rectangles)
{
if (!_enabled) return;
if (!_mapManager.TryGetGrid(chunk.GridId, out var grid) ||
!EntityManager.TryGetEntity(grid.GridEntityId, out var gridEnt)) return;
DebugTools.Assert(chunk.ValidTiles > 0);
if (!gridEnt.TryGetComponent(out PhysicsComponent? physicsComponent))
{
Logger.ErrorS("physics", $"Trying to regenerate collision for {gridEnt} that doesn't have {nameof(physicsComponent)}");
return;
}
var origin = chunk.Indices * chunk.ChunkSize;
// So we store a reference to the fixture on the chunk because it's easier to cross-reference it.
// This is because when we get multiple fixtures per chunk there's no easy way to tell which the old one
// corresponds with.
// We also ideally want to avoid re-creating the fixture every time a tile changes and pushing that data
// to the client hence we diff it.
// Additionally, we need to handle map deserialization where content may have stored its own data
// on the grid (e.g. mass) which we want to preserve.
var newFixtures = new List<Fixture>();
var oldFixtures = chunk.Fixtures.ToList();
Span<Vector2> vertices = stackalloc Vector2[4];
foreach (var rectangle in rectangles)
{
var bounds = rectangle.Translated(origin);
var poly = new PolygonShape();
vertices[0] = bounds.BottomLeft;
vertices[1] = bounds.BottomRight;
vertices[2] = bounds.TopRight;
vertices[3] = bounds.TopLeft;
poly.SetVertices(vertices);
var newFixture = new Fixture(
poly,
MapGridHelpers.CollisionGroup,
MapGridHelpers.CollisionGroup,
true) {ID = $"grid_chunk-{bounds.Left}-{bounds.Bottom}",
Body = physicsComponent};
newFixtures.Add(newFixture);
}
foreach (var oldFixture in chunk.Fixtures.ToArray())
{
var existing = false;
// Handle deleted / updated fixtures
// (TODO: Check IDs and cross-reference for updates?)
for (var i = newFixtures.Count - 1; i >= 0; i--)
{
var fixture = newFixtures[i];
if (!oldFixture.Equals(fixture)) continue;
existing = true;
newFixtures.RemoveAt(i);
break;
}
// Doesn't align with any new fixtures so delete
if (existing) continue;
chunk.Fixtures.Remove(oldFixture);
_broadphase.DestroyFixture(physicsComponent, oldFixture);
}
// Anything remaining is a new fixture (or at least, may have not serialized onto the chunk yet).
foreach (var fixture in newFixtures)
{
var existingFixture = physicsComponent.GetFixture(fixture.ID);
// Check if it's the same (otherwise remove anyway).
if (existingFixture?.Shape is PolygonShape poly &&
poly.EqualsApprox((PolygonShape) fixture.Shape))
{
chunk.Fixtures.Add(existingFixture);
continue;
}
chunk.Fixtures.Add(fixture);
_broadphase.CreateFixture(physicsComponent, fixture);
}
EntityManager.EventBus.RaiseLocalEvent(gridEnt.Uid,new GridFixtureChangeEvent {OldFixtures = oldFixtures, NewFixtures = chunk.Fixtures});
}
}
public sealed class GridFixtureChangeEvent : EntityEventArgs
{
public List<Fixture> OldFixtures { get; init; } = default!;
public List<Fixture> NewFixtures { get; init; } = default!;
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Prometheus;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -101,17 +102,35 @@ namespace Robust.Shared.GameObjects
Logger.DebugS("physics", $"Found {_controllers.Count} physics controllers.");
IoCManager.Resolve<IIslandManager>().Initialize();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.AutoClearForces, OnAutoClearChange, true);
}
private void HandlePhysicsMapInit(EntityUid uid, SharedPhysicsMapComponent component, ComponentInit args)
{
component.ContactManager = new ContactManager();
IoCManager.InjectDependencies(component);
component.BroadphaseSystem = Get<SharedBroadphaseSystem>();
component.PhysicsSystem = this;
component.ContactManager = new();
component.ContactManager.Initialize();
component.ContactManager.MapId = component.MapId;
component.ContactManager.KinematicControllerCollision += KinematicControllerCollision;
}
private void OnAutoClearChange(bool value)
{
foreach (var component in ComponentManager.EntityQuery<SharedPhysicsMapComponent>(true))
{
component.AutoClearForces = value;
}
}
private void HandlePhysicsMapRemove(EntityUid uid, SharedPhysicsMapComponent component, ComponentRemove args)
{
component.ContactManager.KinematicControllerCollision -= KinematicControllerCollision;
component.ContactManager.Shutdown();
}
public T GetController<T>() where T : VirtualController
@@ -204,6 +223,9 @@ namespace Robust.Shared.GameObjects
}
MapManager.MapCreated -= HandleMapCreated;
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CVars.AutoClearForces, OnAutoClearChange);
}
protected abstract void HandleMapCreated(object? sender, MapEventArgs eventArgs);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Maths;
@@ -8,118 +9,72 @@ namespace Robust.Shared.Map
/// </summary>
internal static class GridChunkPartition
{
public static void PartitionChunk(IMapChunk chunk, out Box2i bounds)
/// <summary>
///
/// </summary>
/// <param name="chunk"></param>
/// <param name="bounds">The overall bounds that covers every rectangle.</param>
/// <param name="rectangles">Each individual rectangle comprising the chunk's bounds</param>
public static void PartitionChunk(IMapChunk chunk, out Box2i bounds, out List<Box2i> rectangles)
{
var size = chunk.ChunkSize;
rectangles = new List<Box2i>();
// copy 2d img
bool[,] image = new bool[size,size];
// TODO: Use the existing PartitionChunk version because that one is likely faster and you can Span that shit.
// Convert each line into boxes as long as they can be.
for (ushort y = 0; y < chunk.ChunkSize; y++)
{
var origin = 0;
var running = false;
for(ushort x=0;x<size;x++)
for (ushort y = 0; y < size; y++)
image[x, y] = !chunk.GetTile(x, y).IsEmpty;
for (ushort x = 0; x < chunk.ChunkSize; x++)
{
if (!chunk.GetTile(x, y).IsEmpty)
{
running = true;
continue;
}
Partition(size, size, image, out var blocks, out var blockCount);
// Still empty
if (running)
{
rectangles.Add(new Box2i(origin, y, x, y + 1));
}
origin = x + 1;
running = false;
}
if (running)
rectangles.Add(new Box2i(origin, y, chunk.ChunkSize, y + 1));
}
// Patch them together as available
for (var i = rectangles.Count - 1; i >= 0; i--)
{
var box = rectangles[i];
for (var j = i - 1; j >= 0; j--)
{
var other = rectangles[j];
// Gone down as far as we can go.
if (other.Top < box.Bottom) break;
if (box.Left == other.Left && box.Right == other.Right)
{
box = new Box2i(box.Left, other.Bottom, box.Right, box.Top);
rectangles[i] = box;
rectangles.RemoveAt(j);
i -= 1;
continue;
}
}
}
bounds = new Box2i();
// convert blocks to rectangles array.
for(int i=0;i< blockCount; i++)
foreach (var rectangle in rectangles)
{
var block = blocks[i];
// block are in indices and rotated 90 degrees :(
var left = block.y1;
var right = block.y2 + 1;
var bottom = block.x1;
var top = block.x2 + 1;
if(bounds.Size.Equals(Vector2i.Zero))
bounds = new Box2i(left, bottom, right, top);
else
bounds = bounds.Union(new Box2i(left, bottom, right, top));
}
}
private struct Block
{
public int x1;
public int x2;
public int y1;
public int y2;
}
private static void Partition(in int L, in int W, in bool[,] img, out Block[] block, out int blockno)
{
// Credit: http://utopia.duth.gr/spiliot/papers/j7.pdf
block = new Block[L*W];
int[] p = new int[W];
int[] c = new int[W];
int kp = 0;
blockno = 0;
for (int y = 0; y < L; y++)
{
int x1 = 0;
int x2 = 0;
int kc = 0;
bool intervalfound = false;
int j_last = 0;
int j_curr = 0;
for (int x = 0; x < W; x++)
{
bool try2match = false;
if (img[y,x] && !intervalfound)
{
intervalfound = true;
x1 = x;
}
if (!img[y,x] && intervalfound)
{
intervalfound = false;
x2 = x - 1;
try2match = true;
}
if (x == W - 1 && img[y,x] && intervalfound)
{
x2 = x;
try2match = true;
}
if (try2match)
{
bool intervalmatched = false;
for (int j = j_last; j < kp && x1 >= block[p[j]].x1; j++)
if (x1 == block[p[j]].x1 && x2 == block[p[j]].x2)
{
c[kc] = p[j];
block[p[j]].y2 = y;
intervalmatched = true;
j_curr = j;
}
j_last = j_curr;
if (!intervalmatched)
{
block[blockno].x1 = x1;
block[blockno].x2 = x2;
block[blockno].y1 = y;
block[blockno].y2 = y;
c[kc] = blockno++;
}
if (!intervalmatched)
kc++;
}
}
for (var i = 0; i < kc; i++)
p[i] = c[i];
kp = kc;
bounds = bounds.IsEmpty() ? rectangle : bounds.Union(rectangle);
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Robust.Shared.Map
/// <inheritdoc />
internal interface IMapChunkInternal : IMapChunk
{
Fixture? Fixture { get; set; }
List<Fixture> Fixtures { get; set; }
bool SuppressCollisionRegeneration { get; set; }

View File

@@ -27,12 +27,7 @@ namespace Robust.Shared.Map
/// <param name="pos">Position of the entity in local tile indices.</param>
void AnchoredEntDirty(Vector2i pos);
/// <summary>
/// Regenerates anything that is based on chunk collision data.
/// This wouldn't even be separate if not for the whole "ability to suppress automatic collision regeneration" thing.
/// As it is, YamlGridSerializer performs manual collision regeneration and that wasn't properly getting propagated to the grid. Thus, this needs to exist.
/// </summary>
void NotifyChunkCollisionRegenerated(MapChunk chunk);
void UpdateAABB();
/// <summary>
/// Returns the chunk at the given indices. If the chunk does not exist,

View File

@@ -33,7 +33,7 @@ namespace Robust.Shared.Map
private Box2i _cachedBounds;
public Fixture? Fixture { get; set; }
public List<Fixture> Fixtures { get; set; } = new();
/// <inheritdoc />
public GameTick LastTileModifiedTick { get; private set; }
@@ -284,8 +284,13 @@ namespace Robust.Shared.Map
}
// generate collision rects
GridChunkPartition.PartitionChunk(this, out _cachedBounds);
_grid.NotifyChunkCollisionRegenerated(this);
GridChunkPartition.PartitionChunk(this, out _cachedBounds, out var rectangles);
_grid.UpdateAABB();
// TryGet because unit tests YAY
if (ValidTiles > 0 && EntitySystem.TryGet(out SharedGridFixtureSystem? system))
system.RegenerateCollision(this, rectangles);
}
/// <inheritdoc />

View File

@@ -165,7 +165,7 @@ namespace Robust.Shared.Map
/// Expands the AABB for this grid when a new tile is added. If the tile is already inside the existing AABB,
/// nothing happens. If it is outside, the AABB is expanded to fit the new tile.
/// </summary>
private void UpdateAABB()
public void UpdateAABB()
{
LocalBounds = new Box2();
foreach (var chunk in _chunks.Values)
@@ -195,24 +195,6 @@ namespace Robust.Shared.Map
_mapManager.RaiseOnTileChanged(tileRef, oldTile);
}
/// <inheritdoc />
public void NotifyChunkCollisionRegenerated(MapChunk chunk)
{
// TODO: Ideally we wouldn't have LocalBounds on the grid and we could just treat it like a physics object
// (eventually, well into the future).
// For now we'll just attach a fixture to each chunk.
// Not raising directed because the grid's EntityUid isn't set yet.
// Don't call GridFixtureSystem directly because it's server-only.
if (chunk.ValidTiles > 0)
IoCManager
.Resolve<IEntityManager>()
.EventBus
.RaiseEvent(EventSource.Local, new RegenerateChunkCollisionEvent(chunk));
UpdateAABB();
}
#region TileAccess
/// <inheritdoc />

View File

@@ -19,8 +19,6 @@ namespace Robust.Shared.Map
[Dependency] protected readonly IComponentManager ComponentManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private SharedGridFixtureSystem _gridFixtures = default!;
public IGameTiming GameTiming => _gameTiming;
public IEntityManager EntityManager => _entityManager;
@@ -85,8 +83,6 @@ namespace Robust.Shared.Map
Logger.DebugS("map", "Starting...");
_gridFixtures = EntitySystem.Get<SharedGridFixtureSystem>();
if (!_maps.Contains(MapId.Nullspace))
{
CreateMap(MapId.Nullspace);
@@ -534,7 +530,9 @@ namespace Robust.Shared.Map
{
foreach (var (_, mapGrid) in _grids)
{
if (mapGrid.ParentMapId != mapId || !mapGrid.WorldBounds.Contains(worldPos))
// So not sure if doing the transform for WorldBounds and early out here is faster than just
// checking if we have a relevant chunk, need to profile.
if (mapGrid.ParentMapId != mapId)
continue;
// Turn the worldPos into a localPos and work out the relevant chunk we need to check
@@ -550,26 +548,13 @@ namespace Robust.Shared.Map
var chunkIndices = mapGrid.GridTileToChunkIndices(tile);
if (!mapGrid.HasChunk(chunkIndices)) continue;
if (!gridEnt.TryGetComponent(out PhysicsComponent? body)) continue;
var gridPos = gridEnt.Transform.WorldPosition;
var gridRot = gridEnt.Transform.WorldRotation;
var transform = new Transform(gridPos, (float) gridRot);
// TODO: Client never associates Fixtures with chunks hence we need to look it up by ID.
var chunk = mapGrid.GetChunk(chunkIndices);
var id = _gridFixtures.GetChunkId((MapChunk) chunk);
var fixture = body.GetFixture(id);
var chunkTile = chunk.GetTileRef(chunk.GridTileToChunkTile(tile));
if (fixture == null) continue;
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
// TODO: Use CollisionManager once it's done.
if (!fixture.Shape.ComputeAABB(transform, i).Contains(worldPos)) continue;
grid = mapGrid;
return true;
}
if (chunkTile.Tile.IsEmpty) continue;
grid = mapGrid;
return true;
}
grid = null;
@@ -582,45 +567,56 @@ namespace Robust.Shared.Map
return TryFindGridAt(mapCoordinates.MapId, mapCoordinates.Position, out grid);
}
/// <inheritdoc />
public IEnumerable<IMapGrid> FindGridsIntersecting(MapId mapId, Box2 worldArea)
{
// So despite the fact we have accurate bounds the reason I didn't make this tile-based is because
// at some stage we may want to overwrite the default behavior e.g. if you allow diagonals
foreach (var (_, grid) in _grids)
{
if (grid.ParentMapId != mapId || !grid.WorldBounds.Intersects(worldArea)) continue;
var found = false;
var gridEnt = _entityManager.GetEntity(grid.GridEntityId);
var body = gridEnt.GetComponent<PhysicsComponent>();
var transform = new Transform(gridEnt.Transform.WorldPosition, (float) gridEnt.Transform.WorldRotation);
var anyChunks = false;
var xformComp = _entityManager.ComponentManager.GetComponent<TransformComponent>(gridEnt.Uid);
var transform = new Transform(xformComp.WorldPosition, xformComp.WorldRotation);
foreach (var chunk in grid.GetMapChunks(worldArea))
if (_entityManager.ComponentManager.TryGetComponent<PhysicsComponent>(gridEnt.Uid, out var body))
{
anyChunks = true;
var id = _gridFixtures.GetChunkId((MapChunk) chunk);
var fixture = body.GetFixture(id);
var intersects = false;
if (fixture == null) continue;
for (var i = 0; i < fixture.Shape.ChildCount; i++)
foreach (var chunk in grid.GetMapChunks(worldArea))
{
// TODO: Need to use CollisionManager to test detailed overlap
if (!fixture.Shape.ComputeAABB(transform, i).Intersects(worldArea)) continue;
yield return grid;
found = true;
break;
foreach (var fixture in chunk.Fixtures)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
if (!fixture.Shape.ComputeAABB(transform, i).Intersects(worldArea)) continue;
intersects = true;
break;
}
if (intersects) break;
}
if (intersects) break;
}
if (found) break;
if (intersects)
{
yield return grid;
continue;
}
}
if (!anyChunks && worldArea.Contains(transform.Position))
if (grid.ChunkCount == 0 && worldArea.Contains(transform.Position))
{
yield return grid;
}
}
}
/// <inheritdoc />
public IEnumerable<IMapGrid> FindGridsIntersecting(MapId mapId, Box2Rotated worldArea)
{
var worldBounds = worldArea.CalcBoundingBox();
@@ -629,33 +625,40 @@ namespace Robust.Shared.Map
{
if (grid.ParentMapId != mapId || !grid.WorldBounds.Intersects(worldBounds)) continue;
var found = false;
var gridEnt = _entityManager.GetEntity(grid.GridEntityId);
var body = gridEnt.GetComponent<PhysicsComponent>();
var transform = new Transform(gridEnt.Transform.WorldPosition, (float) gridEnt.Transform.WorldRotation);
var anyChunks = false;
var xformComp = _entityManager.ComponentManager.GetComponent<TransformComponent>(gridEnt.Uid);
var transform = new Transform(xformComp.WorldPosition, xformComp.WorldRotation);
foreach (var chunk in grid.GetMapChunks(worldArea))
if (_entityManager.ComponentManager.TryGetComponent<PhysicsComponent>(gridEnt.Uid, out var body))
{
anyChunks = true;
var id = _gridFixtures.GetChunkId((MapChunk) chunk);
var fixture = body.GetFixture(id);
var intersects = false;
if (fixture == null) continue;
for (var i = 0; i < fixture.Shape.ChildCount; i++)
foreach (var chunk in grid.GetMapChunks(worldArea))
{
// TODO: Need to use CollisionManager to test detailed overlap
if (!fixture.Shape.ComputeAABB(transform, i).Intersects(worldBounds)) continue;
yield return grid;
found = true;
break;
foreach (var fixture in chunk.Fixtures)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
if (!fixture.Shape.ComputeAABB(transform, i).Intersects(worldBounds)) continue;
intersects = true;
break;
}
if (intersects) break;
}
if (intersects) break;
}
if (found) break;
if (intersects)
{
yield return grid;
continue;
}
}
if (!anyChunks && worldArea.Contains(transform.Position))
if (grid.ChunkCount == 0 && worldArea.Contains(transform.Position))
{
yield return grid;
}

View File

@@ -22,7 +22,6 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -40,14 +39,14 @@ namespace Robust.Shared.Physics.Dynamics
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IIslandManager _islandManager = default!;
private SharedBroadphaseSystem _broadphaseSystem = default!;
private SharedPhysicsSystem _physicsSystem = default!;
internal SharedBroadphaseSystem BroadphaseSystem = default!;
internal SharedPhysicsSystem PhysicsSystem = default!;
public override string Name => "PhysicsMap";
internal ContactManager ContactManager = default!;
private bool _autoClearForces;
public bool AutoClearForces;
/// <summary>
/// Change the global gravity vector.
@@ -123,34 +122,6 @@ namespace Robust.Shared.Physics.Dynamics
public MapId MapId => Owner.Transform.MapID;
protected override void Initialize()
{
base.Initialize();
_broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
_physicsSystem = EntitySystem.Get<SharedPhysicsSystem>();
IoCManager.InjectDependencies(this);
ContactManager.Initialize();
ContactManager.MapId = MapId;
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CVars.AutoClearForces, OnAutoClearChange, true);
}
protected override void Shutdown()
{
base.Shutdown();
ContactManager.Shutdown();
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CVars.AutoClearForces, OnAutoClearChange);
}
private void OnAutoClearChange(bool value)
{
_autoClearForces = value;
}
#region AddRemove
public void AddAwakeBody(PhysicsComponent body)
{
@@ -217,7 +188,7 @@ namespace Robust.Shared.Physics.Dynamics
// If the joint prevents collisions, then flag any contacts for filtering.
if (!joint.CollideConnected)
{
_physicsSystem.FilterContactsForJoint(joint);
PhysicsSystem.FilterContactsForJoint(joint);
}
bodyA.Dirty();
@@ -399,12 +370,12 @@ namespace Robust.Shared.Physics.Dynamics
// Box2D does this at the end of a step and also here when there's a fixture update.
// Given external stuff can move bodies we'll just do this here.
// Unfortunately this NEEDS to be predicted to make pushing remotely fucking good.
_broadphaseSystem.FindNewContacts(MapId);
BroadphaseSystem.FindNewContacts(MapId);
var invDt = frameTime > 0.0f ? 1.0f / frameTime : 0.0f;
var dtRatio = _invDt0 * frameTime;
foreach (var controller in _physicsSystem.Controllers)
foreach (var controller in PhysicsSystem.Controllers)
{
controller.UpdateBeforeMapSolve(prediction, this, frameTime);
}
@@ -422,13 +393,13 @@ namespace Robust.Shared.Physics.Dynamics
// TODO: SolveTOI
foreach (var controller in _physicsSystem.Controllers)
foreach (var controller in PhysicsSystem.Controllers)
{
controller.UpdateAfterMapSolve(prediction, this, frameTime);
}
// Box2d recommends clearing (if you are) during fixed updates rather than variable if you are using it
if (!prediction && _autoClearForces)
if (!prediction && AutoClearForces)
ClearForces();
_invDt0 = invDt;

View File

@@ -504,6 +504,11 @@ namespace Robust.Shared.Physics
CreateFixture(body, fixture);
}
public void DestroyFixture(Fixture fixture)
{
DestroyFixture(fixture.Body, fixture);
}
public void DestroyFixture(PhysicsComponent body, Fixture fixture)
{
// TODO: Assert world locked

View File

@@ -3,14 +3,19 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Robust.Client.GameObjects;
using Robust.Server.GameObjects;
using Robust.Server.Physics;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using GridFixtureSystem = Robust.Client.GameObjects.GridFixtureSystem;
namespace Robust.UnitTesting
{
@@ -68,7 +73,6 @@ namespace Robust.UnitTesting
// Required systems
var systems = IoCManager.Resolve<IEntitySystemManager>();
systems.LoadExtraSystemType<GridFixtureSystem>();
systems.Initialize();
var entMan = IoCManager.Resolve<IEntityManager>();
@@ -102,6 +106,11 @@ namespace Robust.UnitTesting
compFactory.RegisterClass<EntityLookupComponent>();
}
if (!compFactory.AllRegisteredTypes.Contains(typeof(SharedPhysicsMapComponent)))
{
compFactory.RegisterClass<PhysicsMapComponent>();
}
if(entMan.EventBus == null)
{
entMan.Startup();

View File

@@ -4,13 +4,11 @@ using NUnit.Framework;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Server.Physics;
using Robust.Shared.Containers;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -105,6 +103,7 @@ entities:
var entMan = IoCManager.Resolve<IEntityManager>();
var mapId = map.CreateMap();
map.GetMapEntity(mapId).EnsureComponent<PhysicsMapComponent>();
var mapLoad = IoCManager.Resolve<IMapLoader>();
var grid = mapLoad.LoadBlueprint(mapId, "/TestMap.yml");

View File

@@ -7,7 +7,7 @@ using Robust.Shared.Maths;
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture, Parallelizable, TestOf(typeof(GridChunkPartition))]
internal class GridChunkPartition_Tests
internal class GridChunkPartition_Tests : RobustUnitTest
{
// origin is top left
private static readonly int[] _testMiscTiles = {
@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Map
var chunk = ChunkFactory(4, _testMiscTiles);
// Act
GridChunkPartition.PartitionChunk(chunk, out var bounds);
GridChunkPartition.PartitionChunk(chunk, out var bounds, out _);
// box origin is top left
// algorithm goes down columns of array, starting on left side, then moves right, expanding rectangles to the right
@@ -53,7 +53,7 @@ namespace Robust.UnitTesting.Shared.Map
var chunk = ChunkFactory(4, _testJoinTiles);
// Act
GridChunkPartition.PartitionChunk(chunk, out var bounds);
GridChunkPartition.PartitionChunk(chunk, out var bounds, out _);
Assert.That(bounds, Is.EqualTo(new Box2i(1, 0, 3, 3)));
}

View File

@@ -1,7 +1,5 @@
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Server.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -23,7 +21,6 @@ namespace Robust.UnitTesting.Shared.Map
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var gridFixtures = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GridFixtureSystem>();
await server.WaitAssertion(() =>
{
@@ -47,14 +44,15 @@ namespace Robust.UnitTesting.Shared.Map
// Now do 2 tiles (same chunk)
grid.SetTile(Vector2i.One, new Tile(1));
Assert.That(gridBody.Fixtures.Count, Is.EqualTo(1));
bounds = gridBody.Fixtures[0].Shape.ComputeAABB(new Transform(Vector2.Zero, (float) Angle.Zero.Theta), 0);
// Because it's a diagonal tile it will actually be 2x2 area (until we get accurate hitboxes anyway).
Assert.That(MathHelper.CloseTo(Box2.Area(bounds), 4.0f, 0.1f));
// If we add a new chunk should be 2 now
grid.SetTile(new Vector2i(-1, -1), new Tile(1));
Assert.That(gridBody.Fixtures.Count, Is.EqualTo(2));
bounds = gridBody.Fixtures[0].Shape.ComputeAABB(new Transform(Vector2.Zero, (float) Angle.Zero.Theta), 0);
// Even if we add a new tile old fixture should stay the same if they don't connect.
Assert.That(MathHelper.CloseTo(Box2.Area(bounds), 1.0f, 0.1f));
// If we add a new chunk should be 3 now
grid.SetTile(new Vector2i(-1, -1), new Tile(1));
Assert.That(gridBody.Fixtures.Count, Is.EqualTo(3));
gridBody.LinearVelocity = Vector2.One;
Assert.That(gridBody.LinearVelocity.Length, Is.EqualTo(0f));

View File

@@ -11,7 +11,7 @@ using Robust.Shared.Timing;
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture, Parallelizable, TestOf(typeof(MapChunk))]
class MapChunk_Tests
sealed class MapChunk_Tests : RobustUnitTest
{
[Test]
public void GetChunkSize()