top of page

Spite: To Hel and Back (& R.O.S.E.)

2024

Specifications

​

Genre: 3D Action RPG

Engine: R.O.S.E.

Language: C++

Editor: Unreal

Time: 14 Weeks (50%)

Team: 17 People​

​​

​​​​​​​

​

Download​

Introduction

As a Viking with a death wish, reclaim the shards of your axe as you fight your way through an undead-infested mountain to prove you're too strong for death.​

​

Spite: To Hel and Back is a 3D action RPG inspired by Diablo with a Viking theme. It features similar mechanics with an isometric view where the player has multiple unique abilities to combat their foes.

Contribution

​My role during Spite was mainly to act as the engine programmer in the team. However, I did also work on some graphics and UI aspects.

​​​

Details

R.O.S.E

R.O.S.E. is the name of the in-house engine developed at The Game Assembly in our group, Ink Inc. The engine is originally derived from a graphics engine that I developed from the Applied Graphics Programming course. Before the project got underway, there were already support for, e.g., rendering meshes, deferred rendering, shadow mapping, and more graphics stuff. What was missing initially were several common engine systems that are important to develop games, e.g., (useful) component system, collisions, audio, NavMesh, and more. 

​

The reason for using Unreal as an editor is simply because we don't have any level editor that works with our engine, and also because it is familiar to many in our group.

Component System & Entities

The component system can be seen as the central system that simplifies or enables communication from the game to other modules of the engine. My goal with the component system was for it to be easy to use and understand (and performing well), because us programmers in the group are going to be using it actively to make games.

 

It starts with the EntityAdmin where entities live, which in turn live in a Scene, which in turn live in the World. Each entity has a name that the developer may specify, a EntityID, a local & global transform, references to their components, and other stuff. 

 

The EntityID is generated using an incrementing index combined with a version so that IDs may be reused but with only the version incremented to prevent conflict on older IDs. Whenever an entity is removed, its ID is put into a queue so that it can be reused when a new one is created.

​

A component is a class that inherits from the base class with the same name. A component can override Init, Awake, Start, Update, LateUpdate, Render, OnDestroy, OnEnable, and OnDisable. Init is called directly when the component is first added, Awake is called before Start next frame when added, Update is called before LateUpdate every frame, and so on.

 

Components can be received, added, or removed through an Entity. However, an Entity does not actually hold the component, but only a reference to the component called ComRef. All components instead live in cache-based pools inside the ComponentManager. A ComponentPool is essentially a FreeVector that contains all components of a specific type and is iterated through when, for example, Update is called. Using such a data structure made it possible to create two classes, ComRef and ComWeakRef. ComRef will remove the component when it is destructed, whereas ComWeakRef will not.​​​​​ ComWeakRef is used commonly to refer to other components and is what is returned when retrieving a component from an Entity.​

​

Entity Hierarchy

​Hierarchy for entities was implemented by adding parent and child data to an entity (pointers to other entities). This will, for example, cause an object to follow its parent entity's world transform. It can also be used for, e.g., searching for an entity with a tag or component in their children.

 

To improve performance, entities are marked as dirty whenever their position, rotation, or scale is updated, along with any children they have. When the entity's transform is used in any context, it is updated accordingly if the flag is marked as dirty. ​

​

The GIF to the right shows a cube at the bottom that rotates the chest, which rotates the character.

​

Hierarchy.gif

Component Import

Because we were using Unreal as an editor to import and load stuff into our engine, we had the problem of adding functionality to specific entities in the game. One solution was to use actor tags in Unreal and add components to entities in our engine based on those tags. However, I decided that solution felt too inflexible, prone to errors, and takes away being able to use tags for what they are meant for. Therefore, I added registration for components in our engine, where you can find the associated ComponentPool via its name. In our export script, the name of an actor's components are exported, and then when importing, they can be used to find which component to add.​​

​​

Asset Manager & Serialization

The AssetManager holds all the assets (naturally) which can be a mesh, texture, material, shader, animation, etc. All assets that are located under the Content directory local to our engine are automatically registered when the application starts. This allows for the name of the asset to be used to find it, which can then be loaded when it is requested for the first time.

 

Serialization for all [.fbx] assets was added to improve the loading speed when an asset is first requested. When the asset successfully loads, it will also serialize its results to a new file with the extension [.rose]. This is done mostly using basic memcpy for trivial types, whereas non-trivial gets special handling such as string or vector. During serialization, its type, the path to the original file, and the version also get serialized. This is so that when the file with the serialized contents gets loaded, it first checks if the asset at the original filepath is up-to-date with the serialized file. If the original asset is newer, then the serialized file gets deleted and the asset must be loaded anew again.​​

​

Double Buffered

​​I added double buffered rendering to the engine, which means that all game logic runs on its own thread separated from the main thread where rendering occurs. To implement this, we have two separate GraphicsCommandList, where one is filled with commands from the update thread, while the other is being executed on the main thread. This is a quick and efficient way to generally improve performance, with the drawback of the game being one frame behind, which isn't noticeable anyway.​​​​

Render Pipeline

To clarify, render pipeline is the steps taken inside the engine to finally draw, for example, lighting or a mesh to the screen, and not the internal graphics pipeline in DirectX (rasterization, shaders, etc.).

​​

To provide a flexible way to add new types of rendering techniques when needed, I created a class called GraphicsAssembler. Through this class, we externally enqueue an item to be drawn by passing a DrawID, where the class then takes care of the rest to make sure it is drawn correctly. DrawID contains flags such as which rendering layer and what transparency the item has to eventually sort the enqueued items, for example, rendering opaque objects sorted by depth first and then all transparent sorted in reverse order. This furthermore improves performance by reducing the number of times the graphics engine has to change its internal pipeline states (pixel shader, vertex shader, etc.) by sorting material IDs.​​

​

Scene Graph & Frustum Culling

FrustumCulling.gif

I furthermore added a SceneGraph which makes use of an Octree to quickly cull objects that are not seen by the player. All drawable things must inherit from a SceneNode which contains logic to handle, e.g., referencing which node it occupies in the SceneGraph to quickly remove itself if need be. The first step when rendering in the GraphicsAssembler is to take the frustum generated by the projection view matrix from the camera and query the SceneGraph for nodes that intersect the frustum. This way, objects that are not seen can be culled as early as possible before even being sent to the graphics command list. ​​​​

​

Frustum culling was also added for shadow casting lights to quickly find objects in their view. Directional light and spotlight cull similarly using a frustum projected from their point of view, while pointlight uses a basic sphere to query.

​

Threaded Scene Loading

Threaded scene loading forced me to rethink the design of several systems in the engine. To reduce the risk of data race, the loading thread is designed to use as little external code as possible. The code that had to be made thread-safe was at least the AssetManager and the component system. The AssetManager because assets are of course loaded in, and the component system because components are being added to new entities being created.​​​​

​

Spite_ThreadedLoading.gif

The AssetManager was solved simply by adding a mutex into its functions to prevent multiple threads from writing at the same time. The component manager was trickier because I desired for it to be possible to load another scene while the game is running at the same time. To solve this, thread-safe pending queues were added for many actions so that they may be dequeued when loading is done.​

​

Quaternion

A considerable problem we had was importing rotations from Unreal into our engine. Specifically, at some times objects would just flip, which I discovered happened occasionally when you convert directly from a rotationmatrix to Euler angles. It seems to be caused from the Euler angles reaching a singularity at the north/south poles. Then there was also animation blending, which requires spherical linear interpolation for rotations to look correct.

 

To solve many of these problems, and also solving the Gimbal lock, I added Quaternion to the math library which replaced most applications of Euler angles in the project. After that, a rotationmatrix could be accurately converted to a quaternion, and vice versa. It could also be used for interpolation when blending two rotations.​

FXAA

​FXAA was implemented to make the jagged edges appear smoother. It works by measuring the difference in luminance to find the areas to improve. (Screenshot below is taken from Shattered because FXAA hasn't been changed since it was introduced).

​

BeforeFXAA.png

Before

AfterFXAA.png

After

User Interface System

​I implemented an extensive UI architecture which makes use of a StateStack.​​​​ For each State in the stack, there exists a Canvas wherein UI elements can be added. Rendering order and input logic is handled in the Canvas that forwards cursor position, mouse press, and more to its children. All elements that exist in the UI must inherit from UIComponent that contains its size, if it is selectable, bounding rect, and more. Furthermore, I added the Container, which acts as a mini-canvas that can move and set the opacity for all of its children. For example, fading out the buttons when unpausing the game by modifying the Container and not each element individually.

bottom of page