Files
RobustToolbox/Robust.Shared/Physics/Dynamics/ContactManager.cs
metalgearsloth fefcc7cba3 Physics (#1602)
* Physics worlds

* Paul's a good boy

* Build working

* Ingame and not lagging to hell

* Why didn't you commit ahhhhh

* Hard collisions working

* Solver parity

* Decent broadphase work done

* BroadPhase outline done

* BroadPhase working

* waiting for pvs

* Fix static PVS AABB

* Stop static bodies from awakening

* Optimise a bunch of stuff

* Even more broadphase stuff

* I'm fucking stupid

* Optimise fixture updates

* Collision solver start

* Building

* A is for Argumentative

* Fix contact caching island flags

* Circle shapes actually workeded

* Damping

* DS2 consumables only

* Slightly more stable

* Even slightlier more stablier

* VV your heart out

* Initial joint support

* 90% of joints I just wanted to push as I'd scream if I lost progress

* JOINT PURGATORY

* Joints barely functional lmao

* Okay these joints slightly more functional

* Remove station FrictionJoint

* Also that

* Some Box2D ports

* Cleanup mass

* Edge shape

* Active contacts

* Fix active contacts

* Optimise active contacts even more

* Boxes be stacking

* I would die for smug oh my fucking god

* In which everything is fixed

* Distance joints working LETS GO

* Remove frequency on distancejoint

* Fix some stuff and break joints

* Crashing fixed mehbeh

* ICollideSpecial and more resilience

* auto-clear

* showbb vera

* Slap that TODO in there

* Fix restartround crash

* Random fixes

* Fix fixture networking

* Add intersection method for broadphase

* Fix contacts

* Licenses done

* Optimisations

* Fix wall clips

* Config caching for island

* allocations optimisations

* Optimise casts

* Optimise events queue for physics

* Contact manager optimisations

* Optimise controllers

* Sloth joint or something idk

* Controller graph

* Remove content cvar

* Random cleanup

* Finally remove VirtualController

* Manifold structs again

* Optimise this absolute retardation

* Optimise

* fix license

* Cleanup physics interface

* AHHHHHHHHHHHHH

* Fix collisions again

* snivybus

* Fix potential nasty manifold bug

* Tests go snivy

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-01 03:09:36 +11:00

489 lines
16 KiB
C#

/*
* Farseer Physics Engine:
* Copyright (c) 2012 Ian Qvist
*
* Original source Box2D:
* Copyright (c) 2006-2011 Erin Catto http://www.box2d.org
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
*/
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Physics.Dynamics.Contacts;
namespace Robust.Shared.Physics.Dynamics
{
internal sealed class ContactManager
{
// TODO: When a static body has no contacts left need to set it to sleep as otherwise it'll just show as awake
// for debug drawing (map never adds static bodies as awake so should be no problem there).
[Dependency] private readonly IConfigurationManager _configManager = default!;
internal MapId MapId { get; set; }
private SharedBroadPhaseSystem _broadPhaseSystem = default!;
/// <summary>
/// Called when the broadphase finds two fixtures close to each other.
/// </summary>
public BroadPhaseDelegate OnBroadPhaseCollision;
/// <summary>
/// The set of active contacts.
/// </summary>
internal HashSet<Contact> ActiveContacts = new(128);
/// <summary>
/// A temporary copy of active contacts that is used during updates so
/// the hash set can have members added/removed during the update.
/// This list is cleared after every update.
/// </summary>
private List<Contact> ActiveList = new(128);
private List<ICollideBehavior> _collisionBehaviors = new();
private List<IPostCollide> _postCollideBehaviors = new();
public ContactManager()
{
OnBroadPhaseCollision = AddPair;
}
public void Initialize()
{
IoCManager.InjectDependencies(this);
_broadPhaseSystem = EntitySystem.Get<SharedBroadPhaseSystem>();
}
public void FindNewContacts(MapId mapId)
{
foreach (var broadPhase in _broadPhaseSystem.GetBroadPhases(mapId))
{
broadPhase.UpdatePairs(OnBroadPhaseCollision);
}
}
internal void UpdateContacts(ContactEdge? contactEdge, bool value)
{
if (value)
{
while (contactEdge != null)
{
var contact = contactEdge.Contact!;
if (!ActiveContacts.Contains(contact))
{
ActiveContacts.Add(contact);
}
contactEdge = contactEdge.Next;
}
}
else
{
while (contactEdge != null)
{
var contact = contactEdge.Contact!;
if (!contactEdge.Other!.Awake)
{
if (ActiveContacts.Contains(contact))
{
ActiveContacts.Remove(contact);
}
}
contactEdge = contactEdge.Next;
}
}
}
internal void RemoveActiveContact(Contact contact)
{
if (!ActiveContacts.Contains(contact))
{
ActiveContacts.Remove(contact);
}
}
/// <summary>
/// Go through the cached broadphase movement and update contacts.
/// </summary>
/// <param name="gridId"></param>
/// <param name="proxyA"></param>
/// <param name="proxyB"></param>
private void AddPair(GridId gridId, in FixtureProxy proxyA, in FixtureProxy proxyB)
{
Fixture fixtureA = proxyA.Fixture;
Fixture fixtureB = proxyB.Fixture;
int indexA = proxyA.ChildIndex;
int indexB = proxyB.ChildIndex;
PhysicsComponent bodyA = fixtureA.Body;
PhysicsComponent bodyB = fixtureB.Body;
// Are the fixtures on the same body?
if (bodyA.Owner.Uid.Equals(bodyB.Owner.Uid)) return;
// Box2D checks the mask / layer below but IMO doing it before contact is better.
// Check default filter
if (!ShouldCollide(fixtureA, fixtureB))
return;
// Does a contact already exist?
var edge = bodyB.ContactEdges;
while (edge != null)
{
if (edge.Other == bodyA)
{
Fixture fA = edge.Contact?.FixtureA!;
Fixture fB = edge.Contact?.FixtureB!;
var iA = edge.Contact!.ChildIndexA;
var iB = edge.Contact!.ChildIndexB;
if (fA == fixtureA && fB == fixtureB && iA == indexA && iB == indexB)
{
// A contact already exists.
return;
}
if (fA == fixtureB && fB == fixtureA && iA == indexB && iB == indexA)
{
// A contact already exists.
return;
}
}
edge = edge.Next;
}
// Does a joint override collision? Is at least one body dynamic?
if (!bodyB.ShouldCollide(bodyA))
return;
//FPE feature: BeforeCollision delegate
/*
if (fixtureA.BeforeCollision != null && fixtureA.BeforeCollision(fixtureA, fixtureB) == false)
return;
if (fixtureB.BeforeCollision != null && fixtureB.BeforeCollision(fixtureB, fixtureA) == false)
return;
*/
// Call the factory.
Contact c = Contact.Create(gridId, fixtureA, indexA, fixtureB, indexB);
// Contact creation may swap fixtures.
fixtureA = c.FixtureA!;
fixtureB = c.FixtureB!;
bodyA = fixtureA.Body;
bodyB = fixtureB.Body;
// Insert into the world.
ActiveContacts.Add(c);
// Connect to island graph.
// Connect to body A
c.NodeA.Contact = c;
c.NodeA.Other = bodyB;
c.NodeA.Previous = null;
c.NodeA.Next = bodyA.ContactEdges;
if (bodyA.ContactEdges != null)
{
bodyA.ContactEdges.Previous = c.NodeA;
}
bodyA.ContactEdges = c.NodeA;
// Connect to body B
c.NodeB.Contact = c;
c.NodeB.Other = bodyA;
c.NodeB.Previous = null;
c.NodeB.Next = bodyB.ContactEdges;
if (bodyB.ContactEdges != null)
{
bodyB.ContactEdges.Previous = c.NodeB;
}
bodyB.ContactEdges = c.NodeB;
// Wake up the bodies
if (fixtureA.Hard && fixtureB.Hard)
{
bodyA.Awake = true;
bodyB.Awake = true;
}
}
private bool ShouldCollide(Fixture fixtureA, Fixture fixtureB)
{
// TODO: Should we only be checking one side's mask? I think maybe fixtureB? IDK
return !((fixtureA.CollisionMask & fixtureB.CollisionLayer) == 0x0 &&
(fixtureB.CollisionMask & fixtureA.CollisionLayer) == 0x0);
}
public void Destroy(Contact contact)
{
Fixture fixtureA = contact.FixtureA!;
Fixture fixtureB = contact.FixtureB!;
PhysicsComponent bodyA = fixtureA.Body;
PhysicsComponent bodyB = fixtureB.Body;
if (contact.IsTouching)
{
//Report the separation to both participants:
// TODO: Needs to do like a comp message and system message
// fixtureA?.OnSeparation(fixtureA, fixtureB);
//Reverse the order of the reported fixtures. The first fixture is always the one that the
//user subscribed to.
// fixtureB.OnSeparation(fixtureB, fixtureA);
// EndContact(contact);
}
// Remove from body 1
if (contact.NodeA.Previous != null)
{
contact.NodeA.Previous.Next = contact.NodeA.Next;
}
if (contact.NodeA.Next != null)
{
contact.NodeA.Next.Previous = contact.NodeA.Previous;
}
if (contact.NodeA == bodyA.ContactEdges)
{
bodyA.ContactEdges = contact.NodeA.Next;
}
// Remove from body 2
if (contact.NodeB.Previous != null)
{
contact.NodeB.Previous.Next = contact.NodeB.Next;
}
if (contact.NodeB.Next != null)
{
contact.NodeB.Next.Previous = contact.NodeB.Previous;
}
if (contact.NodeB == bodyB.ContactEdges)
{
bodyB.ContactEdges = contact.NodeB.Next;
}
ActiveContacts.Remove(contact);
contact.Destroy();
}
internal void Collide()
{
// TODO: We need to handle collisions during prediction but also handle the start / stop colliding shit during sim ONLY
// Update awake contacts
ActiveList.AddRange(ActiveContacts);
// Can be changed while enumerating
foreach (var contact in ActiveList)
{
Fixture fixtureA = contact.FixtureA!;
Fixture fixtureB = contact.FixtureB!;
int indexA = contact.ChildIndexA;
int indexB = contact.ChildIndexB;
PhysicsComponent bodyA = fixtureA.Body;
PhysicsComponent bodyB = fixtureB.Body;
// Do not try to collide disabled bodies
// FPE just continues here but in our case I think it's better to also destroy the contact.
if (!bodyA.CanCollide || !bodyB.CanCollide)
{
Contact cNuke = contact;
Destroy(cNuke);
continue;
}
// Is this contact flagged for filtering?
if (contact.FilterFlag)
{
// Should these bodies collide?
if (!bodyB.ShouldCollide(bodyA))
{
Contact cNuke = contact;
Destroy(cNuke);
continue;
}
// Check default filtering
if (!ShouldCollide(fixtureA, fixtureB))
{
Contact cNuke = contact;
Destroy(cNuke);
continue;
}
// Check user filtering.
/*
if (ContactFilter != null && ContactFilter(fixtureA, fixtureB) == false)
{
Contact cNuke = c;
Destroy(cNuke);
continue;
}
*/
// Clear the filtering flag.
contact.FilterFlag = false;
}
var activeA = bodyA.Awake && bodyA.BodyType != BodyType.Static;
var activeB = bodyB.Awake && bodyB.BodyType != BodyType.Static;
// At least one body must be awake and it must be dynamic or kinematic.
if (!activeA && !activeB)
{
ActiveContacts.Remove(contact);
continue;
}
// TODO: Need to handle moving grids
bool? overlap = false;
// Sloth addition: Kind of hacky and might need to be removed at some point.
// One of the bodies was probably put into nullspace so we need to remove I think.
if (fixtureA.Proxies.ContainsKey(contact.GridId) && fixtureB.Proxies.ContainsKey(contact.GridId))
{
var proxyIdA = fixtureA.Proxies[contact.GridId][indexA].ProxyId;
var proxyIdB = fixtureB.Proxies[contact.GridId][indexB].ProxyId;
var broadPhase = _broadPhaseSystem.GetBroadPhase(MapId, contact.GridId);
overlap = broadPhase?.TestOverlap(proxyIdA, proxyIdB);
}
// Here we destroy contacts that cease to overlap in the broad-phase.
if (overlap == false)
{
Contact cNuke = contact;
Destroy(cNuke);
continue;
}
// The contact persists.
contact.Update(this);
}
ActiveList.Clear();
}
public void PreSolve()
{
// We'll do pre and post-solve around all islands rather than each specific island as it seems cleaner with race conditions.
foreach (var contact in ActiveContacts)
{
if (!contact.IsTouching) continue;
// God this area's hard to read but tl;dr run ICollideBehavior and IPostCollide and try to optimise it a little.
var bodyA = contact.FixtureA!.Body;
var bodyB = contact.FixtureB!.Body;
if (!bodyA.Entity.Deleted)
{
foreach (var behavior in bodyA.Owner.GetAllComponents<ICollideBehavior>())
{
_collisionBehaviors.Add(behavior);
}
foreach (var behavior in _collisionBehaviors)
{
if (bodyB.Deleted) break;
behavior.CollideWith(bodyA, bodyB);
}
_collisionBehaviors.Clear();
}
if (!bodyB.Entity.Deleted)
{
foreach (var behavior in bodyB.Owner.GetAllComponents<ICollideBehavior>())
{
_collisionBehaviors.Add(behavior);
}
foreach (var behavior in _collisionBehaviors)
{
if (bodyA.Deleted) break;
behavior.CollideWith(bodyB, bodyA);
}
_collisionBehaviors.Clear();
}
if (!bodyA.Entity.Deleted)
{
foreach (var behavior in bodyA.Owner.GetAllComponents<IPostCollide>())
{
_postCollideBehaviors.Add(behavior);
}
foreach (var behavior in _postCollideBehaviors)
{
behavior.PostCollide(bodyA, bodyB);
if (bodyB.Deleted) break;
}
_postCollideBehaviors.Clear();
}
if (!bodyB.Entity.Deleted)
{
foreach (var behavior in bodyB.Owner.GetAllComponents<IPostCollide>())
{
_postCollideBehaviors.Add(behavior);
}
foreach (var behavior in _postCollideBehaviors)
{
behavior.PostCollide(bodyB, bodyA);
if (bodyA.Deleted) break;
}
_postCollideBehaviors.Clear();
}
}
}
public void PostSolve()
{
}
}
public delegate void BroadPhaseDelegate(GridId gridId, in FixtureProxy proxyA, in FixtureProxy proxyB);
}