Fix NPC obstacle handling (#13007)

This commit is contained in:
metalgearsloth
2022-12-15 15:30:28 +11:00
committed by GitHub
parent 6fa2391e42
commit a5b04b49b6
8 changed files with 63 additions and 107 deletions

View File

@@ -1,38 +0,0 @@
using Content.Server.NPC.Systems;
namespace Content.Server.NPC.Components;
/// <summary>
/// Stores data for RVO collision avoidance
/// </summary>
[RegisterComponent]
public sealed class NPCRVOComponent : Component
{
/// <summary>
/// Maximum number of dynamic neighbors to consider for collision avoidance.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("maxNeighbors")]
public int MaxNeighbors = 5;
/// <summary>
/// Time horizon to consider for dynamic neighbor collision
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] public float TimeHorizon = 3f;
/// <summary>
/// Time horizon to consider for static neighbor collision.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] public float ObstacleTimeHorizon = 3f;
/// <summary>
/// Range considered for neighbor agents
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("neighborRange")]
public float NeighborRange = 3f;
[ViewVariables]
public readonly HashSet<EntityUid> ObstacleNeighbors = new();
[ViewVariables]
public readonly HashSet<EntityUid> AgentNeighbors = new();
}

View File

@@ -42,6 +42,11 @@ public sealed class NPCBlackboard : IEnumerable<KeyValuePair<string, object>>
/// </summary>
public bool ReadOnly = false;
public void Clear()
{
_blackboard.Clear();
}
public NPCBlackboard ShallowClone()
{
var dict = new NPCBlackboard();

View File

@@ -8,7 +8,7 @@ using Robust.Shared.Utility;
namespace Content.Server.NPC;
public sealed class NPCBlackboardSerializer : ITypeReader<NPCBlackboard, MappingDataNode>
public sealed class NPCBlackboardSerializer : ITypeReader<NPCBlackboard, MappingDataNode>, ITypeCopier<NPCBlackboard>
{
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
@@ -79,4 +79,17 @@ public sealed class NPCBlackboardSerializer : ITypeReader<NPCBlackboard, Mapping
return value;
}
public void CopyTo(ISerializationManager serializationManager, NPCBlackboard source, ref NPCBlackboard target, bool skipHook,
ISerializationContext? context = null)
{
target.Clear();
using var enumerator = source.GetEnumerator();
while (enumerator.MoveNext())
{
var current = enumerator.Current;
target.SetValue(current.Key, current.Value);
}
}
}

View File

@@ -1,38 +0,0 @@
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
namespace Content.Server.NPC.Systems;
public sealed partial class NPCSteeringSystem
{
// TODO
// Derived from RVO2 library which uses ORCA (optimal reciprocal collision avoidance).
// Could also potentially use something force based or RVO or detour crowd.
public bool CollisionAvoidanceEnabled { get; set; } = true;
public bool ObstacleAvoidanceEnabled { get; set; } = true;
private const float Radius = 0.35f;
private const float RVO_EPSILON = 0.00001f;
private void InitializeAvoidance()
{
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.OnValueChanged(CCVars.NPCCollisionAvoidance, SetCollisionAvoidance);
}
private void ShutdownAvoidance()
{
var configManager = IoCManager.Resolve<IConfigurationManager>();
configManager.UnsubValueChanged(CCVars.NPCCollisionAvoidance, SetCollisionAvoidance);
}
// I deleted all of my relevant code for now as I only had dynamic body avoidance working and not static
// but it will be added back real soon.
private void SetCollisionAvoidance(bool obj)
{
CollisionAvoidanceEnabled = obj;
}
}

View File

@@ -86,7 +86,7 @@ public sealed partial class NPCSteeringSystem
}
else
{
arrivalDistance = SharedInteractionSystem.InteractionRange - 0.8f;
arrivalDistance = SharedInteractionSystem.InteractionRange - 0.65f;
}
// Check if mapids match.
@@ -105,9 +105,15 @@ public sealed partial class NPCSteeringSystem
if (direction.Length <= arrivalDistance)
{
// Node needs some kind of special handling like access or smashing.
if (steering.CurrentPath.TryPeek(out var node))
if (steering.CurrentPath.TryPeek(out var node) && !node.Data.IsFreeSpace)
{
var status = TryHandleFlags(steering, node, bodyQuery);
SteeringObstacleStatus status;
// Breaking behaviours and the likes.
lock (_obstacles)
{
status = TryHandleFlags(steering, node, bodyQuery);
}
// TODO: Need to handle re-pathing in case the target moves around.
switch (status)

View File

@@ -1,11 +1,12 @@
using Content.Server.CombatMode;
using Content.Server.Destructible;
using Content.Server.NPC.Components;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Doors.Components;
using Content.Shared.NPC;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
namespace Content.Server.NPC.Systems;
@@ -33,9 +34,7 @@ public sealed partial class NPCSteeringSystem
private SteeringObstacleStatus TryHandleFlags(NPCSteeringComponent component, PathPoly poly, EntityQuery<PhysicsComponent> bodyQuery)
{
if (poly.Data.IsFreeSpace)
return SteeringObstacleStatus.Completed;
DebugTools.Assert(!poly.Data.IsFreeSpace);
// TODO: Store PathFlags on the steering comp
// and be able to re-check it.
@@ -111,26 +110,35 @@ public sealed partial class NPCSteeringSystem
return SteeringObstacleStatus.Completed;
}
// Try smashing obstacles.
else if ((component.Flags & PathFlags.Smashing) != 0x0 && TryComp<NPCMeleeCombatComponent>(component.Owner, out var melee) &&
TryComp<MeleeWeaponComponent>(melee.Weapon, out var meleeWeapon))
else if ((component.Flags & PathFlags.Smashing) != 0x0)
{
var destructibleQuery = GetEntityQuery<DestructibleComponent>();
var meleeWeapon = _melee.GetWeapon(component.Owner);
// TODO: This is a hack around grilles and windows.
_random.Shuffle(obstacleEnts);
foreach (var ent in obstacleEnts)
if (meleeWeapon != null && meleeWeapon.NextAttack <= _timing.CurTime && TryComp<CombatModeComponent>(component.Owner, out var combatMode))
{
// TODO: Validate we can damage it
if (destructibleQuery.HasComponent(ent))
{
_melee.AttemptLightAttack(component.Owner, meleeWeapon, ent);
return SteeringObstacleStatus.Continuing;
}
}
combatMode.IsInCombatMode = true;
var destructibleQuery = GetEntityQuery<DestructibleComponent>();
if (obstacleEnts.Count == 0)
return SteeringObstacleStatus.Completed;
// TODO: This is a hack around grilles and windows.
_random.Shuffle(obstacleEnts);
foreach (var ent in obstacleEnts)
{
// TODO: Validate we can damage it
if (destructibleQuery.HasComponent(ent))
{
_melee.AttemptLightAttack(component.Owner, meleeWeapon, ent);
break;
}
}
combatMode.IsInCombatMode = false;
if (obstacleEnts.Count == 0)
return SteeringObstacleStatus.Completed;
return SteeringObstacleStatus.Continuing;
}
}
return SteeringObstacleStatus.Failed;

View File

@@ -41,6 +41,7 @@ namespace Content.Server.NPC.Systems
[Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
@@ -66,6 +67,8 @@ namespace Content.Server.NPC.Systems
private readonly HashSet<ICommonSession> _subscribedSessions = new();
private object _obstacles = new();
public override void Initialize()
{
base.Initialize();
@@ -76,7 +79,6 @@ namespace Content.Server.NPC.Systems
}
UpdatesBefore.Add(typeof(SharedPhysicsSystem));
InitializeAvoidance();
_configManager.OnValueChanged(CCVars.NPCEnabled, SetNPCEnabled);
_configManager.OnValueChanged(CCVars.NPCPathfinding, SetNPCPathfinding);
@@ -114,7 +116,6 @@ namespace Content.Server.NPC.Systems
public override void Shutdown()
{
base.Shutdown();
ShutdownAvoidance();
_configManager.UnsubValueChanged(CCVars.NPCEnabled, SetNPCEnabled);
}
@@ -131,6 +132,7 @@ namespace Content.Server.NPC.Systems
private void OnSteeringShutdown(EntityUid uid, NPCSteeringComponent component, ComponentShutdown args)
{
// Cancel any active pathfinding jobs as they're irrelevant.
component.PathfindToken?.Cancel();
}
@@ -151,7 +153,6 @@ namespace Content.Server.NPC.Systems
component.Flags = _pathfindingSystem.GetFlags(uid);
}
EnsureComp<NPCRVOComponent>(uid);
component.Coordinates = coordinates;
return component;
}
@@ -185,7 +186,6 @@ namespace Content.Server.NPC.Systems
component.PathfindToken?.Cancel();
component.PathfindToken = null;
RemComp<NPCRVOComponent>(uid);
RemComp<NPCSteeringComponent>(uid);
}
@@ -203,7 +203,7 @@ namespace Content.Server.NPC.Systems
var npcs = EntityQuery<NPCSteeringComponent, ActiveNPCComponent, InputMoverComponent, TransformComponent>()
.ToArray();
var options = new ParallelOptions()
var options = new ParallelOptions
{
MaxDegreeOfParallelism = _parallel.ParallelProcessCount,
};
@@ -261,6 +261,8 @@ namespace Content.Server.NPC.Systems
EntityQuery<TransformComponent> xformQuery,
float frameTime)
{
IoCManager.InitThread(_dependencies, replaceExisting: true);
if (Deleted(steering.Coordinates.EntityId))
{
SetDirection(mover, steering, Vector2.Zero);

View File

@@ -542,8 +542,6 @@ namespace Content.Shared.CCVar
/// </summary>
public static readonly CVarDef<bool> NPCPathfinding = CVarDef.Create("npc.pathfinding", true);
public static readonly CVarDef<bool> NPCCollisionAvoidance = CVarDef.Create("npc.collision_avoidance", true);
/*
* Net
*/