AI Wander & Barker (#286)

* Retrofit the AI system with new IoC features.
Fixed bug with turret rotation.

* Added new AI WanderProcessor, and it works.

* RNG walking directions are a bit more random now.

* Wander now actually uses the MoverSystem to move.
Wander now talks when he reaches his destination.

* Adds a new Static Barker AI for vending machines, so that they periodically advertise their brand.

* Barker now says some generic slogans.
Misc bug cleanup.

* Removed useless UsedImplicitly attribute from AI dependencies, suppressed unused variable warnings instead.
This commit is contained in:
Acruid
2019-08-10 05:19:52 -07:00
committed by Pieter-Jan Briers
parent 4b30c7e710
commit 8b593d28c6
8 changed files with 464 additions and 26 deletions

View File

@@ -20,9 +20,11 @@ namespace Content.Server.AI
[AiLogicProcessor("AimShootLife")] [AiLogicProcessor("AimShootLife")]
class AimShootLifeProcessor : AiLogicProcessor class AimShootLifeProcessor : AiLogicProcessor
{ {
private readonly IPhysicsManager _physMan; #pragma warning disable 649
private readonly IServerEntityManager _entMan; [Dependency] private readonly IPhysicsManager _physMan;
private readonly IGameTiming _timeMan; [Dependency] private readonly IServerEntityManager _entMan;
[Dependency] private readonly IGameTiming _timeMan;
#pragma warning restore 649
private readonly List<IEntity> _workList = new List<IEntity>(); private readonly List<IEntity> _workList = new List<IEntity>();
@@ -32,16 +34,6 @@ namespace Content.Server.AI
private IEntity _curTarget; private IEntity _curTarget;
/// <summary>
/// Creates an instance of this LogicProcessor.
/// </summary>
public AimShootLifeProcessor()
{
_physMan = IoCManager.Resolve<IPhysicsManager>();
_entMan = IoCManager.Resolve<IServerEntityManager>();
_timeMan = IoCManager.Resolve<IGameTiming>();
}
/// <inheritdoc /> /// <inheritdoc />
public override void Update(float frameTime) public override void Update(float frameTime)
{ {

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Content.Server.Interfaces.Chat;
using JetBrains.Annotations;
using Robust.Server.AI;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Content.Server.AI
{
/// <summary>
/// Designed for a a stationary entity that regularly advertises things (vending machine).
/// </summary>
[AiLogicProcessor("StaticBarker")]
class StaticBarkerProcessor : AiLogicProcessor
{
#pragma warning disable 649
[Dependency] private readonly IGameTiming _timeMan;
[Dependency] private readonly IChatManager _chatMan;
#pragma warning restore 649
private static readonly TimeSpan MinimumDelay = TimeSpan.FromSeconds(15);
private TimeSpan _nextBark;
private static List<string> slogans = new List<string>
{
"Come try my great products today!",
"More value for the way you live.",
"Quality you'd expect at prices you wouldn't.",
"The right stuff. The right price.",
};
public override void Update(float frameTime)
{
if(_timeMan.CurTime < _nextBark)
return;
var rngState = GenSeed();
_nextBark = _timeMan.CurTime + MinimumDelay + TimeSpan.FromSeconds(Random01(ref rngState) * 10);
var pick = (int)Math.Round(Random01(ref rngState) * (slogans.Count - 1));
_chatMan.EntitySay(SelfEntity, slogans[pick]);
}
private uint GenSeed()
{
return RotateRight((uint)_timeMan.CurTick.GetHashCode(), 11) ^ (uint)SelfEntity.Uid.GetHashCode();
}
private uint RotateRight(uint n, int s)
{
return (n << (32 - s)) | (n >> s);
}
private float Random01(ref uint state)
{
DebugTools.Assert(state != 0);
//xorshift32
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state / (float)uint.MaxValue;
}
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.Chat;
using Content.Shared.Physics;
using Robust.Server.AI;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Server.AI
{
/// <summary>
/// Designed to control a mob. The mob will wander around, then idle at a the destination for awhile.
/// </summary>
[AiLogicProcessor("Wander")]
class WanderProcessor : AiLogicProcessor
{
#pragma warning disable 649
[Dependency] private readonly IPhysicsManager _physMan;
[Dependency] private readonly IServerEntityManager _entMan;
[Dependency] private readonly IGameTiming _timeMan;
[Dependency] private readonly IEntitySystemManager _entSysMan;
[Dependency] private readonly IChatManager _chatMan;
#pragma warning restore 649
private static readonly TimeSpan IdleTimeSpan = TimeSpan.FromSeconds(1);
private static readonly TimeSpan WalkingTimeout = TimeSpan.FromSeconds(3);
private static readonly TimeSpan DisabledTimeout = TimeSpan.FromSeconds(10);
private static List<string> _normalAssistantConversation = new List<string>
{
"stat me",
"roll it easy!",
"waaaaaagh!!!",
"red wonz go fasta",
"FOR TEH EMPRAH",
"lol2cat",
"dem dwarfs man, dem dwarfs",
"SPESS MAHREENS",
"hwee did eet fhor khayosss",
"lifelike texture ;_;",
"luv can bloooom",
"PACKETS!!!",
"SARAH HALE DID IT!!!",
"Don't tell Chase",
"not so tough now huh",
"WERE NOT BAY!!",
"IF YOU DONT LIKE THE CYBORGS OR SLIMES WHY DONT YU O JUST MAKE YORE OWN!",
"DONT TALK TO ME ABOUT BALANCE!!!!",
"YOU AR JUS LAZY AND DUMB JAMITORS AND SERVICE ROLLS",
"BLAME HOSHI!!!",
"ARRPEE IZ DED!!!",
"THERE ALL JUS MEATAFRIENDS!",
"SOTP MESING WITH THE ROUNS SHITMAN!!!",
"SKELINGTON IS 4 SHITERS!",
"MOMMSI R THE WURST SCUM!!",
"How do we engiener=",
"try to live freely and automatically good bye",
"why woud i take a pin pointner??",
"How do I set up the. SHow do I set u p the Singu. how I the scrungularity????",
};
private const float MaxWalkDistance = 3; // meters
private const float AdditionalIdleTime = 2; // 0 to this many more seconds
private FsmState _CurrentState;
private TimeSpan _startStateTime;
private Vector2 _walkTargetPos;
public override void Update(float frameTime)
{
if (SelfEntity == null)
return;
ProcessState();
}
private void ProcessState()
{
switch (_CurrentState)
{
case FsmState.None:
_CurrentState = FsmState.Idle;
break;
case FsmState.Idle:
IdleState();
break;
case FsmState.Walking:
WalkingState();
break;
case FsmState.Disabled:
DisabledState();
break;
}
}
private void IdlePositiveEdge(ref uint rngState)
{
_startStateTime = _timeMan.CurTime + IdleTimeSpan + TimeSpan.FromSeconds(Random01(ref rngState) * AdditionalIdleTime);
_CurrentState = FsmState.Idle;
EmitProfanity(ref rngState);
}
private void IdleState()
{
if (!ActionBlockerSystem.CanMove(SelfEntity))
{
DisabledPositiveEdge();
return;
}
if (_timeMan.CurTime < _startStateTime + IdleTimeSpan)
return;
var entWorldPos = SelfEntity.Transform.WorldPosition;
if (SelfEntity.TryGetComponent<BoundingBoxComponent>(out var bounds))
entWorldPos = bounds.WorldAABB.Center;
var rngState = GenSeed();
for (var i = 0; i < 3; i++) // you get 3 chances to find a place to walk
{
var dir = new Vector2(Random01(ref rngState) * 2 - 1, Random01(ref rngState) *2 -1);
var ray = new Ray(entWorldPos, dir, (int) CollisionGroup.Grid);
var rayResult = _physMan.IntersectRay(ray, MaxWalkDistance, SelfEntity);
if (rayResult.DidHitObject && rayResult.Distance > 1) // hit an impassable object
{
// set the new position back from the wall a bit
_walkTargetPos = entWorldPos + dir * (rayResult.Distance - 0.5f);
WalkingPositiveEdge();
return;
}
if (!rayResult.DidHitObject) // hit nothing (path clear)
{
_walkTargetPos = dir * MaxWalkDistance;
WalkingPositiveEdge();
return;
}
}
// can't find clear spot, do nothing, sleep longer
_startStateTime = _timeMan.CurTime;
}
private void WalkingPositiveEdge()
{
_startStateTime = _timeMan.CurTime;
_CurrentState = FsmState.Walking;
}
private void WalkingState()
{
var rngState = GenSeed();
if (_timeMan.CurTime > _startStateTime + WalkingTimeout) // walked too long, go idle
{
IdlePositiveEdge(ref rngState);
return;
}
var targetDiff = _walkTargetPos - SelfEntity.Transform.WorldPosition;
if (targetDiff.LengthSquared < 0.1) // close enough
{
// stop walking
if (SelfEntity.TryGetComponent<AiControllerComponent>(out var mover))
{
mover.VelocityDir = Vector2.Zero;
}
IdlePositiveEdge(ref rngState);
return;
}
// continue walking
if (SelfEntity.TryGetComponent<AiControllerComponent>(out var moverTwo))
{
moverTwo.VelocityDir = targetDiff.Normalized;
}
}
private void DisabledPositiveEdge()
{
_startStateTime = _timeMan.CurTime;
_CurrentState = FsmState.Disabled;
}
private void DisabledState()
{
if(_timeMan.CurTime < _startStateTime + DisabledTimeout)
return;
if (ActionBlockerSystem.CanMove(SelfEntity))
{
var rngState = GenSeed();
IdlePositiveEdge(ref rngState);
}
else
DisabledPositiveEdge();
}
private void EmitProfanity(ref uint rngState)
{
if(Random01(ref rngState) < 0.5f)
return;
var pick = (int) Math.Round(Random01(ref rngState) * (_normalAssistantConversation.Count - 1));
_chatMan.EntitySay(SelfEntity, _normalAssistantConversation[pick]);
}
private uint GenSeed()
{
return RotateRight((uint)_timeMan.CurTick.GetHashCode(), 11) ^ (uint)SelfEntity.Uid.GetHashCode();
}
private uint RotateRight(uint n, int s)
{
return (n << (32 - s)) | (n >> s);
}
private float Random01(ref uint state)
{
DebugTools.Assert(state != 0);
//xorshift32
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state / (float)uint.MaxValue;
}
private enum FsmState
{
None,
Idle,
Walking,
Disabled
}
}
}

View File

@@ -1,11 +1,15 @@
using Content.Server.Interfaces.GameObjects.Components.Movement; using Content.Server.Interfaces.GameObjects.Components.Movement;
using Robust.Server.AI; using Robust.Server.AI;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Movement namespace Content.Server.GameObjects.Components.Movement
{ {
[RegisterComponent] [RegisterComponent, ComponentReference(typeof(IMoverComponent))]
public class AiControllerComponent : Component, IMoverComponent public class AiControllerComponent : Component, IMoverComponent
{ {
private string _logicName; private string _logicName;
@@ -13,15 +17,37 @@ namespace Content.Server.GameObjects.Components.Movement
public override string Name => "AiController"; public override string Name => "AiController";
public string LogicName => _logicName; [ViewVariables(VVAccess.ReadWrite)]
public string LogicName
{
get => _logicName;
set
{
_logicName = value;
Processor = null;
}
}
public AiLogicProcessor Processor { get; set; } public AiLogicProcessor Processor { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float VisionRadius public float VisionRadius
{ {
get => _visionRadius; get => _visionRadius;
set => _visionRadius = value; set => _visionRadius = value;
} }
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
// This component requires a physics component.
if (!Owner.HasComponent<PhysicsComponent>())
Owner.AddComponent<PhysicsComponent>();
}
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer) public override void ExposeData(ObjectSerializer serializer)
{ {
base.ExposeData(serializer); base.ExposeData(serializer);
@@ -29,5 +55,34 @@ namespace Content.Server.GameObjects.Components.Movement
serializer.DataField(ref _logicName, "logic", null); serializer.DataField(ref _logicName, "logic", null);
serializer.DataField(ref _visionRadius, "vision", 8.0f); serializer.DataField(ref _visionRadius, "vision", 8.0f);
} }
/// <summary>
/// Movement speed (m/s) that the entity walks.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float WalkMoveSpeed { get; set; } = 4.0f;
/// <summary>
/// Movement speed (m/s) that the entity sprints.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float SprintMoveSpeed { get; set; } = 10.0f;
/// <summary>
/// Is the entity Sprinting (running)?
/// </summary>
[ViewVariables]
public bool Sprinting { get; set; }
/// <summary>
/// Calculated linear velocity direction of the entity.
/// </summary>
[ViewVariables]
public Vector2 VelocityDir { get; set; }
public GridCoordinates LastPosition { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public float StepSoundDistance { get; set; }
} }
} }

View File

@@ -1,10 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.Components.Movement;
using Content.Server.Interfaces.GameObjects.Components.Movement;
using Robust.Server.AI; using Robust.Server.AI;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Server.Interfaces.Timing; using Robust.Server.Interfaces.Timing;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection; using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -12,17 +16,23 @@ namespace Content.Server.GameObjects.EntitySystems
{ {
internal class AiSystem : EntitySystem internal class AiSystem : EntitySystem
{ {
private readonly Dictionary<string, Type> _processorTypes = new Dictionary<string, Type>(); #pragma warning disable 649
private IPauseManager _pauseManager; [Dependency] private readonly IPauseManager _pauseManager;
[Dependency] private readonly IDynamicTypeFactory _typeFactory;
[Dependency] private readonly IReflectionManager _reflectionManager;
#pragma warning restore 649
public AiSystem() private readonly Dictionary<string, Type> _processorTypes = new Dictionary<string, Type>();
/// <inheritdoc />
public override void Initialize()
{ {
base.Initialize();
// register entity query // register entity query
EntityQuery = new TypeEntityQuery(typeof(AiControllerComponent)); EntityQuery = new TypeEntityQuery(typeof(AiControllerComponent));
_pauseManager = IoCManager.Resolve<IPauseManager>();
var reflectionMan = IoCManager.Resolve<IReflectionManager>(); var processors = _reflectionManager.GetAllChildren<AiLogicProcessor>();
var processors = reflectionMan.GetAllChildren<AiLogicProcessor>();
foreach (var processor in processors) foreach (var processor in processors)
{ {
var att = (AiLogicProcessorAttribute)Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute)); var att = (AiLogicProcessorAttribute)Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute));
@@ -33,6 +43,7 @@ namespace Content.Server.GameObjects.EntitySystems
} }
} }
/// <inheritdoc />
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
var entities = EntityManager.GetEntities(EntityQuery); var entities = EntityManager.GetEntities(EntityQuery);
@@ -61,11 +72,45 @@ namespace Content.Server.GameObjects.EntitySystems
{ {
if (_processorTypes.TryGetValue(name, out var type)) if (_processorTypes.TryGetValue(name, out var type))
{ {
return (AiLogicProcessor)Activator.CreateInstance(type); return (AiLogicProcessor)_typeFactory.CreateInstance(type);
} }
// processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name // 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)); throw new ArgumentException($"Processor type {name} could not be found.", nameof(name));
} }
private class AddAiCommand : IClientCommand
{
public string Command => "addai";
public string Description => "Add an ai component with a given processor to an entity.";
public string Help => "addai <processorId> <entityId>";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
if(args.Length != 2)
{
shell.SendText(player, "Wrong number of args.");
return;
}
var processorId = args[0];
var entId = new EntityUid(int.Parse(args[1]));
var ent = IoCManager.Resolve<IEntityManager>().GetEntity(entId);
if (ent.HasComponent<AiControllerComponent>())
{
shell.SendText(player, "Entity already has an AI component.");
return;
}
if (ent.HasComponent<IMoverComponent>())
{
ent.RemoveComponent<IMoverComponent>();
}
var comp = ent.AddComponent<AiControllerComponent>();
comp.LogicName = processorId;
shell.SendText(player, "AI component added.");
}
}
} }
} }

View File

@@ -46,7 +46,7 @@ namespace Content.Server.GameObjects.EntitySystems
/// <inheritdoc /> /// <inheritdoc />
public override void Initialize() public override void Initialize()
{ {
EntityQuery = new TypeEntityQuery(typeof(PlayerInputMoverComponent)); EntityQuery = new TypeEntityQuery(typeof(IMoverComponent));
var moveUpCmdHandler = InputCmdHandler.FromDelegate( var moveUpCmdHandler = InputCmdHandler.FromDelegate(
session => HandleDirChange(session, Direction.North, true), session => HandleDirChange(session, Direction.North, true),
@@ -117,14 +117,14 @@ namespace Content.Server.GameObjects.EntitySystems
{ {
continue; continue;
} }
var mover = entity.GetComponent<PlayerInputMoverComponent>(); var mover = entity.GetComponent<IMoverComponent>();
var physics = entity.GetComponent<PhysicsComponent>(); var physics = entity.GetComponent<PhysicsComponent>();
UpdateKinematics(entity.Transform, mover, physics); UpdateKinematics(entity.Transform, mover, physics);
} }
} }
private void UpdateKinematics(ITransformComponent transform, PlayerInputMoverComponent mover, PhysicsComponent physics) private void UpdateKinematics(ITransformComponent transform, IMoverComponent mover, PhysicsComponent physics)
{ {
if (mover.VelocityDir.LengthSquared < 0.001 || !ActionBlockerSystem.CanMove(mover.Owner)) if (mover.VelocityDir.LengthSquared < 0.001 || !ActionBlockerSystem.CanMove(mover.Owner))
{ {

View File

@@ -1,4 +1,6 @@
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.Interfaces.GameObjects.Components.Movement namespace Content.Server.Interfaces.GameObjects.Components.Movement
{ {
@@ -6,6 +8,28 @@ namespace Content.Server.Interfaces.GameObjects.Components.Movement
// There can only be one. // There can only be one.
public interface IMoverComponent : IComponent public interface IMoverComponent : IComponent
{ {
/// <summary>
/// Movement speed (m/s) that the entity walks.
/// </summary>
float WalkMoveSpeed { get; set; }
/// <summary>
/// Movement speed (m/s) that the entity sprints.
/// </summary>
float SprintMoveSpeed { get; set; }
/// <summary>
/// Is the entity Sprinting (running)?
/// </summary>
bool Sprinting { get; set; }
/// <summary>
/// Calculated linear velocity direction of the entity.
/// </summary>
Vector2 VelocityDir { get; }
GridCoordinates LastPosition { get; set; }
float StepSoundDistance { get; set; }
} }
} }

View File

@@ -16,6 +16,7 @@
- type: Sprite - type: Sprite
drawdepth: WallMountedItems drawdepth: WallMountedItems
texture: Buildings/TurrTop.png texture: Buildings/TurrTop.png
directional: false
- type: AiController - type: AiController
logic: AimShootLife logic: AimShootLife
vision: 6.0 vision: 6.0
@@ -29,6 +30,7 @@
- type: Sprite - type: Sprite
drawdepth: WallMountedItems drawdepth: WallMountedItems
texture: Buildings/TurrLamp.png texture: Buildings/TurrLamp.png
directional: false
- type: AiController - type: AiController
logic: AimShootLife logic: AimShootLife
vision: 6.0 vision: 6.0