Replaced static entity lists (e.g., HullList, GapList, MapEntityList, etc.) with thread-safe copy-on-write wrappers to improve concurrency and prevent race conditions. Updated usages and related methods to support the new thread-safe collections, ensuring atomic operations and lock-free reads throughout the codebase.
1096 lines
41 KiB
C#
1096 lines
41 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Xml.Linq;
|
|
using LimbParams = Barotrauma.RagdollParams.LimbParams;
|
|
using ColliderParams = Barotrauma.RagdollParams.ColliderParams;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
/// <summary>
|
|
/// Thread-safe wrapper for PhysicsBody list operations.
|
|
/// Uses copy-on-write pattern for lock-free reads.
|
|
/// </summary>
|
|
internal class ThreadSafePhysicsBodyList : IEnumerable<PhysicsBody>
|
|
{
|
|
private volatile List<PhysicsBody> _list = new List<PhysicsBody>();
|
|
private readonly object _writeLock = new object();
|
|
|
|
public int Count => _list.Count;
|
|
|
|
public void Add(PhysicsBody body)
|
|
{
|
|
lock (_writeLock)
|
|
{
|
|
var newList = new List<PhysicsBody>(_list) { body };
|
|
Interlocked.Exchange(ref _list, newList);
|
|
}
|
|
}
|
|
|
|
public bool Remove(PhysicsBody body)
|
|
{
|
|
lock (_writeLock)
|
|
{
|
|
var newList = new List<PhysicsBody>(_list);
|
|
bool removed = newList.Remove(body);
|
|
if (removed)
|
|
{
|
|
Interlocked.Exchange(ref _list, newList);
|
|
}
|
|
return removed;
|
|
}
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
Interlocked.Exchange(ref _list, new List<PhysicsBody>());
|
|
}
|
|
|
|
public bool Contains(PhysicsBody body) => _list.Contains(body);
|
|
|
|
public PhysicsBody this[int index] => _list[index];
|
|
|
|
public IEnumerator<PhysicsBody> GetEnumerator() => _list.GetEnumerator();
|
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
|
|
|
// LINQ-friendly methods
|
|
public List<PhysicsBody> ToList() => new List<PhysicsBody>(_list);
|
|
public PhysicsBody FirstOrDefault(Func<PhysicsBody, bool> predicate) => _list.FirstOrDefault(predicate);
|
|
public PhysicsBody Find(Predicate<PhysicsBody> predicate) => _list.Find(predicate);
|
|
public IEnumerable<PhysicsBody> Where(Func<PhysicsBody, bool> predicate) => _list.Where(predicate);
|
|
public bool Any() => _list.Any();
|
|
public bool Any(Func<PhysicsBody, bool> predicate) => _list.Any(predicate);
|
|
}
|
|
|
|
class PosInfo
|
|
{
|
|
public Vector2 Position
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float? Rotation
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Vector2 LinearVelocity
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float? AngularVelocity
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public readonly float Timestamp;
|
|
public readonly UInt16 ID;
|
|
|
|
|
|
public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, float time)
|
|
: this(pos, rotation, linearVelocity, angularVelocity, 0, time)
|
|
{
|
|
}
|
|
|
|
public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID)
|
|
: this(pos, rotation, linearVelocity, angularVelocity, ID, 0.0f)
|
|
{
|
|
}
|
|
|
|
protected PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID, float time)
|
|
{
|
|
Position = pos;
|
|
Rotation = rotation;
|
|
LinearVelocity = linearVelocity;
|
|
AngularVelocity = angularVelocity;
|
|
|
|
this.ID = ID;
|
|
Timestamp = time;
|
|
}
|
|
|
|
public void TransformOutToInside(Submarine submarine)
|
|
{
|
|
//transform outside coordinates to in-sub coordinates
|
|
Position -= ConvertUnits.ToSimUnits(submarine.Position);
|
|
}
|
|
|
|
public void TransformInToOutside()
|
|
{
|
|
var sub = Submarine.FindContainingInLocalCoordinates(ConvertUnits.ToDisplayUnits(Position));
|
|
if (sub != null)
|
|
{
|
|
Position += ConvertUnits.ToSimUnits(sub.Position);
|
|
}
|
|
}
|
|
|
|
public void Translate(Vector2 posAmount,float rotationAmount)
|
|
{
|
|
Position += posAmount; Rotation += rotationAmount;
|
|
}
|
|
}
|
|
|
|
partial class PhysicsBody
|
|
{
|
|
public enum Shape
|
|
{
|
|
Circle, Rectangle, Capsule, HorizontalCapsule
|
|
};
|
|
|
|
public const float MinDensity = 0.01f;
|
|
public const float DefaultAngularDamping = 5.0f;
|
|
|
|
private static readonly ThreadSafePhysicsBodyList list = new ThreadSafePhysicsBodyList();
|
|
public static ThreadSafePhysicsBodyList List
|
|
{
|
|
get { return list; }
|
|
}
|
|
|
|
protected Vector2 prevPosition;
|
|
protected float prevRotation;
|
|
|
|
protected Vector2? targetPosition;
|
|
protected float? targetRotation;
|
|
|
|
private Vector2 drawPosition;
|
|
private float drawRotation;
|
|
|
|
public bool Removed
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Vector2 LastSentPosition
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private Shape bodyShape;
|
|
public float Height { get; private set; }
|
|
public float Width { get; private set; }
|
|
public float Radius { get; private set; }
|
|
|
|
private readonly float density;
|
|
|
|
//the direction the item is facing (for example, a gun has to be
|
|
//flipped horizontally if the Character holding it turns around)
|
|
float dir = 1.0f;
|
|
|
|
private Vector2 drawOffset;
|
|
private float rotationOffset;
|
|
|
|
private float lastProcessedNetworkState;
|
|
|
|
public float? PositionSmoothingFactor;
|
|
|
|
public Shape BodyShape
|
|
{
|
|
get { return bodyShape; }
|
|
}
|
|
|
|
public Vector2? TargetPosition
|
|
{
|
|
get { return targetPosition; }
|
|
set
|
|
{
|
|
if (value == null)
|
|
{
|
|
targetPosition = null;
|
|
}
|
|
else
|
|
{
|
|
if (!IsValidValue(value.Value, "target position", -1e5f, 1e5f)) return;
|
|
targetPosition = new Vector2(
|
|
MathHelper.Clamp(((Vector2)value).X, -10000.0f, 10000.0f),
|
|
MathHelper.Clamp(((Vector2)value).Y, -10000.0f, 10000.0f));
|
|
}
|
|
}
|
|
}
|
|
|
|
public float? TargetRotation
|
|
{
|
|
get { return targetRotation; }
|
|
set
|
|
{
|
|
if (value == null)
|
|
{
|
|
targetRotation = null;
|
|
}
|
|
else
|
|
{
|
|
if (!IsValidValue(value.Value, "target rotation")) return;
|
|
targetRotation = value;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public Vector2 DrawPosition
|
|
{
|
|
get { return Submarine == null ? drawPosition : drawPosition + Submarine.DrawPosition; }
|
|
}
|
|
|
|
public float DrawRotation
|
|
{
|
|
get { return drawRotation; }
|
|
}
|
|
|
|
public Submarine Submarine;
|
|
|
|
public float Dir
|
|
{
|
|
get { return dir; }
|
|
set { dir = value; }
|
|
}
|
|
|
|
private bool isEnabled = true;
|
|
private bool isPhysEnabled = true;
|
|
|
|
public bool Enabled
|
|
{
|
|
get { return isEnabled; }
|
|
set
|
|
{
|
|
isEnabled = value;
|
|
try
|
|
{
|
|
FarseerBody.Enabled = isEnabled && isPhysEnabled;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = " + value + " (" + isPhysEnabled + ")", e);
|
|
if (UserData != null)
|
|
{
|
|
DebugConsole.NewMessage("PhysicsBody UserData: " + UserData.GetType(), Color.Red);
|
|
}
|
|
if (GameMain.World.ContactManager == null)
|
|
{
|
|
DebugConsole.NewMessage("ContactManager is null!", Color.Red);
|
|
}
|
|
else if (GameMain.World.ContactManager.BroadPhase == null)
|
|
{
|
|
DebugConsole.NewMessage("Broadphase is null!", Color.Red);
|
|
}
|
|
if (FarseerBody.FixtureList == null)
|
|
{
|
|
DebugConsole.NewMessage("FixtureList is null!", Color.Red);
|
|
}
|
|
if (UserData is Entity entity)
|
|
{
|
|
DebugConsole.NewMessage("Entity \"" + entity + "\" removed!", Color.Red);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool PhysEnabled
|
|
{
|
|
get { return FarseerBody.Enabled; }
|
|
set
|
|
{
|
|
isPhysEnabled = value;
|
|
if (Enabled)
|
|
{
|
|
FarseerBody.Enabled = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public Vector2 SimPosition
|
|
{
|
|
get { return FarseerBody.Position; }
|
|
}
|
|
|
|
public Vector2 Position
|
|
{
|
|
get { return ConvertUnits.ToDisplayUnits(FarseerBody.Position); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Offset of the DrawPosition from the Position (i.e. how much the interpolated draw position is offset from the "actual position"). In display units.
|
|
/// </summary>
|
|
public Vector2 DrawPositionOffset => DrawPosition - Position;
|
|
|
|
public Vector2 PrevPosition
|
|
{
|
|
get { return prevPosition; }
|
|
}
|
|
|
|
public float Rotation
|
|
{
|
|
get { return FarseerBody.Rotation; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes flipping (Dir) into account.
|
|
/// </summary>
|
|
public float TransformedRotation => TransformRotation(Rotation, Dir);
|
|
|
|
public float TransformRotation(float rotation) => TransformRotation(rotation, dir);
|
|
|
|
public static float TransformRotation(float rot, float dir) => dir < 0 ? rot - MathHelper.Pi : rot;
|
|
|
|
public Vector2 LinearVelocity
|
|
{
|
|
get { return FarseerBody.LinearVelocity; }
|
|
set
|
|
{
|
|
if (!IsValidValue(value, "velocity", -1000.0f, 1000.0f)) return;
|
|
FarseerBody.LinearVelocity = value;
|
|
}
|
|
}
|
|
|
|
public float AngularVelocity
|
|
{
|
|
get { return FarseerBody.AngularVelocity; }
|
|
set
|
|
{
|
|
if (!IsValidValue(value, "angular velocity", -1000f, 1000f)) return;
|
|
FarseerBody.AngularVelocity = value;
|
|
}
|
|
}
|
|
|
|
public float Mass
|
|
{
|
|
get { return FarseerBody.Mass; }
|
|
}
|
|
|
|
public float Density
|
|
{
|
|
get { return density; }
|
|
}
|
|
|
|
public Body FarseerBody { get; private set; }
|
|
|
|
public object UserData
|
|
{
|
|
get { return FarseerBody.UserData; }
|
|
set { FarseerBody.UserData = value; }
|
|
}
|
|
|
|
public float Friction
|
|
{
|
|
set { FarseerBody.Friction = value; }
|
|
}
|
|
|
|
public BodyType BodyType
|
|
{
|
|
get { return FarseerBody.BodyType; }
|
|
set { FarseerBody.BodyType = value; }
|
|
}
|
|
|
|
private Category _collisionCategories;
|
|
|
|
public Category CollisionCategories
|
|
{
|
|
set
|
|
{
|
|
_collisionCategories = value;
|
|
FarseerBody.CollisionCategories = value;
|
|
}
|
|
get
|
|
{
|
|
return _collisionCategories;
|
|
}
|
|
}
|
|
|
|
private Category _collidesWith;
|
|
public Category CollidesWith
|
|
{
|
|
set
|
|
{
|
|
_collidesWith = value;
|
|
FarseerBody.CollidesWith = value;
|
|
}
|
|
get
|
|
{
|
|
return _collidesWith;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ignore rotation calls for the rest of this and the next update. Automatically disabled after that. Used for temporarily suppressing the SmoothRotate calls to prevent conflicting or unitentionally amplified rotations.
|
|
/// </summary>
|
|
public bool SuppressSmoothRotationCalls
|
|
{
|
|
get => _suppressSmoothRotationCalls;
|
|
set
|
|
{
|
|
_suppressSmoothRotationCalls = value;
|
|
smoothRotationSuppressionCounter = 0;
|
|
}
|
|
}
|
|
|
|
private bool _suppressSmoothRotationCalls;
|
|
private int smoothRotationSuppressionCounter;
|
|
|
|
public PhysicsBody(XElement element, float scale = 1.0f, bool findNewContacts = true) : this(element, Vector2.Zero, scale, findNewContacts: findNewContacts) { }
|
|
public PhysicsBody(ColliderParams cParams, bool findNewContacts = true) : this(cParams, Vector2.Zero, findNewContacts) { }
|
|
public PhysicsBody(LimbParams lParams, bool findNewContacts = true) : this(lParams, Vector2.Zero, findNewContacts) { }
|
|
|
|
public PhysicsBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true)
|
|
{
|
|
density = Math.Max(density, MinDensity);
|
|
CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts);
|
|
LastSentPosition = FarseerBody.Position;
|
|
list.Add(this);
|
|
}
|
|
|
|
public PhysicsBody(Body farseerBody)
|
|
{
|
|
FarseerBody = farseerBody;
|
|
if (FarseerBody.UserData == null) { FarseerBody.UserData = this; }
|
|
LastSentPosition = FarseerBody.Position;
|
|
list.Add(this);
|
|
}
|
|
|
|
public PhysicsBody(ColliderParams colliderParams, Vector2 position, bool findNewContacts = true)
|
|
{
|
|
float radius = ConvertUnits.ToSimUnits(colliderParams.Radius) * colliderParams.Ragdoll.LimbScale;
|
|
float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale;
|
|
float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale;
|
|
density = Physics.NeutralDensity;
|
|
CreateBody(width, height, radius, density, colliderParams.BodyType,
|
|
Physics.CollisionCharacter,
|
|
Physics.CollisionWall | Physics.CollisionLevel,
|
|
findNewContacts);
|
|
FarseerBody.AngularDamping = DefaultAngularDamping;
|
|
FarseerBody.FixedRotation = true;
|
|
FarseerBody.Friction = 0.05f;
|
|
FarseerBody.Restitution = 0.05f;
|
|
SetTransformIgnoreContacts(position, 0.0f);
|
|
LastSentPosition = position;
|
|
list.Add(this);
|
|
}
|
|
|
|
public PhysicsBody(LimbParams limbParams, Vector2 position, bool findNewContacts = true)
|
|
{
|
|
float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Scale * limbParams.Ragdoll.LimbScale;
|
|
float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Scale * limbParams.Ragdoll.LimbScale;
|
|
float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Scale * limbParams.Ragdoll.LimbScale;
|
|
density = Math.Max(limbParams.Density, MinDensity);
|
|
|
|
Category collisionCategory = Physics.CollisionCharacter;
|
|
Category collidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
|
|
if (limbParams.IgnoreCollisions)
|
|
{
|
|
collisionCategory = Category.None;
|
|
collidesWith = Category.None;
|
|
}
|
|
CreateBody(width, height, radius, density, BodyType.Dynamic,
|
|
collisionCategory: collisionCategory,
|
|
collidesWith: collidesWith,
|
|
findNewContacts: findNewContacts);
|
|
FarseerBody.Friction = limbParams.Friction;
|
|
FarseerBody.Restitution = limbParams.Restitution;
|
|
FarseerBody.AngularDamping = limbParams.AngularDamping;
|
|
FarseerBody.UserData = this;
|
|
_collisionCategories = collisionCategory;
|
|
_collidesWith = collidesWith;
|
|
SetTransformIgnoreContacts(position, 0.0f);
|
|
LastSentPosition = position;
|
|
list.Add(this);
|
|
}
|
|
|
|
public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f, float? forceDensity = null, Category collisionCategory = Physics.CollisionItem, Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform, bool findNewContacts = true)
|
|
{
|
|
float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale;
|
|
float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale;
|
|
float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale;
|
|
density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", Physics.NeutralDensity), MinDensity);
|
|
Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType);
|
|
if (element.GetAttributeBool("ignorecollision", false))
|
|
{
|
|
_collisionCategories = Category.None;
|
|
_collidesWith = Category.None;
|
|
}
|
|
else
|
|
{
|
|
_collisionCategories = collisionCategory;
|
|
_collidesWith = collidesWith;
|
|
}
|
|
CreateBody(width, height, radius, density, bodyType, _collisionCategories, _collidesWith, findNewContacts);
|
|
FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f);
|
|
FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f);
|
|
FarseerBody.GravityScale = element.GetAttributeFloat("gravityscale", 1.0f);
|
|
FarseerBody.UserData = this;
|
|
SetTransformIgnoreContacts(position, 0.0f);
|
|
LastSentPosition = position;
|
|
list.Add(this);
|
|
}
|
|
|
|
private void CreateBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true)
|
|
{
|
|
if (IsValidShape(radius, height, width))
|
|
{
|
|
bodyShape = DefineBodyShape(radius, width, height);
|
|
switch (bodyShape)
|
|
{
|
|
case Shape.Capsule:
|
|
FarseerBody = GameMain.World.CreateCapsule(height, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); ;
|
|
break;
|
|
case Shape.HorizontalCapsule:
|
|
FarseerBody = GameMain.World.CreateCapsuleHorizontal(width, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts);
|
|
break;
|
|
case Shape.Circle:
|
|
FarseerBody = GameMain.World.CreateCircle(radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts);
|
|
break;
|
|
case Shape.Rectangle:
|
|
FarseerBody = GameMain.World.CreateRectangle(width, height, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts);
|
|
break;
|
|
default:
|
|
throw new NotImplementedException(bodyShape.ToString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.ThrowError("Invalid physics body dimensions (width: " + width + ", height: " + height + ", radius: " + radius + ")");
|
|
}
|
|
Width = width;
|
|
Height = height;
|
|
Radius = radius;
|
|
_collisionCategories = collisionCategory;
|
|
_collidesWith = collidesWith;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the farthest point towards the forward of the body.
|
|
/// For capsules and circles, the front is at the top.
|
|
/// For horizontal capsules, the front is at the right-most point.
|
|
/// For rectangles, the front is either at the top or at the right, depending on which one of the two is greater: width or height.
|
|
/// The rotation is in radians.
|
|
/// </summary>
|
|
public Vector2 GetLocalFront(float? spritesheetRotation = null)
|
|
{
|
|
Vector2 pos;
|
|
switch (bodyShape)
|
|
{
|
|
case Shape.Capsule:
|
|
pos = new Vector2(0.0f, Height / 2 + Radius);
|
|
break;
|
|
case Shape.HorizontalCapsule:
|
|
pos = new Vector2(Width / 2 + Radius, 0.0f);
|
|
break;
|
|
case Shape.Circle:
|
|
pos = new Vector2(0.0f, Radius);
|
|
break;
|
|
case Shape.Rectangle:
|
|
pos = Height > Width ? new Vector2(0, Height / 2) : new Vector2(Width / 2, 0);
|
|
break;
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
return spritesheetRotation.HasValue ? Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation.Value)) : pos;
|
|
}
|
|
|
|
public float GetMaxExtent()
|
|
{
|
|
switch (bodyShape)
|
|
{
|
|
case Shape.Capsule:
|
|
return Height / 2 + Radius;
|
|
case Shape.HorizontalCapsule:
|
|
return Width / 2 + Radius;
|
|
case Shape.Circle:
|
|
return Radius;
|
|
case Shape.Rectangle:
|
|
return new Vector2(Width * 0.5f, Height * 0.5f).Length();
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
public Vector2 GetSize()
|
|
{
|
|
switch (bodyShape)
|
|
{
|
|
case Shape.Capsule:
|
|
return new Vector2(Radius * 2, Height + Radius * 2);
|
|
case Shape.HorizontalCapsule:
|
|
return new Vector2(Width + Radius * 2, Radius * 2);
|
|
case Shape.Circle:
|
|
return new Vector2(Radius * 2);
|
|
case Shape.Rectangle:
|
|
return new Vector2(Width, Height);
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
public void SetSize(Vector2 size)
|
|
{
|
|
switch (bodyShape)
|
|
{
|
|
case Shape.Capsule:
|
|
Radius = Math.Max(size.X / 2, 0);
|
|
Height = Math.Max(size.Y - size.X, 0);
|
|
Width = 0;
|
|
break;
|
|
case Shape.HorizontalCapsule:
|
|
Radius = Math.Max(size.Y / 2, 0);
|
|
Width = Math.Max(size.X - size.Y, 0);
|
|
Height = 0;
|
|
break;
|
|
case Shape.Circle:
|
|
Radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0);
|
|
Width = 0;
|
|
Height = 0;
|
|
break;
|
|
case Shape.Rectangle:
|
|
Width = Math.Max(size.X, 0);
|
|
Height = Math.Max(size.Y, 0);
|
|
Radius = 0;
|
|
break;
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
public bool IsValidValue(float value, string valueName, float minValue = float.MinValue, float maxValue = float.MaxValue)
|
|
{
|
|
if (!MathUtils.IsValid(value) || value < minValue || value > maxValue)
|
|
{
|
|
string userData = UserData == null ? "null" : UserData.ToString();
|
|
string errorMsg =
|
|
"Attempted to apply invalid " + valueName +
|
|
" to a physics body (userdata: " + userData +
|
|
"), value: " + value;
|
|
if (GameMain.NetworkMember != null)
|
|
{
|
|
errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
|
|
}
|
|
errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace();
|
|
|
|
if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce(
|
|
"PhysicsBody.SetPosition:InvalidPosition" + userData,
|
|
GameAnalyticsManager.ErrorSeverity.Error,
|
|
errorMsg);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool IsValidValue(Vector2 value, string valueName, float minValue = float.MinValue, float maxValue = float.MaxValue)
|
|
{
|
|
if (!MathUtils.IsValid(value) ||
|
|
(value.X < minValue || value.Y < minValue) ||
|
|
(value.X > maxValue || value.Y > maxValue))
|
|
{
|
|
string userData = UserData == null ? "null" : UserData.ToString();
|
|
string errorMsg =
|
|
"Attempted to apply invalid " + valueName +
|
|
" to a physics body (userdata: " + userData +
|
|
"), value: " + value;
|
|
if (GameMain.NetworkMember != null)
|
|
{
|
|
errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
|
|
}
|
|
errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace();
|
|
|
|
if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce(
|
|
"PhysicsBody.SetPosition:InvalidPosition" + userData,
|
|
GameAnalyticsManager.ErrorSeverity.Error,
|
|
errorMsg);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void ResetDynamics()
|
|
{
|
|
FarseerBody.ResetDynamics();
|
|
}
|
|
|
|
public void ApplyLinearImpulse(Vector2 impulse)
|
|
{
|
|
if (!IsValidValue(impulse / FarseerBody.Mass, "new velocity", -1000f, 1000f)) return;
|
|
if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;
|
|
|
|
FarseerBody.ApplyLinearImpulse(impulse);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply an impulse to the body without increasing it's velocity above a specific limit.
|
|
/// </summary>
|
|
public void ApplyLinearImpulse(Vector2 impulse, float maxVelocity)
|
|
{
|
|
if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;
|
|
if (!IsValidValue(maxVelocity, "max velocity")) return;
|
|
|
|
Vector2 velocityAddition = impulse / Mass;
|
|
Vector2 newVelocity = FarseerBody.LinearVelocity + velocityAddition;
|
|
float newSpeedSqr = newVelocity.LengthSquared();
|
|
if (newSpeedSqr > maxVelocity * maxVelocity)
|
|
{
|
|
newVelocity = newVelocity.ClampLength(maxVelocity);
|
|
}
|
|
|
|
if (!IsValidValue((newVelocity - FarseerBody.LinearVelocity), "new velocity", -1000.0f, 1000.0f)) return;
|
|
|
|
FarseerBody.ApplyLinearImpulse((newVelocity - FarseerBody.LinearVelocity) * Mass);
|
|
}
|
|
|
|
public void ApplyLinearImpulse(Vector2 impulse, Vector2 point)
|
|
{
|
|
if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;
|
|
if (!IsValidValue(point, "point")) return;
|
|
if (!IsValidValue(impulse / FarseerBody.Mass, "new velocity", -1000.0f, 1000.0f)) return;
|
|
FarseerBody.ApplyLinearImpulse(impulse, point);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply an impulse to the body without increasing it's velocity above a specific limit.
|
|
/// </summary>
|
|
public void ApplyLinearImpulse(Vector2 impulse, Vector2 point, float maxVelocity)
|
|
{
|
|
if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;
|
|
if (!IsValidValue(point, "point")) return;
|
|
if (!IsValidValue(maxVelocity, "max velocity")) return;
|
|
|
|
Vector2 velocityAddition = impulse / Mass;
|
|
Vector2 newVelocity = FarseerBody.LinearVelocity + velocityAddition;
|
|
float newSpeedSqr = newVelocity.LengthSquared();
|
|
if (newSpeedSqr > maxVelocity * maxVelocity)
|
|
{
|
|
newVelocity = newVelocity.ClampLength(maxVelocity);
|
|
}
|
|
|
|
if (!IsValidValue((newVelocity - FarseerBody.LinearVelocity), "new velocity", -1000.0f, 1000.0f)) return;
|
|
|
|
FarseerBody.ApplyLinearImpulse((newVelocity - FarseerBody.LinearVelocity) * Mass, point);
|
|
FarseerBody.AngularVelocity = MathHelper.Clamp(
|
|
FarseerBody.AngularVelocity,
|
|
-NetConfig.MaxPhysicsBodyAngularVelocity,
|
|
NetConfig.MaxPhysicsBodyAngularVelocity);
|
|
}
|
|
|
|
public void ApplyForce(Vector2 force, float maxVelocity = NetConfig.MaxPhysicsBodyVelocity)
|
|
{
|
|
if (!IsValidValue(maxVelocity, "max velocity")) { return; }
|
|
|
|
if (force.LengthSquared() < 0.01f) { return; }
|
|
|
|
Vector2 velocityAddition = force / Mass * (float)Timing.Step;
|
|
Vector2 newVelocity = FarseerBody.LinearVelocity + velocityAddition;
|
|
|
|
float newSpeedSqr = newVelocity.LengthSquared();
|
|
if (newSpeedSqr > maxVelocity * maxVelocity)
|
|
{
|
|
float currSpeed = FarseerBody.LinearVelocity.Length();
|
|
//limit velocity if the force is increasing the velocity in the current or new direction of travel
|
|
// = we don't want to limit it if the force is slowing down the movement: if a projectile is moving at 50 m/s
|
|
// and we apply a force that can only impart a maximum velocity of 1 m/s, we don't want to clamp that projectile to 1 m/s!
|
|
|
|
//force is acting in the same direction as the current velocity
|
|
// -> the maximum allowed change is the difference from current to max speed
|
|
if (Vector2.Dot(FarseerBody.LinearVelocity, force) > 0.0f)
|
|
{
|
|
force = velocityAddition.ClampLength(Math.Max(maxVelocity - currSpeed, 0)) * Mass / (float)Timing.Step;
|
|
}
|
|
//the new velocity will be in the direction of the force
|
|
// -> make sure it's not too much, maximum allowed change is current speed plus the max speed
|
|
else if (Vector2.Dot(newVelocity, force) > 0.0f)
|
|
{
|
|
force = velocityAddition.ClampLength(maxVelocity + currSpeed) * Mass / (float)Timing.Step;
|
|
}
|
|
}
|
|
|
|
if (!IsValidValue(force, "clamped force", -1e10f, 1e10f)) { return; }
|
|
FarseerBody.ApplyForce(force);
|
|
}
|
|
|
|
public void ApplyForce(Vector2 force, Vector2 point)
|
|
{
|
|
if (!IsValidValue(force, "force", -1e10f, 1e10f)) { return; }
|
|
if (!IsValidValue(point, "point")) { return; }
|
|
FarseerBody.ApplyForce(force, point);
|
|
}
|
|
|
|
public void ApplyTorque(float torque)
|
|
{
|
|
if (!IsValidValue(torque, "torque")) { return; }
|
|
FarseerBody.ApplyTorque(torque);
|
|
}
|
|
|
|
public bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform = true)
|
|
{
|
|
System.Diagnostics.Debug.Assert(MathUtils.IsValid(simPosition));
|
|
System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f);
|
|
System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f);
|
|
|
|
if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; }
|
|
if (!IsValidValue(rotation, "rotation")) { return false; }
|
|
|
|
FarseerBody.SetTransform(simPosition, rotation);
|
|
if (setPrevTransform) { SetPrevTransform(simPosition, rotation); }
|
|
return true;
|
|
}
|
|
|
|
public bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool setPrevTransform = true)
|
|
{
|
|
System.Diagnostics.Debug.Assert(MathUtils.IsValid(simPosition));
|
|
System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f);
|
|
System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f);
|
|
|
|
if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; }
|
|
if (!IsValidValue(rotation, "rotation")) { return false; }
|
|
|
|
FarseerBody.SetTransformIgnoreContacts(ref simPosition, rotation);
|
|
if (setPrevTransform) { SetPrevTransform(simPosition, rotation); }
|
|
return true;
|
|
}
|
|
|
|
public void SetPrevTransform(Vector2 simPosition, float rotation)
|
|
{
|
|
#if DEBUG
|
|
if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return; }
|
|
if (!IsValidValue(rotation, "rotation")) { return; }
|
|
#endif
|
|
prevPosition = simPosition;
|
|
prevRotation = rotation;
|
|
}
|
|
|
|
public void MoveToTargetPosition(bool lerp = true)
|
|
{
|
|
if (targetPosition == null) { return; }
|
|
|
|
if (lerp)
|
|
{
|
|
if (Vector2.DistanceSquared((Vector2)targetPosition, FarseerBody.Position) < 10.0f * 10.0f)
|
|
{
|
|
drawOffset = -((Vector2)targetPosition - (FarseerBody.Position + drawOffset));
|
|
prevPosition = (Vector2)targetPosition;
|
|
}
|
|
else
|
|
{
|
|
drawOffset = Vector2.Zero;
|
|
}
|
|
if (targetRotation.HasValue)
|
|
{
|
|
rotationOffset = -MathUtils.GetShortestAngle(FarseerBody.Rotation + rotationOffset, targetRotation.Value);
|
|
}
|
|
}
|
|
|
|
SetTransformIgnoreContacts((Vector2)targetPosition, targetRotation == null ? FarseerBody.Rotation : (float)targetRotation);
|
|
targetPosition = null;
|
|
targetRotation = null;
|
|
}
|
|
|
|
public void MoveToPos(Vector2 simPosition, float force, Vector2? pullPos = null)
|
|
{
|
|
if (pullPos == null) { pullPos = FarseerBody.Position; }
|
|
|
|
if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return; }
|
|
if (!IsValidValue(force, "force")) { return; }
|
|
|
|
Vector2 vel = FarseerBody.LinearVelocity;
|
|
Vector2 deltaPos = simPosition - (Vector2)pullPos;
|
|
if (deltaPos.LengthSquared() > 100.0f * 100.0f)
|
|
{
|
|
#if DEBUG
|
|
DebugConsole.ThrowError("Attempted to move a physics body to an invalid position.\n" + Environment.StackTrace.CleanupStackTrace());
|
|
#endif
|
|
return;
|
|
}
|
|
deltaPos *= force;
|
|
ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies buoyancy, drag and angular drag caused by water
|
|
/// </summary>
|
|
public void ApplyWaterForces()
|
|
{
|
|
//buoyancy
|
|
Vector2 buoyancy = new Vector2(0, Mass * 9.6f);
|
|
|
|
Vector2 dragForce = Vector2.Zero;
|
|
|
|
float speedSqr = LinearVelocity.LengthSquared();
|
|
if (speedSqr > 0.00001f)
|
|
{
|
|
//drag
|
|
float speed = (float)Math.Sqrt(speedSqr);
|
|
Vector2 velDir = LinearVelocity / speed;
|
|
|
|
float vel = speed * 2.0f;
|
|
float drag = vel * vel * Math.Max(Height + Radius * 2, Height);
|
|
dragForce = Math.Min(drag, Mass * 500.0f) * -velDir;
|
|
}
|
|
|
|
ApplyForce(dragForce + buoyancy);
|
|
ApplyTorque(FarseerBody.AngularVelocity * FarseerBody.Mass * -0.08f);
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
if (drawOffset.LengthSquared() < 0.01f)
|
|
{
|
|
PositionSmoothingFactor = null;
|
|
}
|
|
drawOffset = NetConfig.InterpolateSimPositionError(drawOffset, PositionSmoothingFactor);
|
|
rotationOffset = NetConfig.InterpolateRotationError(rotationOffset);
|
|
if (SuppressSmoothRotationCalls)
|
|
{
|
|
if (smoothRotationSuppressionCounter > 0)
|
|
{
|
|
SuppressSmoothRotationCalls = false;
|
|
}
|
|
else
|
|
{
|
|
smoothRotationSuppressionCounter++;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void UpdateDrawPosition(bool interpolate = true)
|
|
{
|
|
if (interpolate)
|
|
{
|
|
drawPosition = Timing.Interpolate(prevPosition, FarseerBody.Position);
|
|
drawPosition = ConvertUnits.ToDisplayUnits(drawPosition + drawOffset);
|
|
drawRotation = Timing.InterpolateRotation(prevRotation, FarseerBody.Rotation) + rotationOffset;
|
|
}
|
|
else
|
|
{
|
|
prevPosition = FarseerBody.Position;
|
|
drawPosition = ConvertUnits.ToDisplayUnits(FarseerBody.Position);
|
|
drawRotation = prevRotation = FarseerBody.Rotation;
|
|
drawOffset = Vector2.Zero;
|
|
rotationOffset = 0.0f;
|
|
}
|
|
}
|
|
|
|
public void CorrectPosition<T>(List<T> positionBuffer,
|
|
out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity) where T : PosInfo
|
|
{
|
|
newVelocity = LinearVelocity;
|
|
newPosition = SimPosition;
|
|
newRotation = Rotation;
|
|
newAngularVelocity = AngularVelocity;
|
|
|
|
while (positionBuffer.Count > 0 && positionBuffer[0].Timestamp < lastProcessedNetworkState)
|
|
{
|
|
positionBuffer.RemoveAt(0);
|
|
}
|
|
|
|
if (positionBuffer.Count == 0) { return; }
|
|
|
|
lastProcessedNetworkState = positionBuffer[0].Timestamp;
|
|
|
|
newVelocity = positionBuffer[0].LinearVelocity;
|
|
newPosition = positionBuffer[0].Position;
|
|
newRotation = positionBuffer[0].Rotation ?? Rotation;
|
|
newAngularVelocity = positionBuffer[0].AngularVelocity ?? AngularVelocity;
|
|
|
|
positionBuffer.RemoveAt(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotate the body towards the target rotation in the "shortest direction", taking into account the current angular velocity to prevent overshooting.
|
|
/// </summary>
|
|
/// <param name="targetRotation">Desired rotation in radians</param>
|
|
/// <param name="force">How fast the body should be rotated. Does not represent any real unit, you may want to experiment with different values to get the desired effect.</param>
|
|
/// <param name="wrapAngle">Should the angles be wrapped. Set to false if it makes a difference whether the angle of the body is 0.0f or 360.0f.</param>
|
|
public void SmoothRotate(float targetRotation, float force = 10.0f, bool wrapAngle = true)
|
|
{
|
|
if (SuppressSmoothRotationCalls) { return; }
|
|
float nextAngle = FarseerBody.Rotation + FarseerBody.AngularVelocity * (float)Timing.Step;
|
|
float angle = wrapAngle ?
|
|
MathUtils.GetShortestAngle(nextAngle, targetRotation) :
|
|
MathHelper.Clamp(targetRotation - nextAngle, -MathHelper.Pi, MathHelper.Pi);
|
|
float torque = angle * 60.0f * (force / 100.0f);
|
|
|
|
if (FarseerBody.BodyType == BodyType.Kinematic)
|
|
{
|
|
if (!IsValidValue(torque, "torque")) { return; }
|
|
FarseerBody.AngularVelocity = torque;
|
|
}
|
|
else
|
|
{
|
|
ApplyTorque(FarseerBody.Mass * torque);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wraps the angle so it has "has the same number of revolutions" as this body, i.e. that the angles are at most 180 degrees apart.
|
|
/// For example, if the angle of this body was 720, an angle of 5 would get wrapped to 725.
|
|
/// </summary>
|
|
public float WrapAngleToSameNumberOfRevolutions(float angle)
|
|
{
|
|
if (float.IsInfinity(angle)) { return angle; }
|
|
while (Rotation - angle > MathHelper.TwoPi)
|
|
{
|
|
angle += MathHelper.TwoPi;
|
|
}
|
|
while (Rotation - angle < -MathHelper.TwoPi)
|
|
{
|
|
angle -= MathHelper.TwoPi;
|
|
}
|
|
return angle;
|
|
}
|
|
|
|
public void Remove()
|
|
{
|
|
list.Remove(this);
|
|
GameMain.World.Remove(FarseerBody);
|
|
|
|
Removed = true;
|
|
|
|
DisposeProjSpecific();
|
|
}
|
|
|
|
public static void RemoveAll()
|
|
{
|
|
for (int i = list.Count - 1; i >= 0; i--)
|
|
{
|
|
list[i].Remove();
|
|
}
|
|
System.Diagnostics.Debug.Assert(list.Count == 0);
|
|
}
|
|
|
|
public static bool IsValidShape(float radius, float height, float width) => radius > 0 || (height > 0 && width > 0);
|
|
|
|
public static Shape DefineBodyShape(float radius, float width, float height)
|
|
{
|
|
Shape bodyShape;
|
|
if (width <= 0 && height <= 0 && radius > 0)
|
|
{
|
|
bodyShape = Shape.Circle;
|
|
}
|
|
else if (radius > 0)
|
|
{
|
|
if (width > height)
|
|
{
|
|
bodyShape = Shape.HorizontalCapsule;
|
|
}
|
|
else
|
|
{
|
|
bodyShape = Shape.Capsule;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bodyShape = Shape.Rectangle;
|
|
}
|
|
return bodyShape;
|
|
}
|
|
|
|
partial void DisposeProjSpecific();
|
|
|
|
}
|
|
}
|