Project description
This is a 1 year C++ game project I've done with 3 other students.
Unlike last project, we had to implement our own 2d renderer with OpenGL and of course, we built our own game engine with C++.
I was the technical lead of this team so I was in charge of the architecture of our engine and what kind of libraries we should adopt if needed.
Engine Core
This is the logical structure of our engine. 6 individual system components and a level where all the game objects and components are stored. There are scenes in a level, objects in a scene, components in an object. Classic and simple but good enough for our game. Since our game didn't really have that many objects, I chose to keep it simple rather than over-engineering for cache friendly structure like ECS system.
After configuring all the specs for our engine, I distributed Logger, Asset management, Input, ImGUI, Audio system to my teammates for implementation and I worked on Renderer and Level, Scene, Object, Component and the engine flow.
I made the object and component functions as virtual functions so we can simply inherit them and implement gameplay logics. Also classic but powerful. But there is a reason why they are named IObject and IComponent. Our game's objects and components doesn't directly inherit from IObject and IComponent but inherit from Object<T> and Component<T> classes and the reason will be described later with map loading feature.
Renderer
namespace ggm
{
class Texture;
class Font;
class Renderer
{
public:
Renderer() = delete;
struct Vertex
{
glm::vec2 pos;
glm::vec2 textureCoord;
};
struct Model
{
unsigned int vaoID;
unsigned int vboID;
unsigned int eboID;
unsigned int primitiveType;
unsigned int drawCount;
void ReleaseResource();
};
enum class RectMode
{
CENTER,
BOTTOM_LEFT_CORNER,
BOTTOM_RIGHT_CORNER,
TOP_LEFT_CORNER,
TOP_RIGHT_CORNER
};
enum class TextureMode
{
CENTER,
BOTTOM_LEFT_CORNER,
BOTTOM_RIGHT_CORNER,
TOP_LEFT_CORNER,
TOP_RIGHT_CORNER
};
enum class TextMode
{
CENTER,
LEFT,
RIGHT
};
struct ViewportInfo
{
int x;
int y;
int width;
int height;
};
struct Settings
{
glm::mat3 scaleMatrix{ 1 };
glm::mat3 rotationMatrix{ 1 };
glm::mat3 translateMatrix{ 1 };
glm::vec4 fillColor{0,0,0,1};
glm::vec4 lineColor{ 0,0,0,1 };
glm::vec4 textColor{ 0,0,0,1 };
glm::vec4 backgroundColor{ 1,1,1,1 };
RectMode rectMode = RectMode::CENTER;
TextureMode textureMode = TextureMode::CENTER;
TextMode textMode = TextMode::LEFT;
ViewportInfo viewportInfo{ 0,0,static_cast<int>(WINDOW_WIDTH),static_cast<int>(WINDOW_HEIGHT)};
float fontScale = 1;
};
struct TextBoxInfo
{
float yOffset = 0;
glm::vec2 size{0,0};
};
public:
/*!
* \brief Initialize Renderer resources.(Models, Shaders)
*/
static void Init();
/*!
* \brief Release Renderer resources.
*/
static void CleanUp();
/*!
* \brief Update Renderer.
* @param dt delta time in second.
*/
static void Update(double dt);
/*!
* \breif Push current drawing settings.
*/
static void PushSettings();
/*!
* \brief Pop previous drawing settings.
*/
static void PopSettings();
static void SetViewport(int leftBottomX,int leftBottomY,int width,int height);
static void SetTextScale(float scale);
/*!
* \brief Accumulate rotation offset.
* @param rotOffset Rotation offset in radian.
*/
static void ApplyRotation(float rotOffset);
static void ApplyScale(float sx, float sy);
static void ApplyTranslation(float dx, float dy);
/*!
* \brief Set primitive fill color.
* @param r,g,b,a color components 0 ~ 255.
*/
static void SetFillColor(unsigned r, unsigned g, unsigned b, unsigned a = 255);
static void SetBackgroundColor(unsigned r, unsigned g, unsigned b, unsigned a = 255);
static void SetLineColor(unsigned r, unsigned g, unsigned b, unsigned a = 255);
static void SetTextColor(unsigned r, unsigned g, unsigned b, unsigned a = 255);
static void SetRectMode(RectMode mode);
static void SetTextureMode(TextureMode mode);
static void SetTextMode(TextMode mode);
static void DrawEllipse(float x, float y, float width, float height, float zOrder = 0.f);
static void DrawRectangle(float x, float y, float width, float height, float zOrder = 0.f);
static void DrawTexture(float x, float y, int width, int height,const Texture& texture, float zOrder = 0.f);
static void DrawTexture(float x, float y, const Texture& texture, float zOrder = 0.f);
static void DrawLine(float x1, float y1, float x2, float y2, float zOrder = 0.f);
static void DrawFontText(float leftBottomX, float leftBottomY, const std::string& text, const Font& font, float zOrder = 0.f);
static void DrawFontText(float leftBottomX, float leftBottomY, const std::string& text, float zOrder = 0.f);
static void DrawFontTextAutoNewLine(float beginningLeftBottomX,float beginningLeftBottomY, float width, const std::string& text,const Font& font, float zOrder = 0.f);
static void DrawFontTextAutoNewLine(float beginningLeftBottomX, float beginningLeftBottomY, float width, const std::string& text, float zOrder = 0.f);
static TextBoxInfo MeasureTextBox(const std::string& text,const Font& font);
static TextBoxInfo MeasureTextBox(const std::string& text);
static glm::mat3 GetViewProjectionMatrix();
inline static Camera camera;
private:
static glm::mat3 GetWorldMatrix(const glm::vec2& scale, const glm::vec2& translate);
static void createCircleModel();
static void createRectModel();
static void createTextureModel();
static void createLineModel();
static void createTextModel();
private:
inline static Model circleModel;
inline static Model rectModel;
inline static Model textureModel;
inline static Model lineModel;
inline static Model textModel;
inline static Shader textureDrawShader;
inline static Shader primitiveDrawShader;
inline static Shader textDrawShader;
inline static Font* pDefaultFont = nullptr;
inline static Settings currentSetting;
inline static std::stack<Settings> prevSettings;
};
}
This is the header file of our 2D renderer. It has following features:
● Push/Pop drawing setting
● Set viewport
● Set font scale
● Apply scale / rotation / translation
● Set fill / background / line / text color
● Set rect / texture mode (setting at which point of the rectangle / texture should be the position of the rectangle)
● Draw ellipse / rectangle / texture / line / text
● Auto new line text drawing
● Measuring text box size
Very intuitive and simple to use.
If you want to draw something, simply push the drawing setting and call drawing function for the shape you want.
when you finish drawing whatever, just pop the setting and it returns the settings back where it was before.
UI editor
As we finished developing our engine, now it was time to write actual gameplay logics for our game.
We quickly tried prototyping our game and soon realized our game needs good amount of UI objects.
Since hard coding the coordinates and properties for UI objects sounded very inefficient to me I decided to create an UI editor that allows to conveniently form an UI object just like we do in modern UI editor programs. Something like winform and Qt creator but simpler yet good enough for our game.
This is the initial screen of our editor.
You can select what kind of component you want to add to your UI object in this section.
Pressing "Add Component" button will instantly add a component to your UI object.
When you add a component, it will show up on the screen like this. I added a button component for example.
There are multiple properties you can set. For texture, you can set it by typing a name of any texture from the "Textures" folder. For callbacks, you can set the callback function by putting the name of the C++ function you registered in the Lua virtual machine table.
After finish forming a nice looking UI, you can click the save button and it will save as a JSON format file.
Loading
Our game rely on Lua virtual machine for loading feature. There is a C++ library called sol, Lua wrapper API.
The way we used to load game objects were by writing conditional statement to handle different types of objects.
something ike...this
switch(objName)
{
case: HashedString("Goblin")
scene.spawnObject<Goblin>();
case: HashedString("Magician")
scene.spawnObject<Magician>();
So on and so forth...
}
It did the job and wasn't terrible but there is a problem with method.
Whenever we write new objects, we have to add new codes to handle them and it's so annoying and easy to forget.
So I decided to automate this process with the combination of template class, Lua virtual machine and rttr library, which enables reflection feature for C++ code.
This is how our Object<T> class looks. How I did the trick was using the constructor. Since it's a template class taking ObjectType as a template parameter, Whenever you write a new Object class and inherit from this class, it will call the constructor of objectLoaderRegisterer class and it looks like this.
As you see in the picture, it registeres a function to the Lua virtual machine with the name "Load+Class type name".
It has a potential issue as described in comment but didn't matter to our project since it was a college project and we have no plan to build this game with other compiler on different platforms other than windows.
Registered functions get called when loading the scene and successfully spawn objects in the scene.
void Game::LoadUIObjects()
{
GGM_INFO("Loading UIObjects");
gTypeUIObjectInfoMap.clear();
for(auto& file : std::filesystem::directory_iterator("UIObjects"))
{
const std::filesystem::path& path = file.path();
std::ifstream is(path);
rapidjson::IStreamWrapper stream(is);
rapidjson::Document jsonFile;
jsonFile.ParseStream(stream);
if(jsonFile.HasParseError())
{
GGM_CORE_ERROR("file {0} is corrupted.", path.string());
return;
}
std::string objectTypeString = jsonFile["Type"].GetString();
HashedString objectType = HashedString(objectTypeString.c_str());
UIObjectInfo& objectInfo = gTypeUIObjectInfoMap[objectType.Hash()];
objectInfo.mType = objectType;
const rapidjson::Value& components = jsonFile["Components"].GetArray();
for (auto componentIt = components.Begin(); componentIt != components.End(); ++componentIt)
{
const rapidjson::Value& component = *componentIt;
std::string componentTypeName = component["Type"].GetString();
rttr::type componentType = rttr::type::get_by_name(componentTypeName.c_str());
if(componentType.is_valid() == false)
{
GGM_CORE_ERROR("Failed to get component {}'s type by rttr.", componentTypeName);
}
HashedString componentID(component["Id"].GetString());
objectInfo.mUIComponentInfos.emplace_back(new UIComponentInfo{ componentTypeName, componentID });
UIComponentInfo& componentInfo = *objectInfo.mUIComponentInfos.back();
const rapidjson::Value& properties = component["Properties"];
for(rapidjson::Value::ConstMemberIterator propertyIt = properties.MemberBegin(); propertyIt != properties.MemberEnd(); ++propertyIt)
{
std::string propertyName = propertyIt->name.GetString();
unsigned int propertyType = gPropertyTypeMap.at(propertyName);
rapidjson::Value propertyValue;
std::function<bool(UIComponent*, rttr::property&, rapidjson::Value&)> propertySetterCallback;
switch(propertyType)
{
case TYPE_INT:
propertyValue = propertyIt->value.GetInt();
propertySetterCallback = [](UIComponent* pComponent, rttr::property& property, rapidjson::Value& value)
{
return property.set_value(*pComponent, value.GetInt());
};
break;
case TYPE_UNSIGNED_INT:
propertyValue = propertyIt->value.GetUint();
propertySetterCallback = [](UIComponent* pComponent, rttr::property& property, rapidjson::Value& value)
{
return property.set_value(*pComponent, value.GetUint());
};
break;
case TYPE_DOUBLE:
propertyValue = propertyIt->value.GetDouble();
propertySetterCallback = [](UIComponent* pComponent, rttr::property& property, rapidjson::Value& value)
{
return property.set_value(*pComponent, value.GetDouble());
};
break;
case TYPE_FLOAT:
propertyValue = propertyIt->value.GetFloat();
propertySetterCallback = [](UIComponent* pComponent, rttr::property& property, rapidjson::Value& value)
{
return property.set_value(*pComponent, value.GetFloat());
};
break;
case TYPE_STRING:
{
auto strPropertySetterCallback = [](UIComponent* pComponent, rttr::property& property, std::string& value)
{
return property.set_value(*pComponent, value);
};
componentInfo.mStringProperties.emplace_back(componentType.get_property(propertyName.c_str()), propertyIt->value.GetString(), strPropertySetterCallback);
continue;
}
case TYPE_BOOL:
propertyValue = propertyIt->value.GetBool();
propertySetterCallback = [](UIComponent* pComponent, rttr::property& property, rapidjson::Value& value)
{
return property.set_value(*pComponent, value.GetBool());
};
break;
default:
GGM_CORE_ERROR("Property {}'s type is unknown.",propertyName);
break;
}
componentInfo.mProperties.emplace_back( componentType.get_property(propertyName.c_str()),std::move(propertyValue),propertySetterCallback);
}
}
std::string bgName(jsonFile["Background Name"].GetString());
int bgWidth = jsonFile["Background Width"].GetInt();
int bgHeight = jsonFile["Background Height"].GetInt();
auto UIObjectFactory = [&objectInfo, bgName, bgWidth, bgHeight](Level* pLevel,UIObject* pParent = nullptr)
{
UIObject* pObject = pLevel->CreateObject<UIObject>(objectInfo.mType, pParent);
if (bgName != "")
{
pObject->SetBackground(bgName, bgWidth, bgHeight);
}
for(auto& componentInfo : objectInfo.mUIComponentInfos)
{
UIComponent* pComponent = gUIComponentFactoryMap["Create" + componentInfo->mComponentType](pLevel,pObject, componentInfo->mId);
for(auto& propertyInfo : componentInfo->mProperties)
{
rttr::property& property = std::get<0>(propertyInfo);
rapidjson::Value& propertyValue = std::get<1>(propertyInfo);
auto propertySetterCallback = std::get<2>(propertyInfo);
bool bPropertyValueSetSuccess = propertySetterCallback(pComponent,property,propertyValue);
if(bPropertyValueSetSuccess == false)
{
GGM_CORE_ERROR("Failed to set component property, {}'s value.", property.get_name());
}
}
for (auto& strPropertyInfo : componentInfo->mStringProperties)
{
rttr::property& property = std::get<0>(strPropertyInfo);
std::string& propertyValue = std::get<1>(strPropertyInfo);
auto propertySetterCallback = std::get<2>(strPropertyInfo);
bool bPropertyValueSetSuccess = propertySetterCallback(pComponent, property, propertyValue);
if (bPropertyValueSetSuccess == false)
{
GGM_CORE_ERROR("Failed to set component property, {}'s value.", property.get_name());
}
}
pComponent->SetupAfterLoad();
}
return pObject;
};
LUA_VM.set_function("Load" + objectTypeString,UIObjectFactory);
}
}
Loading UI object was much more complicated since I had to read the values of the properties from the JSON file and load them to the components. To put it simple, what I did was
1. Loop through JSON file and extract the object and component information
2. Loop through component information and fill in component information struct in a vector
3. Build a lambda function that creates object, create & attach component in the object and fills the component property values passed along the information structure and register it on the Lua virtual machine.
I had so much fun building this pipeline and it was very cool to see my teammates utilizing my tool to create UIs about 10 times faster then they used to, manually. This project really taught me the joy of creating a tool and winding up Lua, rttr, JSON libraries to build this pipline felt like solving a very satisfying puzzle.