Fix NPC obstacle handling (#13007)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user