Add utility AI (#806)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com> Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com> Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.AI;
|
||||
using Robust.Client.Interfaces.Graphics.ClientEye;
|
||||
using Robust.Client.Interfaces.UserInterface;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.GameObjects.EntitySystems.AI
|
||||
{
|
||||
#if DEBUG
|
||||
public class ClientAiDebugSystem : EntitySystem
|
||||
{
|
||||
private AiDebugMode _tooltips = AiDebugMode.None;
|
||||
private readonly Dictionary<IEntity, PanelContainer> _aiBoxes = new Dictionary<IEntity,PanelContainer>();
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
if (_tooltips == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eyeManager = IoCManager.Resolve<IEyeManager>();
|
||||
foreach (var (entity, panel) in _aiBoxes)
|
||||
{
|
||||
if (entity == null) continue;
|
||||
|
||||
if (!eyeManager.GetWorldViewport().Contains(entity.Transform.WorldPosition))
|
||||
{
|
||||
panel.Visible = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var (x, y) = eyeManager.WorldToScreen(entity.Transform.GridPosition).Position;
|
||||
var offsetPosition = new Vector2(x - panel.Width / 2, y - panel.Height - 50f);
|
||||
panel.Visible = true;
|
||||
|
||||
LayoutContainer.SetPosition(panel, offsetPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<SharedAiDebug.UtilityAiDebugMessage>(HandleUtilityAiDebugMessage);
|
||||
SubscribeNetworkEvent<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage);
|
||||
SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(HandleJpsRouteMessage);
|
||||
}
|
||||
|
||||
private void HandleUtilityAiDebugMessage(SharedAiDebug.UtilityAiDebugMessage message)
|
||||
{
|
||||
if ((_tooltips & AiDebugMode.Thonk) != 0)
|
||||
{
|
||||
// I guess if it's out of range we don't know about it?
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var entity = entityManager.GetEntity(message.EntityUid);
|
||||
if (entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryCreatePanel(entity);
|
||||
|
||||
// Probably shouldn't access by index but it's a debugging tool so eh
|
||||
var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(0);
|
||||
label.Text = $"Current Task: {message.FoundTask}\n" +
|
||||
$"Task score: {message.ActionScore}\n" +
|
||||
$"Planning time (ms): {message.PlanningTime * 1000:0.0000}\n" +
|
||||
$"Considered {message.ConsideredTaskCount} tasks";
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
|
||||
{
|
||||
if ((_tooltips & AiDebugMode.Paths) != 0)
|
||||
{
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var entity = entityManager.GetEntity(message.EntityUid);
|
||||
if (entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryCreatePanel(entity);
|
||||
|
||||
var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1);
|
||||
label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" +
|
||||
$"Nodes traversed: {message.ClosedTiles.Count}\n" +
|
||||
$"Nodes per ms: {message.ClosedTiles.Count / (message.TimeTaken * 1000)}";
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
|
||||
{
|
||||
if ((_tooltips & AiDebugMode.Paths) != 0)
|
||||
{
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var entity = entityManager.GetEntity(message.EntityUid);
|
||||
if (entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryCreatePanel(entity);
|
||||
|
||||
var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1);
|
||||
label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" +
|
||||
$"Jump Nodes: {message.JumpNodes.Count}\n" +
|
||||
$"Jump Nodes per ms: {message.JumpNodes.Count / (message.TimeTaken * 1000)}";
|
||||
}
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
foreach (var tooltip in _aiBoxes.Values)
|
||||
{
|
||||
tooltip.Dispose();
|
||||
}
|
||||
_aiBoxes.Clear();
|
||||
_tooltips = AiDebugMode.None;
|
||||
}
|
||||
|
||||
|
||||
private void EnableTooltip(AiDebugMode tooltip)
|
||||
{
|
||||
_tooltips |= tooltip;
|
||||
}
|
||||
|
||||
private void DisableTooltip(AiDebugMode tooltip)
|
||||
{
|
||||
_tooltips &= ~tooltip;
|
||||
}
|
||||
|
||||
public void ToggleTooltip(AiDebugMode tooltip)
|
||||
{
|
||||
if ((_tooltips & tooltip) != 0)
|
||||
{
|
||||
DisableTooltip(tooltip);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnableTooltip(tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCreatePanel(IEntity entity)
|
||||
{
|
||||
if (!_aiBoxes.ContainsKey(entity))
|
||||
{
|
||||
var userInterfaceManager = IoCManager.Resolve<IUserInterfaceManager>();
|
||||
|
||||
var actionLabel = new Label
|
||||
{
|
||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
||||
};
|
||||
|
||||
var pathfindingLabel = new Label
|
||||
{
|
||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
||||
};
|
||||
|
||||
var vBox = new VBoxContainer()
|
||||
{
|
||||
SeparationOverride = 15,
|
||||
Children = {actionLabel, pathfindingLabel},
|
||||
};
|
||||
|
||||
var panel = new PanelContainer
|
||||
{
|
||||
StyleClasses = {"tooltipBox"},
|
||||
Children = {vBox},
|
||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
||||
ModulateSelfOverride = Color.White.WithAlpha(0.75f),
|
||||
};
|
||||
|
||||
userInterfaceManager.StateRoot.AddChild(panel);
|
||||
|
||||
_aiBoxes[entity] = panel;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AiDebugMode : byte
|
||||
{
|
||||
None = 0,
|
||||
Paths = 1 << 1,
|
||||
Thonk = 1 << 2,
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.AI;
|
||||
using Robust.Client.Graphics.Drawing;
|
||||
using Robust.Client.Graphics.Overlays;
|
||||
using Robust.Client.Graphics.Shaders;
|
||||
using Robust.Client.Interfaces.Graphics.ClientEye;
|
||||
using Robust.Client.Interfaces.Graphics.Overlays;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timers;
|
||||
|
||||
namespace Content.Client.GameObjects.EntitySystems.AI
|
||||
{
|
||||
#if DEBUG
|
||||
public class ClientPathfindingDebugSystem : EntitySystem
|
||||
{
|
||||
private PathfindingDebugMode _modes = PathfindingDebugMode.None;
|
||||
private float _routeDuration = 4.0f; // How long before we remove a route from the overlay
|
||||
private DebugPathfindingOverlay _overlay;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage);
|
||||
SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(HandleJpsRouteMessage);
|
||||
SubscribeNetworkEvent<SharedAiDebug.PathfindingGraphMessage>(HandleGraphMessage);
|
||||
}
|
||||
|
||||
private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
|
||||
{
|
||||
if ((_modes & PathfindingDebugMode.Nodes) != 0 ||
|
||||
(_modes & PathfindingDebugMode.Route) != 0)
|
||||
{
|
||||
_overlay.AStarRoutes.Add(message);
|
||||
Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
|
||||
{
|
||||
if (_overlay == null) return;
|
||||
_overlay.AStarRoutes.Remove(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
|
||||
{
|
||||
if ((_modes & PathfindingDebugMode.Nodes) != 0 ||
|
||||
(_modes & PathfindingDebugMode.Route) != 0)
|
||||
{
|
||||
_overlay.JpsRoutes.Add(message);
|
||||
Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
|
||||
{
|
||||
if (_overlay == null) return;
|
||||
_overlay.JpsRoutes.Remove(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGraphMessage(SharedAiDebug.PathfindingGraphMessage message)
|
||||
{
|
||||
if ((_modes & PathfindingDebugMode.Graph) != 0)
|
||||
{
|
||||
_overlay.UpdateGraph(message.Graph);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableOverlay()
|
||||
{
|
||||
if (_overlay != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
_overlay = new DebugPathfindingOverlay {Modes = _modes};
|
||||
overlayManager.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void DisableOverlay()
|
||||
{
|
||||
if (_overlay == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_overlay.Modes = 0;
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
overlayManager.RemoveOverlay(_overlay.ID);
|
||||
_overlay = null;
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
_modes = PathfindingDebugMode.None;
|
||||
DisableOverlay();
|
||||
}
|
||||
|
||||
|
||||
private void EnableMode(PathfindingDebugMode tooltip)
|
||||
{
|
||||
_modes |= tooltip;
|
||||
if (_modes != 0)
|
||||
{
|
||||
EnableOverlay();
|
||||
}
|
||||
_overlay.Modes = _modes;
|
||||
|
||||
if (tooltip == PathfindingDebugMode.Graph)
|
||||
{
|
||||
var systemMessage = new SharedAiDebug.RequestPathfindingGraphMessage();
|
||||
EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableMode(PathfindingDebugMode mode)
|
||||
{
|
||||
_modes &= ~mode;
|
||||
if (_modes == 0)
|
||||
{
|
||||
DisableOverlay();
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlay.Modes = _modes;
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleTooltip(PathfindingDebugMode mode)
|
||||
{
|
||||
if ((_modes & mode) != 0)
|
||||
{
|
||||
DisableMode(mode);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnableMode(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DebugPathfindingOverlay : Overlay
|
||||
{
|
||||
// TODO: Add a box like the debug one and show the most recent path stuff
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
public PathfindingDebugMode Modes { get; set; } = PathfindingDebugMode.None;
|
||||
|
||||
// Graph debugging
|
||||
public readonly Dictionary<int, List<Vector2>> Graph = new Dictionary<int, List<Vector2>>();
|
||||
private readonly Dictionary<int, Color> _graphColors = new Dictionary<int, Color>();
|
||||
|
||||
// Route debugging
|
||||
// As each pathfinder is very different you'll likely want to draw them completely different
|
||||
public readonly List<SharedAiDebug.AStarRouteMessage> AStarRoutes = new List<SharedAiDebug.AStarRouteMessage>();
|
||||
public readonly List<SharedAiDebug.JpsRouteMessage> JpsRoutes = new List<SharedAiDebug.JpsRouteMessage>();
|
||||
|
||||
public DebugPathfindingOverlay() : base(nameof(DebugPathfindingOverlay))
|
||||
{
|
||||
Shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
|
||||
}
|
||||
|
||||
public void UpdateGraph(Dictionary<int, List<Vector2>> graph)
|
||||
{
|
||||
Graph.Clear();
|
||||
_graphColors.Clear();
|
||||
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
||||
foreach (var (chunk, nodes) in graph)
|
||||
{
|
||||
Graph[chunk] = nodes;
|
||||
_graphColors[chunk] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(),
|
||||
robustRandom.NextFloat(), 0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGraph(DrawingHandleScreen screenHandle)
|
||||
{
|
||||
var eyeManager = IoCManager.Resolve<IEyeManager>();
|
||||
var viewport = IoCManager.Resolve<IEyeManager>().GetWorldViewport();
|
||||
|
||||
foreach (var (chunk, nodes) in Graph)
|
||||
{
|
||||
foreach (var tile in nodes)
|
||||
{
|
||||
if (!viewport.Contains(tile)) continue;
|
||||
|
||||
var screenTile = eyeManager.WorldToScreen(tile);
|
||||
|
||||
var box = new UIBox2(
|
||||
screenTile.X - 15.0f,
|
||||
screenTile.Y - 15.0f,
|
||||
screenTile.X + 15.0f,
|
||||
screenTile.Y + 15.0f);
|
||||
|
||||
screenHandle.DrawRect(box, _graphColors[chunk]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region pathfinder
|
||||
|
||||
private void DrawAStarRoutes(DrawingHandleScreen screenHandle)
|
||||
{
|
||||
var eyeManager = IoCManager.Resolve<IEyeManager>();
|
||||
var viewport = eyeManager.GetWorldViewport();
|
||||
|
||||
foreach (var route in AStarRoutes)
|
||||
{
|
||||
// Draw box on each tile of route
|
||||
foreach (var position in route.Route)
|
||||
{
|
||||
if (!viewport.Contains(position)) continue;
|
||||
var screenTile = eyeManager.WorldToScreen(position);
|
||||
// worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
|
||||
var box = new UIBox2(
|
||||
screenTile.X - 15.0f,
|
||||
screenTile.Y - 15.0f,
|
||||
screenTile.X + 15.0f,
|
||||
screenTile.Y + 15.0f);
|
||||
screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAStarNodes(DrawingHandleScreen screenHandle)
|
||||
{
|
||||
var eyeManager = IoCManager.Resolve<IEyeManager>();
|
||||
var viewport = eyeManager.GetWorldViewport();
|
||||
|
||||
foreach (var route in AStarRoutes)
|
||||
{
|
||||
var highestgScore = route.GScores.Values.Max();
|
||||
|
||||
foreach (var (tile, score) in route.GScores)
|
||||
{
|
||||
if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) ||
|
||||
!viewport.Contains(tile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var screenTile = eyeManager.WorldToScreen(tile);
|
||||
|
||||
var box = new UIBox2(
|
||||
screenTile.X - 15.0f,
|
||||
screenTile.Y - 15.0f,
|
||||
screenTile.X + 15.0f,
|
||||
screenTile.Y + 15.0f);
|
||||
|
||||
screenHandle.DrawRect(box, new Color(
|
||||
0.0f,
|
||||
score / highestgScore,
|
||||
1.0f - (score / highestgScore),
|
||||
0.1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawJpsRoutes(DrawingHandleScreen screenHandle)
|
||||
{
|
||||
var eyeManager = IoCManager.Resolve<IEyeManager>();
|
||||
var viewport = eyeManager.GetWorldViewport();
|
||||
|
||||
foreach (var route in JpsRoutes)
|
||||
{
|
||||
// Draw box on each tile of route
|
||||
foreach (var position in route.Route)
|
||||
{
|
||||
if (!viewport.Contains(position)) continue;
|
||||
var screenTile = eyeManager.WorldToScreen(position);
|
||||
// worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
|
||||
var box = new UIBox2(
|
||||
screenTile.X - 15.0f,
|
||||
screenTile.Y - 15.0f,
|
||||
screenTile.X + 15.0f,
|
||||
screenTile.Y + 15.0f);
|
||||
screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawJpsNodes(DrawingHandleScreen screenHandle)
|
||||
{
|
||||
var eyeManager = IoCManager.Resolve<IEyeManager>();
|
||||
var viewport = eyeManager.GetWorldViewport();
|
||||
|
||||
foreach (var route in JpsRoutes)
|
||||
{
|
||||
foreach (var tile in route.JumpNodes)
|
||||
{
|
||||
if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) ||
|
||||
!viewport.Contains(tile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var screenTile = eyeManager.WorldToScreen(tile);
|
||||
|
||||
var box = new UIBox2(
|
||||
screenTile.X - 15.0f,
|
||||
screenTile.Y - 15.0f,
|
||||
screenTile.X + 15.0f,
|
||||
screenTile.Y + 15.0f);
|
||||
|
||||
screenHandle.DrawRect(box, new Color(
|
||||
0.0f,
|
||||
1.0f,
|
||||
0.0f,
|
||||
0.2f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle)
|
||||
{
|
||||
if (Modes == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var screenHandle = (DrawingHandleScreen) handle;
|
||||
|
||||
if ((Modes & PathfindingDebugMode.Route) != 0)
|
||||
{
|
||||
DrawAStarRoutes(screenHandle);
|
||||
DrawJpsRoutes(screenHandle);
|
||||
}
|
||||
|
||||
if ((Modes & PathfindingDebugMode.Nodes) != 0)
|
||||
{
|
||||
DrawAStarNodes(screenHandle);
|
||||
DrawJpsNodes(screenHandle);
|
||||
}
|
||||
|
||||
if ((Modes & PathfindingDebugMode.Graph) != 0)
|
||||
{
|
||||
DrawGraph(screenHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum PathfindingDebugMode {
|
||||
None = 0,
|
||||
Route = 1 << 0,
|
||||
Graph = 1 << 1,
|
||||
Nodes = 1 << 2,
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user