Miracle is a cross-platform, framework-style C++20 game engine, that is still early in its life cycle, for developing minimalistic 2D and 3D games on Windows, macOS and Linux. It uses the Vulkan SDK for rendering and relies on vcpkg to manage all other dependencies. Both must be installed to configure and build this project.
The following demonstrations were built from demo projects which come included as part of this repo.
The source code for the 2D demo can be found here.
The source code for the 3D demo can be found here.
Miracle requires very little code to get started. Here is a minimal setup for an empty application:
#include <Miracle/Miracle.hpp>
int main() {
return Miracle::App("My app").run();
}Let's break that down:
#include <Miracle/Miracle.hpp>
using namespace Miracle;
int main() {
// Step 1: Create the app
auto app = App("My app");
// Step 2: Run the app
int exitCode = app.run();
// Step 3 (optional): Return the app's exitcode
return exitCode;
}In order to add content to your app and configure its behavior, we use an AppConfig during construction of the app, as shown below.
auto app = App("My app", AppConfig{});AppConfig is a struct containing several smaller structs that configure different parts of your app. Here's an example of an app with a resizable window and v-sync enabled:
auto app = App(
"My app",
AppConfig{
.windowConfig = WindowConfig{
.size = WindowSize{
.width = 1280,
.height = 720
},
.resizable = true
},
.rendererConfig = RendererConfig{
.swapchainConfig = SwapchainConfig{
.useVsync = true
}
}
}
);Miracle uses a scene system where content is split up into different scenes, and only a single scene is active and displayed to the user at a time. Right now, Miracle does not support creating multiple scenes, but this will change in the future. The content of a scene is made up of entities. Everything in a scene, except the solid color background of a scene, is an entity. A scene can be created by adding a SceneConfig to the app's AppConfig, while entities that belong to this scene can be created by adding EntityConfigs to the SceneConfig.
AppConfig{
// A scene containing three entities
.sceneConfig = SceneConfig{
.entityConfigs = std::vector{
EntityConfig{},
EntityConfig{},
EntityConfig{}
}
}
}An entity is an object that can represent anything in a 2D or 3D environment. For example, it can represent a controllable character, an enemy character, an inanimate object, a camera to view into the environment, a light source to light up the environment, or even the ground itself that all other entities stand on. Entities are made up of components which represents the different abilities that an entity possesses. An entity can only possess one of each component type. The Transform component is the only component that is mandatory for an entity to possess, and will be added even if not supplied by the developer in the EntityConfig. All other components are optional to include in an entity.
SceneConfig{
.entityConfigs = std::vector{
// An entity containing two components
EntityConfig{
// A Transform component describing the position of the entity in 3D space
.transformConfig = TransformConfig{
.translation = Vector3{ .x = 2.0f, .y = 0.0f, .z = -1.0f }
},
// A Camera component describing the entity's ability to view the scene with a perspective lens
.cameraConfig = PerspectiveCameraConfig{
.fieldOfView = 80.0_deg
}
}
}
}Right now Miracle only supports one type of asset, which is 3D meshes, and the AppConfig does not provide a dedicated section to add assets yet, so for now, meshes are added directly into the RendererConfig instead.
AppConfig{
.rendererConfig = RendererConfig{
.meshes = std::vector{
// The mesh of a square
Mesh{
.vertices = std::vector{
Vertex{ .position = Vector3{ .x = -0.5f, .y = -0.5f, .z = 0.0f } },
Vertex{ .position = Vector3{ .x = 0.5f, .y = -0.5f, .z = 0.0f } },
Vertex{ .position = Vector3{ .x = 0.5f, .y = 0.5f, .z = 0.0f } },
Vertex{ .position = Vector3{ .x = -0.5f, .y = 0.5f, .z = 0.0f } }
},
.faces = std::vector{
Face{ .indices = { 0, 1, 2 } },
Face{ .indices = { 0, 2, 3 } }
}
}
}
}
}Once your meshes have been added to the meshes section of the RendererConfig entities can use them by referring to the correct index in an Appearance component.
EntityConfig{
// An Appearance component describing an appearance that is using the first mesh in the RendererConfig's meshes colored as magenta
.appearanceConfig = AppearanceConfig{
.meshIndex = 0,
.color = ColorRgb::createFromNonlinearSrgbColorCode(0xFF00FF)
}
}Behaviors are what powers entities to act on its own, or through user input. Miracle provides a polymorphic Behavior component which allows you to author any kinds of behavior that you want. To do so you have to create a class that extends BehaviorBase.
class MySimpleBehavior : public BehaviorBase {
private:
float m_movementSpeed;
public:
// The behavior receives an entity context that lets it manipulate the state of the entity. The context must be the first parameter supplied to the constructor, and must be forwarded to the underlying BehaviorBase.
// Following the context, any other parameters can be accepted by a behavior depending on your needs.
MySimpleBehavior(const EntityContext& context, float movementSpeed) : BehaviorBase(context), m_movementSpeed(movementSpeed) {}
// The act method acts out the behavior, and is called every update in the app
virtual void act() override {
// Moves the entity forward in the Z-axis when the W-key is held down on the keyboard
if (Keyboard::isKeyHeld(KeyboardKey::keyW)) {
m_context.getTransform().translate(Vector3s::forward * m_movementSpeed * DeltaTime::get());
}
}
};Once a behavior class has been created the behavior can be assigned to an entity. Since the behavior needs to be instantiated after the entity has been instantiated as part of the scene, you need to describe in the EntityConfig both which behavior it should use and what parameters it should be instantiated with. This is done using a BehaviorFactory.
EntityConfig{
// A behavior component using our MySimpleBehavior class which will be instantiated by forwarding the factory's parameters to the class's constructor, in the supplied order
.behaviorFactory = BehaviorFactory::createFactoryFor<MySimpleBehavior>(10.0f) // movementSpeed = 10.0f
}Miracle allows you to add scripts to the app that are not attatched to any entities. There are two types of scripts like this, the Start script and the Update script, both of which can be set in the app's AppConfig.
AppConfig{
.startScript = []() { Logger::info("Starting up..."); },
.updateScript = []() { Logger::info("Running logic during update..."); }
}The Start script will run once during startup of your app, and the Update script runs once every update, before the behaviors belonging to the current scene's entities run.
Miracle allows you to generate new entities during the app's runtime. This is helpful in games where you want to spawn in new content during runtime. In order to do so you can simply grab the current scene and provide it with an EntityConfig for your brand new entity.
// Creates a new entity in the scene
CurrentScene::createEntity(EntityConfig{});
// Creates a new entity in the scene, and returns the context for it
EntityContext context = CurrentScene::createAndGetEntity(EntityConfig{});Any entity can also be deleted during runtime from its EntityContext.
context.destroyEntity();Miracle uses the MIT license.

