Spite: Fractured Shards

“You, the Guardian, must defeat the beast that is corrupting the town’s sacred magical tree.”

Our pitch

A game made in 14 weeks half time in Eggine(our own engine) with a total of 17 teammembers.

Trailer

My contributions

The goal for this project was not only to do a game but also the game engine that the game was created in. Creating an engine from scratch wasn’t the only thing I tried for the first time in this project. Our group worked with an entity component system which was quite different compared to how I have worked earlier. We also used Cmake which changed the workflow a bit but mostly for the better. During this project I worked mainly with the levelloader, textrendering and playersaving/checkpoints. I also made a logger and the texture factory. You can read more about some of the stuff I did down below!

Level loader

My first big task was implementing a level loader. I didn’t have to create one from scratch thanks to one of my classmates (Tobias Nilsson). However, I had to implement it and change it a bit to make it work for our engine and our way of working.

The first thing I tried was to just get any data from unity in to our engine. I created some objects in a scene in Unity and then converted a c++ file to c# with the framework I got from my classmate. I changed an integer on that script in Unity and printed the result and it worked. This meant that I could not only get normal unity data like transforms, colliders and such but also custom scripts that we wrote in c++.

Here is some code snippets from around that time in development. At this time the levelloader could load boxcolliders,transforms and “models”. If an object had a model in unity it would be represented as a head model because we had not yet come up with a solution for loading the correct model paths. At this time we had every loading lambda in the same function “LoadLevel”, this was changed later when we extracted the lambdas’ to their own functions.

Levelloader.h (Early edition)
#pragma once
#include <string>
#include <Engine\ComponentLoader.h>

namespace flecs
{
	class world;
}


class LevelLoader
{
public:
	LevelLoader();
	void Init(flecs::world& aWorld);
	void LoadLevel(const std::string& aLevelPath);
	static LevelLoader* GetInstance();
	void ClearLevel();

private:
	ComponentLoader myLoader;
	static LevelLoader* ourInstance;
	flecs::world* myWorld;
};
Levelloader.cpp (Early edition)
#include "LevelLoader.h"
#include <ECS/ECS.h>
#include <Engine\Components.h>
#include <Engine\BaseComponent.h>
#include <Engine\ModelFactory.h>
#include <Engine/Components/ModelComponent.h>
#include <CommonUtilities\DL_Debug.h>
#include "Components.h"

LevelLoader* LevelLoader::ourInstance = nullptr;

LevelLoader::LevelLoader() : loader(), myWorld(nullptr)
{
	ourInstance = this;
}

void LevelLoader::Init(flecs::world& aWorld)
{
	myWorld = &aWorld;

}

void LevelLoader::LoadLevel(const std::string& aLevelPath)
{
	ClearLevel();
	std::unordered_map<int, flecs::entity> unityEntities;
	// temporary code to test level loader
	loader.RegisterComponent<TransformData>(
		[this, &unityEntities](std::vector<TransformData> aList) {
			// We assume that every object with a transform should be an entity
			// in our engine

			DL_Debug::Debug::GetInstance()->WriteLog("LevelLoader", "Loaded", aList.size(), " transform components from Unity!");
			for (auto& transform : aList)
			{
				auto entity = myWorld->entity();

				auto fileIterator = unityEntities.find(transform.myEntityIndex);
				if (fileIterator == unityEntities.end())
				{
					unityEntities.emplace(transform.myEntityIndex, entity);
				}
				else
				{
					assert(false && "maybe you didnt clear the level??");
				}

				ECS::Position pos;
				ECS::Rotation rot;
				ECS::Hierarchy hier;
				ECS::UnityObject uo;

				// Don't know why we have to do negative values for position and
				// rotation but it looks correct now
				pos.myPosition = transform.myPosition * -100.f;
				pos.myPosition.y *= -1;

				rot.myRotation = {
					transform.myRotation.x * -1,
					transform.myRotation.y * 1,
					transform.myRotation.z * -1 };

				entity.set<ECS::Position>(pos);
				entity.set<ECS::Rotation>(rot);
				entity.set<ECS::Hierarchy>(hier);
				entity.set<ECS::UnityObject>(uo);

			}
		});

	loader.RegisterComponent<ModelRendererData>(
		[this, &unityEntities](std::vector<ModelRendererData> aList) {

			DL_Debug::Debug::GetInstance()->WriteLog("LevelLoader", "Loaded", aList.size(), " Model components from Unity!");

			for (auto& modelRenderer : aList)
			{
				const char* head = modelRenderer.myPath;
				auto& model = ModelFactory::GetInstance()->GetModel(head);
				auto entity = unityEntities[modelRenderer.myEntityIndex];

				entity.set<ECS::ModelComponent>({ StringId::FromString(head) });
			}
		});
	loader.RegisterComponent<BoxColliderData>(
	    [this, &unityEntities](std::vector<BoxColliderData> aList) {
		    DL_Debug::Debug::GetInstance()->WriteLog(
		        "LevelLoader",
		        "Loaded",
		        aList.size(),
		        " Box components from Unity!");

		    for (auto& boxCollider : aList)
		    {
			    auto entity = unityEntities[boxCollider.myEntityIndex];
			    boxCollider.myCenter.y *= -1;
			    entity.set<ECS::BoxCollider>({boxCollider.mySize * 100.0f, boxCollider.myCenter * -100.0f});
			    entity.set<ECS::FloorTag>({});
			    entity.set<ECS::Collision>({});
		    }
	    });
	loader.LoadComponents(aLevelPath);
}

LevelLoader* LevelLoader::GetInstance()
{
	return ourInstance;
}

void LevelLoader::ClearLevel()
{
	loader.ClearComponents();
	auto f = flecs::filter(*myWorld)
		.include<ECS::UnityObject>()
		.include_kind(flecs::MatchAll);
	myWorld->delete_entities(f);

}

The most interesting thing from this part of the development was the loading of transforms. Every entity has a transform, both in Unity and our own engine. That is why we are loading the transforms first and creating the entity when the transform is being loaded. We then save the created entity in an unordered_map with the entity id that we get from Unity as the key. Since all components derives from a basecomponent that contains the entity id from unity we can easily add new components to the correct entity later on in the loading process. This solution was something that we used troughout the whole project. Another interesting thing with the loading of the transform is the magic numbers we had to use. We quickly noticed that the orientation of entities in our engine and Unity differed, so we had to solve this by converting it to our way of handling rotation and position. We later figured out that it is possible to solve this in Unity if you enable “Bake axis conversion” on all models. Unity will then export it correctly.

The first time we tried to load a level with both models and materials loaded from Unity

When we had come so far that we could load levels with models,colliders and custom scripts from Unity, I started to look into how to make the life for the leveldesigners easier. Their workflow was very tedious. If they wanted to see their level ingame they had to first export the level they worked in by pressing “Ctrl + E”, then they had to choose what folder they wanted the levelfiles to export to. Then they had to open the engine and load the level they just had exported. The first improvement I made was removing the part where you choose the folder where the level get’s exported. We all had the same folderstructure in our subversion program so instead of having to choose a unique path everytime you exported I made the path relative. This removed an unneccesary part of exporting because they always chose the same path each time they exported. I also made the game bootable from Unity. If you pressed “Ctrl + Q” the level did not only get exported but it also started the executable and loaded the level you previewed in Unity. These changes made the workflow much more effective considering that we didn’t have to choose export paths or even open the game to playtest our levels.

Text rendering

I was responsible for the text rendering in our engine. I used the directxtoolkit and made a wrapper for the rendering and component for the text. We used the text for menus, UI, some debug purposes like fps counter and for gameplay purposes like names of enemies and interaction texts.

Video showing some text in action

Player saving/Checkpoints

We used flecs as our entity component system and there wasn’t any easy way (atleast not any I found) to make copies of entities. Because I couldn’t make copies I used the memento pattern and made a copy of the player that way. The saving was used mainly for saving data on the player like health and mana between levels and abilities both between levels and between lifes. Doors that had been opened or closed also saved their state and scripted encounters saved their state.

Video showing respawning at checkpoint

Enemy spawner

The spawners were pretty simple for this project. I created a script that you added to an existing enemy and then the enemy became a spawner of that enemy type. You could choose how many enemies you wanted the spawner to spawn, if you choose 0, the spawner would spawn an infinite amount of enemies. You could also change the delay between the spawns. I also created a script that you could add that made the spawner a boss spawner. The only difference between a normal spawner and a boss spawner was that the boss spawner got enabled during a certain boss phase.

Video showing an enemy spawner

Logging

I created a logger that I built upon an earlier assignment we had in our “Applied Software development” course. When you wanted something important to get logged you called the function WriteLog() and inserted the type, the category and the information that it should log. The class I created wrote the information out both to the console and into a .txt file with the same name as the type. The category changed the font color in the console and if you chose the category “Warning” or “Error” the information would also get written to the warning/error text besides the unique type text file.

template <typename... TArgs>
void WriteLog(const char* aLogType, eLogCategory aLogCategory, TArgs... args);
Video showing the logging in action

The team behind Spite : Fractured shards