UnigineScript
The Language
Core Library
Engine Library
Node-Related Classes
GUI-Related Classes
Plugins Library
High-Level Systems
Samples
C++ API
API Reference
Integration Samples
Usage Examples
C++ Plugins
Content Creation
Materials
Unigine Material Library
Tutorials

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:

  1. Initialization
  2. Main loop
  3. Shutdown

Unigine execution sequence

Unigine execution sequence in a signle-threaded and a multi-threaded mode

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:

  1. 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).
  2. 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.
  3. 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.
    Notice
    External #define can be also be set any time from the console.
    Command-line values will override the default values and values specified in the configuration file.
  4. 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.
  5. 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.
  6. An application window is created based on the specified settings.
    Notice
    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()).
  7. 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.
  8. External difines passed as -extern_define CLI option are set.
  9. Plugin libraries are loaded, if they were specified in the command line or in the config. Their init() function is called.
    Notice
    A custom Plugin library can be loaded at any moment of Unigine runtime, with one exception.
  10. Console commands from the command line are run.
  11. 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.
      Notice
      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).
      If necessary, additional libraries can also be loaded.
    • 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.

    Notice
    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.
    If the initialization is completed successfully, a non-zero value is returned, and the execution process continues.

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:

  1. Update
  2. Rendering
  3. Swap

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.

Single-threaded mode

Single-threaded mode

2.1. Update

The update part consists of the following steps.

  1. The FPS value starts to be calculated.
    Notice
    Calculation of FPS starts only with the second rendered frame, while the very first one is skipped.
  2. 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.
  3. If the video mode has been changed or the Unigine-based application has been restarted, the engine calls Plugin destroy().
  4. Plugin update() is performed. What happens during it solely depends on the content of this custom function.
  5. 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.
  6. The editor script that handles all editor-related GUI and logic is updated.
  7. 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.
  8. 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).
    Notice
    Physics, if any, and non-rendering game logic should be implemented separately in a flush().
    The world with its script is updated in the following order:
    1. update() function is executed: node parameters are updated, transformations for non-physical nodes are set, etc.
    2. 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).
  9. 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).

  1. Plugin render() is called, if there is such a function.
  2. 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.

  3. Plugin flush() is called, if there is such a function.
  4. Now, the physics module calls flush() of the world script to be executed. In the flush function you can:

    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).

  5. 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.
  6. Pathfinding module is updated. In the performance profiler, the total time of pathfinding is displayed by the PathFind counter.
  7. 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:

  1. Plugin swap() is called, if there is such a function.
  2. 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.
    Notice
    As synchronization of physics follows the rendering stage, applied physical transformations will be visible on the screen only in the next following frame.
  3. 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.

  1. 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.
    Notice
    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.
  2. 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.
    Notice
    Nodes with computationally heavy bodies (like cloths and ropes) should not have one parent; otherwise, they will be updated in one thread.
  3. 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.

Multi-threaded mode

Main thread and auxiliary threads in a multi-threaded mode

3. Shutdown

When Unigine is told to stop execution of application, it does the following:

  1. If the editor script was run, it shuts down.
  2. The world script calls the shutdown() function.
  3. 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.
  4. Plugin shutdown() is called.
  5. If the user has tweaked any settings, the engine saves changes into the configuration file.
  6. 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 number of times physical calculations are be performed given the rendering framerate and the physics framerate

The number of times physical calculations are performed given the rendering framerate and the physics framerate

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.
    Notice
    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.)

Last update: 2017-07-03