Raycasting + Turret AI (#532)

* Ray -> Box intersection works.

* Turret AI finished.

* Turret Works :D

* Light masks can now be rotated.

* Shoddy angle MoveTowards works.

* Shoddy Vector2 MoveTowards works.

* And pretty broken Quaternion version..

* Slept on it, rotation works good enough now.

* Fixed nuget dependencies.

* Moved AimShootLifeProcessor.cs to content.
This commit is contained in:
Acruid
2018-03-09 12:48:34 -08:00
committed by Pieter-Jan Briers
parent 2fb6f27cd1
commit 3a5ea35c0b
33 changed files with 886 additions and 30 deletions

View File

@@ -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; }

View File

@@ -43,7 +43,9 @@ namespace SS14.Client.Graphics.Lighting
Calculated = false;
}
}
public Angle Rotation { get; set; }
public LightMode LightMode { get; set; }
public LightState LightState

View File

@@ -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();

View File

@@ -54,6 +54,8 @@ namespace SS14.Client.GameObjects
RegisterReference<ClickableComponent, IClickableComponent>();
Register<OccluderComponent>();
RegisterIgnore("AiController");
}
}
}

View File

@@ -38,6 +38,20 @@ namespace SS14.Client.GameObjects
}
}
/// <summary>
/// Determines if the light mask should automatically rotate with the entity. (like a flashlight)
/// </summary>
public bool MaskAutoRotate { get; set; }
/// <summary>
/// Local rotation of the light mask around the center origin
/// </summary>
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();
}
}
/// <inheritdoc />
@@ -153,6 +172,14 @@ namespace SS14.Client.GameObjects
public override void Update(float frameTime)
{
base.Update(frameTime);
var worldRotation = Owner.GetComponent<TransformComponent>().WorldRotation;
if (MaskAutoRotate && Light.Rotation != worldRotation)
{
Light.Rotation = worldRotation;
Light.Calculated = false;
}
Light.Update(frameTime);
}

View File

@@ -0,0 +1,26 @@
using SS14.Shared.Interfaces.GameObjects;
namespace SS14.Server.AI
{
/// <summary>
/// Base class for all AI Processors.
/// </summary>
public abstract class AiLogicProcessor
{
/// <summary>
/// Radius in meters that the AI can "see".
/// </summary>
public float VisionRadius { get; set; }
/// <summary>
/// Entity this AI is controlling.
/// </summary>
public IEntity SelfEntity { get; set; }
/// <summary>
/// Gives life to the AI.
/// </summary>
/// <param name="frameTime">Time since last update in seconds.</param>
public abstract void Update(float frameTime);
}
}

View File

@@ -0,0 +1,25 @@
using System;
namespace SS14.Server.AI
{
/// <summary>
/// This attribute is used to mark a class as a LogicProcessor for the AI system.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class AiLogicProcessorAttribute : Attribute
{
/// <summary>
/// Name of this LogicProcessor in serialized files.
/// </summary>
public string SerializeName { get; }
/// <summary>
/// Creates an instance of this Attribute.
/// </summary>
/// <param name="serializeName">Name of this LogicProcessor in serialized files.</param>
public AiLogicProcessorAttribute(string serializeName)
{
SerializeName = serializeName;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -93,7 +93,8 @@ namespace SS14.Server.GameObjects
RebuildMatrices();
}
}
/// <inheritdoc />
public Angle WorldRotation
{
get

View File

@@ -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<string, Type> _processorTypes = new Dictionary<string, Type>();
public AiSystem()
{
// register entity query
EntityQuery = new ComponentEntityQuery
{
OneSet = new List<Type>
{
typeof(AiControllerComponent),
},
};
var reflectionMan = IoCManager.Resolve<IReflectionManager>();
var processors = reflectionMan.GetAllChildren<AiLogicProcessor>();
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<AiControllerComponent>();
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));
}
}
}

View File

@@ -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<ContainerManagerComponent>();
RegisterReference<ContainerManagerComponent, IContainerManager>();
Register<AiControllerComponent>();
}
}
}

View File

@@ -74,6 +74,7 @@ namespace SS14.Server.GameObjects
return ForceSpawnEntityAt(entityType, new LocalCoordinates(position, map.FindGridAt(position)));
}
/// <inheritdoc />
public List<EntityState> GetEntityStates()
{
var stateEntities = new List<EntityState>();
@@ -85,6 +86,7 @@ namespace SS14.Server.GameObjects
return stateEntities;
}
/// <inheritdoc />
public void SaveGridEntities(EntitySerializer serializer, GridId gridId)
{
// serialize all entities to disk
@@ -101,6 +103,7 @@ namespace SS14.Server.GameObjects
#region EntityGetters
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Box2 position)
{
foreach (var entity in GetEntities())
@@ -124,6 +127,7 @@ namespace SS14.Server.GameObjects
}
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesIntersecting(MapId mapId, Vector2 position)
{
foreach (var entity in GetEntities())
@@ -147,11 +151,13 @@ namespace SS14.Server.GameObjects
}
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesIntersecting(LocalCoordinates position)
{
return GetEntitiesIntersecting(position.MapID, position.ToWorld().Position);
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesIntersecting(IEntity entity)
{
if (entity.TryGetComponent<BoundingBoxComponent>(out var component))
@@ -164,18 +170,21 @@ namespace SS14.Server.GameObjects
}
}
/// <inheritdoc />
public IEnumerable<IEntity> 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);
}
/// <inheritdoc />
public IEnumerable<IEntity> 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);
}
/// <inheritdoc />
public IEnumerable<IEntity> GetEntitiesInRange(IEntity entity, float range)
{
if (entity.TryGetComponent<BoundingBoxComponent>(out var component))
@@ -191,6 +200,7 @@ namespace SS14.Server.GameObjects
#endregion LocationGetters
/// <inheritdoc />
public override void Startup()
{
base.Startup();

View File

@@ -96,6 +96,8 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="AI\AiLogicProcessor.cs" />
<Compile Include="AI\AiLogicProcessorAttribute.cs" />
<Compile Include="ClientConsoleHost\Commands\ChatCommands.cs" />
<Compile Include="ClientConsoleHost\Commands\MapCommands.cs" />
<Compile Include="ClientConsoleHost\Commands\PlayerCommands.cs" />
@@ -103,6 +105,8 @@
<Compile Include="ClientConsoleHost\Commands\SysCommands.cs" />
<Compile Include="GameObjects\Components\Container\Container.cs" />
<Compile Include="GameObjects\Components\Container\ContainerManagerComponent.cs" />
<Compile Include="GameObjects\Components\Mover\AiControllerComponent.cs" />
<Compile Include="GameObjects\EntitySystems\AiSystem.cs" />
<Compile Include="Interfaces\GameObjects\IServerTransformComponent.cs" />
<Compile Include="Interfaces\Log\IServerLogManager.cs" />
<Compile Include="Interfaces\Maps\IMapLoader.cs" />

View File

@@ -71,6 +71,12 @@ namespace SS14.Shared.GameObjects
EntityNetworkManager = networkManager;
}
/// <inheritdoc />
public bool IsValid()
{
return !Deleted;
}
/// <summary>
/// Initialize the entity's UID. This can only be called once.
/// </summary>

View File

@@ -38,7 +38,13 @@ namespace SS14.Shared.Interfaces.GameObjects
/// The prototype that was used to create this entity.
/// </summary>
EntityPrototype Prototype { get; }
/// <summary>
/// Determines if this entity is still valid.
/// </summary>
/// <returns>True if this entity is still valid.</returns>
bool IsValid();
/// <summary>
/// "Matches" this entity with the provided entity query, returning whether or not the query matched.
/// This is effectively equivalent to calling <see cref="IEntityQuery.Match(IEntity)" /> with this entity.

View File

@@ -6,6 +6,11 @@ namespace SS14.Shared.Interfaces.Physics
{
public interface ICollidable
{
/// <summary>
/// Entity that this collidable represents.
/// </summary>
IEntity Owner { get; }
/// <summary>
/// AABB of this entity in world space.
/// </summary>

View File

@@ -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);
/// <summary>
/// Casts a ray in the world and returns the first thing it hit.
/// </summary>
/// <param name="ray">Ray to cast in the world.</param>
/// <param name="maxLength">Maximum length of the ray in meters.</param>
/// <returns>Owning entity of the object that was hit, or null if nothing was hit.</returns>
RayCastResults IntersectRay(Ray ray, float maxLength = 50);
}
}

View File

@@ -32,6 +32,16 @@ namespace SS14.Shared.Maths
Theta = theta;
}
/// <summary>
/// Constructs an instance of an angle from an (un)normalized direction vector.
/// </summary>
/// <param name="dir"></param>
public Angle(Vector2 dir)
{
dir = dir.Normalized;
Theta = Math.Atan2(dir.Y, dir.X);
}
/// <summary>
/// Converts this angle to a unit direction vector.
/// </summary>

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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);
/// <summary>
/// Defines the identity quaternion.
/// </summary>
@@ -356,6 +376,18 @@ namespace SS14.Shared.Maths
#endregion
#region Dot
/// <summary>
/// Calculates the dot product between two Quaternions.
/// </summary>
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
/// <summary>
@@ -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

170
SS14.Shared/Maths/Ray.cs Normal file
View File

@@ -0,0 +1,170 @@
using System;
namespace SS14.Shared.Maths
{
/// <summary>
/// A representation of a 2D ray.
/// </summary>
[Serializable]
public struct Ray : IEquatable<Ray>
{
private readonly Vector2 _position;
private readonly Vector2 _direction;
/// <summary>
/// Specifies the starting point of the ray.
/// </summary>
public Vector2 Position => _position;
/// <summary>
/// Specifies the direction the ray is pointing.
/// </summary>
public Vector2 Direction => _direction;
/// <summary>
/// Creates a new instance of a Ray.
/// </summary>
/// <param name="position">Starting position of the ray.</param>
/// <param name="direction">Unit direction vector that the ray is pointing.</param>
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
/// <summary>
/// Determines if this Ray and another Ray are equivalent.
/// </summary>
/// <param name="other">Ray to compare to.</param>
public bool Equals(Ray other)
{
return _position.Equals(other._position) && _direction.Equals(other._direction);
}
/// <summary>
/// Determines if this ray and another object is equivalent.
/// </summary>
/// <param name="obj">Object to compare to.</param>
public override bool Equals(object obj)
{
if (obj is null) return false;
return obj is Ray ray && Equals(ray);
}
/// <summary>
/// Calculates the hash code of this Ray.
/// </summary>
public override int GetHashCode()
{
unchecked
{
return (_position.GetHashCode() * 397) ^ _direction.GetHashCode();
}
}
/// <summary>
/// Determines if two instances of Ray are equal.
/// </summary>
/// <param name="a">Ray on the left side of the operator.</param>
/// <param name="b">Ray on the right side of the operator.</param>
public static bool operator ==(Ray a, Ray b)
{
return a.Equals(b);
}
/// <summary>
/// Determines if two instances of Ray are not equal.
/// </summary>
/// <param name="a">Ray on the left side of the operator.</param>
/// <param name="b">Ray on the right side of the operator.</param>
public static bool operator !=(Ray a, Ray b)
{
return !(a == b);
}
#endregion
}
}

View File

@@ -30,6 +30,16 @@ namespace SS14.Shared.Maths
/// </summary>
public static readonly Vector2 One = new Vector2(1, 1);
/// <summary>
/// A unit vector pointing in the +X direction.
/// </summary>
public static readonly Vector2 UnitX = new Vector2(1, 0);
/// <summary>
/// A unit vector pointing in the +Y direction.
/// </summary>
public static readonly Vector2 UnitY = new Vector2(0, 1);
/// <summary>
/// Construct a vector from its coordinates.
/// </summary>

View File

@@ -24,6 +24,11 @@ namespace SS14.Shared.Network
/// <inheritdoc />
public string RemoteAddress => _connection.RemoteEndPoint.Address.ToString();
/// <summary>
/// Exposes the lidgren connection.
/// </summary>
public NetConnection Connection => _connection;
/// <summary>
/// Creates a new instance of a NetChannel.
/// </summary>

View File

@@ -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);
}
/// <inheritdoc />

View File

@@ -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;
}
/// <summary>
/// Return the closest object, inside a bucket, to the ray origin that was intersected (if any).
/// </summary>
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;
}
/// <summary>
/// Gets a bucket given a point coordinate
/// </summary>

View File

@@ -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;
}
}
}

View File

@@ -203,6 +203,7 @@
<Compile Include="Maths\Matrix3.cs" />
<Compile Include="Maths\Matrix4.cs" />
<Compile Include="Maths\Quaternion.cs" />
<Compile Include="Maths\Ray.cs" />
<Compile Include="Maths\Vector2.cs" />
<Compile Include="Maths\Vector2i.cs" />
<Compile Include="Maths\Vector2u.cs" />
@@ -237,6 +238,7 @@
<Compile Include="Physics\CollisionManager.cs" />
<Compile Include="Enums\PlacementInformation.cs" />
<Compile Include="Input\KeyFunctions.cs" />
<Compile Include="Physics\RayCastResults.cs" />
<Compile Include="Players\PlayerIndex.cs" />
<Compile Include="Utility\QuadTree.cs" />
<Compile Include="Reflection\ReflectAttribute.cs" />

View File

@@ -49,15 +49,25 @@
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll</HintPath>
</Reference>
<Reference Include="Moq, Version=4.8.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\Moq.4.8.2\lib\net45\Moq.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<SpecificVersion>False</SpecificVersion>
<HintPath>$(SolutionDir)packages\NUnit.3.7.1\lib\net45\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.ValueTuple">
<HintPath>$(SolutionDir)packages\System.ValueTuple.4.3.1\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
<Reference Include="System.Configuration" />
<Reference Include="System.Threading.Tasks.Extensions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="YamlDotNet, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)packages\YamlDotNet.4.3.0\lib\net45\YamlDotNet.dll</HintPath>
</Reference>
@@ -81,8 +91,10 @@
<Compile Include="Shared\IoC\IoCManager_Test.cs" />
<Compile Include="Shared\Maths\Angle_Test.cs" />
<Compile Include="Shared\Maths\Matrix3_Test.cs" />
<Compile Include="Shared\Maths\Ray_Test.cs" />
<Compile Include="Shared\Maths\Vector2_Test.cs" />
<Compile Include="Shared\Maths\Direction_Test.cs" />
<Compile Include="Shared\Physics\CollisionManager_Test.cs" />
<Compile Include="Shared\Prototypes\PrototypeManager_Test.cs" />
<Compile Include="Shared\Reflection\ReflectionManager_Test.cs" />
<Compile Include="Shared\Serialization\NetSerializableAttribute_Test.cs" />

View File

@@ -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<IServerEntityManager>();
var manager = IoCManager.Resolve<IPrototypeManager>();
manager.LoadFromStream(new StringReader(PROTOTYPES));
manager.Resync();
}
[Test]
public void GetEntityInRangePointTest()
{
// Arrange
var baseEnt = EntityManager.SpawnEntity("dummyPoint");
var inRangeEnt = EntityManager.SpawnEntity("dummyPoint");
inRangeEnt.GetComponent<IServerTransformComponent>().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<IServerTransformComponent>().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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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<ICollidable>();
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));
}
}
}

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Castle.Core" version="4.2.1" targetFramework="net451" />
<package id="Moq" version="4.8.2" targetFramework="net451" />
<package id="NUnit" version="3.7.1" targetFramework="net451" />
<package id="NUnit.ConsoleRunner" version="3.7.0" targetFramework="net451" />
<package id="NUnit3TestAdapter" version="3.8.0" targetFramework="net451" />
<package id="System.ValueTuple" version="4.3.1" targetFramework="net451" />
<package id="System.Threading.Tasks.Extensions" version="4.3.0" targetFramework="net451" />
<package id="System.ValueTuple" version="4.4.0" targetFramework="net451" />
<package id="YamlDotNet" version="4.3.0" targetFramework="net451" />
</packages>