BlackCat Engine is an in-house developed 3D game engine written in C++ with multi-platform support in mind. Currently, engine only supports Windows and DirectX 11 and is under development to have more features.
This repository contains the engine source code and a sample project which is a multiplier third-person shooter game developed by the engine called Battle Of Xbots.
- Parallel update/render loops
- Rich task based parallel library
- Messaging APIs including events and queries
- Asset and Content management pipeline with offline processing and streaming features
- Powerful actor-component model
- Entity system to empower actor-component model with entity definitions in json files
- Client/Server network system with robust messaging framework
- 3d Physics simulation powered by PhysX
- Skinned animations powered by ozz-animation
- Ragdoll animations
- Sound system powered by Fmod
- Scripting system powered by ChakraCore engine
- Deferred rendering
- Deferred decal rendering
- Vegetable animation
- Cascaded shadow maps
- Particle system
- Terrain system
The engine update and render loops run in two different threads. While the Nth frame is being updated the N-1 frame is being rendered. The interaction between render thread and update thread are through query objects. At the start of each frame, render thread register queries to gather all required data for the next frame. During update phase, these queries are resolved by multiple threads to set each query result which will be used by render thread at next frame.
Engine is equipped with a thread pool which add/remove threads on the flay .Running a new task is as easy as below:
auto l_task = core::bc_concurrency::start_task
(
core::bc_delegate<void()>
(
[]()
{
// Do pararrel
}
)
);
And waiting for or fetching Tasks result:
// Wait for a single task completion
l_task.get();
// Wait for multiple tasks completion
core::bc_concurrency::when_all(l_task1, l_task2, l_task3);
Events and Queries are two features to notify and inquiry states in engine's multithreaded environment.
Events are based on Publish/Subscribe pattern in which a few subscriber can listen to a specific event type. When subscribing to an event, an object of type core::bc_event_listener_handle
is returned which is a RAII object meaning when it get destroyed the subscription will be cancelled.
// Define an event object
class bc_app_event_window_close : public core::bc_app_event
{
BC_EVENT(app_cls)
public:
explicit bc_app_event_window_close(bc_window::id p_window_id) noexcept;
bc_app_event_window_close(const bc_app_event_window_close&) = default;
~bc_app_event_window_close() override = default;
bc_app_event_window_close& operator =(const bc_app_event_window_close&) = default;
bc_window::id get_window_id() const noexcept;
private:
bc_window::id m_window_id;
};
// Subscribe to it and hold the subscription handle in a variable
core::bc_event_listener_handle m_event_handle_window_close = l_event_manager->register_event_listener<platform::bc_app_event_window_close>
(
core::bc_event_manager::delegate_type(*this, &bc_render_application::_app_event)
);
// Consume it in the function which was used in subscription
void bc_render_application::_app_event(core::bci_event& p_event)
{
if (const auto* l_close_event = core::bci_message::as<platform::bc_app_event_window_close>(p_event))
{
}
}
On the other hand Queries are like Command pattern in which queries include the code which must be executed when query is fetched. Defining a query object is as easy as an event except that the object has an execute method which will be called by a worker thread when the query is fetched in engine's main update cycle, so the result will be available in asynchronous manner.
// Define a query object which holds the result too
class bc_scene_wind_query : public core::bc_query<bc_scene_query_context>
{
BC_QUERY(sc_wnd)
public:
bc_scene_wind_query();
bc_scene_wind_query(bc_scene_wind_query&&) noexcept;
~bc_scene_wind_query() override;
bc_scene_wind_query& operator=(bc_scene_wind_query&&) noexcept;
core::bc_vector<bc_wind> get_winds() noexcept;
protected:
void execute(const bc_scene_query_context& p_context) noexcept override;
private:
core::bc_vector<bc_wind> m_winds;
};
// The result of queuing a query is of type bc_query_result<T>
core::bc_query_result<game::bc_scene_wind_query> m_winds_query;
// Queue query to be executed which will be ready for next frame
m_winds_query = m_query_manager.queue_query(game::bc_scene_wind_query());
// In next frame check to see if the query is executed and result is ready
if(m_winds_query.is_executed())
{
m_winds_query_result = m_winds_query.get().get_winds();
}
In above example the intention is to query the wind objects in the scene and collect them in a vector. This pattern match concurrent update/render loop very well. While the render thread is rendering nth-1 frame, will queue several queries to fetch current state of the game in the next frame. In fact, this fits with game play code too. When an explosion happens in the scene a query can be queued to fetch all the nearby rigid dynamic objects. The result will be ready by one frame delay but that would not be a big deal. The big advantage in this model is batch processing and making use of available hardware threads which usually will be done when main thread is idle and waiting for render thread to finish with GPU in present method.
Assets are imported into engine through content loaders which must be registered inside the engine.
core::bc_register_loader<bc_mesh, bc_mesh_loader>("mesh", core::bc_make_loader<bc_mesh_loader>());
Here the bc_mesh
is a subclass of bci_content
and bc_mesh_loader
is a subclass of bci_content_loader
. By a registered loader for bc_mesh
, an instance of mesh can be loaded by a single line of code:
auto l_mesh = l_content_manager.load<bc_mesh>
(
path_to_file,
{},
core::bc_content_loader_parameter()
);
This way, a new asset or content type can be used inside the engine by simply registering a new loader for that specific content. For example to implement the game mode
and editor mode
functionality inside the editor a special loader is created to create and save a checkpoint object from the scene and load an existing one from the disk to apply the difference in the scene. (Actually, loaders are used not only for loading but also for saving assets too)
The return type of load
function is a smart pointer which has an internal reference counter for the loaded content and will unload the content when its reference count reach zero.
Loading and managing every single asset is hard. The desired way to load assets is to group them and load all of them at once. The content streams is what is needed. Every stream contains a dozen of contents which are defined inside a json file. For example this file defines a couple of different streams which can be loaded by their names:
stream_manager.load_content_stream("engine_shaders");
The load work is spread over multiple threads to speed up the process. Beside organizing assets, streams can be used to load/unload whatever is needed on the fly.
Game objects are modeled as actor-component in which actors can contain multiple distinct components. Before using components they must be registered with engine.
game::bc_register_component_types
(
game::bc_component_register<game::bc_rigid_static_component>("rigid_static"),
game::bc_component_register<game::bc_rigid_dynamic_component>("rigid_dynamic"),
game::bc_component_register<game::bc_rigid_controller_component>("rigid_controller"),
);
The component model also support abstract component concept which means components can have a common base component and at runtime instead of searching for concrete component type the common base component can be queried inside an actor. In above example three components are registered which all of them have a common base component. In order to be able to access to these three components through their base component we should inform their hierarchy with below line of code.
game::bc_register_abstract_component_types
(
game::bc_abstract_component_register<game::bc_rigid_body_component, game::bc_rigid_static_component, game::bc_rigid_dynamic_component, game::bc_rigid_controller_component>(),
);
Here bc_rigid_body_component
is base abstract component of all three other components which can be used to retrieve any of other components at runtime if one of them exist in actor component collection.
auto l_actor = l_actor_component_manager.create_actor();
l_actor.create_component<game::bc_rigid_dynamic_component>();
// Access to created component directly
auto* l_rigid_dynamic = l_actor.get_component<game::bc_rigid_dynamic_component>();
// Access to created component through its base component. The runtime type of returned component will be bc_rigid_dynamic_component
auto* l_rigid_body = l_actor.get_component<game::bc_rigid_body_component>();
Entity System is built on top of Actor-Component model to move game object definitions from code to json file. Game objects and their components and behaviors are created with json notation. Below is a simple game object which is called box
with two components.
simple_mesh
Component loads the mesh called box
and make the actor renderable. The second component, rigid_dynamic
adds rigid dynamic behaviors to the actor.
{
"name": "box",
"components": [
{
"name": "simple_mesh",
"parameters": {
"mesh": "box",
"view_distance": 200.0
}
},
{
"name": "rigid_dynamic",
"parameters": {
"mass": 10.0,
"collider_materials": {
"px_cube": "iron"
}
}
}
]
}
Inheritance is also supported in this notation. Here an entity which is called xbot_idle
is defined with a couple of properties. The next two entities are the same entity with overridden properties which assign different render materials to their base entity. They have inherited from xbot_idle
and defined their own skinned_mesh
components with one overridden property.
{
"name": "xbot_idle_red",
"inherit": "xbot_idle",
"components": [
{
"name": "skinned_mesh",
"parameters": {
"materials": {
"Beta_Surface": "red"
}
}
}
]
},
{
"name": "xbot_idle_blue",
"inherit": "xbot_idle",
"components": [
{
"name": "skinned_mesh",
"parameters": {
"materials": {
"Beta_Surface": "blue"
}
}
}
]
},
Here is the result of this inheritance which two identical actors only differ in their sub mesh material.
The engine network system is a server-client network model which the whole communication is based on a messaging model over UDP sockets. Messages are queued and based on ping time are serialized and sent to remote party. The received messages should be known to the remote party, so all messages must be registered in both server and client.
Messages can be acknowledged and the ones that need acknowledgment are resend periodically until the ack message is received. Out of order and duplicate messages are discarded. Every message is a subclass of bci_network_message
type which its core functionality consist of below functions.
/**
* \brief Indicate whether message must be acknowledged by the receiver or not
* \return
*/
virtual bool need_acknowledgment() const noexcept;
/**
* \brief After message execution, if acknowledgment is required, this string will be included in acknowledge message
* \return
*/
virtual core::bc_string get_acknowledgment_data() const noexcept;
/**
* \brief Serialize command into json key/value pair which is provided as parameter
* \param p_context
*/
void serialize(const bc_network_message_serialization_context& p_context);
/**
* \brief Deserialize command from json key/value pair which is provided as parameter.
* \param p_context
*/
void deserialize(const bc_network_message_deserialization_context& p_context);
/**
* \brief Execute message logic on the remote part of connection
* \param p_context
*/
virtual void execute(const bc_network_message_client_context& p_context) noexcept;
/**
* \brief Execute message logic on the remote part of connection
* \param p_context
*/
virtual void execute(const bc_network_message_server_context& p_context) noexcept;
/**
* \brief Execute message logic when message delivery is acknowledged
* \param p_context
*/
virtual void acknowledge(const bc_network_message_client_acknowledge_context& p_context) noexcept;
/**
* \brief Execute message logic when message delivery is acknowledged
* \param p_context
*/
virtual void acknowledge(const bc_network_message_server_acknowledge_context& p_context) noexcept;
...
For execute
and achnowledge
two counterpart functions are defined and two different object parameters are passed in. In fact, when the receiver of the message is the server, the server version of execute
is called and in case of required acknowledgment the acknowledge
will be called on the sender when the ack message is received. In other case, when the server is the sender, the functions which will be called are the client version of execute
and server version of acknowledge
.
Every actor in the scene can be replicated and synced through the network by adding bc_network_component
to the actor. Network component will register the actor with bc_network_manager
class and will be serialized and synced automatically. In serialization process every component can override load_network_instance
and write_network_instance
to write/read properties which describe the current state of the object.
This video illustrate engine network system functionality.
A wide range of animations are supported by the engine which are powered by ozz-animation library. Complex animations can be created by constructing individual animation jobs and putting them together though animation pipelines. The below animation of the xbot character is created in this function by joining multiple animation layers and constructing a single animation pipeline. Animations are queued and executed by multiple threads in a batch model.
To add Ragdoll animation to a model, a special version of the model mesh should be created which include mesh colliders and joints with a specific naming convention for each node. This 3d mesh version , which is called mesh collider, can be loaded by bc_mesh_collider_loader
to add colliders info to the model object.
Below is a XBot mesh with all of its colliders and joins in Blender.
Colliders are rendered as wireframe and joints which connect colliders to each other are rendered as small boxes positioned in the right place. By adding bc_human_ragdoll_component
to an actor, the component will look for these meta info inside the model to construct the Ragdoll data structures. Below is a video to show the Ragdoll functionality inside the engine.
Native functionalities can be easily exposed, to be used by the scripting language of the engine which is JavaScript. For example in BattleOfXbots game to start the server server.start(port);
and server.load_scene("BattleGround");
should be written in the server console. In fact these are JavaScript codes which run in the context of the engine. The code which bind native functionalities for above example is defined as below.
The code starts with defining functionalities of an object by a builder object. The builder is used to create a prototype object which then is passed to create a script object that has a reference to the native object. Then this object is assigned to a property named server
on the global object in JavaScript.
void bx_server_application::bind(platform::bc_script_context& p_context, platform::bc_script_global_prototype_builder& p_global_prototype, bx_server_application& p_instance)
{
{
platform::bc_script_context::scope l_scope(p_context);
auto l_server_prototype_builder = p_context.create_prototype_builder<bx_server_script>();
l_server_prototype_builder
.function(L"start", &bx_server_script::start)
.function(L"load_scene", &bx_server_script::load_scene)
.function(L"say", &bx_server_script::say);
auto l_server_prototype = p_context.create_prototype(l_server_prototype_builder);
const auto l_server_object = p_context.create_object(l_server_prototype, bx_server_script(*p_instance.m_game_system, p_instance, p_instance));
p_instance.m_server_script_context = &p_context;
p_instance.m_server_script_object.reset(l_server_object);
platform::bc_script_property_descriptor<platform::bc_script_object> l_server_property(&p_instance.m_server_script_object.get(), false);
p_global_prototype.property(L"server", l_server_property);
}
}
A couple of rendering effects are currently implemented in the engine which Include Tile-Based Differed Rendering, Differed Decal Rendering, Vegetable animations, Cascaded shadow maps, GPU based Particle System, Tessellated Terrain System.
- 3rdParty/ 3rdParty libraries.
- Solution/ Development IDE solutions.
- Src/ Engine source code. The source code is separated into physical like folders. Below is source code structure based on build dependencies.
- BlackCat.Core.Platform/ Platform specific functionalities required by
BlackCat.Core
modules. - BlackCat.Core.Platform.Win32/ Windows implementation of
BlackCat.Core.Platform
module. - BlackCat.Core/ Core functionalities of the Engine.
- BlackCat.Platform/ Platform specific functionalities.
- BlackCat.Platform.Win32/ Windows implementation of
BlackCat.Platform
module. - BlackCat.Graphics/ Graphic API abstractions.
- BlackCat.Graphics.DirectX11/ DirectX 11 implementation of
BlackCat.Graphics
module. - BlackCat.Physics/ Physics functionality abstractions.
- BlackCat.Physics.PhysX/ PhysX implementation of
BlackCat.Physics
module. - BlackCat.Sound/ Sound functionality abstractions.
- BlackCat.Sound.Fmod/ Fmod implementation of
BlackCat.Sound
module. - BlackCat.Game/ Contains main functionalities of the engine including animation, input, network, physics, rendering, scripting and sound systems and game play support systems.
- BlackCat.App/ Engine default implementations including
content loaders
andrender passes
. - BlackCat.Editor/ Engine editor.
- BlackCat.Core.Platform/ Platform specific functionalities required by
- BattleOfXbots/ A complete third-person multiplayer shooter game developed with the engine with 3 executable projects in following folders.
- Box/ Game client version.
- Box.Server/ Game server version.
- Box.Editor/ Game editor.