diff --git a/SS14.Client.Graphics/Lighting/ILight.cs b/SS14.Client.Graphics/Lighting/ILight.cs index 62277b6b6..d104bad27 100644 --- a/SS14.Client.Graphics/Lighting/ILight.cs +++ b/SS14.Client.Graphics/Lighting/ILight.cs @@ -14,6 +14,7 @@ namespace SS14.Client.Graphics.Lighting Color Color { get; set; } Texture Mask { get; set; } LocalCoordinates Coordinates { get; set; } + Angle Rotation { get; set; } RenderImage RenderTarget { get; } LightState LightState { get; set; } LightMode LightMode { get; set; } diff --git a/SS14.Client.Graphics/Lighting/Light.cs b/SS14.Client.Graphics/Lighting/Light.cs index b6f4cb0bf..3b2125810 100644 --- a/SS14.Client.Graphics/Lighting/Light.cs +++ b/SS14.Client.Graphics/Lighting/Light.cs @@ -43,7 +43,9 @@ namespace SS14.Client.Graphics.Lighting Calculated = false; } } - + + public Angle Rotation { get; set; } + public LightMode LightMode { get; set; } public LightState LightState diff --git a/SS14.Client.Graphics/Lighting/ShadowMapResolver.cs b/SS14.Client.Graphics/Lighting/ShadowMapResolver.cs index 78a0e697b..ffcab1923 100644 --- a/SS14.Client.Graphics/Lighting/ShadowMapResolver.cs +++ b/SS14.Client.Graphics/Lighting/ShadowMapResolver.cs @@ -93,6 +93,9 @@ namespace SS14.Client.Graphics.Lighting // draw light mask onto lightRT var sprite = new Sprite(lightMask); sprite.Scale = new Vector2((float)lightRt.Width / maskSize.X, (float)lightRt.Height / maskSize.Y); + sprite.Origin = new Vector2(sprite.LocalBounds.Width / 2, sprite.LocalBounds.Height / 2); + sprite.Position = sprite.Origin * sprite.Scale; + sprite.Rotation = -light.Rotation + Math.PI / 2; // convert our angle to sfml angle (negative may be because light mask tex is flipped); lightRt.Draw(sprite); } lightRt.EndDrawing(); diff --git a/SS14.Client/GameObjects/ClientComponentFactory.cs b/SS14.Client/GameObjects/ClientComponentFactory.cs index d737a2383..f6c10dfbc 100644 --- a/SS14.Client/GameObjects/ClientComponentFactory.cs +++ b/SS14.Client/GameObjects/ClientComponentFactory.cs @@ -54,6 +54,8 @@ namespace SS14.Client.GameObjects RegisterReference(); Register(); + + RegisterIgnore("AiController"); } } } diff --git a/SS14.Client/GameObjects/Components/Light/PointLightComponent.cs b/SS14.Client/GameObjects/Components/Light/PointLightComponent.cs index 3cba03d3b..2d0b38e15 100644 --- a/SS14.Client/GameObjects/Components/Light/PointLightComponent.cs +++ b/SS14.Client/GameObjects/Components/Light/PointLightComponent.cs @@ -38,6 +38,20 @@ namespace SS14.Client.GameObjects } } + /// + /// Determines if the light mask should automatically rotate with the entity. (like a flashlight) + /// + public bool MaskAutoRotate { get; set; } + + /// + /// Local rotation of the light mask around the center origin + /// + public Angle Rotation + { + get => Light.Rotation; + set => Light.Rotation = value; + } + public int Radius { get => Light.Radius; @@ -111,6 +125,11 @@ namespace SS14.Client.GameObjects { ModeClass = LightModeClass.Constant; } + + if (mapping.TryGetNode("autoRot", out node)) + { + MaskAutoRotate = node.AsBool(); + } } /// @@ -153,6 +172,14 @@ namespace SS14.Client.GameObjects public override void Update(float frameTime) { base.Update(frameTime); + + var worldRotation = Owner.GetComponent().WorldRotation; + if (MaskAutoRotate && Light.Rotation != worldRotation) + { + Light.Rotation = worldRotation; + Light.Calculated = false; + } + Light.Update(frameTime); } diff --git a/SS14.Server/AI/AiLogicProcessor.cs b/SS14.Server/AI/AiLogicProcessor.cs new file mode 100644 index 000000000..a4c2d2360 --- /dev/null +++ b/SS14.Server/AI/AiLogicProcessor.cs @@ -0,0 +1,26 @@ +using SS14.Shared.Interfaces.GameObjects; + +namespace SS14.Server.AI +{ + /// + /// Base class for all AI Processors. + /// + public abstract class AiLogicProcessor + { + /// + /// Radius in meters that the AI can "see". + /// + public float VisionRadius { get; set; } + + /// + /// Entity this AI is controlling. + /// + public IEntity SelfEntity { get; set; } + + /// + /// Gives life to the AI. + /// + /// Time since last update in seconds. + public abstract void Update(float frameTime); + } +} diff --git a/SS14.Server/AI/AiLogicProcessorAttribute.cs b/SS14.Server/AI/AiLogicProcessorAttribute.cs new file mode 100644 index 000000000..ce1c01b4a --- /dev/null +++ b/SS14.Server/AI/AiLogicProcessorAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace SS14.Server.AI +{ + /// + /// This attribute is used to mark a class as a LogicProcessor for the AI system. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class AiLogicProcessorAttribute : Attribute + { + /// + /// Name of this LogicProcessor in serialized files. + /// + public string SerializeName { get; } + + /// + /// Creates an instance of this Attribute. + /// + /// Name of this LogicProcessor in serialized files. + public AiLogicProcessorAttribute(string serializeName) + { + SerializeName = serializeName; + } + } +} diff --git a/SS14.Server/GameObjects/Components/Mover/AiControllerComponent.cs b/SS14.Server/GameObjects/Components/Mover/AiControllerComponent.cs new file mode 100644 index 000000000..2a5a3225d --- /dev/null +++ b/SS14.Server/GameObjects/Components/Mover/AiControllerComponent.cs @@ -0,0 +1,33 @@ +using SS14.Server.AI; +using SS14.Server.Interfaces.GameObjects; +using SS14.Shared.GameObjects; +using SS14.Shared.GameObjects.Serialization; + +namespace SS14.Server.GameObjects.Components +{ + public class AiControllerComponent : Component, IMoverComponent + { + private string _logicName; + private AiLogicProcessor processor; + private float _visionRadius; + + public override string Name => "AiController"; + + public string LogicName => _logicName; + public AiLogicProcessor Processor { get; set; } + + public float VisionRadius + { + get => _visionRadius; + set => _visionRadius = value; + } + + public override void ExposeData(EntitySerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _logicName, "logic", null); + serializer.DataField(ref _visionRadius, "vision", 8.0f); + } + } +} diff --git a/SS14.Server/GameObjects/Components/Transform/TransformComponent.cs b/SS14.Server/GameObjects/Components/Transform/TransformComponent.cs index e062dfffc..3f25930c5 100644 --- a/SS14.Server/GameObjects/Components/Transform/TransformComponent.cs +++ b/SS14.Server/GameObjects/Components/Transform/TransformComponent.cs @@ -93,7 +93,8 @@ namespace SS14.Server.GameObjects RebuildMatrices(); } } - + + /// public Angle WorldRotation { get diff --git a/SS14.Server/GameObjects/EntitySystems/AiSystem.cs b/SS14.Server/GameObjects/EntitySystems/AiSystem.cs new file mode 100644 index 000000000..022159baf --- /dev/null +++ b/SS14.Server/GameObjects/EntitySystems/AiSystem.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using SS14.Server.AI; +using SS14.Server.GameObjects.Components; +using SS14.Shared.GameObjects; +using SS14.Shared.GameObjects.System; +using SS14.Shared.Interfaces.Reflection; +using SS14.Shared.IoC; + +namespace SS14.Server.GameObjects.EntitySystems +{ + internal class AiSystem : EntitySystem + { + private Dictionary _processorTypes = new Dictionary(); + + public AiSystem() + { + // register entity query + EntityQuery = new ComponentEntityQuery + { + OneSet = new List + { + typeof(AiControllerComponent), + }, + }; + + var reflectionMan = IoCManager.Resolve(); + var processors = reflectionMan.GetAllChildren(); + foreach (var processor in processors) + { + var att = (AiLogicProcessorAttribute)Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute)); + if (att != null) + { + _processorTypes.Add(att.SerializeName, processor); + } + } + } + + public override void Update(float frameTime) + { + var entities = EntityManager.GetEntities(EntityQuery); + foreach (var entity in entities) + { + var aiComp = entity.GetComponent(); + if (aiComp.Processor == null) + { + aiComp.Processor = CreateProcessor(aiComp.LogicName); + aiComp.Processor.SelfEntity = entity; + aiComp.Processor.VisionRadius = aiComp.VisionRadius; + } + + var processor = aiComp.Processor; + + processor.Update(frameTime); + } + } + + private AiLogicProcessor CreateProcessor(string name) + { + if (_processorTypes.TryGetValue(name, out var type)) + { + return (AiLogicProcessor) Activator.CreateInstance(type); + } + + // processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name + throw new ArgumentException($"Processor type {name} could not be found.", nameof(name)); + } + } +} diff --git a/SS14.Server/GameObjects/ServerComponentFactory.cs b/SS14.Server/GameObjects/ServerComponentFactory.cs index 203143cc8..0a77bc533 100644 --- a/SS14.Server/GameObjects/ServerComponentFactory.cs +++ b/SS14.Server/GameObjects/ServerComponentFactory.cs @@ -1,4 +1,5 @@ -using SS14.Server.GameObjects.Components.Container; +using SS14.Server.GameObjects.Components; +using SS14.Server.GameObjects.Components.Container; using SS14.Server.Interfaces.GameObjects; using SS14.Shared.GameObjects; using SS14.Shared.Interfaces.GameObjects.Components; @@ -38,6 +39,8 @@ namespace SS14.Server.GameObjects Register(); RegisterReference(); + + Register(); } } } diff --git a/SS14.Server/GameObjects/ServerEntityManager.cs b/SS14.Server/GameObjects/ServerEntityManager.cs index c23f17000..1b5348687 100644 --- a/SS14.Server/GameObjects/ServerEntityManager.cs +++ b/SS14.Server/GameObjects/ServerEntityManager.cs @@ -74,6 +74,7 @@ namespace SS14.Server.GameObjects return ForceSpawnEntityAt(entityType, new LocalCoordinates(position, map.FindGridAt(position))); } + /// public List GetEntityStates() { var stateEntities = new List(); @@ -85,6 +86,7 @@ namespace SS14.Server.GameObjects return stateEntities; } + /// public void SaveGridEntities(EntitySerializer serializer, GridId gridId) { // serialize all entities to disk @@ -101,6 +103,7 @@ namespace SS14.Server.GameObjects #region EntityGetters + /// public IEnumerable GetEntitiesIntersecting(MapId mapId, Box2 position) { foreach (var entity in GetEntities()) @@ -124,6 +127,7 @@ namespace SS14.Server.GameObjects } } + /// public IEnumerable GetEntitiesIntersecting(MapId mapId, Vector2 position) { foreach (var entity in GetEntities()) @@ -147,11 +151,13 @@ namespace SS14.Server.GameObjects } } + /// public IEnumerable GetEntitiesIntersecting(LocalCoordinates position) { return GetEntitiesIntersecting(position.MapID, position.ToWorld().Position); } + /// public IEnumerable GetEntitiesIntersecting(IEntity entity) { if (entity.TryGetComponent(out var component)) @@ -164,18 +170,21 @@ namespace SS14.Server.GameObjects } } + /// public IEnumerable GetEntitiesInRange(LocalCoordinates position, float range) { var aabb = new Box2(position.Position - new Vector2(range / 2, range / 2), position.Position + new Vector2(range / 2, range / 2)); return GetEntitiesIntersecting(position.MapID, aabb); } + /// public IEnumerable GetEntitiesInRange(MapId mapID, Box2 box, float range) { - var aabb = new Box2(box.Left-range, box.Top+range, box.Right+range, box.Bottom-range); + var aabb = new Box2(box.Left-range, box.Top-range, box.Right+range, box.Bottom+range); return GetEntitiesIntersecting(mapID, aabb); } + /// public IEnumerable GetEntitiesInRange(IEntity entity, float range) { if (entity.TryGetComponent(out var component)) @@ -191,6 +200,7 @@ namespace SS14.Server.GameObjects #endregion LocationGetters + /// public override void Startup() { base.Startup(); diff --git a/SS14.Server/SS14.Server.csproj b/SS14.Server/SS14.Server.csproj index 3834e016a..a66c6b7b7 100644 --- a/SS14.Server/SS14.Server.csproj +++ b/SS14.Server/SS14.Server.csproj @@ -96,6 +96,8 @@ + + @@ -103,6 +105,8 @@ + + diff --git a/SS14.Shared/GameObjects/Entity.cs b/SS14.Shared/GameObjects/Entity.cs index 350a576b8..7936c851f 100644 --- a/SS14.Shared/GameObjects/Entity.cs +++ b/SS14.Shared/GameObjects/Entity.cs @@ -71,6 +71,12 @@ namespace SS14.Shared.GameObjects EntityNetworkManager = networkManager; } + /// + public bool IsValid() + { + return !Deleted; + } + /// /// Initialize the entity's UID. This can only be called once. /// diff --git a/SS14.Shared/Interfaces/GameObjects/IEntity.cs b/SS14.Shared/Interfaces/GameObjects/IEntity.cs index 947385f40..cb976224b 100644 --- a/SS14.Shared/Interfaces/GameObjects/IEntity.cs +++ b/SS14.Shared/Interfaces/GameObjects/IEntity.cs @@ -38,7 +38,13 @@ namespace SS14.Shared.Interfaces.GameObjects /// The prototype that was used to create this entity. /// EntityPrototype Prototype { get; } - + + /// + /// Determines if this entity is still valid. + /// + /// True if this entity is still valid. + bool IsValid(); + /// /// "Matches" this entity with the provided entity query, returning whether or not the query matched. /// This is effectively equivalent to calling with this entity. diff --git a/SS14.Shared/Interfaces/Physics/ICollidable.cs b/SS14.Shared/Interfaces/Physics/ICollidable.cs index faa81a87d..6febb79b6 100644 --- a/SS14.Shared/Interfaces/Physics/ICollidable.cs +++ b/SS14.Shared/Interfaces/Physics/ICollidable.cs @@ -6,6 +6,11 @@ namespace SS14.Shared.Interfaces.Physics { public interface ICollidable { + /// + /// Entity that this collidable represents. + /// + IEntity Owner { get; } + /// /// AABB of this entity in world space. /// diff --git a/SS14.Shared/Interfaces/Physics/ICollisionManager.cs b/SS14.Shared/Interfaces/Physics/ICollisionManager.cs index f61534dff..2f7b3b526 100644 --- a/SS14.Shared/Interfaces/Physics/ICollisionManager.cs +++ b/SS14.Shared/Interfaces/Physics/ICollisionManager.cs @@ -1,5 +1,7 @@ -using SS14.Shared.Interfaces.GameObjects; +using System.Collections.Generic; +using SS14.Shared.Interfaces.GameObjects; using SS14.Shared.Maths; +using SS14.Shared.Physics; namespace SS14.Shared.Interfaces.Physics { @@ -18,5 +20,13 @@ namespace SS14.Shared.Interfaces.Physics void AddCollidable(ICollidable collidable); void RemoveCollidable(ICollidable collidable); void UpdateCollidable(ICollidable collidable); + + /// + /// Casts a ray in the world and returns the first thing it hit. + /// + /// Ray to cast in the world. + /// Maximum length of the ray in meters. + /// Owning entity of the object that was hit, or null if nothing was hit. + RayCastResults IntersectRay(Ray ray, float maxLength = 50); } } diff --git a/SS14.Shared/Maths/Angle.cs b/SS14.Shared/Maths/Angle.cs index d9973cb44..5c24f9091 100644 --- a/SS14.Shared/Maths/Angle.cs +++ b/SS14.Shared/Maths/Angle.cs @@ -32,6 +32,16 @@ namespace SS14.Shared.Maths Theta = theta; } + /// + /// Constructs an instance of an angle from an (un)normalized direction vector. + /// + /// + public Angle(Vector2 dir) + { + dir = dir.Normalized; + Theta = Math.Atan2(dir.Y, dir.X); + } + /// /// Converts this angle to a unit direction vector. /// diff --git a/SS14.Shared/Maths/Box2.cs b/SS14.Shared/Maths/Box2.cs index e59de1f44..504ff8ac2 100644 --- a/SS14.Shared/Maths/Box2.cs +++ b/SS14.Shared/Maths/Box2.cs @@ -19,12 +19,12 @@ namespace SS14.Shared.Maths public float Height => Math.Abs(Top - Bottom); public Vector2 Size => new Vector2(Width, Height); - public Box2(Vector2 topLeft, Vector2 bottomRight) + public Box2(Vector2 leftTop, Vector2 rightBottom) { - Left = topLeft.X; - Top = topLeft.Y; - Bottom = bottomRight.Y; - Right = bottomRight.X; + Left = leftTop.X; + Top = leftTop.Y; + Right = rightBottom.X; + Bottom = rightBottom.Y; } public Box2(float left, float top, float right, float bottom) @@ -57,10 +57,7 @@ namespace SS14.Shared.Maths public bool Encloses(Box2 inner) { - return Left <= inner.Left - && inner.Right <= Right - && Top <= inner.Top - && inner.Bottom <= Bottom; + return !(Left <= inner.Left) || !(inner.Right <= Right) || !(Top <= inner.Top) || !(inner.Bottom <= Bottom); } public bool Contains(float x, float y) @@ -71,9 +68,7 @@ namespace SS14.Shared.Maths public bool Contains(Vector2 point, bool closedRegion = true) { var xOK = closedRegion == Left <= Right ? point.X >= Left != point.X > Right : point.X > Left != point.X >= Right; - var yOK = closedRegion == Top <= Bottom ? point.Y >= Top != point.Y > Bottom : point.Y > Top != point.Y >= Bottom; - return xOK && yOK; } diff --git a/SS14.Shared/Maths/FloatMath.cs b/SS14.Shared/Maths/FloatMath.cs index 113f2d405..efa5d7655 100644 --- a/SS14.Shared/Maths/FloatMath.cs +++ b/SS14.Shared/Maths/FloatMath.cs @@ -6,6 +6,8 @@ namespace SS14.Shared.Maths { private const int LookupSize = 1024 * 64; //has to be power of 2 private static readonly float[] getSin, getCos; + public const float RadToDeg = (float)(180.0 / Math.PI); + public const float DegToRad = (float)(Math.PI / 180.0); static FloatMath() { @@ -50,7 +52,17 @@ namespace SS14.Shared.Maths cos = getCos[rot]; } - public const float Pi = (float) Math.PI; + public static float Min(float a, float b) + { + return Math.Min(a, b); + } + + public static float Max(float a, float b) + { + return Math.Max(a, b); + } + + public const float Pi = (float) Math.PI; public static float ToDegrees(float radians) { diff --git a/SS14.Shared/Maths/Quaternion.cs b/SS14.Shared/Maths/Quaternion.cs index 0f871b7de..637642018 100644 --- a/SS14.Shared/Maths/Quaternion.cs +++ b/SS14.Shared/Maths/Quaternion.cs @@ -139,6 +139,23 @@ namespace SS14.Shared.Maths set => w = value; } + public float x + { + get => xyz.X; + set => xyz.X = value; + } + + public float y + { + get => xyz.Y; + set => xyz.Y = value; + } + + public float z + { + get => xyz.Z; + set => xyz.Z = value; + } #endregion #region Instance @@ -238,6 +255,9 @@ namespace SS14.Shared.Maths #region Fields + private const float RadToDeg = (float)(180.0 / Math.PI); + private const float DegToRad = (float)(Math.PI / 180.0); + /// /// Defines the identity quaternion. /// @@ -356,6 +376,18 @@ namespace SS14.Shared.Maths #endregion + #region Dot + + /// + /// Calculates the dot product between two Quaternions. + /// + public static float Dot(Quaternion a, Quaternion b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W; + } + + #endregion + #region Conjugate /// @@ -530,6 +562,145 @@ namespace SS14.Shared.Maths #endregion + #region RotateTowards + + public static Quaternion RotateTowards(Quaternion from, Quaternion to, float maxDegreesDelta) + { + var num = Angle(from, to); + if (num == 0f) + { + return to; + } + var t = Math.Min(1f, maxDegreesDelta / num); + return Slerp(from, to, t); + } + + #endregion + + #region Angle + + public static float Angle(Quaternion a, Quaternion b) + { + var f = Dot(a, b); + return (float) (Math.Acos(Math.Min(Math.Abs(f), 1f)) * 2f * RadToDeg); + } + + #endregion + + #region LookRotation + + // from http://answers.unity3d.com/questions/467614/what-is-the-source-code-of-quaternionlookrotation.html + public static Quaternion LookRotation(ref Vector3 forward, ref Vector3 up) + { + forward = Vector3.Normalize(forward); + Vector3 right = Vector3.Normalize(Vector3.Cross(up, forward)); + up = Vector3.Cross(forward, right); + var m00 = right.X; + var m01 = right.Y; + var m02 = right.Z; + var m10 = up.X; + var m11 = up.Y; + var m12 = up.Z; + var m20 = forward.X; + var m21 = forward.Y; + var m22 = forward.Z; + + var num8 = (m00 + m11) + m22; + var quaternion = new Quaternion(); + if (num8 > 0f) + { + var num = (float)System.Math.Sqrt(num8 + 1f); + quaternion.w = num * 0.5f; + num = 0.5f / num; + quaternion.X = (m12 - m21) * num; + quaternion.Y = (m20 - m02) * num; + quaternion.Z = (m01 - m10) * num; + return quaternion; + } + if ((m00 >= m11) && (m00 >= m22)) + { + var num7 = (float)System.Math.Sqrt(((1f + m00) - m11) - m22); + var num4 = 0.5f / num7; + quaternion.X = 0.5f * num7; + quaternion.Y = (m01 + m10) * num4; + quaternion.Z = (m02 + m20) * num4; + quaternion.W = (m12 - m21) * num4; + return quaternion; + } + if (m11 > m22) + { + var num6 = (float)System.Math.Sqrt(((1f + m11) - m00) - m22); + var num3 = 0.5f / num6; + quaternion.X = (m10 + m01) * num3; + quaternion.Y = 0.5f * num6; + quaternion.Z = (m21 + m12) * num3; + quaternion.W = (m20 - m02) * num3; + return quaternion; + } + var num5 = (float)System.Math.Sqrt(((1f + m22) - m00) - m11); + var num2 = 0.5f / num5; + quaternion.X = (m20 + m02) * num2; + quaternion.Y = (m21 + m12) * num2; + quaternion.Z = 0.5f * num5; + quaternion.W = (m01 - m10) * num2; + return quaternion; + } + + #endregion + + #region Euler Angles + + // from http://stackoverflow.com/questions/12088610/conversion-between-euler-quaternion-like-in-unity3d-engine + public static Vector3 ToEulerRad(Quaternion rotation) + { + float sqw = rotation.w * rotation.w; + float sqx = rotation.x * rotation.x; + float sqy = rotation.y * rotation.y; + float sqz = rotation.z * rotation.z; + float unit = sqx + sqy + sqz + sqw; // if normalised is one, otherwise is correction factor + float test = rotation.x * rotation.w - rotation.y * rotation.z; + Vector3 v; + + if (test > 0.4995f * unit) + { // singularity at north pole + v.Y = (float) (2f * Math.Atan2(rotation.y, rotation.x)); + v.X = (float) (Math.PI / 2); + v.Z = 0; + return NormalizeAngles(v * RadToDeg); + } + if (test < -0.4995f * unit) + { // singularity at south pole + v.Y = (float) (-2f * Math.Atan2(rotation.y, rotation.x)); + v.X = (float) (-Math.PI / 2); + v.Z = 0; + return NormalizeAngles(v * RadToDeg); + } + Quaternion q = new Quaternion(rotation.w, rotation.z, rotation.x, rotation.y); + v.Y = (float)System.Math.Atan2(2f * q.x * q.w + 2f * q.y * q.z, 1 - 2f * (q.z * q.z + q.w * q.w)); // Yaw + v.X = (float)System.Math.Asin(2f * (q.x * q.z - q.w * q.y)); // Pitch + v.Z = (float)System.Math.Atan2(2f * q.x * q.y + 2f * q.z * q.w, 1 - 2f * (q.y * q.y + q.z * q.z)); // Roll + return NormalizeAngles(v * RadToDeg); + } + + private static Vector3 NormalizeAngles(Vector3 angles) + { + angles.X = NormalizeAngle(angles.X); + angles.Y = NormalizeAngle(angles.Y); + angles.Z = NormalizeAngle(angles.Z); + return angles; + } + + private static float NormalizeAngle(float angle) + { + while (angle > 360) + angle -= 360; + while (angle < 0) + angle += 360; + return angle; + } + + #endregion + #endregion #region Operators diff --git a/SS14.Shared/Maths/Ray.cs b/SS14.Shared/Maths/Ray.cs new file mode 100644 index 000000000..5043fd957 --- /dev/null +++ b/SS14.Shared/Maths/Ray.cs @@ -0,0 +1,170 @@ +using System; + +namespace SS14.Shared.Maths +{ + /// + /// A representation of a 2D ray. + /// + [Serializable] + public struct Ray : IEquatable + { + private readonly Vector2 _position; + private readonly Vector2 _direction; + + /// + /// Specifies the starting point of the ray. + /// + public Vector2 Position => _position; + + /// + /// Specifies the direction the ray is pointing. + /// + public Vector2 Direction => _direction; + + /// + /// Creates a new instance of a Ray. + /// + /// Starting position of the ray. + /// Unit direction vector that the ray is pointing. + public Ray(Vector2 position, Vector2 direction) + { + _position = position; + _direction = direction; + } + + #region Intersect Tests + + public bool Intersects(Box2 box, out float distance, out Vector2 hitPos) + { + hitPos = Vector2.Zero; + distance = 0; + + float tmin = 0.0f; // set to -FLT_MAX to get first hit on line + float tmax = float.MaxValue; // set to max distance ray can travel (for segment) + const float epsilon = 1.0E-07f; + + // X axis slab + { + if (Math.Abs(_direction.X) < epsilon) + { + // ray is parallel to this slab, it will never hit unless ray is inside box + if (_position.X < FloatMath.Min(box.Left, box.Right) || _position.X > FloatMath.Max(box.Left, box.Right)) + { + return false; + } + } + + // calculate intersection t value of ray with near and far plane of slab + var ood = 1.0f / _direction.X; + var t1 = (FloatMath.Min(box.Left, box.Right) - _position.X) * ood; + var t2 = (FloatMath.Max(box.Left, box.Right) - _position.X) * ood; + + // Make t1 be the intersection with near plane, t2 with far plane + if (t1 > t2) + MathHelper.Swap(ref t1, ref t2); + + // Compute the intersection of slab intersection intervals + tmin = FloatMath.Max(t1, tmin); + tmax = FloatMath.Min(t2, tmax); // Is this Min (SE) or Max(Textbook) + + // Exit with no collision as soon as slab intersection becomes empty + if (tmin > tmax) + { + return false; + } + } + + // Y axis slab + { + if (Math.Abs(_direction.Y) < epsilon) + { + // ray is parallel to this slab, it will never hit unless ray is inside box + if (_position.Y < FloatMath.Min(box.Top, box.Bottom) || _position.Y > FloatMath.Max(box.Top, box.Bottom)) + { + return false; + } + } + + // calculate intersection t value of ray with near and far plane of slab + var ood = 1.0f / _direction.Y; + var t1 = (FloatMath.Min(box.Top, box.Bottom) - _position.Y) * ood; + var t2 = (FloatMath.Max(box.Top, box.Bottom) - _position.Y) * ood; + + // Make t1 be the intersection with near plane, t2 with far plane + if (t1 > t2) + MathHelper.Swap(ref t1, ref t2); + + // Compute the intersection of slab intersection intervals + tmin = FloatMath.Max(t1, tmin); + tmax = FloatMath.Min(t2, tmax); // Is this Min (SE) or Max(Textbook) + + // Exit with no collision as soon as slab intersection becomes empty + if (tmin > tmax) + { + return false; + } + } + + // Ray intersects all slabs. Return point and intersection t value + hitPos = _position + _direction * tmin; + distance = tmin; + return true; + } + + #endregion + + #region Equality + + /// + /// Determines if this Ray and another Ray are equivalent. + /// + /// Ray to compare to. + public bool Equals(Ray other) + { + return _position.Equals(other._position) && _direction.Equals(other._direction); + } + + /// + /// Determines if this ray and another object is equivalent. + /// + /// Object to compare to. + public override bool Equals(object obj) + { + if (obj is null) return false; + return obj is Ray ray && Equals(ray); + } + + /// + /// Calculates the hash code of this Ray. + /// + public override int GetHashCode() + { + unchecked + { + return (_position.GetHashCode() * 397) ^ _direction.GetHashCode(); + } + } + + /// + /// Determines if two instances of Ray are equal. + /// + /// Ray on the left side of the operator. + /// Ray on the right side of the operator. + public static bool operator ==(Ray a, Ray b) + { + return a.Equals(b); + } + + /// + /// Determines if two instances of Ray are not equal. + /// + /// Ray on the left side of the operator. + /// Ray on the right side of the operator. + public static bool operator !=(Ray a, Ray b) + { + return !(a == b); + } + + #endregion + } +} diff --git a/SS14.Shared/Maths/Vector2.cs b/SS14.Shared/Maths/Vector2.cs index 68871a0b0..11960f5b1 100644 --- a/SS14.Shared/Maths/Vector2.cs +++ b/SS14.Shared/Maths/Vector2.cs @@ -30,6 +30,16 @@ namespace SS14.Shared.Maths /// public static readonly Vector2 One = new Vector2(1, 1); + /// + /// A unit vector pointing in the +X direction. + /// + public static readonly Vector2 UnitX = new Vector2(1, 0); + + /// + /// A unit vector pointing in the +Y direction. + /// + public static readonly Vector2 UnitY = new Vector2(0, 1); + /// /// Construct a vector from its coordinates. /// diff --git a/SS14.Shared/Network/NetChannel.cs b/SS14.Shared/Network/NetChannel.cs index f07224131..a59b2154d 100644 --- a/SS14.Shared/Network/NetChannel.cs +++ b/SS14.Shared/Network/NetChannel.cs @@ -24,6 +24,11 @@ namespace SS14.Shared.Network /// public string RemoteAddress => _connection.RemoteEndPoint.Address.ToString(); + /// + /// Exposes the lidgren connection. + /// + public NetConnection Connection => _connection; + /// /// Creates a new instance of a NetChannel. /// diff --git a/SS14.Shared/Network/NetManager.cs b/SS14.Shared/Network/NetManager.cs index e99e85f7e..10c105cff 100644 --- a/SS14.Shared/Network/NetManager.cs +++ b/SS14.Shared/Network/NetManager.cs @@ -441,15 +441,12 @@ namespace SS14.Shared.Network { if (_netPeer == null) return; - - var packet = BuildMessage(message); - var connection = ChanToCon(recipient); - _netPeer.SendMessage(packet, connection, NetDeliveryMethod.ReliableOrdered); - } - private NetConnection ChanToCon(INetChannel channel) - { - return _channels.FirstOrDefault(x => x.Value == channel).Key; + if(!(recipient is NetChannel channel)) + throw new ArgumentException($"Not of type {typeof(NetChannel).FullName}", nameof(recipient)); + + var packet = BuildMessage(message); + _netPeer.SendMessage(packet, channel.Connection, NetDeliveryMethod.ReliableOrdered); } /// diff --git a/SS14.Shared/Physics/CollisionManager.cs b/SS14.Shared/Physics/CollisionManager.cs index 1a62b61a3..2be88457b 100644 --- a/SS14.Shared/Physics/CollisionManager.cs +++ b/SS14.Shared/Physics/CollisionManager.cs @@ -198,6 +198,71 @@ namespace SS14.Shared.Physics b.RemovePoint(point); } + public RayCastResults IntersectRay(Ray ray, float maxLength = 50) + { + var closestResults = new RayCastResults(float.PositiveInfinity, Vector2.Zero, null); + var minDist = float.PositiveInfinity; + var localBounds = new Box2(0, BucketSize, BucketSize, 0); + + // for each bucket index + foreach (var kvIndices in _bucketIndex) + { + var worldBounds = localBounds.Translated(kvIndices.Key * BucketSize); + + // check if ray intersects the bucket AABB + if (ray.Intersects(worldBounds, out var dist, out _)) + { + // bucket is too far away + if(dist > maxLength) + continue; + + // get the object it intersected in the bucket + var bucket = _buckets[kvIndices.Value]; + if (TryGetClosestIntersect(ray, bucket, out var results)) + { + if (results.Distance < minDist) + { + minDist = results.Distance; + closestResults = results; + } + } + } + } + + return closestResults; + } + + /// + /// Return the closest object, inside a bucket, to the ray origin that was intersected (if any). + /// + private static bool TryGetClosestIntersect(Ray ray, CollidableBucket bucket, out RayCastResults results) + { + IEntity entity = null; + var hitPosition = Vector2.Zero; + var minDist = float.PositiveInfinity; + + foreach (var collidablePoint in bucket.GetPoints()) // *goes to kitchen to freshen up his drink...* + { + var worldAABB = collidablePoint.ParentAABB.Collidable.WorldAABB; + + if (ray.Intersects(worldAABB, out var dist, out var hitPos) && !(dist > minDist)) + { + minDist = dist; + hitPosition = hitPos; + entity = collidablePoint.ParentAABB.Collidable.Owner; + } + } + + if (minDist < float.PositiveInfinity) + { + results = new RayCastResults(minDist, hitPosition, entity); + return true; + } + + results = default(RayCastResults); + return false; + } + /// /// Gets a bucket given a point coordinate /// diff --git a/SS14.Shared/Physics/RayCastResults.cs b/SS14.Shared/Physics/RayCastResults.cs new file mode 100644 index 000000000..c15931ceb --- /dev/null +++ b/SS14.Shared/Physics/RayCastResults.cs @@ -0,0 +1,21 @@ +using SS14.Shared.Interfaces.GameObjects; +using SS14.Shared.Maths; + +namespace SS14.Shared.Physics +{ + public struct RayCastResults + { + public bool HitObject => Distance < float.PositiveInfinity; + + public IEntity HitEntity { get; } + public Vector2 HitPos { get; } + public float Distance { get; } + + public RayCastResults(float distance, Vector2 hitPos, IEntity hitEntity) + { + Distance = distance; + HitPos = hitPos; + HitEntity = hitEntity; + } + } +} diff --git a/SS14.Shared/SS14.Shared.csproj b/SS14.Shared/SS14.Shared.csproj index 5502cbbce..e4046f8e1 100644 --- a/SS14.Shared/SS14.Shared.csproj +++ b/SS14.Shared/SS14.Shared.csproj @@ -203,6 +203,7 @@ + @@ -237,6 +238,7 @@ + diff --git a/SS14.UnitTesting/SS14.UnitTesting.csproj b/SS14.UnitTesting/SS14.UnitTesting.csproj index 9f9f7f611..d34a84dba 100644 --- a/SS14.UnitTesting/SS14.UnitTesting.csproj +++ b/SS14.UnitTesting/SS14.UnitTesting.csproj @@ -49,15 +49,25 @@ x64 + + $(SolutionDir)packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll + + + $(SolutionDir)packages\Moq.4.8.2\lib\net45\Moq.dll + False $(SolutionDir)packages\NUnit.3.7.1\lib\net45\nunit.framework.dll - - - $(SolutionDir)packages\System.ValueTuple.4.3.1\lib\netstandard1.0\System.ValueTuple.dll + + + $(SolutionDir)packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + $(SolutionDir)packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll + + $(SolutionDir)packages\YamlDotNet.4.3.0\lib\net45\YamlDotNet.dll @@ -81,8 +91,10 @@ + + diff --git a/SS14.UnitTesting/Server/GameObjects/ServerEntityManager_Test.cs b/SS14.UnitTesting/Server/GameObjects/ServerEntityManager_Test.cs new file mode 100644 index 000000000..32bf7e847 --- /dev/null +++ b/SS14.UnitTesting/Server/GameObjects/ServerEntityManager_Test.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using SS14.Client.Interfaces.GameObjects; +using SS14.Server.GameObjects; +using SS14.Server.Interfaces.GameObjects; +using SS14.Shared.GameObjects; +using SS14.Shared.Interfaces.Map; +using SS14.Shared.IoC; +using SS14.Shared.Map; +using SS14.Shared.Maths; +using SS14.Shared.Prototypes; + +namespace SS14.UnitTesting.Server.GameObjects +{ + [TestFixture] + [TestOf(typeof(ServerEntityManager))] + class ServerEntityManager_Test : SS14UnitTest + { + public override UnitTestProject Project => UnitTestProject.Server; + + private IServerEntityManager EntityManager; + + const string PROTOTYPES = @" +- type: entity + name: dummyPoint + id: dummyPoint + components: + - type: Transform + +- type: entity + name: dummyAABB + id: dummyAABB + components: + - type: Transform + - type: BoundingBox +"; + + [OneTimeSetUp] + public void Setup() + { + EntityManager = IoCManager.Resolve(); + + var manager = IoCManager.Resolve(); + manager.LoadFromStream(new StringReader(PROTOTYPES)); + manager.Resync(); + } + + [Test] + public void GetEntityInRangePointTest() + { + // Arrange + var baseEnt = EntityManager.SpawnEntity("dummyPoint"); + var inRangeEnt = EntityManager.SpawnEntity("dummyPoint"); + inRangeEnt.GetComponent().WorldPosition = new Vector2(-2, -2); + + // Act + var results = EntityManager.GetEntitiesInRange(baseEnt, 4.00f); + + // Cleanup + var list = results.ToList(); + EntityManager.FlushEntities(); + + // Assert + Assert.That(list.Count, Is.EqualTo(2), list.Count.ToString); + } + + [Test] + public void GetEntityInRangeAABBTest() + { + // Arrange + var baseEnt = EntityManager.SpawnEntity("dummyAABB"); + var inRangeEnt = EntityManager.SpawnEntity("dummyAABB"); + inRangeEnt.GetComponent().WorldPosition = new Vector2(-2, -2); + + // Act + var results = EntityManager.GetEntitiesInRange(baseEnt, 4.00f); + + // Cleanup + var list = results.ToList(); + EntityManager.FlushEntities(); + + // Assert + Assert.That(list.Count, Is.EqualTo(2), list.Count.ToString); + } + } +} diff --git a/SS14.UnitTesting/Shared/Maths/Ray_Test.cs b/SS14.UnitTesting/Shared/Maths/Ray_Test.cs new file mode 100644 index 000000000..7ffd75eb0 --- /dev/null +++ b/SS14.UnitTesting/Shared/Maths/Ray_Test.cs @@ -0,0 +1,24 @@ +using NUnit.Framework; +using SS14.Shared.Maths; + +namespace SS14.UnitTesting.Shared.Maths +{ + [TestFixture] + [TestOf(typeof(Ray))] + class Ray_Test + { + [Test] + public void RayIntersectsBoxTest() + { + var box = new Box2(new Vector2(5, 5), new Vector2(10, -5)); + var ray = new Ray(new Vector2(0, 1), Vector2.UnitX); + + var result = ray.Intersects(box, out var dist, out var hitPos); + + Assert.That(result, Is.True); + Assert.That(dist, Is.EqualTo(5)); + Assert.That(hitPos.X, Is.EqualTo(5)); + Assert.That(hitPos.Y, Is.EqualTo(1)); + } + } +} diff --git a/SS14.UnitTesting/Shared/Physics/CollisionManager_Test.cs b/SS14.UnitTesting/Shared/Physics/CollisionManager_Test.cs new file mode 100644 index 000000000..ec6daed6e --- /dev/null +++ b/SS14.UnitTesting/Shared/Physics/CollisionManager_Test.cs @@ -0,0 +1,35 @@ +using Moq; +using NUnit.Framework; +using SS14.Shared.Interfaces.Physics; +using SS14.Shared.Maths; +using SS14.Shared.Physics; + +namespace SS14.UnitTesting.Shared.Physics +{ + [TestFixture] + [TestOf(typeof(CollisionManager))] + class CollisionManager_Test + { + [Test] + public void RayCastTest() + { + // Arrange + var box = new Box2(new Vector2(5, 5), new Vector2(10, -5)); + var ray = new Ray(new Vector2(0, 1), Vector2.UnitX); + var manager = new CollisionManager(); + + var mock = new Mock(); + mock.Setup(foo => foo.WorldAABB).Returns(box); + manager.AddCollidable(mock.Object); + + // Act + var result = manager.IntersectRay(ray); + + // Assert + Assert.That(result.HitObject, 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)); + } + } +} diff --git a/SS14.UnitTesting/packages.config b/SS14.UnitTesting/packages.config index a30bedd91..37ada5fe8 100644 --- a/SS14.UnitTesting/packages.config +++ b/SS14.UnitTesting/packages.config @@ -1,8 +1,11 @@  + + - + + \ No newline at end of file