Valkyrie Engine is a game engine written in modern C++ that is designed to be as modular as possible and as lightweight as you want.
Valkyrie Engine's Core module is designed to be small and easy to implement. Most of the functionality that you would expect from a game engine is defined across Valkyrie Engine's various modules. These modules are collections of systems, events and components designed to help accomplish a specific functionality or behaviour within a game.
The eventual goal of Valkyrie Engine is to create a thriving ecosystem of modules from various developers that have either been torn out of previous Valkyrie Engine projects or developed completely independently of one.
The supply of video games in the current market is increasing at a rate that is far outpacing the demand. Consumers are beginning to raise their overall standards for quality as various niches are quickly becoming saturated. As games become more complex and their specifications more refined, it becomes increasingly difficult to produce a single tool that adequately meets this wide range of requirements and yet most publicly-available engines are trying to do exactly that.
This is where Valkyrie Engine comes in. Instead of giving developers one general-purpose system, Valkyrie Engine is designed to give them a choice between several specialised systems so they can use the best option for their game. Valkyrie Engine lets the developer decide how it's implemented and in what capacity. You can mix-and-match existing modules and use Valkyrie Engine as a fully-featured game engine or forgo them and elect to make your own.
Documentation is currently live here.
Valkyrie Engine doesn't require much in terms of initialization, currently it only requires a call to vlk::Application::Start()
at the beginning of your program. This is the function that starts the main update loop. This loop will run continuously until vlk::Application::Stop()
is called, in which case it will exit once the current update loop is completed.
#include "ValkyrieEngine/ValkyrieEngine.hpp"
int main()
{
vlk::ApplicationArgs args{};
args.applicationName = "My Game";
vlk::Application::Start(args);
}
In Valkyrie Engine, events are the primary way that inter-module communication occurs. Instead of calling a function from another module directly, send an event. Events help keep Valkyrie Engine modular and your code maintainable, both by yourself and others.
In Valkyrie Engine, any object or class can be an event, though we recommend using POD structs.
struct MyEvent
{
const vlk::Int foo = 10;
const vlk::Float bar = 1.0f;
};
Valkyrie Engine Core comes with a few events that help form the main update loop of the engine. All of these events are simply empty structs with no data but their usage varies significantly:
ApplicationStartEvent
is sent once, just before the first event loop of the application runs. This is used primarily for second-stage module initialization and to set up the initial state of your game.ApplicationExitEvent
is sent once, after the event loop exits and the application is due to terminate. This is used to perform cleanup on any entities or systems that need it.PreUpdateEvent
is sent at the beginning of every update loop but should not be considered part of it. It is mostly used to update modules and should not be used to change the state of your game.EarlyUpdateEvent
is sent afterPreUpdateEvent
and is mostly used to read the state of other entities beforeUpdateEvent
is called.UpdateEvent
is sent afterEarlyUpdateEvent
and is used to update the state of most of entities present in the game.LateUpdateEvent
is sent afterUpdateEvent
and is mostly used to react to the changes that occured during theUpdateEvent
.PostUpdateEvent
is sent afterLateUpdateEvent
and is similar to thePreUpdateEvent
in that it is mostly used to update modules and should not be used to alter the game state in any way, redering would usually take place during a PostUpdate..
Sending events in Valkyrie Engine is done through the EventBus
class. This will raise the event handler for every listener of your event.
vlk::SendEvent(MyEvent{});
In order to recieve an event, you need to define a class that inherits from vlk::EventListener
and construct an instance of it.
This instance will then recieve any events of the appropriate type you send down the event bus.
class MyEventListener : public EventListener<MyEvent>
{
/* ... */
void OnEvent(const MyEvent& ev) final override
{
/* Do something in here */
}
};
#include "ValkyrieEngine/ValkyrieEngine.hpp"
#include <iostream>
#include <string>
struct MyEvent
{
const vlk::Int foo;
const std::string bar;
};
class MyEventListener : public vlk::EventListener<MyEvent>
{
public:
MyEventListener() = default;
private:
void OnEvent(const MyEvent& ev) final override
{
std::cout << "Event handler raised: " << std::to_string(ev.foo) << std::endl;
std::cout << "Cow says: " << ev.bar << std::endl;
}
};
int main()
{
MyEventListener listener;
vlk::SendEvent(MyEvent{ 51, "Moo!" });
}
Valkyrie Engine core defines a basic entity-component system that serves as the underlying framework for any game produced in it.
Entities are represented by a primitive ID and are used used to associate components to one another. The Entity
namespace defines several functions for creating and destroying entities.
#include "ValkyrieEngine/ValkyrieEngine.hpp"
void Foo()
{
vlk::EntityID myEntity = vlk::Entity::Create();
...
vlk::Entity::Delete(myEntity);
}
Components are data types that define a particular behaviour or property for the entity they're attached to. It is important to note that components shouldn't be event listeners. This is mostly to avoid congestion on the event bus as constantly adding and removing event listeners can significantly impact performance. See section 3.3.3 for further details.
Component<T>::Create
takes the ID of the entity the component is to be attached to, any subsequent arguments are forwarded to the constructor of your data type.
#include "ValkyrieEngine/ValkyrieEngine.hpp"
class Counter
{
public:
Counter(vlk::UInt _score)
{
this->score = _score;
}
vlk::UInt score;
};
void Foo()
{
vlk::EntityID myEntity = vlk::Entity::Create();
vlk::Component<Counter>* c = vlk::Component<Counter>::Create(myEntity, 10);
}
Where components define behaviour for entities, systems are where that behaviour is implemented. Most systems will take the form of several event listeners with any necessary data stored in member variables or the like. Since systems perform operations on/with components, they need a way to access every instance of that component at runtime. Component
has two static member functions called ForEach
and CForEach
that do exactly that.
#include "ValkyrieEngine/ValkyrieEngine.hpp"
class MySystem :
public vlk::EventListener<vlk::UpdateEvent>
public vlk::EventListener<vlk::LateUpdateEvent>
{
public:
MySystem() = default;
vlk::UInt sumTotal;
private:
void OnEvent(const vlk::UpdateEvent& ev) final override
{
// Modifying all counter components, use for each
Component<Counter>::ForEach([](Component<Counter>* c)
{
// Add 1 to score every update.
c->score++;
});
}
void OnEvent(const vlk::LateUpdateEvent& ev) final override
{
sumTotal = 0;
// Not modifying components, const for each makes more sense
Component<Counter>::CForEach([&sumTotal](const Component<Counter>* c)
{
// Update sum total of all counters
sumTotal += c->score;
});
}
};