mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 14:52:35 +02:00
* MapManager now uses accurate bounds for grids Instead of using the old WorldBounds (which was dirty for multiple reasons) it goes through physics instead using the actual fixtures attached to the grid. * Fix nuke * Test time * AABB tests * feex * how is this failing aaa * feex tests * slightly better Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
1414 lines
43 KiB
C#
1414 lines
43 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.
|
|
*
|
|
* PhysicsComponent is heavily modified from Box2D.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.GameStates;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Physics.Broadphase;
|
|
using Robust.Shared.Physics.Collision.Shapes;
|
|
using Robust.Shared.Physics.Dynamics;
|
|
using Robust.Shared.Physics.Dynamics.Contacts;
|
|
using Robust.Shared.Physics.Dynamics.Joints;
|
|
using Robust.Shared.Players;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Serialization.Manager.Attributes;
|
|
using Robust.Shared.Utility;
|
|
using Robust.Shared.ViewVariables;
|
|
|
|
namespace Robust.Shared.GameObjects
|
|
{
|
|
[ComponentReference(typeof(ILookupWorldBox2Component))]
|
|
[ComponentReference(typeof(IPhysBody))]
|
|
[NetworkedComponent()]
|
|
public sealed class PhysicsComponent : Component, IPhysBody, ISerializationHooks, ILookupWorldBox2Component
|
|
{
|
|
[DataField("status", readOnly: true)]
|
|
private BodyStatus _bodyStatus = BodyStatus.OnGround;
|
|
|
|
/// <inheritdoc />
|
|
public override string Name => "Physics";
|
|
|
|
/// <summary>
|
|
/// Has this body been added to an island previously in this tick.
|
|
/// </summary>
|
|
public bool Island { get; set; }
|
|
|
|
internal BroadphaseComponent? Broadphase { get; set; }
|
|
|
|
/// <summary>
|
|
/// Store the body's index within the island so we can lookup its data.
|
|
/// Key is Island's ID and value is our index.
|
|
/// </summary>
|
|
public Dictionary<int, int> IslandIndex { get; set; } = new();
|
|
|
|
// TODO: Actually implement after the initial pr dummy
|
|
/// <summary>
|
|
/// Gets or sets where this body should be included in the CCD solver.
|
|
/// </summary>
|
|
public bool IsBullet { get; set; }
|
|
|
|
public bool IgnoreCCD { get; set; }
|
|
|
|
[ViewVariables]
|
|
public int ContactCount
|
|
{
|
|
get
|
|
{
|
|
var count = 0;
|
|
var edge = ContactEdges;
|
|
while (edge != null)
|
|
{
|
|
edge = edge.Next;
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
}
|
|
|
|
[ViewVariables]
|
|
public Box2 LocalAABB
|
|
{
|
|
get
|
|
{
|
|
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
|
|
var broadphase = broadphaseSystem.GetBroadphase(this);
|
|
|
|
if (broadphase == null) return new Box2();
|
|
|
|
var worldPos = Owner.Transform.WorldPosition;
|
|
var aabb = new Box2(worldPos, worldPos);
|
|
|
|
foreach (var fixture in Fixtures)
|
|
{
|
|
foreach (var proxy in fixture.Proxies)
|
|
{
|
|
aabb = aabb.Union(proxy.AABB);
|
|
}
|
|
}
|
|
|
|
return aabb;
|
|
}
|
|
}
|
|
|
|
[ViewVariables]
|
|
public Box2 WorldAABB
|
|
{
|
|
get
|
|
{
|
|
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
|
|
var broadphase = broadphaseSystem.GetBroadphase(this);
|
|
|
|
if (broadphase == null) return new Box2();
|
|
var localAABB = LocalAABB;
|
|
var center = broadphase.Owner.Transform.WorldMatrix.Transform(localAABB.Center);
|
|
|
|
return new Box2Rotated(localAABB.Translated(center), broadphase.Owner.Transform.WorldRotation, center).CalcBoundingBox();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Linked-list of all of our contacts.
|
|
/// </summary>
|
|
internal ContactEdge? ContactEdges { get; set; } = null;
|
|
|
|
/// <summary>
|
|
/// Linked-list of all of our joints.
|
|
/// </summary>
|
|
internal JointEdge? JointEdges { get; set; } = null;
|
|
// TODO: Should there be a VV thing for joints? Would be useful. Same with contacts.
|
|
// Though not sure how to do it well with the linked-list.
|
|
|
|
public bool IgnorePaused { get; set; }
|
|
|
|
internal PhysicsMap PhysicsMap { get; set; } = default!;
|
|
|
|
/// <inheritdoc />
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public BodyType BodyType
|
|
{
|
|
get => _bodyType;
|
|
set
|
|
{
|
|
if (_bodyType == value)
|
|
return;
|
|
|
|
var oldType = _bodyType;
|
|
_bodyType = value;
|
|
|
|
ResetMassData();
|
|
|
|
if (_bodyType == BodyType.Static)
|
|
{
|
|
SetAwake(false);
|
|
_linVelocity = Vector2.Zero;
|
|
_angVelocity = 0.0f;
|
|
// SynchronizeFixtures(); TODO: When CCD
|
|
}
|
|
else
|
|
{
|
|
SetAwake(true);
|
|
}
|
|
|
|
Force = Vector2.Zero;
|
|
Torque = 0.0f;
|
|
|
|
EntitySystem.Get<SharedBroadphaseSystem>().RegenerateContacts(this);
|
|
|
|
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PhysicsBodyTypeChangedEvent(_bodyType, oldType), false);
|
|
}
|
|
}
|
|
|
|
|
|
[DataField("bodyType")]
|
|
private BodyType _bodyType = BodyType.Static;
|
|
|
|
/// <summary>
|
|
/// Set awake without the sleeptimer being reset.
|
|
/// </summary>
|
|
internal void ForceAwake()
|
|
{
|
|
if (_awake || _bodyType == BodyType.Static) return;
|
|
|
|
_awake = true;
|
|
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PhysicsWakeMessage(this));
|
|
}
|
|
|
|
// We'll also block Static bodies from ever being awake given they don't need to move.
|
|
/// <inheritdoc />
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool Awake
|
|
{
|
|
get => _awake;
|
|
set
|
|
{
|
|
if (_bodyType == BodyType.Static) return;
|
|
|
|
SetAwake(value);
|
|
}
|
|
}
|
|
|
|
private bool _awake = true;
|
|
|
|
private void SetAwake(bool value)
|
|
{
|
|
if (_awake == value) return;
|
|
_awake = value;
|
|
|
|
if (value)
|
|
{
|
|
_sleepTime = 0.0f;
|
|
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PhysicsWakeMessage(this));
|
|
}
|
|
else
|
|
{
|
|
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PhysicsSleepMessage(this));
|
|
ResetDynamics();
|
|
_sleepTime = 0.0f;
|
|
}
|
|
|
|
Dirty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// You can disable sleeping on this body. If you disable sleeping, the
|
|
/// body will be woken.
|
|
/// </summary>
|
|
/// <value><c>true</c> if sleeping is allowed; otherwise, <c>false</c>.</value>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool SleepingAllowed
|
|
{
|
|
get => _sleepingAllowed;
|
|
set
|
|
{
|
|
if (_sleepingAllowed == value)
|
|
return;
|
|
|
|
if (!value)
|
|
Awake = true;
|
|
|
|
_sleepingAllowed = value;
|
|
Dirty();
|
|
}
|
|
}
|
|
|
|
[DataField("sleepingAllowed")]
|
|
private bool _sleepingAllowed = true;
|
|
|
|
[ViewVariables]
|
|
public float SleepTime
|
|
{
|
|
get => _sleepTime;
|
|
set
|
|
{
|
|
DebugTools.Assert(!float.IsNaN(value));
|
|
|
|
if (MathHelper.CloseTo(value, _sleepTime))
|
|
return;
|
|
|
|
_sleepTime = value;
|
|
}
|
|
}
|
|
|
|
[DataField("sleepTime")]
|
|
private float _sleepTime;
|
|
|
|
/// <inheritdoc />
|
|
public void WakeBody()
|
|
{
|
|
Awake = true;
|
|
}
|
|
|
|
void ISerializationHooks.AfterDeserialization()
|
|
{
|
|
FixtureCount = _fixtures.Count;
|
|
|
|
foreach (var fixture in _fixtures)
|
|
{
|
|
fixture.Body = this;
|
|
fixture.ComputeProperties();
|
|
fixture.ID = GetFixtureName(fixture);
|
|
}
|
|
|
|
ResetMassData();
|
|
|
|
if (_mass > 0f && (BodyType & (BodyType.Dynamic | BodyType.KinematicController)) != 0)
|
|
{
|
|
_invMass = 1.0f / _mass;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override ComponentState GetComponentState(ICommonSession session)
|
|
{
|
|
// TODO: Could optimise the shit out of this because only linear velocity and angular velocity are changing 99% of the time.
|
|
var joints = new List<Joint>();
|
|
for (var je = JointEdges; je != null; je = je.Next)
|
|
{
|
|
joints.Add(je.Joint);
|
|
}
|
|
|
|
return new PhysicsComponentState(_canCollide, _sleepingAllowed, _fixedRotation, _bodyStatus, _fixtures, joints, LinearVelocity, AngularVelocity, BodyType);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
|
{
|
|
if (curState is not PhysicsComponentState newState)
|
|
return;
|
|
|
|
SleepingAllowed = newState.SleepingAllowed;
|
|
FixedRotation = newState.FixedRotation;
|
|
CanCollide = newState.CanCollide;
|
|
BodyStatus = newState.Status;
|
|
|
|
// So transform doesn't apply MapId in the HandleComponentState because ??? so MapId can still be 0.
|
|
// Fucking kill me, please. You have no idea deep the rabbit hole of shitcode goes to make this work.
|
|
|
|
// We will pray that this deferred joint is handled properly.
|
|
|
|
/*
|
|
* -- Joints --
|
|
*/
|
|
|
|
// TODO: Iterating like this is inefficient and bloated as fuck but on the other hand the linked-list is very convenient
|
|
// for bodies with a large number of fixtures / joints.
|
|
// Probably store them in Dictionaries but still store the linked-list stuff on the fixture / joint itself.
|
|
var existingJoints = Joints.ToList();
|
|
var toAddJoints = new List<Joint>();
|
|
var toRemoveJoints = new List<Joint>();
|
|
|
|
foreach (var newJoint in newState.Joints)
|
|
{
|
|
var jointFound = false;
|
|
|
|
foreach (var joint in existingJoints)
|
|
{
|
|
if (joint.ID.Equals(newJoint.ID))
|
|
{
|
|
if (!newJoint.Equals(joint) && TrySetupNetworkedJoint(newJoint))
|
|
{
|
|
toAddJoints.Add(newJoint);
|
|
toRemoveJoints.Add(joint);
|
|
}
|
|
|
|
jointFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!jointFound && TrySetupNetworkedJoint(newJoint))
|
|
{
|
|
toAddJoints.Add(newJoint);
|
|
}
|
|
}
|
|
|
|
foreach (var joint in existingJoints)
|
|
{
|
|
var jointFound = false;
|
|
|
|
foreach (var newJoint in newState.Joints)
|
|
{
|
|
if (joint.ID.Equals(newJoint.ID))
|
|
{
|
|
jointFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (jointFound) continue;
|
|
|
|
toRemoveJoints.Add(joint);
|
|
}
|
|
|
|
foreach (var joint in toRemoveJoints)
|
|
{
|
|
RemoveJoint(joint);
|
|
}
|
|
|
|
foreach (var joint in toAddJoints)
|
|
{
|
|
AddJoint(joint);
|
|
}
|
|
|
|
/*
|
|
* -- Fixtures --
|
|
*/
|
|
|
|
var toAddFixtures = new List<Fixture>();
|
|
var toRemoveFixtures = new List<Fixture>();
|
|
var computeProperties = false;
|
|
|
|
// Given a bunch of data isn't serialized need to sort of re-initialise it
|
|
var newFixtures = new List<Fixture>(newState.Fixtures.Count);
|
|
foreach (var fixture in newState.Fixtures)
|
|
{
|
|
var newFixture = new Fixture();
|
|
fixture.CopyTo(newFixture);
|
|
newFixture.Body = this;
|
|
newFixtures.Add(newFixture);
|
|
}
|
|
|
|
// Add / update new fixtures
|
|
foreach (var fixture in newFixtures)
|
|
{
|
|
var found = false;
|
|
|
|
foreach (var existing in _fixtures)
|
|
{
|
|
if (!fixture.ID.Equals(existing.ID)) continue;
|
|
|
|
if (!fixture.Equals(existing))
|
|
{
|
|
toAddFixtures.Add(fixture);
|
|
toRemoveFixtures.Add(existing);
|
|
}
|
|
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
toAddFixtures.Add(fixture);
|
|
}
|
|
}
|
|
|
|
// Remove old fixtures
|
|
foreach (var existing in _fixtures)
|
|
{
|
|
var found = false;
|
|
|
|
foreach (var fixture in newFixtures)
|
|
{
|
|
if (fixture.ID.Equals(existing.ID))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
toRemoveFixtures.Add(existing);
|
|
}
|
|
}
|
|
|
|
var broadphaseSystem = EntitySystem.Get<SharedBroadphaseSystem>();
|
|
|
|
foreach (var fixture in toRemoveFixtures)
|
|
{
|
|
computeProperties = true;
|
|
broadphaseSystem.DestroyFixture(this, fixture);
|
|
}
|
|
|
|
// TODO: We also still need event listeners for shapes (Probably need C# events)
|
|
foreach (var fixture in toAddFixtures)
|
|
{
|
|
computeProperties = true;
|
|
broadphaseSystem.CreateFixture(this, fixture);
|
|
fixture.Shape.ApplyState();
|
|
}
|
|
|
|
/*
|
|
* -- Sundries --
|
|
*/
|
|
|
|
Dirty();
|
|
if (computeProperties)
|
|
{
|
|
ResetMassData();
|
|
}
|
|
|
|
LinearVelocity = newState.LinearVelocity;
|
|
// Logger.Debug($"{IGameTiming.TickStampStatic}: [{Owner}] {LinearVelocity}");
|
|
AngularVelocity = newState.AngularVelocity;
|
|
BodyType = newState.BodyType;
|
|
Predict = false;
|
|
}
|
|
|
|
private bool TrySetupNetworkedJoint(Joint joint)
|
|
{
|
|
// This can fail if we've already deleted the entity or remove physics from it I think?
|
|
|
|
if (!Owner.EntityManager.TryGetEntity(joint.BodyAUid, out var entityA) ||
|
|
!entityA.TryGetComponent(out PhysicsComponent? bodyA))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Owner.EntityManager.TryGetEntity(joint.BodyBUid, out var entityB) ||
|
|
!entityB.TryGetComponent(out PhysicsComponent? bodyB))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
joint.BodyA = bodyA;
|
|
joint.BodyB = bodyB;
|
|
joint.EdgeA = new JointEdge();
|
|
joint.EdgeB = new JointEdge();
|
|
return true;
|
|
}
|
|
|
|
public Fixture? GetFixture(string name)
|
|
{
|
|
// Sooo I'd rather have fixtures as a list in serialization but there's not really an easy way to have it as a
|
|
// temporary value on deserialization so we can store it as a dictionary of <name, fixture>
|
|
// given 100% of bodies have 1-2 fixtures this isn't really a performance problem right now but
|
|
// should probably be done at some stage
|
|
// If we really need it then you just deserialize onto a dummy field that then just never gets used again.
|
|
foreach (var fixture in _fixtures)
|
|
{
|
|
if (fixture.ID.Equals(name))
|
|
{
|
|
return fixture;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the dynamics of this body.
|
|
/// Sets torque, force and linear/angular velocity to 0
|
|
/// </summary>
|
|
public void ResetDynamics()
|
|
{
|
|
Torque = 0;
|
|
_angVelocity = 0;
|
|
Force = Vector2.Zero;
|
|
_linVelocity = Vector2.Zero;
|
|
Dirty();
|
|
}
|
|
|
|
public Box2 GetWorldAABB(Vector2? worldPos = null, Angle? worldRot = null)
|
|
{
|
|
worldPos ??= Owner.Transform.WorldPosition;
|
|
worldRot ??= Owner.Transform.WorldRotation;
|
|
var transform = new Transform(worldPos.Value, (float) worldRot.Value.Theta);
|
|
|
|
var bounds = new Box2(transform.Position, transform.Position);
|
|
|
|
foreach (var fixture in _fixtures)
|
|
{
|
|
for (var i = 0; i < fixture.Shape.ChildCount; i++)
|
|
{
|
|
var boundy = fixture.Shape.ComputeAABB(transform, i);
|
|
bounds = bounds.Union(boundy);
|
|
}
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
[ViewVariables]
|
|
public IReadOnlyList<Fixture> Fixtures => _fixtures;
|
|
|
|
public IEnumerable<Joint> Joints
|
|
{
|
|
get
|
|
{
|
|
JointEdge? edge = JointEdges;
|
|
|
|
while (edge != null)
|
|
{
|
|
yield return edge.Joint;
|
|
edge = edge.Next;
|
|
}
|
|
}
|
|
}
|
|
|
|
[ViewVariables]
|
|
public int FixtureCount { get; internal set; }
|
|
|
|
[DataField("fixtures")]
|
|
[NeverPushInheritance]
|
|
internal List<Fixture> _fixtures = new();
|
|
|
|
/// <summary>
|
|
/// Enables or disabled collision processing of this component.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Also known as Enabled in Box2D
|
|
/// </remarks>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool CanCollide
|
|
{
|
|
get => _canCollide;
|
|
set
|
|
{
|
|
if (_canCollide == value)
|
|
return;
|
|
|
|
_canCollide = value;
|
|
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollisionChangeMessage(this, Owner.Uid, _canCollide));
|
|
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PhysicsUpdateMessage(this));
|
|
Dirty();
|
|
}
|
|
}
|
|
|
|
[DataField("canCollide")]
|
|
private bool _canCollide = true;
|
|
|
|
/// <summary>
|
|
/// Non-hard physics bodies will not cause action collision (e.g. blocking of movement)
|
|
/// while still raising collision events. Recommended you use the fixture hard values directly
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is useful for triggers or such to detect collision without actually causing a blockage.
|
|
/// </remarks>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool Hard
|
|
{
|
|
get
|
|
{
|
|
foreach (var fixture in Fixtures)
|
|
{
|
|
if (fixture.Hard) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
set
|
|
{
|
|
foreach (var fixture in Fixtures)
|
|
{
|
|
fixture.Hard = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bitmask of the collision layers this component is a part of.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int CollisionLayer
|
|
{
|
|
get
|
|
{
|
|
var layers = 0x0;
|
|
|
|
foreach (var fixture in Fixtures)
|
|
layers |= fixture.CollisionLayer;
|
|
return layers;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bitmask of the layers this component collides with.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int CollisionMask
|
|
{
|
|
get
|
|
{
|
|
var mask = 0x0;
|
|
|
|
foreach (var fixture in Fixtures)
|
|
mask |= fixture.CollisionMask;
|
|
return mask;
|
|
}
|
|
}
|
|
|
|
// I made Mass read-only just because overwriting it doesn't touch inertia.
|
|
/// <summary>
|
|
/// Current mass of the entity in kilograms.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float Mass => (BodyType & (BodyType.Dynamic | BodyType.KinematicController)) != 0 ? _mass : 0.0f;
|
|
|
|
private float _mass;
|
|
|
|
/// <summary>
|
|
/// Inverse mass of the entity in kilograms (1 / Mass).
|
|
/// </summary>
|
|
[ViewVariables]
|
|
public float InvMass => (BodyType & (BodyType.Dynamic | BodyType.KinematicController)) != 0 ? _invMass : 0.0f;
|
|
|
|
private float _invMass;
|
|
|
|
/// <summary>
|
|
/// Moment of inertia, or angular mass, in kg * m^2.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// https://en.wikipedia.org/wiki/Moment_of_inertia
|
|
/// </remarks>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float Inertia
|
|
{
|
|
get => _inertia + Mass * Vector2.Dot(Vector2.Zero, Vector2.Zero); // TODO: Sweep.LocalCenter
|
|
set
|
|
{
|
|
DebugTools.Assert(!float.IsNaN(value));
|
|
|
|
if (_bodyType != BodyType.Dynamic) return;
|
|
|
|
if (MathHelper.CloseTo(_inertia, value)) return;
|
|
|
|
if (value > 0.0f && !_fixedRotation)
|
|
{
|
|
_inertia = value - Mass * Vector2.Dot(LocalCenter, LocalCenter);
|
|
DebugTools.Assert(_inertia > 0.0f);
|
|
InvI = 1.0f / _inertia;
|
|
Dirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
private float _inertia;
|
|
|
|
/// <summary>
|
|
/// Indicates whether this body ignores gravity
|
|
/// </summary>
|
|
public bool IgnoreGravity { get; set; }
|
|
|
|
/// <summary>
|
|
/// Inverse moment of inertia (1 / I).
|
|
/// </summary>
|
|
[ViewVariables]
|
|
public float InvI { get; set; }
|
|
|
|
/// <summary>
|
|
/// Is the body allowed to have angular velocity.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool FixedRotation
|
|
{
|
|
get => _fixedRotation;
|
|
set
|
|
{
|
|
if (_fixedRotation == value)
|
|
return;
|
|
|
|
_fixedRotation = value;
|
|
_angVelocity = 0.0f;
|
|
ResetMassData();
|
|
Dirty();
|
|
}
|
|
}
|
|
|
|
// TODO: Should default to false someday IMO
|
|
[DataField("fixedRotation")]
|
|
private bool _fixedRotation = true;
|
|
|
|
// TODO: Will be used someday
|
|
/// <summary>
|
|
/// Get this body's center of mass offset to world position.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// AKA Sweep.LocalCenter in Box2D.
|
|
/// Not currently in use as this is set after mass data gets set (when fixtures update).
|
|
/// </remarks>
|
|
public Vector2 LocalCenter
|
|
{
|
|
get => _localCenter;
|
|
set
|
|
{
|
|
if (_bodyType != BodyType.Dynamic) return;
|
|
if (value.EqualsApprox(_localCenter)) return;
|
|
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
private Vector2 _localCenter = Vector2.Zero;
|
|
|
|
/// <summary>
|
|
/// Current Force being applied to this entity in Newtons.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The force is applied to the center of mass.
|
|
/// https://en.wikipedia.org/wiki/Force
|
|
/// </remarks>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public Vector2 Force { get; set; }
|
|
|
|
/// <summary>
|
|
/// Current torque being applied to this entity in N*m.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The torque rotates around the Z axis on the object.
|
|
/// https://en.wikipedia.org/wiki/Torque
|
|
/// </remarks>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float Torque { get; set; }
|
|
|
|
/// <summary>
|
|
/// Contact friction between 2 bodies.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float Friction
|
|
{
|
|
get => _friction;
|
|
set
|
|
{
|
|
if (MathHelper.CloseTo(value, _friction))
|
|
return;
|
|
|
|
_friction = value;
|
|
// TODO
|
|
// Dirty();
|
|
}
|
|
}
|
|
|
|
private float _friction;
|
|
|
|
/// <summary>
|
|
/// This is a set amount that the body's linear velocity is reduced by every tick.
|
|
/// Combined with the tile friction.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float LinearDamping
|
|
{
|
|
get => _linearDamping;
|
|
set
|
|
{
|
|
DebugTools.Assert(!float.IsNaN(value));
|
|
|
|
if (MathHelper.CloseTo(value, _linearDamping))
|
|
return;
|
|
|
|
_linearDamping = value;
|
|
// Dirty();
|
|
}
|
|
}
|
|
|
|
[DataField("linearDamping")]
|
|
private float _linearDamping = 0.02f;
|
|
|
|
/// <summary>
|
|
/// This is a set amount that the body's angular velocity is reduced every tick.
|
|
/// Combined with the tile friction.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float AngularDamping
|
|
{
|
|
get => _angularDamping;
|
|
set
|
|
{
|
|
DebugTools.Assert(!float.IsNaN(value));
|
|
|
|
if (MathHelper.CloseTo(value, _angularDamping))
|
|
return;
|
|
|
|
_angularDamping = value;
|
|
// Dirty();
|
|
}
|
|
}
|
|
|
|
[DataField("angularDamping")]
|
|
private float _angularDamping = 0.02f;
|
|
|
|
/// <summary>
|
|
/// Get the linear and angular velocities at the same time.
|
|
/// </summary>
|
|
public (Vector2 Linear, float Angular) MapVelocities
|
|
{
|
|
get
|
|
{
|
|
var linearVelocity = _linVelocity;
|
|
var angularVelocity = _angVelocity;
|
|
var parent = Owner.Transform.Parent?.Owner;
|
|
|
|
while (parent != null)
|
|
{
|
|
if (parent.TryGetComponent(out PhysicsComponent? body))
|
|
{
|
|
linearVelocity += body.LinearVelocity;
|
|
angularVelocity += body.AngularVelocity;
|
|
}
|
|
|
|
parent = parent.Transform.Parent?.Owner;
|
|
}
|
|
|
|
return (linearVelocity, angularVelocity);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Current linear velocity of the entity in meters per second.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public Vector2 LinearVelocity
|
|
{
|
|
get => _linVelocity;
|
|
set
|
|
{
|
|
// Curse you Q
|
|
// DebugTools.Assert(!float.IsNaN(value.X) && !float.IsNaN(value.Y));
|
|
|
|
if (BodyType == BodyType.Static)
|
|
return;
|
|
|
|
if (Vector2.Dot(value, value) > 0.0f)
|
|
Awake = true;
|
|
|
|
if (_linVelocity.EqualsApprox(value, 0.0001))
|
|
return;
|
|
|
|
_linVelocity = value;
|
|
Dirty();
|
|
}
|
|
}
|
|
|
|
private Vector2 _linVelocity;
|
|
|
|
/// <summary>
|
|
/// Get the body's LinearVelocity in map terms.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Consider using <see cref="MapVelocities"/> if you need linear and angular at the same time.
|
|
/// </remarks>
|
|
public Vector2 MapLinearVelocity
|
|
{
|
|
get
|
|
{
|
|
var velocity = _linVelocity;
|
|
var parent = Owner.Transform.Parent?.Owner;
|
|
|
|
while (parent != null)
|
|
{
|
|
if (parent.TryGetComponent(out PhysicsComponent? body))
|
|
{
|
|
velocity += body.LinearVelocity;
|
|
}
|
|
|
|
parent = parent.Transform.Parent?.Owner;
|
|
}
|
|
|
|
return velocity;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Current angular velocity of the entity in radians per sec.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float AngularVelocity
|
|
{
|
|
get => _angVelocity;
|
|
set
|
|
{
|
|
// TODO: This and linearvelocity asserts
|
|
// DebugTools.Assert(!float.IsNaN(value));
|
|
|
|
if (BodyType == BodyType.Static)
|
|
return;
|
|
|
|
if (value * value > 0.0f)
|
|
Awake = true;
|
|
|
|
if (MathHelper.CloseTo(_angVelocity, value))
|
|
return;
|
|
|
|
_angVelocity = value;
|
|
Dirty();
|
|
}
|
|
}
|
|
|
|
private float _angVelocity;
|
|
|
|
/// <summary>
|
|
/// Get the body's AngularVelocity in map terms.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Consider using <see cref="MapVelocities"/> if you need linear and angular at the same time.
|
|
/// </remarks>
|
|
public float MapAngularVelocity
|
|
{
|
|
get
|
|
{
|
|
var velocity = _angVelocity;
|
|
var parent = Owner.Transform.Parent?.Owner;
|
|
|
|
while (parent != null)
|
|
{
|
|
if (parent.TryGetComponent(out PhysicsComponent? body))
|
|
{
|
|
velocity += body.AngularVelocity;
|
|
}
|
|
|
|
parent = parent.Transform.Parent?.Owner;
|
|
}
|
|
|
|
return velocity;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Current momentum of the entity in kilogram meters per second
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public Vector2 Momentum
|
|
{
|
|
get => LinearVelocity * Mass;
|
|
set => LinearVelocity = value / Mass;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The current status of the object
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public BodyStatus BodyStatus
|
|
{
|
|
get => _bodyStatus;
|
|
set
|
|
{
|
|
if (_bodyStatus == value)
|
|
return;
|
|
|
|
_bodyStatus = value;
|
|
Dirty();
|
|
}
|
|
}
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool Predict
|
|
{
|
|
get => _predict;
|
|
set => _predict = value;
|
|
}
|
|
|
|
private bool _predict;
|
|
|
|
public IEnumerable<PhysicsComponent> GetBodiesIntersecting()
|
|
{
|
|
foreach (var entity in EntitySystem.Get<SharedBroadphaseSystem>().GetCollidingEntities(Owner.Transform.MapID, GetWorldAABB()))
|
|
{
|
|
yield return entity;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a local point relative to the body's origin given a world point.
|
|
/// Note that the vector only takes the rotation into account, not the position.
|
|
/// </summary>
|
|
/// <param name="worldPoint">A point in world coordinates.</param>
|
|
/// <returns>The corresponding local point relative to the body's origin.</returns>
|
|
public Vector2 GetLocalPoint(in Vector2 worldPoint)
|
|
{
|
|
return Transform.MulT(GetTransform(), worldPoint);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the world coordinates of a point given the local coordinates.
|
|
/// </summary>
|
|
/// <param name="localPoint">A point on the body measured relative the the body's origin.</param>
|
|
/// <returns>The same point expressed in world coordinates.</returns>
|
|
public Vector2 GetWorldPoint(in Vector2 localPoint)
|
|
{
|
|
return Transform.Mul(GetTransform(), localPoint);
|
|
}
|
|
|
|
public void FixtureChanged(Fixture fixture)
|
|
{
|
|
// TODO: Optimise this a LOT
|
|
Dirty();
|
|
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new FixtureUpdateMessage(this, fixture));
|
|
}
|
|
|
|
public string GetFixtureName(Fixture fixture)
|
|
{
|
|
if (!string.IsNullOrEmpty(fixture.ID)) return fixture.ID;
|
|
|
|
var i = 0;
|
|
|
|
while (true)
|
|
{
|
|
var found = false;
|
|
++i;
|
|
var name = $"fixture_{i}";
|
|
|
|
foreach (var existing in _fixtures)
|
|
{
|
|
if (existing.ID.Equals(name))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
}
|
|
|
|
private string GetJointName(Joint joint)
|
|
{
|
|
var id = joint.ID;
|
|
|
|
if (!string.IsNullOrEmpty(id)) return id;
|
|
|
|
var jointCount = Joints.Count();
|
|
|
|
for (var i = 0; i < jointCount + 1; i++)
|
|
{
|
|
id = $"joint-{i}";
|
|
if (GetJoint(id) != null) continue;
|
|
return id;
|
|
}
|
|
|
|
Logger.WarningS("physics", $"Unable to get a joint ID; using its hashcode instead.");
|
|
return joint.GetHashCode().ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a joint with the specified ID.
|
|
/// </summary>
|
|
public Joint? GetJoint(string id)
|
|
{
|
|
foreach (var joint in Joints)
|
|
{
|
|
if (joint.ID == id) return joint;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal Transform GetTransform()
|
|
{
|
|
return new(Owner.Transform.WorldPosition, (float) Owner.Transform.WorldRotation.Theta);
|
|
}
|
|
|
|
public void ApplyLinearImpulse(in Vector2 impulse)
|
|
{
|
|
if ((_bodyType & (BodyType.Dynamic | BodyType.KinematicController)) == 0x0) return;
|
|
Awake = true;
|
|
|
|
LinearVelocity += impulse * _invMass;
|
|
}
|
|
|
|
public void ApplyAngularImpulse(float impulse)
|
|
{
|
|
if ((_bodyType & (BodyType.Dynamic | BodyType.KinematicController)) == 0x0) return;
|
|
Awake = true;
|
|
|
|
AngularVelocity += impulse * InvI;
|
|
}
|
|
|
|
public void ApplyForce(in Vector2 force)
|
|
{
|
|
if (_bodyType != BodyType.Dynamic) return;
|
|
|
|
Awake = true;
|
|
Force += force;
|
|
}
|
|
|
|
// TOOD: Need SetTransformIgnoreContacts so we can teleport body and /ignore contacts/
|
|
public void DestroyContacts()
|
|
{
|
|
ContactEdge? contactEdge = ContactEdges;
|
|
while (contactEdge != null)
|
|
{
|
|
var contactEdge0 = contactEdge;
|
|
contactEdge = contactEdge.Next;
|
|
PhysicsMap.ContactManager.Destroy(contactEdge0.Contact!);
|
|
}
|
|
|
|
ContactEdges = null;
|
|
}
|
|
|
|
IEnumerable<IPhysBody> IPhysBody.GetCollidingEntities(Vector2 offset, bool approx)
|
|
{
|
|
return EntitySystem.Get<SharedBroadphaseSystem>().GetCollidingEntities(this, offset, approx);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
if (BodyType == BodyType.Static)
|
|
{
|
|
_awake = false;
|
|
}
|
|
|
|
// TODO: Ordering fuckery need a new PR to fix some of this stuff
|
|
PhysicsMap = EntitySystem.Get<SharedPhysicsSystem>().Maps[Owner.Transform.MapID];
|
|
|
|
Dirty();
|
|
// Yeah yeah TODO Combine these
|
|
// Implicitly assume that stuff doesn't cover if a non-collidable is initialized.
|
|
|
|
if (CanCollide)
|
|
{
|
|
if (!Awake)
|
|
{
|
|
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PhysicsSleepMessage(this));
|
|
}
|
|
else
|
|
{
|
|
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PhysicsWakeMessage(this));
|
|
}
|
|
|
|
if (Owner.IsInContainer())
|
|
{
|
|
_canCollide = false;
|
|
}
|
|
else
|
|
{
|
|
// TODO: Probably a bad idea but ehh future sloth's problem; namely that we have to duplicate code between here and CanCollide.
|
|
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new CollisionChangeMessage(this, Owner.Uid, _canCollide));
|
|
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PhysicsUpdateMessage(this));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClearJoints()
|
|
{
|
|
for (var je = JointEdges; je != null; je = je.Next)
|
|
{
|
|
RemoveJoint(je.Joint);
|
|
}
|
|
}
|
|
|
|
public void AddJoint(Joint joint)
|
|
{
|
|
var id = GetJointName(joint);
|
|
|
|
foreach (var existing in Joints)
|
|
{
|
|
// This can happen if a server created joint is sent and applied before the client can create it locally elsewhere
|
|
if (existing.ID.Equals(id)) return;
|
|
}
|
|
|
|
PhysicsMap.AddJoint(joint);
|
|
joint.ID = id;
|
|
Logger.DebugS("physics", $"Added joint id: {joint.ID} type: {joint.GetType().Name} to {Owner}");
|
|
}
|
|
|
|
public void RemoveJoint(Joint joint)
|
|
{
|
|
PhysicsMap.RemoveJoint(joint);
|
|
Logger.DebugS("physics", $"Removed joint id: {joint.ID} type: {joint.GetType().Name} from {Owner}");
|
|
}
|
|
|
|
protected override void OnRemove()
|
|
{
|
|
base.OnRemove();
|
|
// Need to do these immediately in case collision behaviors deleted the body
|
|
// TODO: Could be more optimal as currently broadphase will call this ANYWAY
|
|
DestroyContacts();
|
|
EntitySystem.Get<SharedBroadphaseSystem>().RemoveBody(this);
|
|
CanCollide = false;
|
|
}
|
|
|
|
public void ResetMassData()
|
|
{
|
|
_mass = 0.0f;
|
|
_invMass = 0.0f;
|
|
_inertia = 0.0f;
|
|
InvI = 0.0f;
|
|
// Sweep
|
|
|
|
if (((int) _bodyType & (int) BodyType.Kinematic) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var localCenter = Vector2.Zero;
|
|
|
|
foreach (var fixture in _fixtures)
|
|
{
|
|
if (fixture.Mass <= 0.0f) continue;
|
|
|
|
var fixMass = fixture.Mass;
|
|
|
|
_mass += fixMass;
|
|
|
|
var center = Vector2.Zero;
|
|
|
|
// TODO: God this is garbage
|
|
switch (fixture.Shape)
|
|
{
|
|
case PhysShapeAabb aabb:
|
|
center = aabb.Centroid;
|
|
break;
|
|
case PhysShapeRect rect:
|
|
center = rect.Centroid;
|
|
break;
|
|
case PolygonShape poly:
|
|
center = poly.Centroid;
|
|
break;
|
|
case PhysShapeCircle circle:
|
|
center = circle.Position;
|
|
break;
|
|
}
|
|
|
|
localCenter += center * fixMass;
|
|
_inertia += fixture.Inertia;
|
|
}
|
|
|
|
if (BodyType == BodyType.Static)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_mass > 0.0f)
|
|
{
|
|
_invMass = 1.0f / _mass;
|
|
localCenter *= _invMass;
|
|
}
|
|
else
|
|
{
|
|
// Always need positive mass.
|
|
_mass = 1.0f;
|
|
_invMass = 1.0f;
|
|
}
|
|
|
|
if (_inertia > 0.0f && !_fixedRotation)
|
|
{
|
|
// Center inertia about center of mass.
|
|
_inertia -= _mass * Vector2.Dot(localCenter, localCenter);
|
|
|
|
DebugTools.Assert(_inertia > 0.0f);
|
|
InvI = 1.0f / _inertia;
|
|
}
|
|
else
|
|
{
|
|
_inertia = 0.0f;
|
|
InvI = 0.0f;
|
|
}
|
|
|
|
/* TODO
|
|
// Move center of mass;
|
|
var oldCenter = _sweep.Center;
|
|
_sweep.LocalCenter = localCenter;
|
|
_sweep.Center0 = _sweep.Center = Physics.Transform.Mul(GetTransform(), _sweep.LocalCenter);
|
|
|
|
// Update center of mass velocity.
|
|
var a = _sweep.Center - oldCenter;
|
|
_linVelocity += new Vector2(-_angVelocity * a.y, _angVelocity * a.x);
|
|
*/
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to prevent bodies from colliding; may lie depending on joints.
|
|
/// </summary>
|
|
/// <param name="other"></param>
|
|
/// <returns></returns>
|
|
internal bool ShouldCollide(PhysicsComponent other)
|
|
{
|
|
if ((_bodyType & (BodyType.Kinematic | BodyType.Static)) != 0 &&
|
|
(other._bodyType & (BodyType.Kinematic | BodyType.Static)) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Does a joint prevent collision?
|
|
for (var jn = JointEdges; jn != null; jn = jn.Next)
|
|
{
|
|
if (jn.Other != other) continue;
|
|
if (jn.Joint.CollideConnected) continue;
|
|
return false;
|
|
}
|
|
|
|
var preventCollideMessage = new PreventCollideEvent(this, other);
|
|
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, preventCollideMessage);
|
|
|
|
if (preventCollideMessage.Cancelled) return false;
|
|
|
|
preventCollideMessage = new PreventCollideEvent(other, this);
|
|
Owner.EntityManager.EventBus.RaiseLocalEvent(other.Owner.Uid, preventCollideMessage);
|
|
|
|
if (preventCollideMessage.Cancelled) return false;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Directed event raised when an entity's physics BodyType changes.
|
|
/// </summary>
|
|
public class PhysicsBodyTypeChangedEvent : EntityEventArgs
|
|
{
|
|
/// <summary>
|
|
/// New BodyType of the entity.
|
|
/// </summary>
|
|
public BodyType New { get; }
|
|
|
|
/// <summary>
|
|
/// Old BodyType of the entity.
|
|
/// </summary>
|
|
public BodyType Old { get; }
|
|
|
|
public PhysicsBodyTypeChangedEvent(BodyType newType, BodyType oldType)
|
|
{
|
|
New = newType;
|
|
Old = oldType;
|
|
}
|
|
}
|
|
}
|