You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

396 lines
12 KiB
C#

using System;
using System.Linq;
using Duality;
using Duality.Cloning;
using Duality.Components;
using Duality.Drawing;
using Duality.Editor;
using Duality.Properties;
using Steering.Properties;
namespace Steering
{
/// <summary>
/// An agent is the basic component you want to attach to computer-controlled characters.
/// It contains functionallity to avoid collisions with other agents/obstacles and tries
/// to get to some defined target-location. The avoidance is only local therefore it's
/// possible that the agent get stuck in local minima. For more complex navigation you
/// need a high-level pathfinding layer on top of the local avoidance.
/// </summary>
[RequiredComponent(typeof(Transform))]
[EditorHintCategory(CoreResNames.CategoryAI)]
[EditorHintImage(SteeringResNames.ImageAgent)]
public class Agent : Component
{
private IVelocitySampler sampler = null;
private IAgentCharacteristics characteristics = null;
private ISteeringTarget target = null;
private float radius = 64.0f;
private float toiHorizon = 240.0f;
[DontSerialize] private Vector2 suggestedVel = Vector2.Zero;
public IVelocitySampler Sampler
{
get { return this.sampler; }
set { this.sampler = value; }
}
public IAgentCharacteristics Characteristics
{
get { return this.characteristics; }
set { this.characteristics = value; }
}
public ISteeringTarget Target
{
get { return this.target; }
set { this.target = value; }
}
/// <summary>
/// [GET / SET] The Agents target velocity, i.e. the one which it tries to acquire.
/// This is a convenience property that automatically sets <see cref="TargetSpeed"/> and
/// <see cref="Target"/> to the appropriate values.
/// </summary>
[EditorHintFlags(MemberFlags.AffectsOthers)]
public Vector2 TargetVel
{
get
{
if (this.target is DirectionTarget)
{
return (this.target as DirectionTarget).Direction * this.TargetSpeed;
}
else
{
return Vector2.Zero;
}
}
set
{
if (!(this.target is DirectionTarget)) this.target = new DirectionTarget();
float valueLen = value.Length;
(this.target as DirectionTarget).Direction = value / (valueLen > 0.0f ? valueLen : 1.0f);
this.TargetSpeed = value.Length;
}
}
/// <summary>
/// [GET / SET] The Agents target velocity, i.e. the one which it tries to acquire.
/// This is a convenience property that automatically sets <see cref="TargetSpeed"/> and
/// <see cref="Target"/> to the appropriate values.
/// </summary>
[EditorHintFlags(MemberFlags.AffectsOthers)]
public Vector2 TargetPos
{
get
{
if (this.target is PointTarget)
{
return (this.target as PointTarget).Location;
}
else
{
return Vector2.Zero;
}
}
set
{
if (!(this.target is PointTarget)) this.target = new PointTarget();
(this.target as PointTarget).Location = value;
}
}
/// <summary>
/// [GET / SET] The target speed this Agent attempts to acquire unless distracted by other Agents.
/// </summary>
[EditorHintRange(0.0f, 10000.0f)]
public float TargetSpeed
{
get { return this.characteristics != null ? this.characteristics.PreferredSpeed : 0.0f; }
set
{
this.AcquireConfigObjects();
this.characteristics.PreferredSpeed = value;
}
}
/// <summary>
/// [GET / SET] The radius of the agent (an agent is always representet as circle)
/// </summary>
[EditorHintRange(0.0f, 10000.0f)]
public float Radius
{
get { return this.radius; }
set { this.radius = value; }
}
/// <summary>
/// [GET / SET] The maximum time of impact wich the agent will react on.
/// If you set this too high your agent will oscillate alot in crowded situations and if you set
/// it too low your agent will avoid very late which looks artificial.
/// </summary>
public float ToiHorizon
{
get { return this.toiHorizon; }
set { this.toiHorizon = value; }
}
/// <summary>
/// [GET] The calculated velocity which the agent calculated as optimum.
/// </summary>
public Vector2 SuggestedVel
{
get { return this.suggestedVel; }
}
public Agent()
{
this.AcquireConfigObjects();
}
internal void Update()
{
this.AcquireConfigObjects();
Transform transform = this.GameObj.Transform;
float scaledRad = this.radius * transform.Scale;
Agent[] otherAgents = AgentManager.Instance.FindNeighborAgents(this).ToArray();
this.sampler.Reset();
bool keepSampling = true;
Vector2 bestVelocity = Vector2.Zero;
float bestScore = float.PositiveInfinity;
while (keepSampling)
{
Vector2 sample = this.sampler.GetCurrentSample(this) * this.characteristics.MaxSpeed;
// penalities
float toiPenality = 0f;
// check against every obstacle
foreach (Agent otherAgent in otherAgents)
{
Transform otherTransform = otherAgent.GameObj.Transform;
float curToiPenality = 0f;
// calculate helper variables for RVO
Vector2 relPos = otherTransform.Pos.Xy - transform.Pos.Xy;
// -> calculate side (only sign is of interest) for HRVO
Vector2 averageVel = 0.5f * (transform.Vel.Xy + otherTransform.Vel.Xy);
float side = Vector2.Dot(relPos.PerpendicularRight, sample - averageVel);
float selfFactor = 0f;
float otherFactor = 1f;
if (side >= 0f)
{
// this is different from original RVO - we use the ratio of the observed velocities to determine
// how much responsibility one agent has
float selfSpeed = transform.Vel.Xy.Length;
float otherSpeed = otherTransform.Vel.Xy.Length;
selfFactor = 0.5f;
var selfPlusOtherSpeed = selfSpeed + otherSpeed;
if (selfPlusOtherSpeed > float.Epsilon)
selfFactor = otherSpeed / selfPlusOtherSpeed;
otherFactor = 1f - selfFactor;
}
// check time of impact
float curMinToi;
float curMaxToi;
Vector2 expectedRelVel = sample - (selfFactor * transform.Vel.Xy + otherFactor * otherTransform.Vel.Xy);
float otherScaledRad = otherAgent.radius * otherTransform.Scale;
if (ToiCircleCircle(relPos, scaledRad + otherScaledRad, expectedRelVel, out curMinToi, out curMaxToi) && 0f < curMaxToi)
{
if (curMinToi <= 0f)
{
// we collided - check which way we get here out
if (MathF.Abs(curMinToi) < MathF.Abs(curMaxToi))
{
// => if minT (which is behind us) is lower then it's a bad idea to keep going because the
// other way would bring us out earlier
curToiPenality = float.PositiveInfinity;
}
else
{
curToiPenality = curMaxToi;
}
}
else
{
// this is a new minimum toi
// => calculate penality
curToiPenality = MathF.Max(0f, this.toiHorizon - curMinToi) / this.toiHorizon;
}
}
toiPenality = MathF.Max(toiPenality, curToiPenality);
}
// ask the characteristics implementation how good this sample is
float score = characteristics.CalculateVelocityCost(this, sample, toiPenality);
// update sampler and check if we should stop
keepSampling = sampler.SetCurrentCost(score);
// check if this velocity is better then everything else we've seen so far
if (score < bestScore)
{
bestScore = score;
bestVelocity = sample;
}
#region visual logging of all sampled velocities
#if DEBUG
if (DebugVisualizationMode != VisualLoggingMode.None
&& DebugVisualizationMode != VisualLoggingMode.VelocityOnly
&& DebugVisualizationMode != VisualLoggingMode.AllVelocities)
{
Vector2 debugPos = sample / this.characteristics.MaxSpeed * DebugVelocityRadius;
float debugColorFactor = 0.0f;
switch (DebugVisualizationMode)
{
case VisualLoggingMode.Cost:
debugColorFactor = score;
break;
case VisualLoggingMode.ToiPenalty:
debugColorFactor = toiPenality;
break;
}
ColorRgba debugColor = ColorRgba.Lerp(ColorRgba.White, ColorRgba.Black, MathF.Pow(debugColorFactor, 1f/4f));
VisualDebugLog.DrawCircle(debugPos.X, debugPos.Y, 4f).AnchorAt(this.GameObj).WithColor(debugColor);
}
#endif
#endregion
}
this.suggestedVel = bestVelocity;
#region visual logging of the velocities
#if DEBUG
if (DebugVisualizationMode == VisualLoggingMode.AllVelocities)
{
Vector2 selfDebugVelocity = transform.Vel.Xy / Characteristics.MaxSpeed * DebugVelocityRadius;
VisualDebugLog.DrawVector(0f, 0f, selfDebugVelocity.X, selfDebugVelocity.Y).AnchorAt(GameObj).WithColor(ColorRgba.DarkGrey);
foreach (var otherAgent in otherAgents)
{
Transform otherTransform = otherAgent.GameObj.Transform;
Vector2 debugVelocity = otherTransform.Vel.Xy / otherAgent.Characteristics.MaxSpeed * DebugVelocityRadius;
VisualDebugLog.DrawVector(0f, 0f, debugVelocity.X, debugVelocity.Y).AnchorAt(otherAgent.GameObj).WithColor(ColorRgba.DarkGrey);
}
}
if (DebugVisualizationMode != VisualLoggingMode.None)
{
Vector2 curVelocity = transform.Vel.Xy / Characteristics.MaxSpeed * DebugVelocityRadius;
VisualDebugLog.DrawVector(0f, 0f, curVelocity.X, curVelocity.Y).AnchorAt(GameObj).WithColor(ColorRgba.DarkGrey);
var debugVelocity = this.suggestedVel / characteristics.MaxSpeed * DebugVelocityRadius;
VisualDebugLog.DrawVector(0f, 0f, debugVelocity.X, debugVelocity.Y).AnchorAt(this.GameObj);
}
#endif
#endregion
}
private void AcquireConfigObjects()
{
if (this.sampler == null) this.sampler = new AdaptiveVelocitySampler();
if (this.target == null) this.target = new DirectionTarget();
if (this.characteristics == null) this.characteristics = new DefaultAgentCharacteristics();
}
// TODO: move me to a more general place
private static float RayRayIntersect(Vector2 start1, Vector2 dir1, Vector2 start2, Vector2 dir2)
{
var relStart = start2 - start1;
var num = dir2.Y * relStart.X - dir2.X * relStart.Y;
var den = dir1.X * dir2.Y - dir2.X * dir1.Y;
if (den <= float.Epsilon)
return float.NaN;
return num / den;
}
// TODO: move me to a more general place
private static bool ToiCircleCircle(Vector2 relPos, float obstacleRadius, Vector2 relVel, out float minT, out float maxT)
{
// special handling for zero vector
if (relVel == Vector2.Zero)
{
// check if we collide
if (relVel.Length <= obstacleRadius)
{
minT = 0f;
maxT = float.PositiveInfinity;
return true;
}
else
{
minT = maxT = 0f;
return false;
}
}
/*
* Matlab:
* syms vx vy t cx cy x y r;
* x = vx * t;
* y = vy * t;
* res = solve((x - cx)^2 + (y - cy)^2 - r^2 == 0, t);
*
* (cx*vx + cy*vy + (- cx^2*vy^2 + 2*cx*cy*vx*vy - cy^2*vx^2 + r^2*vx^2 + r^2*vy^2)^(1/2))/(vx^2 + vy^2)
* (cx*vx + cy*vy - (- cx^2*vy^2 + 2*cx*cy*vx*vy - cy^2*vx^2 + r^2*vx^2 + r^2*vy^2)^(1/2))/(vx^2 + vy^2)
*/
var cx = relPos.X;
var cy = relPos.Y;
var vx = relVel.X;
var vy = relVel.Y;
var r = obstacleRadius;
var sqrtTerm = -(cx * cx) * (vy * vy) + 2 * cx * cy * vx * vy - (cy * cy) * (vx * vx) + (r * r) * (vx * vx) + (r * r) * (vy * vy);
if (sqrtTerm < 0f)
{
minT = maxT = float.NaN;
return false;
}
var denom = ((vx * vx) + (vy * vy));
if (Math.Abs(denom) < float.Epsilon) {
if(denom < 0f)
minT = maxT = float.NegativeInfinity;
else
minT = maxT = float.PositiveInfinity;
return true;
}
maxT = (cx * vx + cy * vy + MathF.Sqrt(sqrtTerm)) / denom;
minT = (cx * vx + cy * vy - MathF.Sqrt(sqrtTerm)) / denom;
return true;
}
#region visual logging stuff
public enum VisualLoggingMode
{
None,
VelocityOnly,
AllVelocities,
ToiPenalty,
Cost
}
#if DEBUG
private VisualLoggingMode debugVisualizationMode;
public VisualLoggingMode DebugVisualizationMode
{
get { return debugVisualizationMode; }
set { debugVisualizationMode = value; }
}
static Agent()
{
VisualDebugLog.BaseColor = ColorRgba.White;
}
private static readonly VisualLog VisualDebugLog = VisualLog.Get("agent");
private const float DebugVelocityRadius = 100f;
#endif
#endregion
}
}