/* * 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 JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Dynamics.Contacts; 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(), ComponentProtoName("Physics")] public sealed class PhysicsComponent : Component, IPhysBody, ISerializationHooks, ILookupWorldBox2Component { [Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IEntitySystemManager _sysMan = default!; [DataField("status", readOnly: true)] private BodyStatus _bodyStatus = BodyStatus.OnGround; /// /// Has this body been added to an island previously in this tick. /// public bool Island { get; set; } internal BroadphaseComponent? Broadphase { get; set; } /// /// Store the body's index within the island so we can lookup its data. /// Key is Island's ID and value is our index. /// public Dictionary IslandIndex { get; set; } = new(); // TODO: Actually implement after the initial pr dummy /// /// Gets or sets where this body should be included in the CCD solver. /// public bool IsBullet { get; set; } public bool IgnoreCCD { get; set; } // TODO: Placeholder; look it's disgusting but my main concern is stopping fixtures being serialized every tick // on physics bodies for massive shuttle perf savings. [Obsolete("Use FixturesComponent instead.")] public IReadOnlyList Fixtures => _entMan.GetComponent(Owner).Fixtures.Values.ToList(); public int FixtureCount => _entMan.GetComponent(Owner).Fixtures.Count; [ViewVariables] public int ContactCount => Contacts.Count; /// /// Linked-list of all of our contacts. /// internal LinkedList Contacts = new(); public bool IgnorePaused { get; set; } internal SharedPhysicsMapComponent? PhysicsMap { get; set; } /// [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); _linearVelocity = Vector2.Zero; _angularVelocity = 0.0f; // SynchronizeFixtures(); TODO: When CCD } else { SetAwake(true); } Force = Vector2.Zero; Torque = 0.0f; _sysMan.GetEntitySystem().RegenerateContacts(this); _entMan.EventBus.RaiseLocalEvent(Owner, new PhysicsBodyTypeChangedEvent(_bodyType, oldType), false); } } [DataField("bodyType")] private BodyType _bodyType = BodyType.Static; /// /// Set awake without the sleeptimer being reset. /// internal void ForceAwake() { if (_awake || _bodyType == BodyType.Static) return; _awake = true; _entMan.EventBus.RaiseEvent(EventSource.Local, new PhysicsWakeMessage(this)); } // We'll also block Static bodies from ever being awake given they don't need to move. /// [ViewVariables(VVAccess.ReadWrite)] public bool Awake { get => _awake; set { if (_bodyType == BodyType.Static) return; SetAwake(value); } } internal bool _awake = true; private void SetAwake(bool value) { if (_awake == value) return; _awake = value; if (value) { _sleepTime = 0.0f; _entMan.EventBus.RaiseLocalEvent(Owner, new PhysicsWakeMessage(this)); } else { _entMan.EventBus.RaiseLocalEvent(Owner, new PhysicsSleepMessage(this)); ResetDynamics(); _sleepTime = 0.0f; } Dirty(_entMan); } /// /// You can disable sleeping on this body. If you disable sleeping, the /// body will be woken. /// /// true if sleeping is allowed; otherwise, false. [ViewVariables(VVAccess.ReadWrite)] public bool SleepingAllowed { get => _sleepingAllowed; set { if (_sleepingAllowed == value) return; if (!value) Awake = true; _sleepingAllowed = value; Dirty(_entMan); } } [DataField("sleepingAllowed")] private bool _sleepingAllowed = true; [ViewVariables] public float SleepTime { get => _sleepTime; set { DebugTools.Assert(!float.IsNaN(value)); if (MathHelper.CloseToPercent(value, _sleepTime)) return; _sleepTime = value; } } [DataField("sleepTime")] private float _sleepTime; /// public void WakeBody() { Awake = true; } /// public override ComponentState GetComponentState() { return new PhysicsComponentState(_canCollide, _sleepingAllowed, _fixedRotation, _bodyStatus, _linearVelocity, _angularVelocity, _bodyType); } /// 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. Dirty(_entMan); LinearVelocity = newState.LinearVelocity; // Logger.Debug($"{IGameTiming.TickStampStatic}: [{Owner}] {LinearVelocity}"); AngularVelocity = newState.AngularVelocity; BodyType = newState.BodyType; Predict = false; } /// /// Resets the dynamics of this body. /// Sets torque, force and linear/angular velocity to 0 /// public void ResetDynamics() { Torque = 0; _angularVelocity = 0; Force = Vector2.Zero; _linearVelocity = Vector2.Zero; Dirty(_entMan); } public Box2 GetAABB(Transform transform) { var bounds = new Box2(transform.Position, transform.Position); foreach (var fixture in _entMan.GetComponent(Owner).Fixtures.Values) { for (var i = 0; i < fixture.Shape.ChildCount; i++) { var boundy = fixture.Shape.ComputeAABB(transform, i); bounds = bounds.Union(boundy); } } return bounds; } [Obsolete("Use the GetWorldAABB on EntityLookupSystem")] public Box2 GetWorldAABB(Vector2? worldPos = null, Angle? worldRot = null) { if (worldPos == null && worldRot == null) { (worldPos, worldRot) = _entMan.GetComponent(Owner).GetWorldPositionRotation(); } else { worldPos ??= _entMan.GetComponent(Owner).WorldPosition; worldRot ??= _entMan.GetComponent(Owner).WorldRotation; } var transform = new Transform(worldPos.Value, (float) worldRot.Value.Theta); var bounds = new Box2(transform.Position, transform.Position); foreach (var fixture in _entMan.GetComponent(Owner).Fixtures.Values) { for (var i = 0; i < fixture.Shape.ChildCount; i++) { var boundy = fixture.Shape.ComputeAABB(transform, i); bounds = bounds.Union(boundy); } } return bounds; } /// /// Enables or disabled collision processing of this component. /// /// /// Also known as Enabled in Box2D /// [ViewVariables(VVAccess.ReadWrite)] public bool CanCollide { get => _canCollide; set { if (_canCollide == value || value && Owner.IsInContainer(_entMan)) return; _canCollide = value; _entMan.EventBus.RaiseEvent(EventSource.Local, new CollisionChangeMessage(this, Owner, _canCollide)); Dirty(_entMan); } } [DataField("canCollide")] internal bool _canCollide = true; /// /// 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 /// /// /// This is useful for triggers or such to detect collision without actually causing a blockage. /// [ViewVariables(VVAccess.ReadWrite)] public bool Hard { get; internal set; } /// /// Bitmask of the collision layers this component is a part of. /// [ViewVariables] public int CollisionLayer { get; internal set; } /// /// Bitmask of the layers this component collides with. /// [ViewVariables] public int CollisionMask { get; internal set; } // I made Mass read-only just because overwriting it doesn't touch inertia. /// /// Current mass of the entity in kilograms. /// [ViewVariables(VVAccess.ReadWrite)] public float Mass => (BodyType & (BodyType.Dynamic | BodyType.KinematicController)) != 0 ? _mass : 0.0f; private float _mass; /// /// Inverse mass of the entity in kilograms (1 / Mass). /// [ViewVariables] public float InvMass => (BodyType & (BodyType.Dynamic | BodyType.KinematicController)) != 0 ? _invMass : 0.0f; private float _invMass; /// /// Moment of inertia, or angular mass, in kg * m^2. /// /// /// https://en.wikipedia.org/wiki/Moment_of_inertia /// [ViewVariables(VVAccess.ReadWrite)] public float Inertia { get => _inertia + _mass * Vector2.Dot(_localCenter, _localCenter); set { DebugTools.Assert(!float.IsNaN(value)); if (_bodyType != BodyType.Dynamic) return; if (MathHelper.CloseToPercent(_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(_entMan); } } } private float _inertia; /// /// Indicates whether this body ignores gravity /// public bool IgnoreGravity { get; set; } /// /// Inverse moment of inertia (1 / I). /// [ViewVariables] public float InvI { get; set; } /// /// Is the body allowed to have angular velocity. /// [ViewVariables(VVAccess.ReadWrite)] public bool FixedRotation { get => _fixedRotation; set { if (_fixedRotation == value) return; _fixedRotation = value; _angularVelocity = 0.0f; ResetMassData(); Dirty(_entMan); } } // TODO: Should default to false someday IMO [DataField("fixedRotation")] private bool _fixedRotation = true; /// /// Get this body's center of mass offset to world position. /// /// /// AKA Sweep.LocalCenter in Box2D. /// Not currently in use as this is set after mass data gets set (when fixtures update). /// [ViewVariables] public Vector2 LocalCenter { get => _localCenter; set { if (_bodyType != BodyType.Dynamic) return; if (value.EqualsApprox(_localCenter)) return; _localCenter = value; } } private Vector2 _localCenter = Vector2.Zero; /// /// Current Force being applied to this entity in Newtons. /// /// /// The force is applied to the center of mass. /// https://en.wikipedia.org/wiki/Force /// [ViewVariables(VVAccess.ReadWrite)] public Vector2 Force { get; set; } /// /// Current torque being applied to this entity in N*m. /// /// /// The torque rotates around the Z axis on the object. /// https://en.wikipedia.org/wiki/Torque /// [ViewVariables(VVAccess.ReadWrite)] public float Torque { get; set; } /// /// Contact friction between 2 bodies. /// [ViewVariables(VVAccess.ReadWrite)] public float Friction { get => _friction; set { if (MathHelper.CloseToPercent(value, _friction)) return; _friction = value; // TODO // Dirty(_entMan); } } private float _friction; /// /// This is a set amount that the body's linear velocity is reduced by every tick. /// Combined with the tile friction. /// [ViewVariables(VVAccess.ReadWrite)] public float LinearDamping { get => _linearDamping; set { DebugTools.Assert(!float.IsNaN(value)); if (MathHelper.CloseToPercent(value, _linearDamping)) return; _linearDamping = value; // Dirty(_entMan); } } [DataField("linearDamping")] private float _linearDamping = 0.2f; /// /// This is a set amount that the body's angular velocity is reduced every tick. /// Combined with the tile friction. /// /// [ViewVariables(VVAccess.ReadWrite)] public float AngularDamping { get => _angularDamping; set { DebugTools.Assert(!float.IsNaN(value)); if (MathHelper.CloseToPercent(value, _angularDamping)) return; _angularDamping = value; // Dirty(_entMan); } } [DataField("angularDamping")] private float _angularDamping = 0.2f; /// /// Current linear velocity of the entity in meters per second. /// /// /// This is the velocity relative to the parent, but is defined in terms of map coordinates. I.e., if the /// entity's parents are all stationary, this is the rate of change of this entity's world position (not /// local position). /// [ViewVariables(VVAccess.ReadWrite)] public Vector2 LinearVelocity { get => _linearVelocity; 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 (_linearVelocity.EqualsApprox(value, 0.0001f)) return; _linearVelocity = value; Dirty(_entMan); } } internal Vector2 _linearVelocity; /// /// Current angular velocity of the entity in radians per sec. /// [ViewVariables(VVAccess.ReadWrite)] public float AngularVelocity { get => _angularVelocity; set { // TODO: This and linearvelocity asserts // DebugTools.Assert(!float.IsNaN(value)); if (BodyType == BodyType.Static) return; if (value * value > 0.0f) Awake = true; // CloseToPercent tolerance needs to be small enough such that an angular velocity just above // sleep-tolerance can damp down to sleeping. if (MathHelper.CloseToPercent(_angularVelocity, value, 0.00001f)) return; _angularVelocity = value; Dirty(_entMan); } } private float _angularVelocity; /// /// Current momentum of the entity in kilogram meters per second /// [ViewVariables(VVAccess.ReadWrite)] public Vector2 Momentum { get => LinearVelocity * Mass; set => LinearVelocity = value / Mass; } /// /// The current status of the object /// [ViewVariables(VVAccess.ReadWrite)] public BodyStatus BodyStatus { get => _bodyStatus; set { if (_bodyStatus == value) return; _bodyStatus = value; Dirty(_entMan); } } [ViewVariables(VVAccess.ReadWrite)] public bool Predict { get => _predict; set => _predict = value; } private bool _predict; public IEnumerable GetBodiesIntersecting() { foreach (var entity in _sysMan.GetEntitySystem().GetCollidingEntities(_entMan.GetComponent(Owner).MapID, GetWorldAABB())) { yield return entity; } } /// /// 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. /// /// A point in world coordinates. /// The corresponding local point relative to the body's origin. public Vector2 GetLocalPoint(in Vector2 worldPoint) { return Transform.MulT(GetTransform(), worldPoint); } /// /// Get the world coordinates of a point given the local coordinates. /// /// A point on the body measured relative the the body's origin. /// The same point expressed in world coordinates. public Vector2 GetWorldPoint(in Vector2 localPoint) { return Transform.Mul(GetTransform(), localPoint); } public Vector2 GetLocalVector2(Vector2 worldVector) { return Transform.MulT(new Quaternion2D((float) _entMan.GetComponent(Owner).WorldRotation.Theta), worldVector); } public Transform GetTransform() { var (worldPos, worldRot) = _entMan.GetComponent(Owner).GetWorldPositionRotation(); var xf = new Transform(worldPos, (float) worldRot.Theta); // xf.Position -= Transform.Mul(xf.Quaternion2D, LocalCenter); return xf; } /// /// Applies an impulse to the centre of mass. /// public void ApplyLinearImpulse(in Vector2 impulse) { if ((_bodyType & (BodyType.Dynamic | BodyType.KinematicController)) == 0x0) return; Awake = true; LinearVelocity += impulse * _invMass; } /// /// Applies an impulse from the specified point. /// public void ApplyLinearImpulse(in Vector2 impulse, in Vector2 point) { if ((_bodyType & (BodyType.Dynamic | BodyType.KinematicController)) == 0x0) return; Awake = true; LinearVelocity += impulse * _invMass; // TODO: Sweep here AngularVelocity += InvI * Vector2.Cross(point, impulse); } 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() { var node = Contacts.First; while (node != null) { var contact = node.Value; node = node.Next; PhysicsMap?.ContactManager.Destroy(contact); } DebugTools.Assert(Contacts.Count == 0); } IEnumerable IPhysBody.GetCollidingEntities(Vector2 offset, bool approx) { return _sysMan.GetEntitySystem().GetCollidingEntities(this, offset, approx); } public void ResetMassData(FixturesComponent? fixtures = null) { _mass = 0.0f; _invMass = 0.0f; _inertia = 0.0f; InvI = 0.0f; _localCenter = Vector2.Zero; if (((int) _bodyType & (int) BodyType.Kinematic) != 0) { return; } // Temporary until ECS don't @ me. fixtures ??= IoCManager.Resolve().GetComponent(Owner); var localCenter = Vector2.Zero; var shapeManager = _sysMan.GetEntitySystem(); foreach (var (_, fixture) in fixtures.Fixtures) { if (fixture.Mass <= 0.0f) continue; var data = new MassData {Mass = fixture.Mass}; shapeManager.GetMassData(fixture.Shape, ref data); _mass += data.Mass; localCenter += data.Center * data.Mass; _inertia += data.I; } 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; } _localCenter = localCenter; // TODO: Calculate Sweep /* var oldCenter = Sweep.Center; Sweep.LocalCenter = localCenter; Sweep.Center0 = Sweep.Center = Transform.Mul(GetTransform(), Sweep.LocalCenter); */ // Update center of mass velocity. // _linVelocity += Vector2.Cross(_angVelocity, Worl - oldCenter); } /// /// Used to prevent bodies from colliding; may lie depending on joints. /// /// /// 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? // if one of them doesn't have jointcomp then they can't share a common joint. // otherwise, only need to iterate over the joints of one component as they both store the same joint. if (_entMan.TryGetComponent(Owner, out JointComponent? jointComponentA) && _entMan.TryGetComponent(other.Owner, out JointComponent? jointComponentB)) { var aUid = jointComponentA.Owner; var bUid = jointComponentB.Owner; foreach (var (_, joint) in jointComponentA.Joints) { // Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking. if (!joint.CollideConnected && (aUid == joint.BodyAUid && bUid == joint.BodyBUid) || (bUid == joint.BodyAUid || aUid == joint.BodyBUid)) return false; } } var preventCollideMessage = new PreventCollideEvent(this, other); _entMan.EventBus.RaiseLocalEvent(Owner, preventCollideMessage); if (preventCollideMessage.Cancelled) return false; preventCollideMessage = new PreventCollideEvent(other, this); _entMan.EventBus.RaiseLocalEvent(other.Owner, preventCollideMessage); if (preventCollideMessage.Cancelled) return false; return true; } // View variables conveniences properties. [ViewVariables] private Vector2 _mapLinearVelocity => _sysMan.GetEntitySystem().GetMapLinearVelocity(Owner, this); [ViewVariables] private float _mapAngularVelocity => _sysMan.GetEntitySystem().GetMapAngularVelocity(Owner, this); } /// /// Directed event raised when an entity's physics BodyType changes. /// public sealed class PhysicsBodyTypeChangedEvent : EntityEventArgs { /// /// New BodyType of the entity. /// public BodyType New { get; } /// /// Old BodyType of the entity. /// public BodyType Old { get; } public PhysicsBodyTypeChangedEvent(BodyType newType, BodyType oldType) { New = newType; Old = oldType; } } }