From 8b593d28c6297a67bd56f48fd7c13516fca48f9e Mon Sep 17 00:00:00 2001 From: Acruid Date: Sat, 10 Aug 2019 05:19:52 -0700 Subject: [PATCH] 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. --- Content.Server/AI/AimShootLifeProcessor.cs | 18 +- Content.Server/AI/StaticBarkerProcessor.cs | 71 +++++ Content.Server/AI/WanderProcessor.cs | 249 ++++++++++++++++++ .../Movement/AiControllerComponent.cs | 59 ++++- .../GameObjects/EntitySystems/AiSystem.cs | 59 ++++- .../GameObjects/EntitySystems/MoverSystem.cs | 6 +- .../Components/Movement/IMoverComponent.cs | 26 +- .../Prototypes/Entities/buildings/turret.yml | 2 + 8 files changed, 464 insertions(+), 26 deletions(-) create mode 100644 Content.Server/AI/StaticBarkerProcessor.cs create mode 100644 Content.Server/AI/WanderProcessor.cs diff --git a/Content.Server/AI/AimShootLifeProcessor.cs b/Content.Server/AI/AimShootLifeProcessor.cs index 6d0c165e76..83dc4ad1ad 100644 --- a/Content.Server/AI/AimShootLifeProcessor.cs +++ b/Content.Server/AI/AimShootLifeProcessor.cs @@ -20,9 +20,11 @@ namespace Content.Server.AI [AiLogicProcessor("AimShootLife")] class AimShootLifeProcessor : AiLogicProcessor { - private readonly IPhysicsManager _physMan; - private readonly IServerEntityManager _entMan; - private readonly IGameTiming _timeMan; +#pragma warning disable 649 + [Dependency] private readonly IPhysicsManager _physMan; + [Dependency] private readonly IServerEntityManager _entMan; + [Dependency] private readonly IGameTiming _timeMan; +#pragma warning restore 649 private readonly List _workList = new List(); @@ -32,16 +34,6 @@ namespace Content.Server.AI private IEntity _curTarget; - /// - /// Creates an instance of this LogicProcessor. - /// - public AimShootLifeProcessor() - { - _physMan = IoCManager.Resolve(); - _entMan = IoCManager.Resolve(); - _timeMan = IoCManager.Resolve(); - } - /// public override void Update(float frameTime) { diff --git a/Content.Server/AI/StaticBarkerProcessor.cs b/Content.Server/AI/StaticBarkerProcessor.cs new file mode 100644 index 0000000000..1b85350810 --- /dev/null +++ b/Content.Server/AI/StaticBarkerProcessor.cs @@ -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 +{ + /// + /// Designed for a a stationary entity that regularly advertises things (vending machine). + /// + [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 slogans = new List + { + "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; + } + } +} diff --git a/Content.Server/AI/WanderProcessor.cs b/Content.Server/AI/WanderProcessor.cs new file mode 100644 index 0000000000..c766446e53 --- /dev/null +++ b/Content.Server/AI/WanderProcessor.cs @@ -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 +{ + /// + /// Designed to control a mob. The mob will wander around, then idle at a the destination for awhile. + /// + [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 _normalAssistantConversation = new List + { + "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(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(out var mover)) + { + mover.VelocityDir = Vector2.Zero; + } + + IdlePositiveEdge(ref rngState); + return; + } + + // continue walking + if (SelfEntity.TryGetComponent(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 + } + } +} diff --git a/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs b/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs index 686b097f46..06ccbc734d 100644 --- a/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/AiControllerComponent.cs @@ -1,11 +1,15 @@ using Content.Server.Interfaces.GameObjects.Components.Movement; using Robust.Server.AI; +using Robust.Server.GameObjects; using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Movement { - [RegisterComponent] + [RegisterComponent, ComponentReference(typeof(IMoverComponent))] public class AiControllerComponent : Component, IMoverComponent { private string _logicName; @@ -13,15 +17,37 @@ namespace Content.Server.GameObjects.Components.Movement 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; } + [ViewVariables(VVAccess.ReadWrite)] public float VisionRadius { get => _visionRadius; set => _visionRadius = value; } + /// + public override void Initialize() + { + base.Initialize(); + + // This component requires a physics component. + if (!Owner.HasComponent()) + Owner.AddComponent(); + } + + /// public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); @@ -29,5 +55,34 @@ namespace Content.Server.GameObjects.Components.Movement serializer.DataField(ref _logicName, "logic", null); serializer.DataField(ref _visionRadius, "vision", 8.0f); } + + /// + /// Movement speed (m/s) that the entity walks. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float WalkMoveSpeed { get; set; } = 4.0f; + + /// + /// Movement speed (m/s) that the entity sprints. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float SprintMoveSpeed { get; set; } = 10.0f; + + /// + /// Is the entity Sprinting (running)? + /// + [ViewVariables] + public bool Sprinting { get; set; } + + /// + /// Calculated linear velocity direction of the entity. + /// + [ViewVariables] + public Vector2 VelocityDir { get; set; } + + public GridCoordinates LastPosition { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public float StepSoundDistance { get; set; } } } diff --git a/Content.Server/GameObjects/EntitySystems/AiSystem.cs b/Content.Server/GameObjects/EntitySystems/AiSystem.cs index 40db41a3d1..3ccf140d3e 100644 --- a/Content.Server/GameObjects/EntitySystems/AiSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AiSystem.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; using Content.Server.GameObjects.Components.Movement; +using Content.Server.Interfaces.GameObjects.Components.Movement; using Robust.Server.AI; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Timing; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Reflection; using Robust.Shared.IoC; @@ -12,17 +16,23 @@ namespace Content.Server.GameObjects.EntitySystems { internal class AiSystem : EntitySystem { - private readonly Dictionary _processorTypes = new Dictionary(); - private IPauseManager _pauseManager; +#pragma warning disable 649 + [Dependency] private readonly IPauseManager _pauseManager; + [Dependency] private readonly IDynamicTypeFactory _typeFactory; + [Dependency] private readonly IReflectionManager _reflectionManager; +#pragma warning restore 649 - public AiSystem() + private readonly Dictionary _processorTypes = new Dictionary(); + + /// + public override void Initialize() { + base.Initialize(); + // register entity query EntityQuery = new TypeEntityQuery(typeof(AiControllerComponent)); - _pauseManager = IoCManager.Resolve(); - var reflectionMan = IoCManager.Resolve(); - var processors = reflectionMan.GetAllChildren(); + var processors = _reflectionManager.GetAllChildren(); foreach (var processor in processors) { var att = (AiLogicProcessorAttribute)Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute)); @@ -33,6 +43,7 @@ namespace Content.Server.GameObjects.EntitySystems } } + /// public override void Update(float frameTime) { var entities = EntityManager.GetEntities(EntityQuery); @@ -61,11 +72,45 @@ namespace Content.Server.GameObjects.EntitySystems { 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 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 "; + 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().GetEntity(entId); + + if (ent.HasComponent()) + { + shell.SendText(player, "Entity already has an AI component."); + return; + } + + if (ent.HasComponent()) + { + ent.RemoveComponent(); + } + + var comp = ent.AddComponent(); + comp.LogicName = processorId; + shell.SendText(player, "AI component added."); + } + } } } diff --git a/Content.Server/GameObjects/EntitySystems/MoverSystem.cs b/Content.Server/GameObjects/EntitySystems/MoverSystem.cs index 5bfed02b88..ef0faabec0 100644 --- a/Content.Server/GameObjects/EntitySystems/MoverSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/MoverSystem.cs @@ -46,7 +46,7 @@ namespace Content.Server.GameObjects.EntitySystems /// public override void Initialize() { - EntityQuery = new TypeEntityQuery(typeof(PlayerInputMoverComponent)); + EntityQuery = new TypeEntityQuery(typeof(IMoverComponent)); var moveUpCmdHandler = InputCmdHandler.FromDelegate( session => HandleDirChange(session, Direction.North, true), @@ -117,14 +117,14 @@ namespace Content.Server.GameObjects.EntitySystems { continue; } - var mover = entity.GetComponent(); + var mover = entity.GetComponent(); var physics = entity.GetComponent(); 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)) { diff --git a/Content.Server/Interfaces/GameObjects/Components/Movement/IMoverComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Movement/IMoverComponent.cs index f804144fcb..a8b902f26c 100644 --- a/Content.Server/Interfaces/GameObjects/Components/Movement/IMoverComponent.cs +++ b/Content.Server/Interfaces/GameObjects/Components/Movement/IMoverComponent.cs @@ -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 { @@ -6,6 +8,28 @@ namespace Content.Server.Interfaces.GameObjects.Components.Movement // There can only be one. public interface IMoverComponent : IComponent { + /// + /// Movement speed (m/s) that the entity walks. + /// + float WalkMoveSpeed { get; set; } + /// + /// Movement speed (m/s) that the entity sprints. + /// + float SprintMoveSpeed { get; set; } + + /// + /// Is the entity Sprinting (running)? + /// + bool Sprinting { get; set; } + + /// + /// Calculated linear velocity direction of the entity. + /// + Vector2 VelocityDir { get; } + + GridCoordinates LastPosition { get; set; } + + float StepSoundDistance { get; set; } } } diff --git a/Resources/Prototypes/Entities/buildings/turret.yml b/Resources/Prototypes/Entities/buildings/turret.yml index a0927b3aa9..9228c1d6d5 100644 --- a/Resources/Prototypes/Entities/buildings/turret.yml +++ b/Resources/Prototypes/Entities/buildings/turret.yml @@ -16,6 +16,7 @@ - type: Sprite drawdepth: WallMountedItems texture: Buildings/TurrTop.png + directional: false - type: AiController logic: AimShootLife vision: 6.0 @@ -29,6 +30,7 @@ - type: Sprite drawdepth: WallMountedItems texture: Buildings/TurrLamp.png + directional: false - type: AiController logic: AimShootLife vision: 6.0