Skip to content

Conversation

@sunzenshen
Copy link
Contributor

@sunzenshen sunzenshen commented Sep 28, 2025

Description

  • Introduces caching of visibility calculations to reduce computation overhead
  • Visibility calculations reduced in complexity to improve performance
  • Difficulty scaling for bots related to visibility probability rolls
  • Teammates are always visible so short-circuit calculations then

Toolchain

  • Windows MSVC VS2022

Linked Issues

@sunzenshen sunzenshen requested a review from a team September 28, 2025 05:26
@sunzenshen sunzenshen force-pushed the bot-player-visibility-calculation-rate-limit-cache branch from 04d5fc8 to 56318cf Compare October 3, 2025 06:45
@Rainyan Rainyan self-requested a review October 13, 2025 21:48
AdamTadeusz
AdamTadeusz previously approved these changes Oct 14, 2025
@sunzenshen sunzenshen force-pushed the bot-player-visibility-calculation-rate-limit-cache branch from 56318cf to 5baa513 Compare October 22, 2025 04:25
@sunzenshen sunzenshen added the Refactor Code refactor label Oct 25, 2025
Copy link
Collaborator

@Rainyan Rainyan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good for the most part, I left some questions where I wasn't sure what's going on.

Also, I tried to quantify this performance issue with some light profiling, so here's the data. Warning: wall of text ahead.

The data

This was collected on Windows Release mode builds, with 1 real player (myself) and 9 bots (neo_bot_quota 10), using the following patch to make the bots not kill me:

diff --git a/src/game/server/NextBot/Player/NextBotPlayer.h b/src/game/server/NextBot/Player/NextBotPlayer.h
index 2a3c8463..000f0b00 100644
--- a/src/game/server/NextBot/Player/NextBotPlayer.h
+++ b/src/game/server/NextBot/Player/NextBotPlayer.h
@@ -742,50 +742,53 @@ inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void )
        {
                Update();

-               // build button bits
-               if ( !m_fireButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_ATTACK;

-               if ( !m_meleeButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_ATTACK2;
+               if (NextBotPlayerMove.GetBool()) {
+                       // build button bits
+                       if (!m_fireButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_ATTACK;

-               if ( !m_specialFireButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_ATTACK3;
+                       if (!m_meleeButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_ATTACK2;

-               if ( !m_useButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_USE;
+                       if (!m_specialFireButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_ATTACK3;

-               if ( !m_reloadButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_RELOAD;
+                       if (!m_useButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_USE;

-               if ( !m_forwardButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_FORWARD;
+                       if (!m_reloadButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_RELOAD;

-               if ( !m_backwardButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_BACK;
+                       if (!m_forwardButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_FORWARD;

-               if ( !m_leftButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_MOVELEFT;
+                       if (!m_backwardButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_BACK;

-               if ( !m_rightButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_MOVERIGHT;
+                       if (!m_leftButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_MOVELEFT;

-               if ( !m_jumpButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_JUMP;
+                       if (!m_rightButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_MOVERIGHT;

-               if ( !m_crouchButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_DUCK;
+                       if (!m_jumpButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_JUMP;

-               if ( !m_walkButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_SPEED;
+                       if (!m_crouchButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_DUCK;
+
+                       if (!m_walkButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_SPEED;

 #ifdef NEO
-               if ( !m_leanLeftButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_LEAN_LEFT;
+                       if (!m_leanLeftButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_LEAN_LEFT;

-               if ( !m_leanRightButtonTimer.IsElapsed() )
-                       m_inputButtons |= IN_LEAN_RIGHT;
+                       if (!m_leanRightButtonTimer.IsElapsed())
+                               m_inputButtons |= IN_LEAN_RIGHT;
 #endif
+               }

                m_prevInputButtons = m_inputButtons;
                inputButtons = m_inputButtons;
@@ -854,7 +857,7 @@ inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void )

        if ( !NextBotPlayerMove.GetBool() )
        {
-               inputButtons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP );
+               inputButtons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP | IN_ATTACK );
                forwardSpeed = 0.0f;
                strafeSpeed = 0.0f;
                verticalSpeed = 0.0f;

Then, setting up the map with:

nb_player_move 0;
map ntre_skyline_ctg;
// Join NSF
neo_restart_this 1;
// Spawn as assault
setpos 771 -1411 36; setang 30 0 0;

and sitting there staring at the 5 enemy bots for the duration of one full round (about 3 minutes).

Using vprof

With vprof; console command to toggle profiling on/off, and +showvprof; to show the results.

I also added the budget block:

diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp
index 6cfcd9e2..5dc18ca3 100644
--- a/src/game/server/neo/neo_player.cpp
+++ b/src/game/server/neo/neo_player.cpp
@@ -39,6 +39,8 @@

 #include "neo_player_shared.h"

+#include "vprof.h"
+
 // memdbgon must be the last include file in a .cpp file!!!
 #include "tier0/memdbgon.h"

@@ -1162,6 +1164,8 @@ bool CNEO_Player::IsHiddenByFog(CBaseEntity* target) const
 //-----------------------------------------------------------------------------
 float CNEO_Player::GetFogObscuredRatio(CBaseEntity* target) const
 {
+       VPROF_BUDGET(__FUNCTION__, "NextBotExpensive");
+

Current master (a7da1a7)

old

This PR head (5baa513)

new

Using Visual Studio's CPU Performance Profiler

Current master (a7da1a7)

Function Name Total CPU [unit, %] Self CPU [unit, %] Module
| - IVision::Update 243 (0,14 %) 0 (0,00 %) server
Function Name Total CPU [unit, %] Self CPU [unit, %] Module
| - CNEO_Player::IsHiddenByFog 12 (0,01 %) 0 (0,00 %) server
| - CNEO_Player::GetFogObscuredRatio 3 (0,00 %) 1 (0,00 %) server
Name Total CPU [unit, %] Self CPU [unit, %] Inlined Method Inlined Location
|| + CNEO_Player::GetFogObscuredRatio 3 (0,00 %) 1 (0,00 %)
||| + neo_player.cpp:1179 2 (0,00 %) 0 (0,00 %)
|||| - 0x00B815D1 2 (0,00 %) 0 (0,00 %)
||| + neo_player.cpp:1286 1 (0,00 %) 1 (0,00 %)
|||| - 0x00B817C5 1 (0,00 %) 1 (0,00 %)

This PR head (5baa513)

Function Name Total CPU [unit, %] Self CPU [unit, %] Module
| - IVision::Update 251 (0,12 %) 0 (0,00 %) server
Function Name Total CPU [unit, %] Self CPU [unit, %] Module
| - CNEO_Player::IsHiddenByFog 30 (0,02 %) 3 (0,00 %) server
| - CNEO_Player::GetFogObscuredRatio 1 (0,00 %) 0 (0,00 %) server
Name Total CPU [unit, %] Self CPU [unit, %] Inlined Method Inlined Location
|| + CNEO_Player::GetFogObscuredRatio 1 (0,00 %) 0 (0,00 %)
||| + neo_player.cpp:1260 1 (0,00 %) 0 (0,00 %)
|||| - 0x00B818DC 1 (0,00 %) 0 (0,00 %)

Bot call tree (master)

And for extra context, here's a call tree for the current master, down to the bot sensing code:

Function Name Total CPU [unit, %] Self CPU [unit, %] Module Category
||||||||||||| + engine.dll!0x00007fffe7e749ef 17152 (8,12 %) 0 (0,00 %) engine Graphics | Kernel
|||||||||||||| + engine.dll!0x00007fffe7e76132 17152 (8,12 %) 1 (0,00 %) engine Graphics | Kernel
||||||||||||||| + engine.dll!0x00007fffe7e0caa7 14961 (7,08 %) 0 (0,00 %) engine Kernel
|||||||||||||||| + engine.dll!0x00007fffe7e0da12 14956 (7,08 %) 0 (0,00 %) engine Kernel
||||||||||||||||| + CServerGameDLL::GameFrame 14951 (7,08 %) 13 (0,01 %) server Kernel
|||||||||||||||||| + Physics_RunThinkFunctions 13367 (6,33 %) 0 (0,00 %) server Kernel
||||||||||||||||||| + Physics_SimulateEntity 13336 (6,31 %) 3 (0,00 %) server Kernel
|||||||||||||||||||| + CNEOBot::PhysicsSimulate 11617 (5,50 %) 2 (0,00 %) server Kernel
||||||||||||||||||||| + NextBotPlayer<CNEO_Player>::PhysicsSimulate 11609 (5,49 %) 25 (0,01 %) server Kernel
|||||||||||||||||||||| - CBasePlayer::PhysicsSimulate 10061 (4,76 %) 11 (0,01 %) server Kernel
|||||||||||||||||||||| + CNEOBot::Update 1142 (0,54 %) 1 (0,00 %) server Kernel
||||||||||||||||||||||| + NextBotPlayer<CNEO_Player>::Update 1140 (0,54 %) 2 (0,00 %) server Kernel
|||||||||||||||||||||||| + INextBot::Update 1136 (0,54 %) 0 (0,00 %) server Kernel
||||||||||||||||||||||||| - Behavior<CNEOBot>::Update 586 (0,28 %) 2 (0,00 %) server Kernel
||||||||||||||||||||||||| - CNEOBotLocomotion::Update 307 (0,15 %) 0 (0,00 %) server
||||||||||||||||||||||||| + IVision::Update 236 (0,11 %) 0 (0,00 %) server Kernel
|||||||||||||||||||||||||| + IVision::UpdateKnownEntities 235 (0,11 %) 6 (0,00 %) server Kernel
||||||||||||||||||||||||||| + CollectVisible::operator() 170 (0,08 %) 1 (0,00 %) server
|||||||||||||||||||||||||||| + CNEOBotVision::IsAbleToSee 158 (0,07 %) 0 (0,00 %) server
||||||||||||||||||||||||||||| + IVision::IsAbleToSee 157 (0,07 %) 1 (0,00 %) server
|||||||||||||||||||||||||||||| - IVision::IsLineOfSightClearToEntity 47 (0,02 %) 3 (0,00 %) server
|||||||||||||||||||||||||||||| - IVision::IsInFieldOfView 37 (0,02 %) 1 (0,00 %) server
|||||||||||||||||||||||||||||| - INextBot::IsRangeGreaterThan 34 (0,02 %) 0 (0,00 %) server
|||||||||||||||||||||||||||||| - CNavArea::IsPotentiallyVisible 18 (0,01 %) 3 (0,00 %) server
|||||||||||||||||||||||||||||| - CNEO_Player::IsHiddenByFog 12 (0,01 %) 0 (0,00 %) server

Results

In my (somewhat limited) testing, the game was using under 1% of CPU time in the NextBotPlayer<CNEO_Player>::Update routines. For the IVision::Update path, this was about 0.1%, and for CNEO_Player::IsHiddenByFog about 0.01%. Furthermore at least for my sampling, CNEO_Player::GetFogObscuredRatio was using approximately 0.00% of the CPU time, both before and after this PR.

So at least I can't replicate this report of a performance issue locally. It could be an issue with my setup (more human targets needed? more bots needed? some other variable?), but it may be more likely that the actual hotspot is somewhere else.

In some future playtest, it might be useful for us to run a vprof profiling session over the duration the playtest, and dump out the results to file. I'm not sure if the game has good native utilities for this, but SourceMod is able to produce a dump file using vprof. The documentation for it is somewhat lacking, but for the record: https://wiki.alliedmods.net/SourceMod_Profiler

Or if you're able to reproduce this perf problem locally, then perhaps try to capture it with the VS tooling: https://learn.microsoft.com/en-us/visualstudio/profiling/cpu-usage?view=vs-2022

@AdamTadeusz
Copy link
Contributor

for future reference the convar neo_bot_ignore_real_players should stop the bots from shooting you when testing

Factor in visibility cache delay with cloak disruption timer. Total 0.5 second effect time, but there can be a delay as long as 200-300ms. before the visibility cache is refreshed.
@sunzenshen sunzenshen force-pushed the bot-player-visibility-calculation-rate-limit-cache branch from 5baa513 to 1743604 Compare November 1, 2025 01:06
Rainyan

This comment was marked as resolved.

@sunzenshen
Copy link
Contributor Author

sunzenshen commented Nov 1, 2025

Nit: It might be nice to add a constant variable to neo_player.cpp for the cache window amount, since it's (kind of) referenced at 3 places:

This might be kind of a damning indictment of how I came up with these numbers, but it's possible that these three numbers are not exactly representing the same concept but just happen to line up to my eyeballed preference. It just happens to be that a 200ms delay correlates to an average human reaction time, while the delay for the time the thermoptic flash currently neatly rounds to about half a second. Because a delay could be around 66ms, 0.200x2+0.066 about rounds to slightly less than the time I eyeballed that the flash lasts. (My reasoning being that a human would prefer the bots to lose track of them faster than a human would due to subjective feelings of fairness.)

All that is to say that it might be possible that these numbers could diverge in the future if for example we change how bot ticks are handled or if we wanted to experiment with how long the cloak flash lasts.

@sunzenshen sunzenshen merged commit 902155e into NeotokyoRebuild:master Nov 9, 2025
7 checks passed
@DESTROYGIRL DESTROYGIRL added the Bots Related to bot players label Dec 9, 2025
@sunzenshen sunzenshen deleted the bot-player-visibility-calculation-rate-limit-cache branch December 16, 2025 04:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bots Related to bot players Refactor Code refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants