Execution Sequence
This article is focused on details of Unigine execution sequence. Here you will find what is happening under the hood of Unigine engine. Learn how to leverage its power in your applications: when the initial splash screen and the credits screen are loaded, where to place your game logic code, how custom dynamic libraries are loaded and used at runtime, etc.
A high-level overview of Unigine workflow is provided in the article Engine Architecture.
The following diagram shows the order of execution of Unigine engine internal code and scripts of you project. The main stages are:
Unigine Runtime Scripts
During its runtime, Unigine uses the following scripts:
- World script that contains all logic of your project. It is automatically created together with the new world, named after it (world_name.cpp) and is stored in the same folder.
- System script that does housekeeping.
- Editor scripts handles logic of the editor, when it is loaded.
Besides it, Unigine can also load on-the-fly and access a custom C/C++ plugin library through its API.
1. Initialization
Engine initialization starts when the main.exe application (32-bit (x86) or 64-bit version, debug or release one) loads the corresponding Unigine library (Unigine.dll or Unigine.so). From this point on, the following events take place:
- A Unigine memory allocator is initialized for faster and more optimal allocations if compared to the default system allocator. (In the engine code, it is set via the USE_MEMORY directive).
- The file system is initialized. All files under the specified data_path directory are cached by names. If project files are packed into UNG or ZIP archives protected by a password, the engine checks if the password matches with the binary's one. External packages, if any, are loaded as well.
- Command-line options are parsed. These options specify the Unigine root folder and basic video settings, such as graphics API to be used for rendering (DirectX or OpenGL), application window size, etc. You can also pass any external #define directives and console variables through the command line.
Command-line values will override the default values and values specified in the configuration file.External #define can be also be set any time from the console.
- The configuration file is loaded and parsed:
- First, the engine checks, if the configuration file is specified in the command line as -engine_config. In the config various world-related settings are stored: rendering flags and parameters, editor settings and so on. If settnigs are adjusted in the project, changes will be saved into this file.
- If no configuration file is specified, the engine creates a default config with the name unigine.cfg and stores it in the folder with binaries.
- A log file is created. If not specified in the command-line options, the default log file is placed in the application directory under the name log.html.
- An application window is created based on the specified settings.
Note that this is a simple window created by means of DirectX or OpenGL. If you need to create a more sophisticated user interface around the 3D viewport, it should be done before the engine starts the initialization process or in the setVideoMode() function of the C++ API Unigine::App class (that is called by engine init()).
- After the application window is created, Unigine initializes a set of modules (managers) that will automatically organize and handle data streaming, rendering, sound, physics, etc.
- External difines passed as -extern_define CLI option are set.
- Plugin libraries are loaded, if they were specified in the command line or in the config. Their init() function is called.
A custom Plugin library can be loaded at any moment of Unigine runtime, with one exception.
- Console commands from the command line are run.
- The system script is loaded and started. Basically, the system script performs housekeeping necessary to start and keep the Unigine-based application going. It stays loaded during the whole Unigine runtime.
In detail, the system script when initialized does the following:- Localization file is loaded.
- The main menu is initialized.
- A splash screen is set, if necessary. It can be either a simple texture splash screen, or a Flash one. It is better to set a splash screen before loading the world. In this case, displaying a splash screen will give the engine time to load resources and compile shaders. By default, a standard Unigine screen is shown.
OpenGL does not support shader caching, so they are re-compiled each time the application starts. DirectX, on the contrary, supports shader caching, so subsequent application start-ups on the same hardware will be much faster.
Shaders actually can be loaded any time during execution, but their compilation may take time. - All basic materials are loaded along with corresponding shader definitions. One of the following material libraries is loaded depending on used define directives:
- Default standard materials.
- Simplified standard materials. These are used for Android, iOS or a simplified renderer (set via _ANDROID, _IOS and RENDER_SIMPLE defines respectively).
- A standard properties library is loaded.
- If the editor was loaded when this Unigine project was last exited (there is a special flag in the configuration file for that), the system script loads the editor.
- If any, custom user-defined modules are loaded and initialized.
If the initialization is completed successfully, a non-zero value is returned, and the execution process continues.You can take the default system script located in the data/core/unigine.cpp and replace it with a custom one. In it you can set your own splash screen for the project, specify custom modules or material libraries to load, etc. For big projects it also makes sense to specify the world you want to load right in your system script.
After all these steps are taken, the engine enters the main loop.
2. Main Loop
When Unigine enters the main loop, all its actions can be divided in three stages, which are performed one after another in a cycle. They are:
At most times, Unigine finishes all its stages in the main loop faster then the GPU can actually render the frame. That is why a triple buffering is used. Unigine checks how many frames it has already prepared in advance for the GPU. At this moment, the GPU still continues to execute previous rendering commands and, in fact, draws some previous frame onto the screen. If there are more than one frame prepared beforehand by Unigine, it delays execution of the main loop. When only one frame prepared in advance is left, Unigine synchronizes in time its swap stage with the real swapping of buffers done by the GPU. This approach allows to avoid lags on the one hand, and achieve optimum performance on the other.
In the performance profiler, the total time that the main loop took is displayed by the Total counter.
2.1. Update
The update part consists of the following steps.
- The FPS value starts to be calculated.
Calculation of FPS starts only with the second rendered frame, while the very first one is skipped.
- All pending console commands that were called during the previous frame are executed. The commands are executed in the beginning of the update() cycle, because otherwise they may violate the current process of rendering or physical calculations.
- If the video mode has been changed or the Unigine-based application has been restarted, the engine calls Plugin destroy().
- Plugin update() is performed. What happens during it solely depends on the content of this custom function.
- User controls are processed. After that, scripts can use it for user-application interaction, for example, to reposition the player according to the provided user input.
- The editor script that handles all editor-related GUI and logic is updated.
- The system script is updated.
- System script handles the mouse. It controls whether the mouse is grabbed when clicked (by default), the mouse cursor disappears when not moved for some time (set by MOUSE_SOFT define), or not handled by the system (set by MOUSE_USER define, which allows input handling by some custom module).
- Main menu logic is updated.
- Other system-related user input is handled. For example, if the world state should be saved or restored, or a screenshot of the current Unigine window contents is to be made.
- If the world script has been initialized (it can be done anytime: you only need to load the world from a console or via the system script), it gets updated. In the update() function you can code frame-by-frame behavior of your application. (See the details).
The world with its script is updated in the following order:Physics, if any, and non-rendering game logic should be implemented separately in a flush().
- update() function is executed: node parameters are updated, transformations for non-physical nodes are set, etc.
- State of nodes is updated (mostly for visible nodes): skinned animation is played, particle systems spawn new particles, etc. Triggered world callbacks are added to a stack (they will be executed later).
- At the very end of update cycle, an additional render() function is executed, if necessary (see the details). It can access the updated data on node states and correct the behavior accordingly in the same frame.
In the performance profiler, the total time of update stage is displayed by the Update counter.
2.2. Rendering
As soon as the update stage is completed, Unigine can start rendering the world. In parallel, physics and game logic is calculated. This approach allows to effectively balance the load between CPU and GPU, and thus allows for higher framerate in Unigine-based application.
In detail, here is how the render stage in a single-threaded mode works. (From this point on, if Unigine is multi-threaded, it would behave differently).
- Plugin render() is called, if there is such a function.
- Unigine renders the world and sound sources in it, as they should be in the current frame. As soon as the CPU finishes preparation of data and feeds rendering commands to the GPU, the GPU becomes busy with rendering a frame.
In the performance profiler, the total time of rendering stage is displayed by the Render counter.
After that, CPU is free so we can load it with calculations we need. - Plugin flush() is called, if there is such a function.
-
Now, the physics module calls flush() of the world script to be executed. In the flush function you can:
- modify physics
- implement game logic for your application (see the details below).
flush() is called not every frame. The physics module has its own fixed framerate, which does not depend on the rendering framerate. Each of such physics frames (or ticks), a number of calculation iterations are performed (flush() is called before every iteration).
- Right after the flush() block from the world script with your game logic has been executed, internal physical simulation starts. During this step, Unigine performs collision detection for all objects that have physical bodies and collision shapes.
In the performance profiler, the total time of flush() together with physics simulation is displayed by the Physics counter. - Pathfinding module is updated. In the performance profiler, the total time of pathfinding is displayed by the PathFind counter.
- At last and atop of all GUI is rendered, if required.
2.3. Swap
The swap stage is the last one in the main loop. It includes the following:
- Plugin swap() is called, if there is such a function.
- Synchronization of physics with the rendered world. Results of the physical calculations are applied to the world. That is, during the previous stage we have calculated how physical bodies with collision shapes have changed their position and orientation (due to our flush-based logic or interaction). Now these transformations can be finally applied to nodes, i.e. rendered meshes.
As synchronization of physics follows the rendering stage, applied physical transformations will be visible on the screen only in the next following frame.
- Render buffers are swapped: back buffer is swapped with a front one, making the new frame displayed on the screen.
The main loop iteration is over. In the performance profiler, Present counter is useful to analyse the bottleneck in your application's performance. It indicates how much time has passed from the moment when all scripts have been updated and all calculations on the CPU have been completed, to the moment when the GPU has finished rendering the frame.
If Present time is too high, it may signal that there exists a GPU bottleneck, and art content needs to be optimized. But if by that the framerate is consistently high, it means you still have the CPU resources available to crunch more numbers.
Multi-Threaded Main Loop
In the multi-threaded mode, where two or more CPUs exist, the execution pipeline is implemented differently at some points. They are as follows.
- In the update stage, the world script uses all available threads to update visible nodes, instead of updating them one by one. By that, Unigine analyzes node dependences, so these operations are fully thread-safe. The number of threads to be used is controlled via world_threaded.
Child nodes under one root are always updated in one thread. To benefit from the advantages of multi-threading, computationally heavy nodes (like particles systems) should have different or no parents.
As each Node Reference is handled as a root node without any parent, it is automatically optimized for multi-threading. - In the rendering stage, multi-threaded physics and pathfinding are run in their separate threads before anything else. They will perform their tasks on all available threads in parallel to rendering. The number of threads to use is controlled via physics_threaded and pathfind_threaded respectively.
Nodes with computationally heavy bodies (like cloths and ropes) should not have one parent; otherwise, they will be updated in one thread.
- The swap stage in this scenario includes waiting for all additional threads to finish their tasks. After that, physics and pathfinding threads are synchronized and calculations are applied to the world.
3. Shutdown
When Unigine is told to stop execution of application, it does the following:
- If the editor script was run, it shuts down.
- The world script calls the shutdown() function.
- Unigine calls the shutdown() function of the system script. Here, for example, you can set a splash screen with credits to be shown when exiting the Unigine-based application.
- Plugin shutdown() is called.
- If the user has tweaked any settings, the engine saves changes into the configuration file.
- After all, all resources allocated for Unigine are freed.
Correlation between Rendering and Physics Framerates
The rendering framerate usually varies, while, as we have already mentioned before, physics simulation framerate is fixed. This means that your update() and flush() functions from the world script are called with different frequency.
The picture above describes what happens, when the physics framerate is fixed to 60 FPS, and the rendering framerate varies. In general, there are three possible cases:
- The rendering framerate is much higher. In this case, physical calculations are done once for two or more frames. This does not raise any problems, as positions of moving objects are interpolated between the calculations.
- The rendering framerate is the same or almost the same. This situation is also OK, the calculations are performed once per frame; the physics keeps pace with the graphics and vice versa.
- The rendering framerate is much lower. This is where the problems begin. First, as you see from the picture, the physics should be calculated twice or even more times per frame, and that does not speed up the overall rendering process. Second, you cannot set the physics framerate too low, as in this case the calculations will lose too much precision.
There is no point in setting the physics framerate too high, too, because if the calculations take more than 40 ms, physics is not computed in full, hence, if additional iterations are needed, they are skipped.
Capping Rendering FPS to Physics One
The rendering FPS can be limited to the physics one, if the rendering FPS is higher. For that engine.physics.isFixed(1) flag is set in code. Such FPS limitation allows to calculate physics each rendered frame (rather than interpolate it when this flag is set to 0). In this mode, there is no twitching of physical objects if they have non-linear velocities. (If the rendering FPS is lower than the physics one, this flag has no effect.)