Project Status | Ongoing |
Project Type | Unknown Type |
Project Duration | Unknown Duration |
Software Used | Unknown Sofware |
Languages Used | Unknown Languages |
Primary Role(s) | n/a |
Warlock Engine is an ongoing project that I've been working on since 2016.
The idea behind Warlock was to create my own personal "toolbelt" for my personal projects. I work on different projects in my spare time, and wanted a framework in which I felt completely comfortable and had all tools I need to my disposal so that I can quickly iterate and prototype new ideas.
Warlock consists of a few "big" systems and many "smaller" low level systems. Since the big systems are the most interesting/visual to talk about, I will cover them first.
The Level Editor is where all game logic comes together! Warlock incorporates an advanced level editor with a more "traditional style" entity component system.
The level editor supports an advanced hierarchy system with a "virtual hierarchy" as the main defining structure of a level, and a "node hierarchy" to provide the physical structure. This allows things like folders in the virtual hierarchy while not cluttering the node hierarchy.
Debug drawing is possible with both 3D shapes, meshes and ImGui. You can also tweak object properties live while playing.
You can add as many viewports as you'd like, and each viewport can define its own debug drawing. This was one of the most common limitations I've found in different game engines and I wanted to make sure I did this right. You can also have multiple level editors open simultaneously.
Templates are an important aspect of building level. They are essentially what "prefabs" are in Unity, and "blueprints" in Unreal Engine.
Templates can be instanced in the world, and a new set of UIDs is generated and mapped to the base instance.
This way, you can link to entity components even if they are template instances.
Entity Components can be parented to a bone or multiple bones (with influences). This is still a bit primitive (would be nicer with a bone name selector).
erg
The Sequence Editor incorporates an advanced sequencing system that is flexible and easily expandable. It supports a weak resource binding system that allows sequences to be reused in different contexts, such as them being part of instanced templates.
The Timeline uses my robust Canvas system that supports many quality of life features such as zoom-to-cursor, integrated timeline markers for multiple axes, custom drawing of keyframes, "clips" that have a begin and end time on the timeline, and more. On the left hand side is a hierarchy view with preview values that update in realtime and can be modified to insert new keyframes.
A sequence in Warlock Engine is nothing more than a hierarchy of items, some items can be tracks, some of them are simply hierarchical elements.
Sequences support many predefined types of curves. These can be single value float curves, or compound channels for vectors. It also supports "data" channels that support templating to provide the value type of the keyframe. One common example of data channels is to store UIDs for a certain resource, like camera shots.
struct SeqCameraDirectorKey { uint32 mShotUID = 0; void Pack(C_DataPack& aPack) const { aPack.Set("shotUid", mShotUID); } void Unpack(const C_DataPack& aPack) { aPack.Get("shotUid", mShotUID); } bool operator==(const QDSeqCameraDirectorKey& aOther) { return mShotUID == aOther.mShotUID; } }; class SeqCameraDirectorItem : public SEQ_SequenceChannelData< SeqCameraDirectorKey > { typedef SEQ_SequenceChannelData< SeqCameraDirectorKey > Super; WAR_DECLARE_OBJECT_INHERIT("SeqCameraDirectorItem", SeqCameraDirectorItem, Super); static void ObjectRegisterTypeInfo(SEQ_ItemTypeInfo& info); private: SEQ_SequenceItemInstance* CreateInstance() const override { return new SeqCameraDirectorInstance(); } C_Color GetEditorColor() const override { return C_Colors::OrangeRed; } bool OnEditorHierarchy(float aPlaybackTime) override; };
The result:
These channels, or any kind of sequence "items" can easily be added to other items, with optional serialization of their hierarchy and/or data.
mGameToSequenceCameraBlend = CreateChildItem("Game to Sequence cam"); mGameToSequenceCameraBlend->SetShouldSerializeHierarchy(false); mGameToSequenceCameraBlend->SetDefaultValue(1.f); mLockCamBehindPlayer = CreateChildItem ("Lock Behind Player"); mLockCamBehindPlayer->SetShouldSerializeHierarchy(false); mLockCamBehindPlayer->SetDefaultValue(0.f); mLockedPitchAngle = CreateChildItem ("Locked Pitch Angle"); mLockedPitchAngle->SetShouldSerializeHierarchy(false); mLockedPitchAngle->SetDefaultValue(0.f); mLockCamUserInput = CreateChildItem ("Lock Cam User Input"); mLockCamUserInput->SetShouldSerializeHierarchy(false); mLockCamUserInput->SetDefaultValue(1.f);
Sequence Resources are a common problem in these kinds of systems. A resource is any kind of externally bound data that the sequence can used. For example, an entity of the world. This can be a prefixed entity that only exists once, or the sequence can be instanced and the resource redefined as part of that instance.
I really doubled down on this idea of sequences being a hierarchy, thus this is also visible in the resource system. A "game camera" sequence item, for example, expects it to be parent to a valid player agent:
void GCL_SequenceGameCameraInstance::AddToWorld() { GCL_SequenceEntityResource::Instance* entityRes = GetParentResourceInstance< GCL_SequenceEntityResource >(); if (!entityRes || !entityRes->mEntityHandle.IsValid()) { WAR_LOG_ERROR(CAT_GENERAL, "Failed to get entity"); return; } if (GCL_PlayerAgentComponent* agent = entityRes->mEntityHandle.GetEntity()->GetComponentContainer().GetComponentByType< GCL_PlayerAgentComponent >()) { if (agent->GetCamera()) { mComponentHandle = agent->GetCamera()->GetHandle(); } } if (mComponentHandle.IsValid() == false) { WAR_LOG_WARNING(CAT_GENERAL, "Failed to resolve camera handle, is the parent item a PlayerAgentComponent?"); } }
Sequence resources can be defined as part of the sequence itself:
When reusing a sequence in a template, its resources can be individually overridden when needed to provide a custom binding:
The Animation system of Warlock is driven by a nodegraph, and supports "variables" to communicate to gameplay code. Variables can also be driven from sequences.
A big part of animation systems is a state machine. Warlock supports state machines with a variety of ways to transition to/from states. Either from code, by animation end or by expression from within the node graph:
Warlock uses a node graph to define audio events. They are fairly simple still, but you can also define different effects (reverb, pitch shift, etc.) here.
There are also many lower level systems that solve a variety of problems.
A small type/object system is used within many systems of Warlock that allows for easy registration of new types and safe inheritance casting.
For example, to register new sequence types:
seq->GetItemTypeRegistry().RegisterType< GCL_SequenceFadeTrack >(); seq->GetItemTypeRegistry().RegisterType< GCL_SequenceEntityItem >(); seq->GetItemTypeRegistry().RegisterType< GCL_SequenceAudioEventTrack >(); seq->GetItemTypeRegistry().RegisterType< GCL_SequenceAudioBusVolumeTrack >(); seq->GetItemTypeRegistry().RegisterType< GCL_SequenceEntityTransformTrack >(); seq->GetItemTypeRegistry().RegisterType< GCL_SequenceGameCameraTrack >();
But also entity components:
lvl->RegisterEntityComponent< GCL_MeshComponent >(); lvl->RegisterEntityComponent< GCL_LightComponent >(); lvl->RegisterEntityComponent< GCL_AnimControllerComponent >(); lvl->RegisterEntityComponent< GCL_SequenceComponent >(); lvl->RegisterEntityComponent< GCL_ReflectionProbeComponent >(); lvl->RegisterEntityComponent< GCL_AgentComponent >(); lvl->RegisterEntityComponent< GCL_PlayerAgentComponent >(); lvl->RegisterEntityComponent< GCL_AgentSpawnComponent >(); lvl->RegisterEntityComponent< GCL_JiggleComponent >(); lvl->RegisterEntityComponent< GCL_ThirdPersonCameraComponent >(); lvl->RegisterEntityComponent< GCL_IBLProbeGridComponent >(); lvl->RegisterEntityComponent< GCL_InteractionIconComponent >(); lvl->RegisterEntityComponent< GCL_InteractionDispatchComponent >(); lvl->RegisterEntityComponent< GCL_GameCameraComponent >();
Objects can also store custom statically defined type info that is registered per object type. For example, this stores some editor related data:
void GCL_SequenceGameCameraTrack::ObjectRegisterTypeInfo(SEQ_ItemTypeInfo& info) { #ifdef USING_TOOL_EDITOR info.mIsUserSelectable = true; info.AddRequiredParent< GCL_SequenceEntityItem >(); info.mDisplayName = "Player Camera"; #endif }
I try to use as few external dependencies as possible so that I remain in control over all low level elements of the engine.
The libraries I use are:
I've used Warlock for many projects already. Let's see some of them in action!