Custom Engine C++ Runtimes
Sunday, June 25, 2017 3:15 AMOverview
This document describes how to use the Creature runtimes in your custom C/C++ engine. The language of the runtimes is in C++. We will go through an example of how to load an exported JSON file and play it back in the scene.
Grabbing the runtimes
You can either download the runtimes directly from the Creature Game Runtimes window or grab them from the repository here. For your custom engine, you will need to grab the Cocos2d-x runtimes which contain the core Creature runtime library files. We will be ignoring the Cocos2d-x specific files.
Libaries Needed to compile
Along with the core runtime files, the following libraries are also included:
-
gason (Include the Header and also compile the single gason.cpp source)
-
glm (Header only so make sure your include paths are setup for it)
Make sure to include these 2 libraries in source form in your project.
Files you should care about
-
MeshBone.cpp & MeshBone.h - This is the posing engine of the Creature runtime. You can access the entire skeletal hierarchy, its corresponding bones as well as mesh regions here.
-
CreatureModule.cpp & CreatureModule.h - This is the layer that is responsible for managing Creature characters and posing the characters with loaded in animation files.
Header Includes
The following headers need to be included:
#include "MeshBone.h"
#include "CreatureModule.h"
Loading and Initialization
Let us assume we have an exported dragon animation file called dragonTest.json. We also have its corresponding texture atlas called character-dragon.png. We start off by first loading the file assets:
auto filename = CCFileUtils::getInstance()->fullPathForFilename("dragonTest.json");
auto texture_filename = CCFileUtils::getInstance()->fullPathForFilename("character-dragon.png");
CreatureModule::CreatureLoadDataPacket json_data;
CreatureModule::LoadCreatureJSONData(filename, json_data);
The above will load the JSON data from disk and into memory. Next, let us create the actual objects that can make use of these loaded assets:
auto cur_creature = std::shared_ptr<CreatureModule::Creature>(new CreatureModule::Creature(json_data));
creature_manager = new CreatureModule::CreatureManager(cur_creature);
creature_manager->CreateAnimation(json_data, "default");
creature_manager->CreateAnimation(json_data, "second");
In the example above, the JSON file has 2 animation clips: default and second. Hence, we will need to create 2 animations from the creature_manager object to make them available for playback.
Now that we are done loading, we can set the active animation for playback:
creature_manager->SetActiveAnimationName("default");
This sets default as the currently active animation.
Adapting runtimes into your own Engine
In order to playback and display a Creature character in your engine, you need to have access to the Playback/Posing mechanism as well as the topology, points and texture coordinates of the final Creature character mesh.
-CreatureManager - This is the class that manages animations and the loaded character. You will be using this class to advance the animation by a certain delta time. Your game loop or renderer should interact with the update function of this class at each "game tick".
-Creature - This is the class that contains the loaded creature. It can be accessed from the CreatureManager. This class will contain all the geometry/rendering data required for your game engine to display on screen.
Step 1: Constructor
With that in mind, let us see how it works out in practice. Let us assume you have a renderer class in your engine that will display the loaded character.
In fact, let us examine the Cocos2d-x renderer as an example to see how it is done. The constructor of this renderer does:
Renderer::Renderer(CreatureModule::CreatureManager * manager_in,
cocos2d::Texture2D * texture_in)
: manager(manager_in), texture(texture_in), debug_draw(false)
{
setGLProgram(cocos2d::ShaderCache::getInstance()->getGLProgram(cocos2d::GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR));
scheduleUpdate();
}
You will notice it initializes the member variable manager with an input CreatureManager object. This will allow us to access the manager as well as the internal character later on in the class.
Step 2: Update and Advance the animation
Every game engine has an update/game tick call. In this case we run:
void
Renderer::update(float delta)
{
manager->Update(delta);
}
to tell the manager to update its Creature character by a certain delta time. This will advance the animation forwards by the value delta.
Step 3: Display the mesh
The Creature object returns the following: indices, uvs and points. These attributes are sufficient for you to render out the character mesh. Here is out is done:
void
Renderer::doDraw(const cocos2d::Mat4& transform, uint32_t transformFlags)
{
getGLProgramState()->apply(transform);
cocos2d::GL::bindTexture2D(texture->getName());
cocos2d::Color3B nodeColor = getColor();
manager->GetCreature()->FillRenderColours(nodeColor.r,
nodeColor.g,
nodeColor.b,
getDisplayedOpacity());
glEnableVertexAttribArray(cocos2d::GLProgram::VERTEX_ATTRIB_POSITION);
glEnableVertexAttribArray(cocos2d::GLProgram::VERTEX_ATTRIB_COLOR);
glEnableVertexAttribArray(cocos2d::GLProgram::VERTEX_ATTRIB_TEX_COORDS);
glVertexAttribPointer(cocos2d::GLProgram::VERTEX_ATTRIB_POSITION,
3,
GL_FLOAT,
GL_FALSE,
0,
manager->GetCreature()->GetRenderPts());
glVertexAttribPointer(cocos2d::GLProgram::VERTEX_ATTRIB_COLOR,
4,
GL_UNSIGNED_BYTE,
GL_TRUE,
0,
manager->GetCreature()->GetRenderColours());
glVertexAttribPointer(cocos2d::GLProgram::VERTEX_ATTRIB_TEX_COORDS,
2,
GL_FLOAT,
GL_FALSE,
0,
manager->GetCreature()->GetGlobalUvs());
glDrawElements(GL_TRIANGLES,
manager->GetCreature()->GetTotalNumIndices(),
GL_UNSIGNED_INT,
manager->GetCreature()->GetGlobalIndices());
if(debug_draw)
{
cocos2d::Director* director = cocos2d::Director::getInstance();
director->pushMatrix(cocos2d::MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
director->loadMatrix(cocos2d::MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, transform);
cocos2d::DrawPrimitives::setPointSize(5.0);
glLineWidth(5.0f);
cocos2d::DrawPrimitives::setDrawColor4F(1, 1, 1, 1);
drawDebugBones(manager->GetCreature()->GetRenderComposition()->getRootBone());
director->popMatrix(cocos2d::MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
}
}
As you can see, it is a simple act of sending the points, uvs and indices into your rendering pipeline and drawing the data as a series of triangles.
Custom Time/Frame Range
You can set custom time/frame ranges for the currently active animation. Say you wanted to limit the playback to the frame range of 10 to 20, you would do the following:
creature_manager->SetUseCustomTimeRange(true);
creature_manager->SetCustomTimeRange(10, 20);
Animation Blending
You can blend between 2 animation clips by doing the following:
creature_manager->SetBlending(true);
creature_manager->SetBlendingAnimations("default", "second");
creature_manager->SetBlendingFactor(0.5); // 0 to 1 blends between the 2 clips
Overriding/Modifying Bone Positions
Sometimes you need to modify the bone positions of the character directly. For example, you might want the positions of the bones to follow a number of rigid bodies connected with springs/joints for ragdoll physics. In the cases where you need to set bone positions for your own custom requirements, you should do the following. First, write your custom bone override method. Here is an example that displaces the bones in y by some amount:
// This is an example of how you can use a callback function to modify the position of the bones
// on your character. In this example, we will displace all the bones by a fixed amount in y.
void
HelloWorld::bonesModifyCallback(std::unordered_map<std::string, meshBone *>& bones_map)
{
for(auto& bone_data : bones_map)
{
auto cur_bone = bone_data.second;
auto cur_start_pos = cur_bone->getWorldStartPt();
auto cur_end_pos = cur_bone->getWorldEndPt();
cur_start_pos.y -= 5;
cur_end_pos.y -= 5;
cur_bone->setWorldStartPt(cur_start_pos);
cur_bone->setWorldEndPt(cur_end_pos);
}
}
You will also need to tell the CreatureManager to use your custom bone modify callback like this:
// Example of how to register a callback function to modify the bones
std::function<void (std::unordered_map<std::string, meshBone *>&) > cur_callback =
std::bind(&HelloWorld::bonesModifyCallback, this, std::placeholders::_1);
creature_manager_2->SetBonesOverrideCallback(cur_callback);
Character Instancing and Memory
If you need to instance multiple copies of a character(for example 2 dragons), you should create your animations like this instead:
// Create and load the animations
auto new_animation_1 = std::shared_ptr<CreatureModule::CreatureAnimation>(
new CreatureModule::CreatureAnimation(json_data,
"default"));
auto new_animation_2 = std::shared_ptr<CreatureModule::CreatureAnimation>(
new CreatureModule::CreatureAnimation(json_data,
"pose2"));
You should then proceed to you create a new Creature object, a new CreatureManager object and a new CreatureRenderer object. You will add the created animations into your newly created CreatureManager object like this:
// Second creature instancing example. This shows you how to load a second creature.
// Because both the first and second creature share animation data, you end up
// saving memory.
auto creature_2 = std::shared_ptr<CreatureModule::Creature>(new CreatureModule::Creature(json_data));
CreatureModule::CreatureManager * creature_manager_2 = new CreatureModule::CreatureManager(creature_2);
creature_manager_2->AddAnimation(new_animation_1);
creature_manager_2->AddAnimation(new_animation_2);
creature_manager_2->SetActiveAnimationName("pose2");
creature_manager_2->SetIsPlaying(true);
creature_manager_2->SetUseCustomTimeRange(true);
creature_manager_2->SetCustomTimeRange(10, 60);
The difference between this example and the previous is that the animations are first created, then added into their respective CreatureManagers. This means the memory allocated for the animations(the most expensive) are stored in a standard location. Multiple CreatureManager objects will add animations from a common pool of CreatureAnimation objects.
Speeding up Playback Performance with Point Caches
If you want to increase the speed of the playback animations and not pay the penalty for the posing engine, you can enable point caches on specific animations.
Point caching essentially stores away the vertices of the animation, allowing the playback to bypass the posing engine. Character rigs with very complex skeletons and deformations benefit greatly from point caching.
To enable point caching on a specific animation, do this:
my_creature_manager->MakePointCache("myAnimationName");
That is all that is required to create the cache. Remember that the cache is specific to the animation, not character. This means that if you have instanced multiple characters, you only pay the cost of a single cache for that animation. This results in both memory savings and playback performance speedups.