Run-time
Game Launch
To launch the game you need to use a little improved system script. That is:
- Include a header data/framework/game/game_system.h, which implements the framework system script
- Call the Game::init()method in theinit() function
- Call the Game::update()method in theupdate() function
- Call the Game::shutdown()method in theshutdown() function
The example of the Game Framework system script (can be found in the data/framework/game/game_system_script.cpp directory):
#include <unigine.h>
#include <core/scripts/system/system.h>
#include <core/scripts/system/stereo.h>
#include <core/scripts/system/wall.h>
#include <framework/game/game_system.h>
int init() {
systemInit();
stereoInit();
wallInit();
Game::init();
return 1;
}
int shutdown() {
systemShutdown();
stereoShutdown();
wallShutdown();
Game::shutdown();
return 1;
}
int update() {
systemUpdate();
stereoUpdate();
Game::update();
return 1;
}
int render() {
stereoRender();
wallRender();
return 1;
}
To launch the game, specify the [-game file] parameter as a CLI on the engine loading, where file is a name and a path of the game relatively to the data directory. You can also specify the additional parameter, [-game_level index], where index is number of the level to be loaded. If this parameter is not specified, the first level will be loaded. If there is no levels in the game, the game would not be loaded and the corresponding messages will appear in logs.
The other way to add game path and game level parameters is to specify them right in the engine configuration file in the following way:
<!-- a game path parameter -->
<item name="game" type="string">framework/samples/strategy/strategy.game</item>
<!-- a start level parameter -->
<item name="game_level" type="int">0</item>
<!-- a system script path -->
<item name="system_script" type="string">framework/game/game_system_script.cpp</item>
The system script execution sequence:
While framework system script instance initialization, the MasterGameclass instance is being created (can be found in thedata/framework/game/master_game.h folder). It controls the framework and the game itself, so it has an information about all the levels, which level is currently running. The level can be loaded or reloaded only through the MasterGameclass usage. It is run from the engine initialization till its shutdown.
Level Loading and World Script
Any .world file has the corresponding .cpp file, where the world script is implemented. Game Framework uses the same .cpp files for each world, containing the only line:
#include <framework/game/game_world_script.h>
This is enough for the framework to run.
The world script execution sequence:
Initialization
An init() function is used to create a game and initialize all of the necessary resources on the game launch.
Initialization starts with all of the game data loading from the XML files to the special class, GameData. After, the standardPlayerclass is created in case of the initialization error. Then, theGameclass with the specified current loaded world is created.
The Gameclass loads the level logic (if there is any) and the logic of all of the user entities, and put it in the expression with the"EntitiesLogic" namespace. If the user code has the errors and cannot be compiled, the corresponding message will appear in the logs and the game would not be launched.
After the user logic is compiled, framework creates the instance of the user class Level, applicable for the current loaded level. If the class could not be created, the corresponding message will appear in the logs and the game would not be launched.
The next step is the user code analyzing and checking on the all of the entities properties. All of the user entities are being checked on the presence of the fields. If the fields are found, then the corresponding properties are being checked. If there is any mismatch, the corresponding message will appear in the logs and the game would not be launched.
In the end of the Gameclass initialization, theLevelclass is initialized.
While the Levelclass initialization all of the integrated framework systems (a scheduler, the event system, the entity pool) are created.
Then framework analyzes all of the level node references and checks if they are entities. If a node reference is an entity, framework creates the user class Entityand assign it a name of the node reference. All of the entities with the assigned names re placed in a special map file, where the entity can be found by a name. If there are two or more entities with the same names, the last entity found during the search will be placed into the map file.
At the entity creation, the node of this entity gets the Node::setVariable()function, where variable is an entity instance. Then, while analyzing this node we can assume if this node is an entity or not.
After all of the world entities are created, framework sets their fields. In other words, the set() method is called to the each field. The set() method passes the corresponding parameter from the property. Besides, all of the entities are allocated by the update groups: updateable, flushable or renderable entities. Only after that the user entity and LevelclassonInit()method is called.
If the node or entities has been added to the world during the initialization, the engine.world.updateSpatial()function is called.
Update, Flush, Render
The update() function is the most important in the framework, as the game logic and process are executed in it. The top feature of the framework is providing of the optimal entities update, dynamic and safe update groups exchange, changing of the order or disabling the entities which do not require the update at all. Also framework implements the safe creation and deletion of the entities, puts the deleted entities in the memory pool for them to be used later. It also provides a periodic call of the functions and their automatic time spreading.
The framework update() function works in the following way:
- The engine calls the world script update() function, which calls the Gameclassupdate() function.
- The Gameclass calls the currently usedLevelclass update() function. The level update is split into several parts:
- All the integrated framework systems (a periodic function calls scheduler, profilers etc.) are updated.
- The entities, deleted during the last update, are put in the memory pool or completely deleted.
- The entities, created during the last update are added into the list.
The entity will appear in the entity list only at the next update.
- The onPreUpdate()function is called for the level. It is required for the user code, implemented before entities update (for example, controls update).
- The onUpdate()function is called for all of the updateable entities.
- The onPostUpdate()function is called for the level.
The render() function is called every time after the update(). Framework calls the render() function for the current Levelclass, which, in turn, calls theonRender()function for the all renderable entities. Then, theonRender()function is called for the current level.
The call of the flush() function is similar to the render(), but takes place in the separate engine thread with the fixed FPS.
onUpdate(), onRender() and onFlush() Functions Calls for Entities
Each entity has onUpdate(),onRender()andonFlush()virtual functions. These functions calls can be disabled if required for the entity. They can be disabled dynamically by calling thesetUpdateable(),setRendereable(int mode)andsetFlushable(int mode)functions or by changing Field values set before.
Besides, each entity has the Update Order parameter, which defines the order for entities to be updated. For example, if you need the camera entity to be updated after the character entity, camera entity Update order should be bigger than the character's one.
The Update order parameter can be set in the Game Framework editor or dynamically by calling the setUpdateOrder(int order)entity function. It can take up 17 values from0 to 16 inclusive. The default value is 0. It affects the call orders of the onUpdate(), onRender() and onFlush() functions.
Callbacks
Callbacks or in other words pieces of user code executable by the engine at some convenient time are very important while developing your project. There can be Unigine Widgets callbacks (pressed buttons, unfocused windows), body callbacks (called after collisions), or callbacks caused by Physical or World triggers.
While using callbacks you should remember that all of the user's logic is dynamically compiled into the separate expression with its defined namespace (it's name can be returned by calling a Game::getLogicNamespaceName()method). In other words, while setting callback to the engine's object make sure that you have specified namespaces as prefixes to your function.
The example of Unigine Widgets usage in the custom Entity class:
class MyEntity : Entity {
private:
WidgetButton button;
// button clicked callback
void button_clicked() {
log.message(“my button clicked\n”);
}
// button callback redirector
void button_callback_redirector( MyEntity entity) {
entity.button_clicked();
}
public:
void onInit() {
Gui gui = engine.getGui();
button = new WidgetButton(gui,"Close");
gui.addChild(button,GUI_ALIGN_CENTER);
button.setCallback(GUI_CLICKED,game.getLogicNamespaceName() + "::MyEntity::button_callback_redirector",this);
}
void onShutdown() {
// don't forget remove callbacs from widget
button.setCallback(GUI_CLICKED,NULL);
// or delete widget
delete button;
}
};
At shutdown() you need to unbind all the callbacks from Unigine objects that reference to expressions (at shutdown() the expression along with all user logic is deleted, and if there is a callback binded to the Unigine object, it will reference to the non-existent expression, which may cause an engine crash while calling this callback).
Scheduler
A scheduler provides the periodic call of a function or group of functions and their automatic time spreading. It can manage the complicated logic, for example, if some operations do not need to be counted every update: the path finding, complicated calculations of the object state or even your C++ plugin call with the database request. The time spreading helps to avoid spikes (the sharp increase of the update time if there are a big amount of objects).
The call frequency can vary from 1 to 60 calls per second. The scheduler can call either static or class instance functions.
For the function to be called periodically, in a current loaded level call the Level::setPeriodicUpdate(int instance,string function,int frequency,int priority)function.
Besides, you can pass up to 4 additional arguments when calling the function periodically.
You can safely cancel the function periodic call in any time: in a current loaded level call the Level::removePeriodicUpdate(int instance,string function,int num_args = 0)function.
If you want to change the call frequency for your function, call the Level::setPeriodicUpdate(int instance,string function,int frequency,int priority)for this function with the set new frequency.
You cannot call the same function with different frequencies.
All of the Scheduler functions are divided into groups by the call frequency. When you subscribe to the periodic function update, the function (task), according to its frequency, is automatically moved to the corresponding group. Then the scheduler defines the time for the tick, during which one task is needed to be done. It depends on the number of tasks in the group. For each world update the Scheduler forms the group of tasks that are needed to be performed. If the world update time exceeds the tick time for a group, there will be several tasks from this group in the list. Then the Scheduler sorts the tasks by priority and perform their function calls. Such system provides correct time spreading for complicated tasks and tends to perform tasks with the frequency specified. If the world update time is too big (low FPS), the Scheduler will always perform tasks, but the call frequency for the task will not be guaranteed. If FPS is extremely low, the Scheduler can perform the task more than for one time in one update (in other words, the Scheduler will perform the specified tasks even if there was a hang).
The example of calling the "myPeriodicFunction" function of the MyEntity class to be updated 10 times per second, with the 0 priority and one custom int argument:
class MyEntity : Entity {
public:
void onInit() {
int my_value = 333;
// set periodic update to ”myPeriodicFunction” function with
// the frequency 10 times per second, 0 priority and
// one custom int argument.
level.setPeriodicUpdate(this,”myPeriodicFunction”,10,0,my_value);
}
void onShutdown() {
// remove periodic update
level.removePeriodicUpdate(this,”myPeriodicFunction”,0);
}
void onUpdate() {
}
void onRender() {
}
void onFlush() {
}
void myPeriodicFunction(int some_value) {
log.message(“some_value = %i\n”,some_value);
}
};
If you want the static function, described and compiled in the user code, to be periodically called, do not forget to specify the framework expression namespace in the prefix (you can get its name by calling the Game::getLogicNamespaceName()function in the current game). For example:
namespace MyNamespace {
void myPeriodicFunction() {
log.message(__FUNC__ + “ called\n”);
}
}
class MyEntity : Entity {
private:
public:
void onInit() {
level.setPeriodicUpdate(NULL,game.getLogicNamespaceName() +
”::MyNamespace::myPeriodicFunction”,10,0);
}
void onShutdown() {
level.removePeriodicUpdate(NULL,game.getLogicNamespaceName() +
”::MyNamespace::myPeriodicFunction”);
}
void onUpdate() {
}
void onRender() {
}
void onFlush() {
}
};
Event System
The event system is one of the basic parts of every game. It is needed for interaction of game objects or the world with the other objects. Each user class instance can be attached to the event, so when the event occur, the set functions will be called. Framework allows user to create, call, disable or delete events at any time. Besides, specially for entities there is an event call in the specified area.
To create the event, in the current level call the Level::createEvent(string name)function, so the system will create an event with this name and you will be able to attach to it.
Similarly to the periodic calls scheduler, you can subscribe to the event in the current level by calling the Level::subscribe(string name,int instance,string function)function. You can pass up to 4 user arguments to the function.
You can unsubscribe the function by calling the Level::unsubscribe(string name,int instance,string function,int num_args = 0)function.
You can call the event for the current level by calling the Level::callEvent(string name)function, so the function will be called to all of the subscribers in the level.
There is also a special event call within the specified area. If the subscriber is an Entity class instance, it goes to the special list, for which the call within the specified area is applied. If you call the event by the Level::callEvent(string name,variable p0,variable p1)function, there will be a search depending on thep0 and p1 types through all the nodes (by engine.world.getIntersectionNodes(variable p0, variable p1,int type,int ret_id[]) function). If the node is an entity, the event will be called for it.
p0 and p1 parameters types:
- vec3 and vec3 — search is performed within the bounding box. Variables set the points of the bounding box minimum and maximum (by x, y, and z axes).
- vec3 and float — search is performed within the bounding sphere. Variables set the sphere center and its radius.
- mat4 and mat4 — search is performed within the view frustum. Variables set projection and modelview matrices.
To enable or disable the event, call the Level::setEventEnabled(string name,int mode)function. You can check if the function is enabled by calling theLevel::isEventEnabled(string name)function. You can check if the even exists by calling theLevel::isEvent(string name)function.
Dynamical Entity Creation And Deletion
By means of framework you can dynamically create or delete entities.
To delete the entity, in the current level call the Level::removeEntity(Entity entity,int mode = 0)function. There are 3 ways modes of the entity deletion:
- REMOVE_ENTITY_MODE_DEFAULT - delete the instance of the user entity
- REMOVE_ENTITY_MODE_DELETE_NODE - delete the entity and the node reference connected with it
- REMOVE_ENTITY_MODE_POOL - put the entity and its node reference into the memory pool. This way is much easier when creating a new entity, as it is already in the pool.
Regardless of the entity deletion mode, the onShutdown() method will be called for it. You should clear your resources, unsubscribe from events and cancel the periodic function call in it.
In order to dynamically create a new entity, in the current level call the Level::createEntity(string type,string name = “”)function. The first step on the entity creation is to check the pool on the such entity type and if it exists, take it from there. If it does not exist, the new entity instance and node reference will be created and correspondence between them will be set. After that, all of the fields will be set to the entity. Lastly, theonInit()function will be called for a new entity.
There is another feature of the framework: the entity can safely delete itself in any time if the Level::removeEntity(Entity entity,int mode = 0)function is called and the entity itself (entity = this) is passed as an argument.
Entities Interaction
Besides the event system, there are other ways of entities interaction. As user logic is written in the one expression, you can call the methods of one entity to another. For it you need to find the target entity:
- Find an entity by name. Call the Level::getEntity(variable index)function for it. If the entity with this name exists, the entity will be returned; otherwise,NULL will be returned.
- Define an entity from the node (the fastest way). You can find a node by calling the engine.world.getIntersectionObjects()function or through the body callbacks. If this node is a root for node reference, connected to the entity, the instance of the required entity can be found through theNode::getVariable().
- Find an entity by index. Call the Level::getEntity(int index). You can get the amount of entities by calling theLevel::getNumEntities()function.
- Collect all of the entities. Call the Level::getEntities(Entity ret[],string type = ””,int childs = true)function.
When the entity is found, its methods could be called for other entities.
Passing Parameters Through Levels
As the game logic is located in the world script and is reloaded every time the level is reloaded, common parameter for different levels cannot be hold in the world script.
There is an opportunity in Game Framework to hold any user data in the system script and use it on different levels. For example, you can use the results of the game progress or character state in the next level.
There are two functions of the current class instance to set or get the parameter: Game::setGameParameter(string name,variable value)andGame::getGameParameter(string name).
Furthermore, you can save the parameters values into the file after the end of the game. For it, in the system script after the game initialization (the Game::init(); line), add the required parameters by Game::setGameParameter(string name)function, and after the game is ended (before theGame::shutdown()) get the parameters into a file by calling the Game::getGameParameter(string name)function.