Advertisement

Generally, the best way to have a SceneManager? C++

Started by July 22, 2021 01:46 AM
2 comments, last by frob 3 years, 4 months ago

Hello,

So I know multiple ways through books and tutorials to incorporate some type of “Scene Manager.” A lot of times tutorials and books simplify concepts and don't give you a major complex example or examples of things that could go wrong in the future if you don't do ‘x’. Does anyone have more information on this or experience of what they generally see out in the real world? Perhaps, I'm thinking it can be more complex than it really needs to be and generally it's pretty damn basic. But I'd believe it's a very important concept as it forms together your “scenes" and seems like it should be handled as efficient as possible.

Any help would be appreciated?

PS: I'm not a “complex” amazing programmer and most likely what I know is all I'll ever need. But I'm curious.

To encapsulate a scene you need to keep track of the objects in the scene, including terrain, sky, animated objects, static objects, participating mediums (volumetric fog), cameras, sound sources, emitters (for particles), triggers (for example a box that triggers an action when the player collides with it), etc.

At the most basic level, you need only store all the objects in a scene in some master lists (volumetric fogs don’t have to be in the same list as static renderables) where the goal is processing speed—you don’t want to traverse a huge list just to find cameras if you keep cameras in their own lists, even though cameras and models will both inherit from your “CActor” or “CEntity” base class. Group them logically so that when you are ready to perform certain actions you can easily access the groups of objects necessary for that action. An action can be a game logic update, physics update, a render, a sound update, etc.

Even if you have no use for it yet, it would be better for your future expandability if you divide updates into at least 2 parts, maybe 3:
#1: Pre-update. Maybe this does nothing. Maybe it “dirties” object matrices (though I would typically only do this when the object matrix changes, so as not to dirty objects unnecessarily and cause more matrix updates then necessary later in the pipeline). Maybe it removes objects that were tagged as deleted last frame. Maybe it updates the bones in your models. It is entirely up to you, but the idea would be to allow objects to perform some action while knowing that none of the other objects in the scene have moved or otherwise had a proper update. Even if you don’t know what this should do, you should probably be forward-facing and implement a dummy function into your pipeline just so that you have that flexibility later when you do find a use for it.

#2: Update. Physics, game logic, scripts, rendering. The meat of every update for every system. Think of it as finalizing objects (ensuring a consistent logic state with no objects being stuck in illegal places, calculating final world matrices, applying the results of game logic such as increasing health when an item is found, etc.)

#3: Optional post-update: Another future-facing design choice just to give yourself flexibility in the future. Entirely up to you.

Group types of updates independently of each other. Game logic updates and rendering updates are the primary types of updates. These can run at different rates, so a render update should not be tied to a logic update. Logic should be updated as few times per second as you can manage while maintaining a stable game. If you update only 10 times per second, you will likely get jittery physics and poor response to inputs. 30 times per second is fairly common, but it can depend on the type of game, with high-speed racing games pushing 1000 updates per second.

Render updates should happen every frame. You can avoid strictly tying this to logic updates by keeping object world matrices separate from render matrices. Render matrices can be created every frame by interpolating between an object’s last world matrix and its current world matrix.

If you consider you have a game update and a render update, then you could divide the work as follows (as just an example):
#1: Game pre-update. Update skeletons/bones (think of updating the positions of arms and things as a precursor step for doing physics, bounds checking, etc., which might rely on this updated positional information later). Update scripts and game logic. Think of updating scripts/logic as creating a new set of game conditions that will later be processed and sanitized by the full update—you might want to create the new logical state and then evaluate/sanitize it in a 2nd separate step so that you can better handle “simultaneous” events. For example in a fighting game this allows players to clash and cancel each other, rather than giving priority to whichever player was processed first. It allows 2 spaceships to hit each other at the same time and “draw” rather than giving player 1 priority just because its hit was processed first and the game changed to a win state without realizing that the 2nd player also hit player 1. You can use “loose” logic that allows objects to clip out of bounds or into other objects and then fix them all up later during the update, though there are times when you want to handle clipping and illegal game states immediately as they arise. To know which way to go, you just have to think for yourself if your system will work by allowing objects to be fixed-up later or if they need to be fixed-up immediately—a decision that might be guided not just by stability but maybe also by performance considerations or anything else you think of that makes it better to go one way or the other.

You can implement this by giving objects their own virtual functions, such as object→PreUpdate( Parms ). Since you are likely to have different types of objects in different lists, the scene manager is responsible for making sure that each list is iterated for its pre-update call and that each type of object has a chance to interact with other types of objects in other lists where it makes sense. This is decided by what types of objects can interact with each other and how. Static objects (which includes animated objects that are non-interactive) probably can’t interact with volumetric fog, the sky, or terrain. Player-controlled objects will typically interact with the terrain, static objects, perhaps the camera, etc. You can decide what groups of objects need to interact with what other groups and give them a chance to do so. If you are using a physics engine, you will have previously informed said engine of the objects in your scene and it will be keeping its own internal list in a format optimized for its purposes, and it will handle the interactions of your objects in a way that ignores your scene manager’s organization (IE, your scene manager might have separate lists for each type of object while the physics engine may have only a single list of all of the pointers to those same objects) and other sub-systems will do the same, so you don’t have to take special care there. The physics engine will implicitly allow objects to interact with terrain, but if you want to add other types of custom interactions then your scene manager will have to facilitate that manually.

#2: Game update. Finalizing objects and game logic should be done here. Determine draws, sanitize object locations to avoid clipping or other illegal states, etc. Generate the final world matrices for objects. If you didn’t run physics above you would do that here. Sanitize the camera by moving it closer to the player to avoid clipping objects. Basically, if the pre-update is to determine game conditions, then an update is to enact those game conditions, and again it is your choice how to separate those 2 things. If a system is more stable or simply much faster when done all at once, then don’t separate them into 2 steps—update the system wherever it makes sense, a decision you have to make on your own. If the logical state says that both players have hit each other, you can run the logic for a draw here. This is where you would change from the current game state to another game state as warranted. A logical state might indicate the fine details about your health level, whether both players have just died, etc., whereas a game state is more global and broad—the main menu would be a game state, the lobby would be a game state, the gameplay scene would be a game state, the credits would be a game state, etc. If you section off your game into modular states this way, you would switch between those states here in the main update.

Similarly, if the pre-update is used to update objects in a fuzzy/loose way, such as allowing the camera to go behind objects and for objects to temporarily clip into walls due to animation, then an update is to sanitize (move to prevent clipping, force a crouched player to remain in a crouched state if there is not enough room to stand, etc.) and finalize those changes. Finalizing objects often just means they all have any dirtied matrices updated (for example if an object changes position, its position vector is modified and a flag is set to indicate that the matrix needs to be reconstructed), which will include passing parent matrices down to children if you are using a hierarchical system that allows objects to parent each other (useful for bones/joints, and to allow characters to hold objects such as swords) and combining them into a “final” matrix. Keep a copy of the final matrix from the previous update if you want to generate a render matrix via interpolation later.
Each object would have a virtual object→Update( Parms ) function for it to do its own thing during this update.

#3: Render pre-update. Interpolate between an object’s current world matrix and its previous world matrix in order to generate a render matrix. Gather objects (based on visibility flags, frustum culling, etc.) to be rendered into buckets with performance being the main goal. You can bucket objects however you like using both correctness and performance as your guides. Opaque objects tend to be bucketed separately from translucent objects both for correctness and for performance (a tile-based GPU can optimize renders if you render all opaque objects first, an optimization that stops working as soon as you draw the first translucent/alpha-blended object), and types of objects might be rendered differently enough from each other as to warrant their own buckets, such as terrain (which might be geo mipmaps or geo clipmaps), foliage (which will require a compute update), buildings (which might use occluders), water, etc. This is where you gather your resources for rendering and put them into the best order for rendering to be both correct and fast. It is expensive to switch between render targets, so you would want to group things that go to each render target together. If you use a render queue, sorting of objects for optimal rendering order would take place here. Update your local copies of values that will be sent to uniform buffers.
If the camera is targeting an object, the scene manager should make that happen here by having the camera look at the object’s render matrix (if it looks at its world matrix it will jitter as it follows the object since that is not updated in sync with rendering).

#4: Render update. Render all the objects. The previous update should have sorted the objects such that you can run over them quickly and perform as few sub-optimal operations as possible, such as render-target swaps, etc. With the objects prepared and sorted, you can determine the best way to render such that the smallest number of barriers/semaphores are used. Shadow maps can be generated and every part of the scene can be rendered as needed.

Everything is largely up to you. You don’t have to go all-in right from the start; you can implement basic rendering and then improve it over time. Your scene manager does what it says: it manages the scene and makes sure all of the objects in your scene get all the updates they need and end up in a render. Once you have that as a starting point, you can focus on optimizing it later and over time.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Advertisement

LedMar said:
Does anyone have more information on this or experience of what they generally see out in the real world?

In addition to L Spiro's great answer, you might consider just downloading a few game engines and experimenting with them.

Unity and Unreal have many excellent tutorials that walk you through manipulating objects in the game world. They both include all the steps mentioned above, but they both do it in slightly different ways. There are other engines and tools that expose various amounts of the described processes.

Those tools are seen all the time in the real world — nearly every project I've touched in the past 7 years has used them — so they are a good start for what you will “see out in the real world."

This topic is closed to new replies.

Advertisement