Rewrite the physics engine (#1037)

This commit is contained in:
Jackson Lewis
2020-05-23 00:21:02 +01:00
committed by GitHub
parent 2b3bdf0f28
commit 274334c926
21 changed files with 755 additions and 574 deletions

View File

@@ -1,5 +1,8 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
@@ -9,10 +12,13 @@ namespace Robust.Client.GameObjects
/// in the physics system as a dynamic ridged body object that has physics. This behavior overrides
/// the BoundingBoxComponent behavior of making the entity static.
/// </summary>
internal class PhysicsComponent : Component
public class PhysicsComponent : SharedPhysicsComponent
{
/// <inheritdoc />
public override string Name => "Physics";
private Vector2 _linVel;
private float _angVel;
private float _mass;
private VirtualController _controller;
private BodyStatus _status;
/// <inheritdoc />
public override uint? NetID => NetIDs.PHYSICS;
@@ -21,13 +27,65 @@ namespace Robust.Client.GameObjects
/// Current mass of the entity in kg.
/// </summary>
[ViewVariables]
public float Mass { get; private set; }
public override float Mass
{
get => _mass;
set => _mass = value;
}
/// <summary>
/// Current velocity of the entity.
/// </summary>
[ViewVariables]
public Vector2 Velocity { get; private set; }
public override Vector2 LinearVelocity
{
get => _linVel;
set => _linVel = value;
}
/// <summary>
/// Current angular velocity of the entity
/// </summary>
[ViewVariables]
public override float AngularVelocity
{
get => _angVel;
set => _angVel = value;
}
/// <summary>
/// Current momentum of the entity
/// </summary>
[ViewVariables]
public override Vector2 Momentum
{
get => LinearVelocity * Mass;
set => _linVel = value / Mass;
}
/// <summary>
/// The current status of the object
/// </summary>
public override BodyStatus Status
{
get => _status;
set => _status = value;
}
/// <summary>
/// Whether this component is on the ground
/// </summary>
public override bool OnGround => Status == BodyStatus.OnGround &&
!IoCManager.Resolve<IPhysicsManager>()
.IsWeightless(Owner.Transform.GridPosition);
/// <summary>
/// Represents a virtual controller acting on the physics component.
/// </summary>
public override VirtualController Controller
{
get => _controller;
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
@@ -37,7 +95,8 @@ namespace Robust.Client.GameObjects
var newState = (PhysicsComponentState)curState;
Mass = newState.Mass / 1000f; // gram to kilogram
Velocity = newState.Velocity;
LinearVelocity = newState.LinearVelocity;
AngularVelocity = newState.AngularVelocity;
}
}
}

View File

@@ -0,0 +1,25 @@
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
namespace Robust.Client.Physics
{
[UsedImplicitly]
public class PhysicsSystem: EntitySystem
{
private float _lastServerMsg;
public override void Initialize()
{
base.Initialize();
EntityQuery = new TypeEntityQuery<PhysicsComponent>();
}
public override void Update(float frameTime)
{
}
}
}

View File

@@ -199,7 +199,7 @@ namespace Robust.Client.Placement
bounds.Width,
bounds.Height);
if (pManager.PhysicsManager.IsColliding(collisionbox, pManager.MapManager.GetGrid(coordinates.GridID).ParentMapId))
if (pManager.PhysicsManager.TryCollideRect(collisionbox, pManager.MapManager.GetGrid(coordinates.GridID).ParentMapId))
return true;
return false;

View File

@@ -25,10 +25,10 @@ namespace Robust.Server.Debugging
{
var msg = _net.CreateNetMessage<MsgRay>();
msg.RayOrigin = data.Ray.Position;
if (data.Results.DidHitObject)
if (data.Results != null)
{
msg.DidHit = true;
msg.RayHit = data.Results.HitPos;
msg.RayHit = data.Results.Value.HitPos;
}
else
{

View File

@@ -1,11 +1,9 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -16,11 +14,13 @@ namespace Robust.Server.GameObjects
/// in the physics system as a dynamic ridged body object that has physics. This behavior overrides
/// the BoundingBoxComponent behavior of making the entity static.
/// </summary>
public class PhysicsComponent : Component, ICollideSpecial
public class PhysicsComponent : SharedPhysicsComponent
{
private float _mass;
private Vector2 _linVelocity;
private float _angVelocity;
private VirtualController _controller = null;
private BodyStatus _status;
/// <inheritdoc />
public override string Name => "Physics";
@@ -32,7 +32,7 @@ namespace Robust.Server.GameObjects
/// Current mass of the entity in kilograms.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float Mass
public override float Mass
{
get => _mass;
set
@@ -46,7 +46,7 @@ namespace Robust.Server.GameObjects
/// Current linear velocity of the entity in meters per second.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 LinearVelocity
public override Vector2 LinearVelocity
{
get => _linVelocity;
set
@@ -63,7 +63,7 @@ namespace Robust.Server.GameObjects
/// Current angular velocity of the entity in radians per sec.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float AngularVelocity
public override float AngularVelocity
{
get => _angVelocity;
set
@@ -76,6 +76,41 @@ namespace Robust.Server.GameObjects
}
}
/// <summary>
/// Current momentum of the entity in kilogram meters per second
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public override Vector2 Momentum
{
get => LinearVelocity * Mass;
set => LinearVelocity = value / Mass;
}
/// <summary>
/// The current status of the object
/// </summary>
public override BodyStatus Status
{
get => _status;
set => _status = value;
}
/// <summary>
/// Represents a virtual controller acting on the physics component.
/// </summary>
public override VirtualController Controller
{
get => _controller;
}
/// <summary>
/// Whether this component is on the ground
/// </summary>
public override bool OnGround => Status == BodyStatus.OnGround &&
!IoCManager.Resolve<IPhysicsManager>()
.IsWeightless(Owner.Transform.GridPosition);
[ViewVariables(VVAccess.ReadWrite)]
public bool EdgeSlide { get => edgeSlide; set => edgeSlide = value; }
private bool edgeSlide = true;
@@ -91,6 +126,11 @@ namespace Robust.Server.GameObjects
}
}
public void SetController<T>() where T: VirtualController, new()
{
_controller = new T {ControlledComponent = this};
}
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
@@ -100,72 +140,20 @@ namespace Robust.Server.GameObjects
serializer.DataField(ref _linVelocity, "vel", Vector2.Zero);
serializer.DataField(ref _angVelocity, "avel", 0.0f);
serializer.DataField(ref edgeSlide, "edgeslide", true);
serializer.DataField(ref _anchored, "Anchored", true);
serializer.DataField(ref _anchored, "Anchored", false);
serializer.DataField(ref _status, "Status", BodyStatus.OnGround);
serializer.DataField(ref _controller, "Controller", null);
}
/// <inheritdoc />
public override ComponentState GetComponentState()
{
return new PhysicsComponentState(_mass, _linVelocity);
return new PhysicsComponentState(_mass, _linVelocity, _angVelocity);
}
public override void HandleMessage(ComponentMessage message, IComponent component)
public void RemoveController()
{
base.HandleMessage(message, component);
switch (message)
{
case BumpedEntMsg msg:
if (Anchored)
{
return;
}
if (!msg.Entity.TryGetComponent(out PhysicsComponent physicsComponent))
{
return;
}
physicsComponent.AddVelocityConsumer(this);
break;
}
_controller = null;
}
private List<PhysicsComponent> VelocityConsumers { get; } = new List<PhysicsComponent>();
public List<PhysicsComponent> GetVelocityConsumers()
{
var result = new List<PhysicsComponent> { this };
foreach(var velocityConsumer in VelocityConsumers)
{
result.AddRange(velocityConsumer.GetVelocityConsumers());
}
return result;
}
private void AddVelocityConsumer(PhysicsComponent physicsComponent)
{
if (!physicsComponent.VelocityConsumers.Contains(this) && !VelocityConsumers.Contains(physicsComponent))
{
VelocityConsumers.Add(physicsComponent);
}
}
internal void ClearVelocityConsumers()
{
VelocityConsumers.ForEach(x => x.ClearVelocityConsumers());
VelocityConsumers.Clear();
}
public bool PreventCollide(IPhysBody collidedwith)
{
var velocityConsumers = GetVelocityConsumers();
if (velocityConsumers.Count == 1 || !collidedwith.Owner.TryGetComponent<PhysicsComponent>(out var physicsComponent))
{
return false;
}
return velocityConsumers.Contains(physicsComponent);
}
public bool DidMovementCalculations { get; set; } = false;
}
}

View File

@@ -1,6 +1,6 @@
using JetBrains.Annotations;
using System;
using JetBrains.Annotations;
using Robust.Server.Interfaces.Timing;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
@@ -9,6 +9,10 @@ using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Containers;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.Interfaces.Random;
namespace Robust.Server.GameObjects.EntitySystems
{
@@ -19,10 +23,14 @@ namespace Robust.Server.GameObjects.EntitySystems
[Dependency] private readonly IPauseManager _pauseManager;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager;
[Dependency] private readonly IMapManager _mapManager;
[Dependency] private readonly IPhysicsManager _physicsManager;
[Dependency] private readonly IRobustRandom _random;
#pragma warning restore 649
private const float Epsilon = 1.0e-6f;
private List<Manifold> _collisionCache = new List<Manifold>();
public PhysicsSystem()
{
EntityQuery = new TypeEntityQuery(typeof(PhysicsComponent));
@@ -31,193 +39,247 @@ namespace Robust.Server.GameObjects.EntitySystems
/// <inheritdoc />
public override void Update(float frameTime)
{
// TODO: manifolds
var entities = EntityManager.GetEntities(EntityQuery);
SimulateWorld(frameTime, entities);
SimulateWorld(frameTime, RelevantEntities.Where(e => !e.Deleted && !_pauseManager.IsEntityPaused(e)).ToList());
}
private void SimulateWorld(float frameTime, IEnumerable<IEntity> entities)
private void SimulateWorld(float frameTime, ICollection<IEntity> entities)
{
// simulation can introduce deleted entities into the query results
foreach (var entity in entities)
{
if (entity.Deleted)
{
continue;
}
var physics = entity.GetComponent<PhysicsComponent>();
if (_pauseManager.IsEntityPaused(entity))
{
continue;
}
physics.Controller?.UpdateBeforeProcessing();
}
HandleMovement(_mapManager, _tileDefinitionManager, entity, frameTime);
// Calculate collisions and store them in the cache
ProcessCollisions();
// Remove all entities that were deleted during collision handling
foreach (var entity in entities.Where(e => e.Deleted).ToList())
{
entities.Remove(entity);
}
// Process frictional forces
foreach (var entity in entities)
{
ProcessFriction(entity, frameTime);
}
foreach (var entity in entities)
{
if (entity.Deleted)
{
continue;
}
var physics = entity.GetComponent<PhysicsComponent>();
DoMovement(entity, frameTime);
physics.Controller?.UpdateAfterProcessing();
}
// Remove all entities that were deleted due to the controller
foreach (var entity in entities.Where(e => e.Deleted).ToList())
{
entities.Remove(entity);
}
const float solveIterations = 3.0f;
for (var i = 0; i < solveIterations; i++)
{
foreach (var entity in entities)
{
UpdatePosition(entity, frameTime / solveIterations);
}
FixClipping(_collisionCache);
}
}
private static void HandleMovement(IMapManager mapManager, ITileDefinitionManager tileDefinitionManager, IEntity entity, float frameTime)
// Runs collision behavior and updates cache
private void ProcessCollisions()
{
if (entity.Deleted)
_collisionCache.Clear();
var collisionsWith = new Dictionary<ICollideBehavior, int>();
var physicsComponents = new Dictionary<ICollidableComponent, PhysicsComponent>();
var combinations = new List<(EntityUid, EntityUid)>();
var entities = RelevantEntities.ToList();
foreach (var entity in entities)
{
// Ugh let's hope this fixes the crashes.
return;
}
var velocity = entity.GetComponent<PhysicsComponent>();
if (velocity.DidMovementCalculations)
{
velocity.DidMovementCalculations = false;
return;
}
if (velocity.AngularVelocity == 0 && velocity.LinearVelocity == Vector2.Zero)
{
return;
}
var transform = entity.Transform;
if (ContainerHelpers.IsInContainer(transform.Owner))
{
transform.Parent.Owner.SendMessage(transform, new RelayMovementEntityMessage(entity));
velocity.LinearVelocity = Vector2.Zero;
return;
}
var velocityConsumers = velocity.GetVelocityConsumers();
var initialMovement = velocity.LinearVelocity;
int velocityConsumerCount;
float totalMass;
Vector2 lowestMovement;
var tile =
mapManager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.GridPosition).Tile;
bool hasGravity = mapManager.GetGrid(entity.Transform.GridID).HasGravity && !tile.IsEmpty;
do
{
velocityConsumerCount = velocityConsumers.Count;
totalMass = 0;
lowestMovement = initialMovement;
var copy = new List<Vector2>(velocityConsumers.Count);
float totalFriction = 0;
foreach (var consumer in velocityConsumers)
if (entity.Deleted) continue;
if (entity.TryGetComponent<CollidableComponent>(out var a))
{
var movement = lowestMovement;
totalMass += consumer.Mass;
if (hasGravity)
foreach (var b in a.GetCollidingEntities(Vector2.Zero).Select(e => e.GetComponent<CollidableComponent>()))
{
totalFriction += GetFriction(tileDefinitionManager, mapManager, consumer.Owner);
movement *= velocity.Mass / (totalMass != 0 ? totalMass + (totalMass * totalFriction) : 1);
if (combinations.Contains((a.Owner.Uid, b.Owner.Uid)) ||
combinations.Contains((b.Owner.Uid, a.Owner.Uid))) continue;
combinations.Add((a.Owner.Uid, b.Owner.Uid));
if (a.Owner.TryGetComponent<PhysicsComponent>(out var aPhysics))
{
physicsComponents[a] = aPhysics;
if (b.Owner.TryGetComponent<PhysicsComponent>(out var bPhysics))
{
physicsComponents[b] = bPhysics;
_collisionCache.Add(new Manifold(a, b, aPhysics, bPhysics));
}
else
{
_collisionCache.Add(new Manifold(a, b, aPhysics, null));
}
}
else if (b.Owner.TryGetComponent<PhysicsComponent>(out var bPhysics))
{
_collisionCache.Add(new Manifold(a, b, null, bPhysics));
}
}
consumer.AngularVelocity = velocity.AngularVelocity;
consumer.LinearVelocity = movement;
copy.Add(CalculateMovement(tileDefinitionManager, mapManager, consumer, frameTime, consumer.Owner) / frameTime);
}
}
var counter = 0;
while(GetNextCollision(_collisionCache, counter, out var collision))
{
counter++;
var impulse = _physicsManager.SolveCollisionImpulse(collision);
if (physicsComponents.ContainsKey(collision.A))
{
physicsComponents[collision.A].Momentum -= impulse;
}
copy.Sort(LengthComparer);
lowestMovement = copy[0];
velocityConsumers = velocity.GetVelocityConsumers();
} while (velocityConsumers.Count != velocityConsumerCount);
velocity.ClearVelocityConsumers();
foreach (var consumer in velocityConsumers)
{
consumer.LinearVelocity = lowestMovement;
consumer.DidMovementCalculations = true;
}
velocity.DidMovementCalculations = false;
}
private static void DoMovement(IEntity entity, float frameTime)
{
// TODO: Terrible hack to fix bullets crashing the server.
// Should be handled with deferred physics events instead.
if (entity.Deleted)
{
return;
}
var velocity = entity.GetComponent<PhysicsComponent>();
if (velocity.LinearVelocity.LengthSquared < Epsilon && velocity.AngularVelocity < Epsilon)
return;
float angImpulse = 0;
if (velocity.AngularVelocity > Epsilon)
{
angImpulse = velocity.AngularVelocity * frameTime;
}
var transform = entity.Transform;
transform.LocalRotation += angImpulse;
transform.WorldPosition += velocity.LinearVelocity * frameTime;
}
private static Vector2 CalculateMovement(ITileDefinitionManager tileDefinitionManager, IMapManager mapManager, PhysicsComponent velocity, float frameTime, IEntity entity)
{
if (velocity.Deleted)
{
// Help crashes.
return default;
}
var movement = velocity.LinearVelocity * frameTime;
if (movement.LengthSquared <= Epsilon)
{
return Vector2.Zero;
}
//TODO This is terrible. This needs to calculate the manifold between the two objects.
//Check for collision
if (entity.TryGetComponent(out CollidableComponent collider))
{
var collided = collider.TryCollision(movement, true);
if (collided)
if (physicsComponents.ContainsKey(collision.B))
{
if (velocity.EdgeSlide)
{
//Slide along the blockage in the non-blocked direction
var xBlocked = collider.TryCollision(new Vector2(movement.X, 0));
var yBlocked = collider.TryCollision(new Vector2(0, movement.Y));
physicsComponents[collision.B].Momentum += impulse;
}
movement = new Vector2(xBlocked ? 0 : movement.X, yBlocked ? 0 : movement.Y);
// Apply onCollide behavior
var aBehaviors = (collision.A as CollidableComponent).Owner.GetAllComponents<ICollideBehavior>();
foreach (var behavior in aBehaviors)
{
var entity = (collision.B as CollidableComponent).Owner;
if (entity.Deleted) continue;
behavior.CollideWith(entity);
if (collisionsWith.ContainsKey(behavior))
{
collisionsWith[behavior] += 1;
}
else
{
//Stop movement entirely at first blockage
movement = new Vector2(0, 0);
collisionsWith[behavior] = 1;
}
}
var bBehaviors = (collision.B as CollidableComponent).Owner.GetAllComponents<ICollideBehavior>();
foreach (var behavior in bBehaviors)
{
var entity = (collision.A as CollidableComponent).Owner;
if (entity.Deleted) continue;
behavior.CollideWith(entity);
if (collisionsWith.ContainsKey(behavior))
{
collisionsWith[behavior] += 1;
}
else
{
collisionsWith[behavior] = 1;
}
}
}
return movement;
foreach (var behavior in collisionsWith.Keys)
{
behavior.PostCollide(collisionsWith[behavior]);
}
}
private static float GetFriction(ITileDefinitionManager tileDefinitionManager, IMapManager mapManager, IEntity entity)
private bool GetNextCollision(List<Manifold> collisions, int counter, out Manifold collision)
{
if (entity.TryGetComponent(out CollidableComponent collider) && collider.IsScrapingFloor)
// The *4 is completely arbitrary
if (counter > collisions.Count * 4)
{
collision = collisions[0];
return false;
}
var indexes = new List<int>();
for (int i = 0; i < collisions.Count; i++)
{
indexes.Add(i);
}
_random.Shuffle(indexes);
foreach (var index in indexes)
{
if (collisions[index].Unresolved)
{
collision = collisions[index];
return true;
}
}
collision = collisions[0];
return false;
}
private void ProcessFriction(IEntity entity, float frameTime)
{
// A constant that scales frictional force to work with the rest of the engine
const float frictionScalingConstant = 60.0f;
var physics = entity.GetComponent<PhysicsComponent>();
if (physics.LinearVelocity == Vector2.Zero) return;
// Calculate frictional force
var friction = GetFriction(entity) * frameTime * frictionScalingConstant;
// Clamp friction because friction can't make you accelerate backwards
friction = Math.Min(friction, physics.LinearVelocity.Length);
// No multiplication/division by mass here since that would be redundant.
var frictionVelocityChange = physics.LinearVelocity.Normalized * -friction;
physics.LinearVelocity += frictionVelocityChange;
}
private void UpdatePosition(IEntity entity, float frameTime)
{
var physics = entity.GetComponent<PhysicsComponent>();
physics.LinearVelocity = new Vector2(Math.Abs(physics.LinearVelocity.X) < Epsilon ? 0.0f : physics.LinearVelocity.X, Math.Abs(physics.LinearVelocity.Y) < Epsilon ? 0.0f : physics.LinearVelocity.Y);
if (physics.Anchored || physics.LinearVelocity == Vector2.Zero && Math.Abs(physics.AngularVelocity) < Epsilon) return;
if (ContainerHelpers.IsInContainer(entity) && physics.LinearVelocity != Vector2.Zero)
{
entity.Transform.Parent.Owner.SendMessage(entity.Transform, new RelayMovementEntityMessage(entity));
// This prevents redundant messages from being sent if solveIterations > 1 and also simulates the entity "colliding" against the locker door when it opens.
physics.LinearVelocity = Vector2.Zero;
}
physics.Owner.Transform.WorldRotation += physics.AngularVelocity * frameTime;
physics.Owner.Transform.WorldPosition += physics.LinearVelocity * frameTime;
}
// Based off of Randy Gaul's ImpulseEngine code
private void FixClipping(List<Manifold> collisions)
{
const float allowance = 0.05f;
const float percent = 0.4f;
foreach (var collision in collisions)
{
var penetration = _physicsManager.CalculatePenetration(collision.A, collision.B);
if (penetration > allowance)
{
var correction = collision.Normal * Math.Abs(penetration) * percent;
if (collision.APhysics != null && !(collision.APhysics as PhysicsComponent).Anchored && !collision.APhysics.Deleted)
collision.APhysics.Owner.Transform.WorldPosition -= correction;
if (collision.BPhysics != null && !(collision.BPhysics as PhysicsComponent).Anchored && !collision.BPhysics.Deleted)
collision.BPhysics.Owner.Transform.WorldPosition += correction;
}
}
}
private float GetFriction(IEntity entity)
{
if (entity.HasComponent<CollidableComponent>() && entity.TryGetComponent(out PhysicsComponent physics) && physics.OnGround)
{
var location = entity.Transform;
var grid = mapManager.GetGrid(location.GridPosition.GridID);
var grid = _mapManager.GetGrid(location.GridPosition.GridID);
var tile = grid.GetTileRef(location.GridPosition);
var tileDef = tileDefinitionManager[tile.Tile.TypeId];
var tileDef = _tileDefinitionManager[tile.Tile.TypeId];
return tileDef.Friction;
}
return 0;
return 0.0f;
}
private static readonly IComparer<Vector2> LengthComparer =
Comparer<Vector2>.Create((a, b) => a.LengthSquared.CompareTo(b.LengthSquared));
}
}

View File

@@ -7,16 +7,6 @@ using Robust.Shared.Serialization;
namespace Robust.Shared.GameObjects
{
[Serializable, NetSerializable]
public class BumpedEntMsg : ComponentMessage
{
public IEntity Entity { get; }
public BumpedEntMsg(IEntity entity)
{
Entity = entity;
}
}
public class RelayMovementEntityMessage : ComponentMessage
{

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Physics;
@@ -17,9 +18,8 @@ namespace Robust.Shared.GameObjects.Components
[Dependency] private readonly IPhysicsManager _physicsManager;
#pragma warning restore 649
private bool _collisionEnabled;
private bool _isHardCollidable;
private bool _isScrapingFloor;
private bool _canCollide;
private BodyStatus _status;
private BodyType _bodyType;
private List<IPhysShape> _physShapes = new List<IPhysShape>();
@@ -40,9 +40,8 @@ namespace Robust.Shared.GameObjects.Components
{
base.ExposeData(serializer);
serializer.DataField(ref _collisionEnabled, "on", true);
serializer.DataField(ref _isHardCollidable, "hard", true);
serializer.DataField(ref _isScrapingFloor, "IsScrapingFloor", false);
serializer.DataField(ref _canCollide, "on", true);
serializer.DataField(ref _status, "Status", BodyStatus.OnGround);
serializer.DataField(ref _bodyType, "bodyType", BodyType.None);
serializer.DataField(ref _physShapes, "shapes", new List<IPhysShape>{new PhysShapeAabb()});
}
@@ -50,7 +49,7 @@ namespace Robust.Shared.GameObjects.Components
/// <inheritdoc />
public override ComponentState GetComponentState()
{
return new CollidableComponentState(_collisionEnabled, _isHardCollidable, _isScrapingFloor, _physShapes);
return new CollidableComponentState(_canCollide, _status, _physShapes);
}
/// <inheritdoc />
@@ -61,9 +60,8 @@ namespace Robust.Shared.GameObjects.Components
var newState = (CollidableComponentState)curState;
_collisionEnabled = newState.CollisionEnabled;
_isHardCollidable = newState.HardCollidable;
_isScrapingFloor = newState.ScrapingFloor;
_canCollide = newState.CanCollide;
_status = newState.Status;
//TODO: Is this always true?
if (newState.PhysShapes != null)
@@ -118,22 +116,10 @@ namespace Robust.Shared.GameObjects.Components
/// Enables or disabled collision processing of this component.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool CollisionEnabled
public bool CanCollide
{
get => _collisionEnabled;
set
{
_collisionEnabled = value;
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollisionEnabledEvent(Owner.Uid, value));
}
}
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public bool IsHardCollidable
{
get => _isHardCollidable;
set => _isHardCollidable = value;
get => _canCollide;
set => _canCollide = value;
}
/// <summary>
@@ -167,28 +153,10 @@ namespace Robust.Shared.GameObjects.Components
}
[ViewVariables(VVAccess.ReadWrite)]
public bool IsScrapingFloor
public BodyStatus Status
{
get => _isScrapingFloor;
set => _isScrapingFloor = value;
}
/// <inheritdoc />
void IPhysBody.Bumped(IEntity bumpedby)
{
SendMessage(new BumpedEntMsg(bumpedby));
UpdateEntityTree();
}
/// <inheritdoc />
void IPhysBody.Bump(List<IEntity> bumpedinto)
{
var collidecomponents = Owner.GetAllComponents<ICollideBehavior>().ToList();
for (var i = 0; i < collidecomponents.Count; i++)
{
collidecomponents[i].CollideWith(bumpedinto);
}
get => _status;
set => _status = value;
}
/// <inheritdoc />
@@ -205,7 +173,6 @@ namespace Robust.Shared.GameObjects.Components
protected override void Startup()
{
base.Startup();
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollisionEnabledEvent(Owner.Uid, _collisionEnabled));
_physicsManager.AddBody(this);
}
@@ -213,17 +180,17 @@ namespace Robust.Shared.GameObjects.Components
protected override void Shutdown()
{
_physicsManager.RemoveBody(this);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollisionEnabledEvent(Owner.Uid, false));
base.Shutdown();
}
/// <inheritdoc />
public bool TryCollision(Vector2 offset, bool bump = false)
public bool IsColliding(Vector2 offset)
{
if (!_collisionEnabled || CollisionMask == 0x0)
return false;
return _physicsManager.IsColliding(this, offset);
}
return _physicsManager.TryCollide(Owner, offset, bump);
public IEnumerable<IEntity> GetCollidingEntities(Vector2 offset)
{
return _physicsManager.GetCollidingEntities(this, offset);
}
public bool UpdatePhysicsTree()
@@ -240,18 +207,15 @@ namespace Robust.Shared.GameObjects.Components
}
private bool UpdateEntityTree() => Owner.EntityManager.UpdateEntityTree(Owner);
}
public class CollisionEnabledEvent : EntitySystemMessage
{
public bool Value { get; }
public EntityUid Owner { get; }
public CollisionEnabledEvent(EntityUid uid, bool value)
public bool IsOnGround()
{
Owner = uid;
Value = value;
return Status == BodyStatus.OnGround;
}
public bool IsInAir()
{
return Status == BodyStatus.InAir;
}
}
}

View File

@@ -8,17 +8,15 @@ namespace Robust.Shared.GameObjects.Components
[Serializable, NetSerializable]
public class CollidableComponentState : ComponentState
{
public readonly bool CollisionEnabled;
public readonly bool HardCollidable;
public readonly bool ScrapingFloor;
public readonly bool CanCollide;
public readonly BodyStatus Status;
public readonly List<IPhysShape> PhysShapes;
public CollidableComponentState(bool collisionEnabled, bool hardCollidable, bool scrapingFloor, List<IPhysShape> physShapes)
public CollidableComponentState(bool canCollide, BodyStatus status, List<IPhysShape> physShapes)
: base(NetIDs.COLLIDABLE)
{
CollisionEnabled = collisionEnabled;
HardCollidable = hardCollidable;
ScrapingFloor = scrapingFloor;
CanCollide = canCollide;
Status = status;
PhysShapes = physShapes;
}
}

View File

@@ -8,8 +8,10 @@ namespace Robust.Shared.GameObjects.Components
{
public interface ICollidableComponent : IComponent, IPhysBody
{
bool TryCollision(Vector2 offset, bool bump = false);
bool IsColliding(Vector2 offset);
IEnumerable<IEntity> GetCollidingEntities(Vector2 offset);
bool UpdatePhysicsTree();
void RemovedFromPhysicsTree(MapId mapId);
@@ -23,6 +25,12 @@ namespace Robust.Shared.GameObjects.Components
public interface ICollideBehavior
{
void CollideWith(List<IEntity> collidedwith);
void CollideWith(IEntity collidedWith);
/// <summary>
/// Called after all collisions have been processed, as well as how many collisions occured
/// </summary>
/// <param name="collisionCount"></param>
void PostCollide(int collisionCount) { }
}
}

View File

@@ -16,20 +16,26 @@ namespace Robust.Shared.GameObjects
public readonly int Mass;
/// <summary>
/// Current velocity of the entity.
/// Current linear velocity of the entity.
/// </summary>
public readonly Vector2 Velocity;
public readonly Vector2 LinearVelocity;
/// <summary>
/// Current angular velocity of the entity.
/// </summary>
public readonly float AngularVelocity;
/// <summary>
/// Constructs a new state snapshot of a PhysicsComponent.
/// </summary>
/// <param name="mass">Current Mass of the entity.</param>
/// <param name="velocity">Current Velocity of the entity.</param>
public PhysicsComponentState(float mass, Vector2 velocity)
public PhysicsComponentState(float mass, Vector2 linearVelocity, float angularVelocity)
: base(NetIDs.PHYSICS)
{
Mass = (int) Math.Round(mass *1000); // rounds kg to nearest gram
Velocity = velocity;
LinearVelocity = linearVelocity;
AngularVelocity = angularVelocity;
}
}
}

View File

@@ -262,8 +262,8 @@ namespace Robust.Shared.GameObjects.Components.Transform
var newPos = Parent.InvWorldMatrix.Transform(value);
// float rounding error guard, if the offset is less than 1mm ignore it
if ((newPos - GetLocalPosition()).LengthSquared < 1.0E-3)
return;
//if ((newPos - GetLocalPosition()).LengthSquared < 1.0E-3)
// return;
if (_localPosition == newPos)
return;

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
@@ -20,39 +22,77 @@ namespace Robust.Shared.Interfaces.Physics
/// <param name="collider">Collision rectangle to check</param>
/// <param name="map">Map to check on</param>
/// <returns>true if collides, false if not</returns>
bool IsColliding(Box2 collider, MapId map);
bool TryCollideRect(Box2 collider, MapId map);
bool TryCollide(IEntity entity, Vector2 offset, bool bump = true);
/// <summary>
/// Checks whether a certain grid position is weightless or not
/// </summary>
/// <param name="gridPosition"></param>
/// <returns></returns>
bool IsWeightless(GridCoordinates gridPosition);
/// <summary>
/// Get all entities colliding with a certain body.
/// </summary>
/// <param name="body"></param>
/// <returns></returns>
IEnumerable<IEntity> GetCollidingEntities(IPhysBody body, Vector2 offset, bool approximate = true);
/// <summary>
/// Checks whether a body is colliding
/// </summary>
/// <param name="body"></param>
/// <param name="offset"></param>
/// <returns></returns>
bool IsColliding(IPhysBody body, Vector2 offset);
void AddBody(IPhysBody physBody);
void RemoveBody(IPhysBody physBody);
/// <summary>
/// Casts a ray in the world and returns the first thing it hit.
/// Casts a ray in the world and returns the first entity it hits, or a list of all entities it hits.
/// </summary>
/// <param name="mapId"></param>
/// <param name="ray">Ray to cast in the world.</param>
/// <param name="maxLength">Maximum length of the ray in meters.</param>
/// <param name="ignoredEnt">A single entity that can be ignored by the RayCast. Useful if the ray starts inside the body of an entity.</param>
/// <param name="ignoreNonHardCollidables">If true, the RayCast will ignore any bodies that aren't hard collidables.</param>
/// <returns>A result object describing the hit, if any.</returns>
RayCastResults IntersectRay(MapId mapId, CollisionRay ray, float maxLength = 50, IEntity ignoredEnt = null, bool ignoreNonHardCollidables = false);
/// <param name="returnOnFirstHit">If false, will return a list of everything it hits, otherwise will just return a list of the first entity hit</param>
/// <returns>An enumerable of either the first entity hit or everything hit</returns>
IEnumerable<RayCastResults> IntersectRay(MapId mapId, CollisionRay ray, float maxLength = 50, IEntity ignoredEnt = null, bool returnOnFirstHit = true);
/// <summary>
/// Casts a ray in the world and returns the first thing it hit.
/// Calculates the normal vector for two colliding bodies
/// </summary>
/// <param name="target"></param>
/// <param name="source"></param>
/// <returns></returns>
Vector2 CalculateNormal(ICollidableComponent target, ICollidableComponent source);
/// <summary>
/// Calculates the penetration depth of the axis-of-least-penetration for a
/// </summary>
/// <param name="target"></param>
/// <param name="source"></param>
/// <returns></returns>
float CalculatePenetration(ICollidableComponent target, ICollidableComponent source);
Vector2 SolveCollisionImpulse(Manifold manifold);
/// <summary>
/// Casts a ray in the world, returning the first entity it hits (or all entities it hits, if so specified)
/// </summary>
/// <param name="mapId"></param>
/// <param name="ray">Ray to cast in the world.</param>
/// <param name="maxLength">Maximum length of the ray in meters.</param>
/// <param name="predicate">A predicate to check whether to ignore an entity or not. If it returns true, it will be ignored.</param>
/// <param name="ignoreNonHardCollidables">If true, the RayCast will ignore any bodies that aren't hard collidables.</param>
/// <param name="returnOnFirstHit">If true, will only include the first hit entity in results. Otherwise, returns all of them.</param>
/// <returns>A result object describing the hit, if any.</returns>
RayCastResults IntersectRayWithPredicate(MapId mapId, CollisionRay ray, float maxLength = 50, Func<IEntity, bool> predicate = null, bool ignoreNonHardCollidables = false);
IEnumerable<RayCastResults> IntersectRayWithPredicate(MapId mapId, CollisionRay ray, float maxLength = 50, Func<IEntity, bool> predicate = null, bool returnOnFirstHit = true);
event Action<DebugRayData> DebugDrawRay;
IEnumerable<(IPhysBody, IPhysBody)> GetCollisions();
bool Update(IPhysBody collider);
void RemovedFromMap(IPhysBody body, MapId mapId);
@@ -61,15 +101,70 @@ namespace Robust.Shared.Interfaces.Physics
public struct DebugRayData
{
public DebugRayData(Ray ray, float maxLength, RayCastResults results)
public DebugRayData(Ray ray, float maxLength, [CanBeNull] RayCastResults? results)
{
Ray = ray;
MaxLength = maxLength;
Results = results;
}
public Ray Ray { get; }
public RayCastResults Results { get; }
public Ray Ray
{
get;
}
[CanBeNull]
public RayCastResults? Results { get; }
public float MaxLength { get; }
}
public struct Manifold
{
public Vector2 RelativeVelocity
{
get
{
if (APhysics != null)
{
if (BPhysics != null)
{
return BPhysics.LinearVelocity - APhysics.LinearVelocity;
}
else
{
return -APhysics.LinearVelocity;
}
}
if (BPhysics != null)
{
return BPhysics.LinearVelocity;
}
else
{
return Vector2.Zero;
}
}
}
public readonly Vector2 Normal;
public readonly ICollidableComponent A;
public readonly ICollidableComponent B;
[CanBeNull] public SharedPhysicsComponent APhysics;
[CanBeNull] public SharedPhysicsComponent BPhysics;
public float InvAMass => 1 / APhysics?.Mass ?? 0.0f;
public float InvBMass => 1 / BPhysics?.Mass ?? 0.0f;
public bool Unresolved => Vector2.Dot(RelativeVelocity, Normal) < 0;
public Manifold(ICollidableComponent A, ICollidableComponent B, [CanBeNull] SharedPhysicsComponent aPhysics, [CanBeNull] SharedPhysicsComponent bPhysics)
{
var physicsManager = IoCManager.Resolve<IPhysicsManager>();
this.A = A;
this.B = B;
Normal = physicsManager.CalculateNormal(A, B);
APhysics = aPhysics;
BPhysics = bPhysics;
}
}
}

View File

@@ -471,8 +471,7 @@ namespace Robust.Shared.Map
gridComp.GridIndex = grid.Index;
var collideComp = newEnt.AddComponent<CollidableComponent>();
collideComp.CollisionEnabled = true;
collideComp.IsHardCollidable = true;
collideComp.CanCollide = true;
collideComp.PhysicsShapes.Add(new PhysShapeGrid(grid));
newEnt.Transform.AttachParent(_entityManager.GetEntity(_mapEntities[currentMapID]));

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Maths;
namespace Robust.Shared.Physics
{
/// <summary>
///
///
/// </summary>
public interface IPhysBody
{
@@ -28,14 +28,9 @@ namespace Robust.Shared.Physics
List<IPhysShape> PhysicsShapes { get; }
/// <summary>
/// Enables or disabled collision processing of this body.
/// Whether or not this body can collide.
/// </summary>
bool CollisionEnabled { get; set; }
/// <summary>
/// True if collisions should prevent movement, or just trigger bumps.
/// </summary>
bool IsHardCollidable { get; set; }
bool CanCollide { get; set; }
/// <summary>
/// Bitmask of the collision layers this body is a part of. The layers are calculated from
@@ -49,18 +44,6 @@ namespace Robust.Shared.Physics
/// </summary>
int CollisionMask { get; }
/// <summary>
/// Called when the physBody is bumped into by someone/something
/// </summary>
/// <param name="bumpedby"></param>
void Bumped(IEntity bumpedby);
/// <summary>
/// Called when the physBody bumps into this entity
/// </summary>
/// <param name="bumpedinto"></param>
void Bump(List<IEntity> bumpedinto);
/// <summary>
/// The map index this physBody is located upon
/// </summary>

View File

@@ -1,9 +1,8 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
@@ -18,7 +17,9 @@ namespace Robust.Shared.Physics
/// <inheritdoc />
public class PhysicsManager : IPhysicsManager
{
private readonly List<IPhysBody> _results = new List<IPhysBody>();
#pragma warning disable 649
[Dependency] private readonly IMapManager _mapManager;
#pragma warning restore 649
private readonly ConcurrentDictionary<MapId,BroadPhase> _treesPerMap =
new ConcurrentDictionary<MapId, BroadPhase>();
@@ -26,20 +27,19 @@ namespace Robust.Shared.Physics
private BroadPhase this[MapId mapId] => _treesPerMap.GetOrAdd(mapId, _ => new BroadPhase());
/// <summary>
/// returns true if collider intersects a physBody under management. Does not trigger Bump.
/// returns true if collider intersects a physBody under management.
/// </summary>
/// <param name="collider">Rectangle to check for collision</param>
/// <param name="map">Map ID to filter</param>
/// <returns></returns>
public bool IsColliding(Box2 collider, MapId map)
public bool TryCollideRect(Box2 collider, MapId map)
{
foreach (var body in this[map].Query(collider))
{
if (!body.CollisionEnabled || body.CollisionLayer == 0x0)
if (!body.CanCollide || body.CollisionLayer == 0x0)
continue;
if (body.MapID == map &&
body.IsHardCollidable &&
body.WorldAABB.Intersects(collider))
return true;
}
@@ -47,118 +47,97 @@ namespace Robust.Shared.Physics
return false;
}
/// <summary>
/// returns true if collider intersects a physBody under management and calls Bump.
/// </summary>
/// <param name="entity">Rectangle to check for collision</param>
/// <param name="offset"></param>
/// <param name="bump"></param>
/// <returns></returns>
public bool TryCollide(IEntity entity, Vector2 offset, bool bump = true)
public bool IsWeightless(GridCoordinates gridPosition)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
var collidable = (IPhysBody) entity.GetComponent<ICollidableComponent>();
// This will never collide with anything
if (!collidable.CollisionEnabled || collidable.CollisionLayer == 0x0)
return false;
var colliderAABB = collidable.WorldAABB;
if (offset.LengthSquared > 0)
{
colliderAABB = colliderAABB.Translated(offset);
}
// Test this physBody against every other one.
_results.Clear();
DoCollisionTest(collidable, colliderAABB, _results);
// collided with nothing
if (_results.Count == 0)
return false;
//See if our collision will be overridden by a component
var collisionmodifiers = entity.GetAllComponents<ICollideSpecial>().ToList();
var collidedwith = new List<IEntity>();
//try all of the AABBs against the target rect.
var collided = false;
foreach (var otherCollidable in _results)
{
if (!otherCollidable.IsHardCollidable)
{
continue;
}
var othercollisionmodifiers = otherCollidable.Owner.GetAllComponents<ICollideSpecial>();
//Provides component level overrides for collision behavior based on the entity we are trying to collide with
var preventcollision = false;
foreach (var mods in collisionmodifiers)
{
preventcollision |= mods.PreventCollide(otherCollidable);
}
foreach (var othermods in othercollisionmodifiers)
{
preventcollision |= othermods.PreventCollide(collidable);
}
if (preventcollision)
{
continue;
}
collided = true;
if (!bump)
{
continue;
}
otherCollidable.Bumped(entity);
collidedwith.Add(otherCollidable.Owner);
}
collidable.Bump(collidedwith);
//TODO: This needs multi-grid support.
return collided;
var tile = _mapManager.GetGrid(gridPosition.GridID).GetTileRef(gridPosition).Tile;
return !_mapManager.GetGrid(gridPosition.GridID).HasGravity || tile.IsEmpty;
}
/// <summary>
/// Tests a physBody against every other registered physBody.
/// </summary>
/// <param name="physBody">Body being tested.</param>
/// <param name="colliderAABB">The AABB of the physBody being tested. This can be IPhysBody.WorldAABB, or a modified version of it.</param>
/// <param name="results">An empty list that the function stores all colliding bodies inside of.</param>
internal bool DoCollisionTest(IPhysBody physBody, Box2 colliderAABB, List<IPhysBody> results)
public Vector2 CalculateNormal(ICollidableComponent target, ICollidableComponent source)
{
// TODO: Terrible hack to fix bullets crashing the server.
// Should be handled with deferred physics events instead.
if(physBody.Owner.Deleted)
return false;
var any = false;
foreach ( var body in this[physBody.MapID].Query(colliderAABB))
var manifold = target.WorldAABB.Intersect(source.WorldAABB);
if (manifold.IsEmpty()) return Vector2.Zero;
if (manifold.Height > manifold.Width)
{
// X is the axis of seperation
var leftDist = source.WorldAABB.Right - target.WorldAABB.Left;
var rightDist = target.WorldAABB.Right - source.WorldAABB.Left;
return new Vector2(leftDist > rightDist ? 1 : -1, 0);
}
else
{
// Y is the axis of seperation
var bottomDist = source.WorldAABB.Top - target.WorldAABB.Bottom;
var topDist = target.WorldAABB.Top - source.WorldAABB.Bottom;
return new Vector2(0, bottomDist > topDist ? 1 : -1);
}
}
// TODO: Terrible hack to fix bullets crashing the server.
// Should be handled with deferred physics events instead.
public float CalculatePenetration(ICollidableComponent target, ICollidableComponent source)
{
var manifold = target.WorldAABB.Intersect(source.WorldAABB);
if (manifold.IsEmpty()) return 0.0f;
return manifold.Height > manifold.Width ? manifold.Width : manifold.Height;
}
// Impulse resolution algorithm based on Box2D's approach in combination with Randy Gaul's Impulse Engine resolution algorithm.
public Vector2 SolveCollisionImpulse(Manifold manifold)
{
var aP = manifold.APhysics;
var bP = manifold.BPhysics;
if (aP == null && bP == null) return Vector2.Zero;
var restitution = 0.01f;
var normal = CalculateNormal(manifold.A, manifold.B);
var rV = aP != null
? bP != null ? bP.LinearVelocity - aP.LinearVelocity : -aP.LinearVelocity
: bP.LinearVelocity;
var vAlongNormal = Vector2.Dot(rV, normal);
if (vAlongNormal > 0)
{
return Vector2.Zero;
}
var impulse = -(1.0f + restitution) * vAlongNormal;
// So why the 100.0f instead of 0.0f? Well, because the other object needs to have SOME mass value,
// or otherwise the physics object can actually sink in slightly to the physics-less object.
// (the 100.0f is equivalent to a mass of 0.01kg)
impulse /= (aP != null && aP.Mass > 0.0f ? 1 / aP.Mass : 100.0f) +
(bP != null && bP.Mass > 0.0f ? 1 / bP.Mass : 100.0f);
return manifold.Normal * impulse;
}
public IEnumerable<IEntity> GetCollidingEntities(IPhysBody physBody, Vector2 offset, bool approximate = true)
{
var modifiers = physBody.Owner.GetAllComponents<ICollideSpecial>();
foreach ( var body in this[physBody.MapID].Query(physBody.WorldAABB, approximate))
{
if (body.Owner.Deleted) {
continue;
}
if (CollidesOnMask(physBody, body))
{
results.Add(body);
var preventCollision = false;
var otherModifiers = body.Owner.GetAllComponents<ICollideSpecial>();
foreach (var modifier in modifiers)
{
preventCollision |= modifier.PreventCollide(body);
}
foreach (var modifier in otherModifiers)
{
preventCollision |= modifier.PreventCollide(physBody);
}
if (preventCollision) continue;
yield return body.Owner;
}
any = true;
}
}
return any;
public bool IsColliding(IPhysBody body, Vector2 offset)
{
return GetCollidingEntities(body, offset).Any();
}
public static bool CollidesOnMask(IPhysBody a, IPhysBody b)
@@ -166,7 +145,7 @@ namespace Robust.Shared.Physics
if (a == b)
return false;
if (!a.CollisionEnabled || !b.CollisionEnabled)
if (!a.CanCollide || !b.CanCollide)
return false;
if ((a.CollisionMask & b.CollisionLayer) == 0x0 &&
@@ -188,10 +167,6 @@ namespace Robust.Shared.Physics
}
}
#pragma warning disable 649
[Dependency] private readonly IMapManager _mapManager;
#pragma warning restore 649
/// <summary>
/// Removes a physBody from the manager
/// </summary>
@@ -252,23 +227,28 @@ namespace Robust.Shared.Physics
}
/// <inheritdoc />
public RayCastResults IntersectRayWithPredicate(MapId mapId, CollisionRay ray, float maxLength = 50, Func<IEntity, bool> predicate = null, bool ignoreNonHardCollidables = false)
public IEnumerable<RayCastResults> IntersectRayWithPredicate(MapId mapId, CollisionRay ray,
float maxLength = 50F,
Func<IEntity, bool> predicate = null, bool returnOnFirstHit = true)
{
RayCastResults result = default;
List<RayCastResults> results = new List<RayCastResults>();
this[mapId].Query((ref IPhysBody body, in Vector2 point, float distFromOrigin) => {
this[mapId].Query((ref IPhysBody body, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0) return true;
if (distFromOrigin > maxLength)
{
return true;
}
if (predicate.Invoke(body.Owner))
if (predicate != null && predicate.Invoke(body.Owner))
{
return true;
}
if (!body.CollisionEnabled)
if (!body.CanCollide)
{
return true;
}
@@ -278,62 +258,26 @@ namespace Robust.Shared.Physics
return true;
}
if (ignoreNonHardCollidables && !body.IsHardCollidable)
{
return true;
}
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (result.Distance != 0f && distFromOrigin > result.Distance)
{
return true;
}
result = new RayCastResults(distFromOrigin, point, body.Owner);
var result = new RayCastResults(distFromOrigin, point, body.Owner);
results.Add(result);
DebugDrawRay?.Invoke(new DebugRayData(ray, maxLength, result));
return true;
}, ray.Position, ray.Direction);
if (results.Count == 0)
{
DebugDrawRay?.Invoke(new DebugRayData(ray, maxLength, null));
}
DebugDrawRay?.Invoke(new DebugRayData(ray, maxLength, result));
return result;
results.Sort((a, b) => a.Distance.CompareTo(b.Distance));
return results;
}
/// <inheritdoc />
public RayCastResults IntersectRay(MapId mapId, CollisionRay ray, float maxLength = 50, IEntity ignoredEnt = null, bool ignoreNonHardCollidables = false)
=> IntersectRayWithPredicate(mapId, ray, maxLength, entity => entity == ignoredEnt, ignoreNonHardCollidables);
public IEnumerable<RayCastResults> IntersectRay(MapId mapId, CollisionRay ray, float maxLength = 50, IEntity ignoredEnt = null, bool returnOnFirstHit = true)
=> IntersectRayWithPredicate(mapId, ray, maxLength, entity => entity == ignoredEnt, returnOnFirstHit);
public event Action<DebugRayData> DebugDrawRay;
public IEnumerable<(IPhysBody, IPhysBody)> GetCollisions()
{
foreach (var mapId in _mapManager.GetAllMapIds())
{
foreach (var collision in this[mapId].GetCollisions(true))
{
var (a, b) = collision;
if (!a.CollisionEnabled || !b.CollisionEnabled)
{
continue;
}
if (((a.CollisionLayer & b.CollisionMask) == 0x0)
||(b.CollisionLayer & a.CollisionMask) == 0x0)
{
continue;
}
if (!a.WorldAABB.Intersects(b.WorldAABB))
{
continue;
}
yield return collision;
}
}
}
public bool Update(IPhysBody collider)
=> this[collider.MapID].Update(collider);

View File

@@ -5,10 +5,6 @@ namespace Robust.Shared.Physics
{
public readonly struct RayCastResults
{
/// <summary>
/// True if an object was indeed hit. False otherwise.
/// </summary>
public bool DidHitObject => HitEntity != null;
/// <summary>
/// The entity that was hit. <see langword="null" /> if no entity was hit.

View File

@@ -0,0 +1,31 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Robust.Shared.Physics
{
public abstract class SharedPhysicsComponent: Component
{
/// <inheritdoc />
public override string Name => "Physics";
public abstract Vector2 LinearVelocity { get; set; }
public abstract float AngularVelocity { get; set; }
public abstract float Mass { get; set; }
public abstract Vector2 Momentum { get; set; }
public abstract BodyStatus Status { get; set; }
public abstract bool OnGround { get; }
[CanBeNull]
public abstract VirtualController Controller { get; }
}
[Serializable, NetSerializable]
public enum BodyStatus
{
OnGround,
InAir
}
}

View File

@@ -0,0 +1,21 @@
namespace Robust.Shared.Physics
{
/// <summary>
/// The VirtualController allows dynamic changes in the properties of a physics component, usually to simulate a complex physical interaction (such as player movement).
/// </summary>
public abstract class VirtualController
{
public abstract SharedPhysicsComponent ControlledComponent { set; }
/// <summary>
/// Modify a physics component before processing impulses
/// </summary>
public virtual void UpdateBeforeProcessing() { }
/// <summary>
/// Modify a physics component after processing impulses
/// </summary>
public virtual void UpdateAfterProcessing() { }
}
}

View File

@@ -15,10 +15,10 @@ namespace Robust.UnitTesting.Shared.Physics
private void SetupDefault()
{
A = new Mock<IPhysBody>();
A.Setup(x => x.CollisionEnabled).Returns(true);
A.Setup(x => x.CanCollide).Returns(true);
B = new Mock<IPhysBody>();
B.Setup(x => x.CollisionEnabled).Returns(true);
B.Setup(x => x.CanCollide).Returns(true);
}
private void Act()

View File

@@ -1,4 +1,7 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Moq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
@@ -23,15 +26,14 @@ namespace Robust.UnitTesting.Shared.Physics
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.IsHardCollidable).Returns(true);
mock.Setup(foo => foo.MapID).Returns(new MapId(0));
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CanCollide).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(0x4);
mock.Setup(foo => foo.CollisionMask).Returns(0x04);
manager.AddBody(mock.Object);
// Act
var result = manager.IsColliding(testBox, new MapId(0));
var result = manager.TryCollideRect(testBox, new MapId(0));
// Assert
Assert.That(result, Is.False);
@@ -47,44 +49,19 @@ namespace Robust.UnitTesting.Shared.Physics
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.IsHardCollidable).Returns(true);
mock.Setup(foo => foo.MapID).Returns(new MapId(0));
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CanCollide).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(0x4);
mock.Setup(foo => foo.CollisionMask).Returns(0x04);
manager.AddBody(mock.Object);
// Act
var result = manager.IsColliding(testBox, new MapId(0));
var result = manager.TryCollideRect(testBox, new MapId(0));
// Assert
Assert.That(result, Is.True);
}
[Test]
public void IsCollidingNotHard()
{
// Arrange
var box = new Box2(5, -5, 10, 6);
var testBox = new Box2(-3, -3, 5, 6);
var manager = new PhysicsManager();
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.IsHardCollidable).Returns(false);
mock.Setup(foo => foo.MapID).Returns(new MapId(0));
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(0x4);
mock.Setup(foo => foo.CollisionMask).Returns(0x04);
manager.AddBody(mock.Object);
// Act
var result = manager.IsColliding(testBox, new MapId(0));
// Assert
Assert.That(result, Is.False);
}
[Test]
public void IsCollidingTrue()
{
@@ -95,15 +72,14 @@ namespace Robust.UnitTesting.Shared.Physics
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.IsHardCollidable).Returns(true);
mock.Setup(foo => foo.MapID).Returns(new MapId(0));
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CanCollide).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(0x4);
mock.Setup(foo => foo.CollisionMask).Returns(0x04);
manager.AddBody(mock.Object);
// Act
var result = manager.IsColliding(testBox, new MapId(0));
var result = manager.TryCollideRect(testBox, new MapId(0));
// Assert
Assert.That(result, Is.True);
@@ -119,15 +95,14 @@ namespace Robust.UnitTesting.Shared.Physics
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.IsHardCollidable).Returns(true);
mock.Setup(foo => foo.MapID).Returns(new MapId(0));
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CanCollide).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(0); // Collision layer is None
mock.Setup(foo => foo.CollisionMask).Returns(0x04);
manager.AddBody(mock.Object);
// Act
var result = manager.IsColliding(testBox, new MapId(0));
var result = manager.TryCollideRect(testBox, new MapId(0));
// Assert
Assert.That(result, Is.False);
@@ -143,15 +118,14 @@ namespace Robust.UnitTesting.Shared.Physics
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.IsHardCollidable).Returns(true);
mock.Setup(foo => foo.MapID).Returns(new MapId(3));
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CanCollide).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(0x4);
mock.Setup(foo => foo.CollisionMask).Returns(0x04);
manager.AddBody(mock.Object);
// Act
var result = manager.IsColliding(testBox, new MapId(0));
var result = manager.TryCollideRect(testBox, new MapId(0));
// Assert
Assert.That(result, Is.False);
@@ -168,17 +142,19 @@ namespace Robust.UnitTesting.Shared.Physics
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.Owner).Returns(new Entity()); // requires IPhysBody not have null owner
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CanCollide).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(1);
mock.Setup(foo => foo.CollisionMask).Returns(1);
mock.Setup(foo => foo.IsHardCollidable).Returns(true);
manager.AddBody(mock.Object);
// Act
var result = manager.IntersectRay(new MapId(0), ray);
var results = manager.IntersectRay(new MapId(0), ray).ToList();
Assert.That(results.Count, Is.EqualTo(1));
var result = results.First();
// Assert
Assert.That(result.DidHitObject, Is.True);
Assert.That(result.Distance, Is.EqualTo(5));
Assert.That(result.HitPos.X, Is.EqualTo(5));
Assert.That(result.HitPos.Y, Is.EqualTo(1));
@@ -195,25 +171,63 @@ namespace Robust.UnitTesting.Shared.Physics
var mock = new Mock<IPhysBody>();
mock.Setup(foo => foo.WorldAABB).Returns(box);
mock.Setup(foo => foo.Owner).Returns(new Entity()); // requires IPhysBody not have null owner
mock.Setup(foo => foo.CollisionEnabled).Returns(true);
mock.Setup(foo => foo.CanCollide).Returns(true);
mock.Setup(foo => foo.CollisionLayer).Returns(1);
mock.Setup(foo => foo.CollisionMask).Returns(1);
manager.AddBody(mock.Object);
// Act
var result = manager.IntersectRay(new MapId(0), ray);
var results = manager.IntersectRay(new MapId(0), ray);
// Assert
Assert.That(result.DidHitObject, Is.False);
Assert.That(result.Distance, Is.EqualTo(0.0f));
Assert.That(result.HitPos, Is.EqualTo(Vector2.Zero));
Assert.That(results.Count(), Is.EqualTo(0));
}
[Test]
public void MultiHitRayCast()
{
// Arrange
var b1 = new Box2(5, -5, 10, 6);
var b2 = new Box2(6, -10, 7, 10);
var ray = new CollisionRay(Vector2.UnitY, Vector2.UnitX, 1);
var manager = new PhysicsManager();
var e1 = new Entity();
var e2 = new Entity();
var m1 = new Mock<IPhysBody>();
m1.Setup(foo => foo.WorldAABB).Returns(b1);
m1.Setup(foo => foo.Owner).Returns(e1);
m1.Setup(foo => foo.CanCollide).Returns(true);
m1.Setup(foo => foo.CollisionLayer).Returns(1);
m1.Setup(foo => foo.CollisionMask).Returns(1);
manager.AddBody(m1.Object);
var m2 = new Mock<IPhysBody>();
m2.Setup(foo => foo.WorldAABB).Returns(b2);
m2.Setup(foo => foo.Owner).Returns(e2);
m2.Setup(foo => foo.CanCollide).Returns(true);
m2.Setup(foo => foo.CollisionLayer).Returns(1);
m2.Setup(foo => foo.CollisionMask).Returns(1);
manager.AddBody(m2.Object);
var results = manager.IntersectRay(new MapId(0), ray, returnOnFirstHit: false).ToList();
Assert.That(results.Count, Is.EqualTo(2));
Assert.That(results[0].HitEntity.Uid, Is.EqualTo(e1.Uid));
Assert.That(results[1].HitEntity.Uid, Is.EqualTo(e2.Uid));
Assert.That(results[0].Distance, Is.EqualTo(5));
Assert.That(results[0].HitPos.X, Is.EqualTo(5));
Assert.That(results[0].HitPos.Y, Is.EqualTo(1));
Assert.That(results[1].Distance, Is.EqualTo(6));
Assert.That(results[1].HitPos.X, Is.EqualTo(6));
Assert.That(results[1].HitPos.Y, Is.EqualTo(1));
}
[Test]
public void DoCollisionTestTrue()
{
// Arrange
var results = new List<IPhysBody>(1);
var manager = new PhysicsManager();
var mockEntity0 = new Mock<IEntity>().Object;
@@ -221,9 +235,8 @@ namespace Robust.UnitTesting.Shared.Physics
var mock0 = new Mock<IPhysBody>();
mock0.Setup(foo => foo.WorldAABB).Returns(new Box2(-3, -3, 6, 6));
mock0.Setup(foo => foo.IsHardCollidable).Returns(true);
mock0.Setup(foo => foo.MapID).Returns(new MapId(1));
mock0.Setup(foo => foo.CollisionEnabled).Returns(true);
mock0.Setup(foo => foo.CanCollide).Returns(true);
mock0.Setup(foo => foo.CollisionLayer).Returns(0x4);
mock0.Setup(foo => foo.CollisionMask).Returns(0x04);
mock0.Setup(foo => foo.Owner).Returns(mockEntity0);
@@ -232,9 +245,8 @@ namespace Robust.UnitTesting.Shared.Physics
var mock1 = new Mock<IPhysBody>();
mock1.Setup(foo => foo.WorldAABB).Returns(new Box2(5, -5, 10, 6));
mock1.Setup(foo => foo.IsHardCollidable).Returns(true);
mock1.Setup(foo => foo.MapID).Returns(new MapId(1));
mock1.Setup(foo => foo.CollisionEnabled).Returns(true);
mock1.Setup(foo => foo.CanCollide).Returns(true);
mock1.Setup(foo => foo.CollisionLayer).Returns(0x4);
mock1.Setup(foo => foo.CollisionMask).Returns(0x04);
mock1.Setup(foo => foo.Owner).Returns(mockEntity1);
@@ -242,11 +254,11 @@ namespace Robust.UnitTesting.Shared.Physics
manager.AddBody(testBody);
// Act
manager.DoCollisionTest(testBody, testBody.WorldAABB, results);
var results = manager.GetCollidingEntities(testBody, Vector2.Zero).ToImmutableList();
// Assert
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0], Is.EqualTo(staticBody));
Assert.That(results[0], Is.EqualTo(mockEntity0));
}
}
}