Progress updateSorry for a huge delay with a progress update, I've been very busy, mostly finishing the refactoring stuff and making new things!. And now I can say that for the most part it's complete.
Here's how much stuff I've changed last month!
Yep. That's a lot. This includes new stuff I've made this month, so maybe I wrote lots of negative code.
This was the biggest refactoring and engine improvement I've done ever. I'm very glad I did this, because now I can make stuff faster, easier and there'll be a lot less bugs in the future. Some people prefer to create workarounds for the problems they have, but if I did this, the game would be buggy, the engine would be a pain to work with and my creativity would be very limited by the stuff I coudn't implement because of the engine structure. So, I'm satisfied with most of the stuff I've remade and now I'm working on the game itself most of the time.
What about the game?Here's a gif to get you interested. Not much have changed, but here you can see, that I've created bigger rooms for buildings, made scrolling a lot smoother etc.. The talking animation also stops when the character finishes the part he's talking.
This post will give a brief overview about what I've done and I'll write a more detailed post later.
Separating drawing, input, event handlingThis started when I tried to implement fixed time step. I've realized, that in my game input, drawing and other stuff wasn't separated well enough, so implementing fixed time step wasn't really possible. "SFML Game Development" book gave me some awesome ideas about this and stack of game states, so I did it mostly as described there. This also helped me get rid of lots of global variables. For example, window was a global variable, but now I can pass it in draw function as an argument from the main loop. The main loop looks like this now:
void Game::mainLoop() {
sf::Time delta = mainClock.restart();
timeSinceLastUpdate += delta;
while (timeSinceLastUpdate > TimePerFrame) {
timeSinceLastUpdate -= TimePerFrame;
processEvents();
update(TimePerFrame);
}
draw(window);
}
This is as simple as it can get and it feels great.
Using stack of GameStates helped me fix some bugs and improve code readability a lot.
EventManagerI've talked about this one before, but let's look at it again. Now I can send events like this:
Event event("HpChangedEvent");
event.setData("hp", healthComponent->getHp());
eventManager.queueEvent(event);
Subscribing to events looks like this:
eventManager.addListener(this, "HpChangedEvent", &GUIPlayingState::onHpChanged);
In this example,
GUIPlayingState wants to know when player's hp changed, but other entities can send this event too, so
GUIPlayingState can subscribe to the player entity only:
eventManager.addSubjectToListener(this, "HpChangedEvent", playerEntity);
Now, when the HpChangedEvent is triggered,
onHpChanged callback is called by
EventManager. It looks like this:
void GUIPlayingState::onHpChanged(const Event& event) {
playerHp = event.getData<int>("hp");
... // update GUI!
}
I can also send events from Lua scripts!
event = {}
event.hp = entity:getHp()
event.type = "HpChangedEvent"
queueEvent(event)
Event Lua table is converted to C++ event and then is queued in EventManager. The most cool part is that no matter what sends the event, Lua or C++, both Lua and C++ listeners' callbacks will be called! When the Lua callback is called, I can do stuff like this:
someLuaCallback = function(event)
print("Hp changed to " .. event.data.hp)
end
I previously used
EventType enum for event types, but I now use string hash ids, so I don't have to recompile a lot of stuff when I add a new event. But this raises another question: what if I mistype something? Like this:
Event event("HpChangeEvent"); // should be HpChangedEvent
No problem, I will check the string hash and if it wasn't registered before (all event types are registered during EventManager initialization) nice little
assert will tell me about this (crashing my game which is good, because I'd have to fix the error immediately!). (I would love to have some
static_assert for checking this, but is this even possible?)
Event class is not a base class for all event types and it's great. It looks like this:
class Event {
...
std::map<std::string, EventArg> args;
}
What is
EventArg? It's a cool class which has an
unrestricted union which may contain everything that I need for events:
bool,
int,
float,
string, etc. I can easily cast it to the needed type like this:
auto someInt = eventArg.cast<int>();
args map is easily converted to Lua table and vice versa, which is what makes using events easy with Lua. I'm very satisfied with the event system now.
C++11/14 stuffC++11 makes your code much better. Back when I started making Re:creation, some C++11 features were not known to me or the Visual Studio version I was using didn't support these features. Now I've moved to VS2015 and now most of the cool stuff is available to me. Let's talk about the stuff which is available for most people first:
*
PointersMostly I was using
shared_ptrs everywhere, but then I've realized that I was thinking about smart pointers wrong. Using raw pointers is okay as long as they are non-owning! So, it's better to pass a raw pointer to a function which does something with this pointer, than to pass
unique/
shared_ptr. So, I do it like this:
void someFunc(SomeClass* ptr) {...}
...
std::unique_ptr<SomeClass> ptr;
...
someFunc(ptr.get());
And using
unique_ptr is better than using
shared_ptr most of the time. And it's best to not use pointers at all, so I tried to reduce the number of pointers to minimum and use
unique_ptr where it's needed.
There were some problems with forward declaration, though. This will produce an error:
// A.h
...
class B;
class A {
...
std::unique_ptr<B> ptr;
};
Why? That's because the compiler creates default destructor in the A.h and B is not available yet, so the compiler tells you that it can't know how to delete B!
Here's an easy way to fix this:
// A.h
...
class B;
class A {
public:
~A();
private:
...
std::unique_ptr<B> ptr;
};
// A.cpp
#include "B.h"
A::~A() = default;
...
Good! But this creates another problem when you need move ctor/operator=.
Defining a constructor will prevent move ctor/operator from generation, so you have to declare them for yourself. You can use
=default for them, but VS2013 can't do that (but VS2015 can!)
*
autoThere's no need to explain how cool
auto is. It helped me refactor stuff a lot, because I was using it a lot before, so I didn't have to manually replace a lot of code.
*
for-ranged loopInstead of writing stuff like this:
for(auto it = someStuff.begin(); it != som
eStuff.end(); ++it) {
auto& something = *it;
...
}
I can now write this:
for(auto& something : someStuff) {
...
}
Saves a lot of typing and this is easier to write. Now I wish that stuff like this was possible (it will be possible in C++17, most likely!):
for({someKey, someValue} : someMap) { ... }
*
STL algoritms + lambdasI didn't use algorithms much before, because writing functions and then passing them to another function wasn't fast. Lambdas make it a lot better. Suppose you have to find something which matches some condition. You can do it like this:
auto foundIt = someVector.end();
for(auto it = someVector.begin(); it != someVector.end(); ++it) {
auto& elem = *it;
if(.../*condition*/) {
foundIt = it;
}
}
Or like this:
auto it = std::find_if(someVector.begin(), someVector.end(), [](const SomeClass& elem) { return ...; /*condition*/ });
This is especially awesome when you need to erase something. Previously I had to do it like this:
auto it = someVector.begin();
while(it != someVector.end()) {
if(... /*condition*/) {
it = someVector.erase(it);
} else {
++it;
}
}
You can't just do it in for loop, because erasing something will invalidate some iterators.
But now I can do it like this!
std::erase(std::remove_if(someVector.begin(), someVector.end(),
[](const SomeClass& elem) { return ...; /*condition*/}), someVector.end());
Much better.
One more thing, when you stuff with maps or vectors which store objects with long class names, lambdas can be pretty painful to write, because you have to do stuff like this:
[](const std::pair<KeyClass, ValueClass>& elem) { ... }
Generic lambdas (supported in VS2015 now!) make it a lot easier:
[](const auto& elem) { ...}
Awesome. If you can't use generic lambdas, the best thing I can come up with is this:
[](const decltype(someMap)::value_type& elem) { ... }
Not pretty, but works.
// end of C++11/14 stuff
Const correctnessNot too much to write about this, but I've improved const correctness a lot and this helped me fix a bunch of subtle bugs and will probably save me a lot in the future!
What, there's more?Yeah, I have lots of stuff to write about (mostly about Lua). Lua enums, new cool LuaScriptManager, removing header dependencies (a.k.a. how LuaBridge caused compiler to recompile everything all the time), improving scripts and more! Stay tuned.
I would love to hear your thought about the stuff I've done and the stuff which you do to improve the quality of your code.