For this project we where required to create an engine that would run a self-made demo of the game Gradius IV, the requirements for the engine where it must be 3D DirectX12, use some form of serialization, have an entity structure and have a simple editor. The team included two engine programmers, one graphics programmer and one gameplay programmer. The focus for me and the other engine programmer was to create a simplified engine based partly on Unity.
As I was part of the Engine team I mainly focused on the whole structure and back-end of the engine I also helped a lot afterwards with the editor. My key creation where:
As an especially important system of the engine; serialization was one of the first things I started on. A recommendation by the teachers we used RTTR as the back end for the serialization.
void Serializer::ToJson(const rttr::instance& a_objInst, rapidjson::PrettyWriter<rapidjson::StringBuffer>& a_writer, bool a_recursively)
{
a_writer.StartObject();
const rttr::instance obj = a_objInst.get_type().get_raw_type().is_wrapper() ? a_objInst.get_wrapped_instance() : a_objInst;
//Always add the type to the json file.
a_writer.String(S_TYPE);
const std::string typeName = obj.get_derived_type().get_name().to_string();
a_writer.String(typeName.c_str(), static_cast<rapidjson::SizeType>(typeName.size()));
const auto propList = obj.get_derived_type().get_properties();
for (auto& prop : propList)
{
if (prop.get_metadata(NO_SERIALIZE))
{
continue;
}
const rttr::variant propValue = prop.get_value(obj);
if (!propValue)
{
continue; // cannot serialize, because we cannot retrieve the value
}
const auto name = prop.get_name();
a_writer.String(name.data(), static_cast<rapidjson::SizeType>(name.length()), false);
if (!WriteVariant(propValue, a_writer, a_recursively))
{
HE_CORE_ERROR("Cannot serialize property: {0}", name);
}
}
a_writer.EndObject();
}
//Register Transform
RTTR_REGISTER_COMPONENT(Helios::Transform, S_TRANSFORM_NAME, Helios::Entity&, Helios::ComponentID)
RTTR_PROPERTY(S_TRANSFORM_PROP_LOCAL_POS, Helios::Transform::m_localPosition)
RTTR_PROPERTY(S_TRANSFORM_PROP_LOCAL_ROTATION, Helios::Transform::m_localRotation)
RTTR_PROPERTY_GET_SET_OVERLOAD(S_TRANSFORM_PROP_POS, &Helios::Transform::GetPosition,
RTTR_SELECT_OVERLOAD(void, Helios::Transform::SetPosition, Vec3))(SET_NO_SERIALIZE, SET_NO_DESERIALIZE)
RTTR_PROPERTY_GET_SET_OVERLOAD(S_TRANSFORM_PROP_ROTATION, &Helios::Transform::GetRotation,
RTTR_SELECT_OVERLOAD(void, Helios::Transform::SetRotation, Quaternion))(SET_NO_SERIALIZE, SET_NO_DESERIALIZE)
RTTR_PROPERTY(S_TRANSFORM_PROP_SCALE, Helios::Transform::m_localScale)
RTTR_PROPERTY(S_TRANSFORM_PROP_PARENT, Helios::Transform::m_parentOwnerId)(SET_HIDE_FROM_INSPECTOR);
Two key things where the serializer and the deserializer these were called whenever a scene is getting loaded or saved and it will serialize or deserialize the scene with all the objects in them. As shown above I used RapidJSOn for the writing and reading to the serialization files. These files are getting stored and read from whenever a scene is getting loaded or saved. At this time the serialization is still done in JSON rather than binary due the fact that we were still learning and debugging binary is more annoying then pure JSON.
Part of the editor system was the undoing and redoing of things you did inside the editor. Using the serialization system as the backbone this was much easier achieved then I initially thought. Whenever a change has happened tell the editor history class that a change has happened and whenever you press CTRL+Z go back one step in the history grabbing the serialized data and reset all data to that point of the editor history.
void EditorHistory::AddChange(EditorChange::EChangeType a_type, const std::string& a_oldValue, const std::string& a_newValue)
{
if(m_redoList.size() >= MAX_HISTORY)
{
m_redoList.pop_back();
}
EditorChange editorChange;
editorChange.m_changeType = a_type;
editorChange.m_oldValue = a_oldValue;
editorChange.m_newValue = a_newValue;
m_undoList.push_back(editorChange);
}
void EditorHistory::UndoChange()
{
if (!m_undoList.empty())
{
const EditorChange change = m_undoList.back();
if (change.m_changeType == EditorChange::EChangeType::CHANGE_TYPE_COMPONENT)
{
if (!CanModifyComponent(change.m_oldValue))
{
RemoveComponent(change.m_newValue);
}
}
else if (change.m_changeType == EditorChange::EChangeType::CHANGE_TYPE_ENTITY)
{
if (!CanModifyEntity(change.m_oldValue))
{
RemoveEntity(change.m_newValue);
}
}
m_redoList.push_back(change);
m_undoList.pop_back();
}
}
//First find the unique ID in the serialized string and check if it already exists.
const std::size_t uniqueId = doc.FindMember(S_ENTITY_PROP_ENTITY_ID)->value.GetObjectW().FindMember(S_ENTITYID_PROP_ID)->value.GetInt();
const EntityID entityId(uniqueId);
Entity* foundEntity = EntityManager::GetInstance().GetEntityByUniqueId(entityId);
if (foundEntity == nullptr)
{
//This means we have to create a new entity...
const std::string entityName = doc.FindMember(S_ENTITY_PROP_NAME)->value.GetString();
Entity* entity = EntityManager::GetInstance().CreateEntityDirectly(entityName, SceneManager::GetInstance().GetActiveScene(), false);
Deserializer::DeserializeEntity(*entity, doc);
entity->SetEntityID(entityId);
modified = true;
}
else
{
//It exists! There for we want to only change a or some values.
const std::string oldName = doc.FindMember(S_ENTITY_PROP_NAME)->value.GetString();
const rttr::type e = rttr::type::get(*foundEntity);
const rttr::property prop = e.get_property(S_ENTITY_PROP_NAME);
prop.set_value(*foundEntity, oldName);
modified = true;
}
As shown above calling the AddChange function will allow you to pass in the change type (entity, component, etc.) and the serialized old value and new value. Then whenever you do an undo change (or redo) it will grab that change from the list and checks what you changed. If you changed an entity the code in the image below the top image will execute, checking if an entity needs to be re-created or just values need to be deserialized.
An important feature of the editor system and the way we want to handle entities in the scene is the transform hierarchy. This system is somewhat based on how Unity handles their scene/transform hierarchy, where each entity can have several children and they all move with their parent and each position of the child is relative to that of their parent.
void Transform::SetParent(Transform* a_parent)
{
if (a_parent != nullptr && CanBeParent(*a_parent))
{
//We already have a parent! De-child from the old parent
if (m_parent != nullptr)
{
m_parent->RemoveChild(*this);
}
//If our parent has no parent this means that is the root of our hierarchy.
if (a_parent->GetParent() == nullptr)
{
m_root = a_parent;
}
//Otherwise we ask our parent for the root.
else
{
m_root = a_parent->GetRoot();
}
//Tell any children we have that our root has changed.
RecalculateRoot();
m_parent = a_parent;
m_parentOwnerId = int(a_parent->GetOwner().GetID().Value());
a_parent->AddChild(this);
}
else
{
ResetParent();
}
}
Each transform can have a parent, if their parent is NULL it means the root is the transform itself. If you then set the parent, the root will then be that transform you gave as a parent.
void Transform::SetPosition(Vec3 a_pos)
{
if (m_parent != nullptr)
{
m_localPosition = WorldToLocalMatrix() * Vec4(a_pos, 1.f);
}
else
{
m_localPosition = a_pos;
}
}
Mat4 Transform::WorldToLocalMatrix() const
{
return glm::inverse(LocalToWorldMatrix());
}
Mat4 Transform::LocalToWorldMatrix() const
{
Mat4 world = CalculateModelMatrixColumn();
if (m_parent != nullptr)
{
world = m_parent->LocalToWorldMatrix() * world;
}
return world;
}
Setting the position of a transform is easy by either just moving a Vect3 around or if the transform is a child of another transform grab the world matrix of the child that is the model matrix of the child transform multiplied by the world transform of the parent.
As we took some inspiration from Unity one of the key things Unity has is the Prefab system. Saving entities and their states and loading them back-in allowing you to duplicate the same entity you created in the editor using prefabs as their base.
void PrefabUtility::UpdateClones(const EntityID& updateCloneId, const std::string& a_prefabPath, const std::string& a_prefabMeta)
{
rapidjson::Document doc;
if (doc.Parse(a_prefabMeta.c_str()).HasParseError())
{
HE_CORE_ERROR("Cannot deserialize Prefab");
HE_CORE_ASSERT(false, "Can't deserialize prefab because JSON parse error!");
return;
}
Scene& activeScene = SceneManager::GetInstance().GetActiveScene();
for (auto e : EntityManager::GetInstance().GetAllEntities())
{
//Find an entity that also uses this prefab and no need to update ourselves.
if (e->GetID() != updateCloneId && e->GetPrefabPath() == a_prefabPath)
{
Transform* parent = e->GetTransform().GetParent();
Transform originalTransform = e->GetTransform();
EntityManager::GetInstance().DestroyEntityDirectly(e, activeScene);
Entity* updatedEntity = Deserializer::DeserializePrefab(doc, a_prefabPath);
updatedEntity->GetTransform().SetLocalPosition(originalTransform.GetLocalPosition());
updatedEntity->GetTransform().SetLocalRotation(originalTransform.GetLocalRotation());
updatedEntity->GetTransform().SetScale(originalTransform.GetScale());
//If we had a parent reattach ourselves to it.
if (parent != nullptr)
{
updatedEntity->GetTransform().SetParent(parent);
}
}
}
}
Prefabs in an essence are miniature scenes (it seems Unity has handles it like that) because of the transform hierarchy. Serializing and deserializing a prefab works a bit different then just one entity. When loading in a prefab the prefab gets "cloned" from the original serialized entity updating any of the clones should propagate to the other clones as seen above.
Entity* Deserializer::DeserializePrefab(const rapidjson::Value& a_jsonObj, const std::string& a_prefabPath, bool a_recursively, bool a_setUniqueIds, bool a_createInstant)
{
Entity* prefabEntity;
//This is just one entity with no children so just deserialize it.
const std::string type = a_jsonObj.FindMember(S_TYPE)->value.GetString();
if (type == S_ENTITY_NAME)
{
const std::string entityName = a_jsonObj.FindMember(S_ENTITY_PROP_NAME)->value.GetString();
const bool hasCloneInName = entityName.find("(Clone)") != std::string::npos;
if (a_createInstant)
{
prefabEntity = EntityManager::GetInstance().CreateEntityDirectly(entityName + (hasCloneInName ? "" : " (Clone)"),
SceneManager::GetInstance().GetActiveScene(), false);
}
else
{
prefabEntity = EntityManager::GetInstance().CreateEntity(entityName + (hasCloneInName ? "" : " (Clone)"), false);
}
DeserializeEntity(*prefabEntity, a_jsonObj, a_recursively);
}
else
{
//This entity has children, threat it as a scene, deserialize everything than set parents.
prefabEntity = DeserializeEntityScene(a_jsonObj, a_recursively, a_setUniqueIds, a_createInstant);
}
prefabEntity->m_hasPrefab = true;
prefabEntity->m_prefabPath = a_prefabPath;
return prefabEntity;
}
When deserializing a prefab a key thing is that prefabs can have children due the transform hierarchy otherwise deserialization works normally. If the prefab has children it becomes like a scene where you would loop recursively through the entities children and re-attach them to their parents recreating the scene in essence, this is what DeserializeEntityScene does.
A key component of the whole UI is how it works, using ImGUI we tried to create a simple version of the Unity UI with our own twist. Initially created by the other programmer I reworked the system in such a way that it can handle multiple windows for the editor with docking included (as ImGUI supports that).
bool ComponentEditor::GenerateUIElementVector3(const rttr::instance& a_obj, const rttr::property& a_prop, int a_index, bool a_doSerialize) const
{
ImGui::PushID(a_index);
rttr::variant varProp = a_prop.get_value(a_obj);
const Vec3 v3 = varProp.get_value<Vec3>();
ImGui::Text("%s", a_prop.get_name().to_string().c_str());
float array1[3] = { v3.x,v3.y, v3.z };
float array2[3] = { v3.x,v3.y, v3.z };
ImGui::InputFloat3("#X|Y|Z", array1, 3);
if (ImGui::IsItemDeactivatedAfterEdit())
{
const Vec3 old = Vec3(array2[0], array2[1], array2[2]);
Vec3 current = Vec3(array1[0], array1[1], array1[2]);
if (current != old)
{
const std::string oldValue = Serializer::Serialize(a_obj, true);
a_prop.set_value(a_obj, current);
const std::string newValue = Serializer::Serialize(a_obj, true);
if (a_doSerialize)
{
EditorHistory::AddChange(EditorHistory::EditorChange::EChangeType::CHANGE_TYPE_COMPONENT, oldValue, newValue);
};
ImGui::PopID();
return true;
}
}
ImGui::PopID();
return false;
}
The main window called ComponentEditor would allow you to tweak components on selected entities to draw the component functions I created a system that allows us to easily extend with new functions like the one shown above. GenerateUIElementVector3 would simply draw 3 editable squares that allows you to set the x, y, z coordinates of the vector variable, this is used for example by the transform component allowing you to move the entity around by editing the numbers.
The main part of the start of the engine was creating a solid structure for us to work on but fast enough to allow bot the gameplay and graphics programmer to also start working on stuff, I looked at some examples and my main inspiration was TheCherno.
A core part of the whole engine was the Entity System where most other teams tried to create a Data-Oriented Entity Component System I stuck to an Object-Oriented style Entity Component Model. Where each entity has components and a transform instead of it all being separated and neatly data aligned in memory.
Entity* EntityManager::CreateEntity(const std::string& a_name, bool a_addTransform)
{
std::size_t availableSpot;
if (!m_availableSpots.empty())
{
availableSpot = m_availableSpots.top();
m_availableSpots.pop();
}
else
{
availableSpot = m_numIndices;
m_numIndices++;
}
EntityID uniqueId(m_numCreatedEntities++);
m_entityAddQueue.push(std::make_unique(uniqueId, availableSpot, a_name));
if (a_addTransform)
{
m_entityAddQueue.back()->AddComponent();
}
m_allEntities.push_back(m_entityAddQueue.back().get());
return m_entityAddQueue.back().get();
}
Entities are created through the Entity Manager which is a global service allowing you to create entities whenever you want. When Creating an entity, it is queued allowing the resources to be loaded and to make sure that nothing is referencing it while it is being loaded as soon as the first frame comes the entity is pushed on the active entity list and remove from the queue. The same happens with removing when removing an entity, it is pushed on a remove queue waiting for the end of the frame before remove all marked entities to ensure safety for references.