A For...Next Loop essentially is quite lightweight.
20,000 Loops will typically take ~80,000 Cycles (on a Single Core), which when even "Low Frequency" Processors are performing 1.8 Billion Cycles / Second... yeah, you're "Good" in terms of performance in that regard.
The issue isn't so much the actual number of Loops, but what
you're doing For Each Loop.
Both the Object In Screen
) and Hide Object
/ Show Object
are fairly performance intensive... although for different reasons.
Object In Screen, has to do a Collision Check between the Object Bounds and View Frustum (which typically is done via Ray Casting).
This is CPU Expensive!!
Where-as Hide/Show Object are Object State Changes., this incurs an additional Draw Call (yes EVEN if it's Hidden)… So you ONLY
want to call these IF
the State actually changes.
< • >
So, how do we Optimise this?
Well … what we first want to do is create 2 Array Lists.
Global DIM ObjectsActive( numActiveObjects ) As Dword
Global DIM ObjectsStatic( numStaticObjects ) As Dword
We could technically have these all as a Single Array with a Custom Type Attribute such-as IsStatic As Boolean
… but if we're Optimising, let's not do it half-arsed.
As only the Active Objects will ever Move, this means we only actually need
to check and update said Active Entities.
Once we've done this, the next step is to determine the World Size Boundaries.
Ideally you actually want to have a Preset Maximum Values for this... even assuming that we're in a Contiguous Open World with "No Loading Screens", we are still going to want to break it down into "Areas" that Load / Unload as needed, to ensure that you only have the Objects and Data Loaded that are actually going to be required.
If we take something like Fallout 4 (for example) … while sure, the map is 10×10km … it's only actually loading between 1 - 4x 1×1km Chunks at any given time rather than the whole map.
We're not just doing this for Performance / Memory Space purposes either, but it also allows for "Streaming" (background loading) of Data to remove Loading Screens as much as possible...
A Good Example of this would be The Division 2... while a counter-example would be Anthem.
It's also why you'll notice a lot of games (especially open world) have a Grid Overlay on their Maps., such-as Fortnite, The Division 2, Fallout 76, etc.
Typically those Grid Squares as "Arbitrary" in size as they might appear, are actually typically the Chunk Size used for each Area being Streamed in.
In order to keep this simple... let's follow the basic approach of:
Area > Sector > Sub-Sector
Each of these are 1/2 the Size of the Previous:
500×500×500 = Area
250×250×250 = Sector
125×125×125 = Sub-Sector
As such we have: 1 Area = 4 Sector = 16 Sub-Sector
Now as a note, if these are Dark BASIC Professional Boxes, that are Hidden … we can still perform Object Collision, to check to see if 2 Objects Intersect.
For Static Objects we ONLY have to do this OnLoad( ) … even then we don't have to check all of the Potential Collisions., but rather (at most) 9; to figure out what Sub-Sector(s) that said Objects "Reside" in.
Now what's even better is, we also then don't have to check individual objects if they're "On Screen" … but instead we just have to Check if the Sub-Sectors are.
As a result... those 20,000 Checks are reduced to 16 * Visible Areas., and as you want (at most) a Visible Range of ~5,560 Units as a "Maximum Draw Distance" … but manually control this via a Game Settings., for example Draw Distance Low = 750 : Medium = 1,500 : High = 3,000 : Ultra = 6,000 or whatever ends up a good balance of Performance Vs. Perceptive.
Then we're still looking < 512 Sub-Sectors Visible at any given point.
Assuming you track the Visible Sub-Sectors from the Previous Frame, then what you're looking for at that point is just "Which Sub-Sectors are no Longer Visible, and which have Become Visible" … then simply go through that Sub-Sector Array List to Show / Hide the assigned Objects.
As a note there's more that can be done with this as well... because you can also use this for an LOD System., remember you don't need the Physical Objects there; only a Root Collision Box... that you can then replace with an instance of said preloaded base object(s).
In this respect; instead of individually checking Distance of each Object for LOD Swapping; basically you can say "All Objects in Sub-Sectors X > Y > Z Away get High > Med > Low Polygon Versions of the Object"
This should (when all is said and done) not just dramatically reduce the number of Per-Frame Checks, Object State Changes but also the Overall Scene Complexity.
It also means you don't need to focus much on specific Individual Objects., but rather now you're just essentially performing various Look-Up Table Checks.
< • >
Hopefully that should be helpful ^_^