Jump to content

sleep transitions warp stationary NPCs standing on structures without navmesh

Pending 3.3.7

tsuyoshi_ito
tsuyoshi_ito

Posted

Hello,

First of all, thank you for developing and maintaining NpcSpawn — we use it on our PvE server through several of your and Adem's plugins.

■How we found it

We noticed the issue while using Dynamic Monuments (by Adem). Its "Static NPCs" are stationary guards placed through NpcSpawn on monuments that are spawned dynamically while the server is running. Those NPCs sometimes ended up standing under the monument floor, on the terrain. The trigger was distance: if a player was near the monument when it spawned, the NPCs stayed in place; if nobody was around, they fell under the floor.

We investigated and confirmed that Dynamic Monuments places the NPCs at the correct positions — the displacement happens later, inside NpcSpawn's sleep system. So the report below is about NpcSpawn itself and can be reproduced without Dynamic Monuments.

■The issue in NpcSpawn

A stationary NPC (Speed = 0, or States containing only IdleState / CombatStationaryState) is spawned with its NavMeshAgent disabled: CustomScientistBrain.AddStates() calls DisableNavAgent(). The NPC then stands exactly where the SpawnNpc API placed it, even if there is no navmesh at that position. This is correct behavior.

The problem is in CustomScientistNpc.UpdateSleep() (around line 3632 in 3.3.7). When the sleep state toggles, it always runs one of two movement-related calls, including for stationary NPCs:

Falling asleep: SetDestination(HomePosition, 2f, NavigationSpeed.Fast). This goes through BaseNavigator.SetDestination, which re-enables the disabled NavMeshAgent (SetNavMeshEnabled(true) → PlaceOnNavMesh()).

Waking up: NavAgent.enabled = true directly.

Enabling a NavMeshAgent makes Unity snap it to the nearest navmesh surface. Entities spawned at runtime are not part of the baked navmesh, so for an NPC standing on such a structure the nearest navmesh is the terrain below it — the agent gets warped under the floor.

This also explains why the bug looks intermittent: if a player stays within SleepDistance from the moment the NPC spawns, no sleep transition ever happens and the NPC keeps its position.

For a stationary NPC both calls are unnecessary: it never moves, so there is nothing to navigate on sleep/wake transitions.

■Steps to reproduce

  1. Create a preset with Speed = 0 (or States containing only IdleState + CombatStationaryState), CanSleep = true, SleepDistance = 100.
  2. Spawn any prefab with a walkable floor at runtime and place the NPC on top of it via the SpawnNpc API, with no player within 100 m.
  3. Wait a few seconds (UpdateTick runs every 2 s), then approach as a player.
  4. The NPC is standing on the terrain under the floor. Repeat with a player standing nearby the whole time — the NPC stays in the correct position.

■The fix we applied (verified on our server)

We changed only the Sleep region of CustomScientistNpc:

Added an IsStationary property using exactly the same condition AddStates() uses for its local isStationary variable.

In UpdateSleep(), after Brain.sleeping is updated, return early for stationary NPCs, so neither SetDestination(...) nor NavAgent.enabled = true runs.

Brain.sleeping is still updated, so the sleep optimization (the early return in Think()) keeps working for stationary NPCs, and mobile NPCs keep their current behavior. After deploying this change, stationary NPCs no longer fall under runtime-spawned structures on our server.

The modified Sleep region:

#region Sleep
// Same stationary condition as CustomScientistBrain.AddStates() — keep both in sync
public bool IsStationary => Config.Speed == 0f ||
                            Config.States == null ||
                            Config.States.Count == 0 ||
                            (Config.States.Contains("IdleState") && Config.States.Contains("CombatStationaryState")) ||
                            (Config.States.Contains("IdleState") && Config.States.Count == 1) ||
                            (Config.States.Contains("CombatStationaryState") && Config.States.Count == 1);

private void UpdateSleep()
{
    if (!Config.CanSleep) return;

    bool sleep = Query.Server.PlayerGrid.Query(transform.position.x, transform.position.z, Config.SleepDistance, AIBrainSenses.playerQueryResults, x => x.IsPlayer() && !x.IsSleeping()) == 0;

    if (Brain.sleeping == sleep) return;
    Brain.sleeping = sleep;

    // Stationary NPCs must not touch the NavAgent: enabling it snaps them to the nearest
    // navmesh, which is the terrain under dynamically spawned monuments (no navmesh on floors)
    if (IsStationary) return;

    if (Brain.sleeping) SetDestination(HomePosition, 2f, BaseNavigator.NavigationSpeed.Fast);
    else if (!ActiveCustomNavMesh) NavAgent.enabled = true;
}
#endregion Sleep

■Patch and how to apply it

The attached NpcSpawn-stationary-sleep.patch is a unified diff against NpcSpawn.cs 3.3.7. Put the patch file in the same directory as NpcSpawn.cs and apply it with either of:

git apply NpcSpawn-stationary-sleep.patch (inside a git repository)

patch -p1 < NpcSpawn-stationary-sleep.patch

The change is small (one added property and one early return in UpdateSleep), so applying it by hand from the code block above works just as well.

Would you consider including this fix (or an equivalent one) in a future release? Thank you very much!

NpcSpawn-stationary-sleep.patch.txt

About Us

Codefling is the largest marketplace for plugins, maps, tools, and more, making it easy for customers to discover new content and for creators to monetize their work.

Downloads
2.7m
Total downloads
Customers
11.6k
Customers served
Files Sold
165.5k
Total sales
Payments
3.6m
Processed total
×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.