Engine: Save/Load and other fun

Published December 09, 2018
Advertisement

I'd like to make this a bit more regular hobby, releasing updates on my own game engine (among other projects), yet sometimes my real work steps in and I tend to be quite busy. Anyways back to the important part and that is what is this article about.

Making a game engine with WYSIWYG editor is quite a challenge, it takes years of practice and learning, any resources are scarce - and therefore you have to improvise a lot. Sometimes it takes dozens of attempts before you got it right, and the feeling when you finally did it right is awesome. This time I'm going to dig a bit into how I designed save, load & publish system, and which parts of it are done.

Introduction

To understand save/load/publish system, one needs to understand how his engine is going to work. In Skye Cuillin (codename for my game engine) you have editor application, which is used to create your game scenes, test them and possibly publish them. On the other hand, the planned runtime application takes in exported scene a plays it.

There are 2 possible ways to save a scene - first is something I call  Editable Scene, second thing is what I call Exported Scene. Both file formats serves different purposes and stores all the data in completely different way.

  • Editable Scene is saving primarily just a scenegraph, with all entities and their components in a way where editor can easily load them again. It doesn't store any resource files (meshes, textures, etc.) but uses the one on the hard drive. This is mainly to keep the amount of data in the file to minimum (after all it is a text file, not binary file).
  • Exported Scene is storing both - scenegraph, with all entities and their components, along with all resource files (meshes, textures, etc.) in a specific compressed and binary encoded format, so it is fast to load, but losing possibility to be loaded back into editor.

I won't get into details of Exported Scene type today, as that part is still in testing - yet I want to show some details on how Editable Scene is working. Further, I will describe and talk only about Editable Scene format.

Saving & File Format

In the engine, the scenegraph is a N-ary tree with a single root node. This node can't be edited in any way and can't have any components except transform component, which is at coordinates of (0, 0, 0). It is intentionally shown also in the scene graph view on the left side in the editor, and user is unable to select it through there. The only action that can be performed on it is adding new Entity as its child (either by assigning Entity's parent as root, or adding new entity into scene under root), or removing Entity as its child (either by re-assignment under different parent Entity, or actual deletion).

Entity class has a Serialize method, which is this as of now:


std::string Entity::Serialize()
{
	std::stringstream ss;

	ss << "Entity" << std::endl;

	// Store the name
	ss << mName << std::endl;

	// Note. Each entity except Root will always have a parent!
	// Store the parent name
	if (mParent)
	{
		ss << mParent->mName << std::endl;
	}
	else
	{
		ss << std::endl;
	}

	// Store the transform
	ss << mTransform.Serialize();

	// Store components
	for (auto it = mObject.ComponentsItBegin(); it != mObject.ComponentsItEnd(); it++)
	{
		ss << it->second->Serialize();
	}

	ss << "(" << std::endl;

	// Store children (recursively)
	if (mChildren.size() > 0)
	{
		for (auto child : mChildren)
		{
			ss << child->Serialize();
		}
	}

	ss << ")" << std::endl;

	return ss.str();
}

Simply store some keyword, base entity data (name, parents and transformation), component data and then list of children within parentheses. Also note that each component has Serialize method which simply serializes it into text. On side, all this is also being used for Undo/Redo functionality of the engine.

Example of serialized entity (spotlight) can look like this:


Entity
SpotLight
Root
Transformation 
-450 90 -40
0 0 0 0
1 1 1
LightComponent
1 1 1 1 1 1 2000 1 0 0.01 256 1 5000 900 
-0.565916 -0.16169 0.808452 0 45 0 0
(
)

Another example (with links to resources) is F.e. this cube:


Entity
cube_Cube_Cube.001_0
cube
Transformation 
0 0 0
0 0 0 0
1 1 1
MaterialComponent
../Data/Shared/Models/Textures/Default_basecolor.tga
../Data/Shared/Models/Textures/Default_normal.tga
../Data/Shared/Models/Textures/Default_metallic.tga
../Data/Shared/Models/textures/Default_roughness.tga
../Data/Shared/Models/Textures/Default_height.tga
MeshComponent
cube_Cube_Cube.001_0
(
)

The names of resources are simply names used in key-value databases (key is name, value is pointer to resource). All resources in project folder are loaded when starting editor - therefore these reference names will always point to correct resources in key-value database for resources (this is assuming project folder matches the one which should be for scene).

Loading

When saving was done with Serialize method, then it makes sense to implement Deserialize method, which takes text as input and initializes everything for given entity based on it. While it is a bit longer, I'm going to paste here while source of it:


void Entity::Deserialize(Scene* scene, const std::string& s, Entity* parent)
{
	std::vector<std::string> lines = String::Split(s, '\n');

	// Read name and store it
	mName = lines[1];

	// We need to prevent creation of entity if it already exists in scene. This is mainly because root entity will be stored,
	// while at the same time it also already exists in scene (it always has to exist).
	if (scene->GetEntity(mName) == nullptr)
	{
		// If we didn't specify parent for deserialize through parameter
		if (parent == nullptr)
		{
			// Get entity from next line, locate it in scene and use it
			Entity* parsedParent = scene->GetEntity(lines[2]);
			parsedParent->AddChild(this);
		}
		else
		{
			// Parent is current node (only when passed through parameter - deserialize for some Undo/Redo cases)
			parent->AddChild(this);
		}
	}

	// Restore transformation, which is composed of 4 lines:
	// 1. keyword - "Transformation"
	// 2. translation X Y Z numbers representing 3D position - "0 0 0"
	// 3. rotation X Y Z W numbers representing quaternion - "0 0 0 1"
	// 4. scale X Y Z numbers - "0 0 0"
	// These 4 lines need to be joined, and passed in as a parameter for Deserialize method into Transform object
	std::vector<std::string> transformData;
	transformData.push_back(lines[3]);
	transformData.push_back(lines[4]);
	transformData.push_back(lines[5]);
	transformData.push_back(lines[6]);
	std::string transform = String::Join(transformData, '\n');
	mTransform.Deserialize(transform);

	// Deserialize each component one by one
	unsigned int lineID = 7;
	while (true)
	{
		if (lineID == lines.size())
		{
			break;
		}

		if (lines[lineID].size() < 1)
		{
			lineID++;
			continue;
		}

		if (lines[lineID][0] == '(')
		{
			break;
		}

		ComponentId compId = ComponentTypeId::Get(lines[lineID]);
		Component* c = ComponentFactory::CreateComponent(compId);

		std::vector<std::string> componentData;
		componentData.push_back(lines[lineID]);

		unsigned int componentEnd = lineID + 1;
		while (true)
		{
			if (componentEnd == lines.size())
			{
				break;
			}

			if (lines[componentEnd].size() < 1)
			{
				componentEnd++;
				continue;
			}

			if (lines[componentEnd][0] == '(')
			{
				break;
			}

			if (lines[componentEnd].find("Component") != std::string::npos)
			{
				break;
			}

			componentData.push_back(lines[componentEnd]);

			componentEnd++;
		}

		std::string component = String::Join(componentData, '\n');
		c->Deserialize(component);

		mObject.AddComponent(compId, c);

		lineID = componentEnd;
	}

	// If at this point we're not at the end yet, it means there are children for the node, parsing
	// those out is a bit tricky - we need to take whole list of entities within node, each into 
	// separate string buffer

	// Children tree depth search (only level 1 has to be taken into account, all other levels have
	// to be included in string buffer for respective entity
	int level = 0;

	// Should we instantiate entity?
	int instEntity = 0;

	// String buffer
	std::vector<std::string> entityData;

	// Note: When deserializing children we can always pass 'this' as parent, simply because due to 
	// the format, we know the hierarchy of the entities
  
	while (true)
	{
		if (lineID == lines.size())
		{
			break;
		}

		if (lines[lineID].size() < 1)
		{
			lineID++;
			continue;
		}

		if (lines[lineID][0] == '(')
		{
			level++;
			if (level != 1)
			{
				entityData.push_back(lines[lineID]);
			}
			lineID++;
			continue;
		}

		if (level == 0)
		{
			break;
		}

		if (lines[lineID][0] == ')')
		{
			level--;
			if (level != 1)
			{
				entityData.push_back(lines[lineID]);
			}
			lineID++;
			continue;
		}

		if (level == 1)
		{
			if (lines[lineID].find("Entity") != std::string::npos)
			{
				if (instEntity == 1)
				{
					Entity* e = new Entity("_TempChild");
					std::string entityString = String::Join(entityData, '\n');
					e->Deserialize(scene, entityString, this);
					entityData.clear();

					unsigned int id = scene->GetIDGenerator()->Next();
					e->mSceneID = id;
					scene->GetSearchMap()->Add(id, e->GetName(), e);
				}

				// Offset line here by 1, in case name contained 'Entity' so we don't double-hit keyword
				instEntity = 1;
				entityData.push_back(lines[lineID]);
				lineID++;
			}

			// Push line
			entityData.push_back(lines[lineID]);
		}
		else
		{
			// Push line
			entityData.push_back(lines[lineID]);
		}

		lineID++;
	}

	// If there is one more, instantiate it
	if (instEntity == 1)
	{
		Entity* e = new Entity("_TempChild");
		std::string entityString = String::Join(entityData, '\n');
		e->Deserialize(scene, entityString, this);
		entityData.clear();

		unsigned int id = scene->GetIDGenerator()->Next();
		e->mSceneID = id;
		scene->GetSearchMap()->Add(id, e->GetName(), e);
	}
}

Which will load whole scene, and instantiate it into current scene graph (before loading scene it is cleared so that only root remains).

Showtime

And of course - save/load system has to be showed out!

 

2 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement

Latest Entries

Merry Christmas!

7701 views

Progress

3731 views

Roadmap for 2020

5267 views

DOOM: Post Mortem

4870 views

DOOM: GROOM

4274 views

DOOM: Placeholders

4846 views

Ludum Dare 45

4579 views
Advertisement