During this project we were tasked with creating a production ready voxel engine. The goal was to make an engine that could be used by other disciplines to create a game. The engine is meant to be cross-platform, running on both PC and PS5. Meaning we had to keep in mind the limitations of PC and PS5 hardware.
My main role for this project was to be the engine & tools programmer. Some features I developed:
Below I will go over some of these features in more detail.
In previous projects everyone used C++ to program the engine and the game. The downside of programming the game logic in C++ is that it requires a recompile every time you want to test a change. Additionally, not many designers are familiar with C++. It was my task to implement a scripting system that would allow us to write game logic without having to recompile the engine.
It started out simple, with just being able to move an entity.
Simple movement script
But expanded to more complex demos. One of which is a Teardown-like destruction demo.
Teardown destruction demo
MY main goal was to make the scripting system as scalable and user-friendly as possible. I wanted to there to be no limit to what designers could do with the scripting.
Below I will go into more technical details on how I achieved this.
One of the most important things in coding might be variables. Being able to store and retrieve data is crucial. Without them, it's hard to make games. The scripting offered a way of making variables in a class/script.
Example variables in script.
But whenever you want to test something you don't want to recompile the scripts. So what I added was a way to change variables in the editor. You can mark a variable with [editable] and it would show up in the inspector. This would give users more control over the scripts without having to change the code.
Variables marked with [editable]
Inspector showing the variables
It's all fun and games if you can change the variables of a script in the editor. But if you can't save them, it's useless. So I added a way to serialize the properties of a script. It's basically the same way I do with the inspector. But instead of marking a variable with [editable], you mark it with [serialize].
Variables marked with [serialize]
Serialized variables in a file
As a programmer one of the most important tools might just be the debugger. Being able to see what's going on in your code is crucial. Seeing the values of variables, the call stack, and being able to step through the code is a must-have.
So I added a way to attach a debugger to any running script in the engine. This was a huge step for the usability of the scripting system.
Stepping through the code when mouse is clicked
A big role this block for me was the level editor. I was the main engine & tools guy. The focus for me this block was User Experience. With previous blocks, the only person that used the engine was me. So I could get away with a lot of things. as I knew how everything worked.
But since the goal of this project was to get it to the next block for the artists and designers. I had to make sure everything was as easy as possible. And feels good to use.
Below I will go over some of the features I implemented.
To allow designers to experiment with the level design I added an undo/redo system. This would allow designers to quickly iterate on their designs without having to worry about making mistakes. This is essential for a good user experience.
Undo/Redo system in action
The code for the undo/redo system is quite simple. It heavily relies on serialization. Below is the main class I use that allows me to undo/redo any action.
template <typename T>
class DiffUtil : public IDiffOperation {
public:
DiffUtil() = default;
void take_snapshot(T* value) {
target = value;
m_snapshot_before = JsonSerializer::serialize(*value);
}
void commit_changes(const std::string commit_message) {
m_snapshot_after = JsonSerializer::serialize(*target);
auto op = std::make_shared>(*this);
Engine.undo_redo_manager().add_operation(op, commit_message);
};
virtual void undo() override {
if (target) {
JsonDeserializer::deserialize(m_snapshot_before, *target);
}
};
virtual void redo() override {
if (target) {
JsonDeserializer::deserialize(m_snapshot_after, *target);
}
};
private:
std::string m_snapshot_before;
std::string m_snapshot_after;
T* target;
};
Then when the user makes a change in the editor, I Would call something like this:
Transform& t = Engine.ecs().registry.get<Transform>(entity); // get the transform of the entity
glm::vec3 translation(t.GetTranslation()); // get a copy of the translation
if (DragFloat3("Position", translation, 0.01f)) { // if the user changes the copy
DiffUtil<Transform> diff_util;
diff_util.take_snapshot(&t); // take a snapshot of the original transform
t.SetTranslation(translation); // set the new translation
diff_util.commit_changes("Set Translation"); // commit the changes
}
Being able to quickly test your level is crucial. So I added a way to play and stop the game without having to open/close the game.
To make it easier to create levels I added a way to duplicate entities.
Being able to copy/paste components is very handy. This can save a lot of time when creating levels.
Move, Rotate, Scale gizmos are essential for level design. Including their respective shortcuts.
To make sure everything aligns correctly I added a way to snap entities to a grid when holding ctrl.
Smaller features that improve the editor overall, making it more user-friendly. And giving the designers and artists a better experience.