Been busy a couple of days. Really happy with the response this has got!
I hope you follow some formatting/naming convention since then (because you haven't mentioned it explicitly, or I'm just this tired
).
Yeah, I've developed one based off engines I've worked with the past, like torque and phaser. Right now I always:
- Put everything in camel case (i.e. likeThis)
- Never use '_' in class names
I don't tend to prefix, though. That would actually make things a lot easier with intellisense detection being effectively filtered and such.
Actually, I'm going to start writing a section on
Conventions for each engine now, too.
I do worry that in games, we are entering an age where middleware consolidation and the advent of big engines pricing appropriately for indies means there are now only three or four viable engines. A custom engine will cost more than buying one, and you can't hope to make up the hundreds of man-years of effort that have gone into Unreal or Unity.
Maybe we shouldn't let that stop us.
I've tried things like unity and unreal a couple of times and I have to say I like them a lot, even if I find them a little hard to understand. But there's always something for me about booting up a game and realizing it's unity (unity config dialog on startup, unity simplified options menus, unity fullscreen flicker on some computers etc) that takes away the novelty a bit for me. No disrespect to anyone who uses proprietary engines, though.
Anyway! I said I'd post about my next game engine, so here goes:
VOLT 2D(The space you see here is part of the images - they're in their raw format, uncropped)
Github Link:
https://github.com/Memory-Melt/VoltEngineDespite having abandoned VOLT 2D before even finishing the game I was making (i.e. starting over), I quite liked this one.
It started off inheriting a few things from VOLTILES. It was originally intended to be another 2D tile engine, like VOLTILES 1, but I ended up trying something more - more specifically, I ended up making the terrain 3D. One thing I remember about it
really well, however, is that it had this cool feature where you could export the maps it generated as huge images, like the ones you see above.
DesignVOLT was made for a large, amazing, show stopping, hugely overambitious game called Delvinator. Are you seeing a pattern?
While it was still entirely C++ (i.e. no integrated scripting engine, no JSON configs etc) it had some clever dynamic features:
- Like last time, it had a dynamic asset manager.
- It had a dynamic input manager with bindable keys
- It had a scene system, and inheritance capability for new kinds of scene class; the tileScene pictures you see above are from one such inheritance. And the scene class was truly abstract this time - not just a collection of virtuals to force C++ to let me stick hugely different objects in a single array.
Unfortunately, it had a big problem. VOLT still used traditional OOP design. physicsObject inherits staticObject, staticObject inherits object, etc. And the amount of times I had to go back and make huge design changes due to bad design decisions that had to be rectified meant that development was unbearably slow. Ultimately, it was the cause of it stopping altogether.
I also had a lot of trouble getting the right objects to be able to access each other, for world collision etc. Eventually, I had to just throw out the rules altogether. Here's the massive hunk of code I had to add to physicsObject (an engine object) just to handle collisions with tileScene (a supposedly game-specific object, only required and used for delvinator, that as a result of this is now part of the engine):
// COLLISION SOLVER FOR PHYSICSOBJECTS
int possibleCollisions = getParent()->getObjectCount();
// check
sf::FloatRect nextXBounds(bounds);
nextXBounds.left += vel.x;
sf::FloatRect nextYBounds(bounds);
nextYBounds.top += vel.y;
sf::FloatRect nextBounds(nextXBounds.left, nextYBounds.top, nextXBounds.width, nextYBounds.height);
sf::Vector2f nextPos(nextXBounds.left, nextYBounds.top);
bool doXCollision = false;
eng::game::gameObject* colObjX = NULL;
bool doYCollision = false;
eng::game::gameObject* colObjY = NULL;
delve::tileScene* tileWorld = dynamic_cast<delve::tileScene*>(getParent());
// delvinator tile world collision check
if (tileWorld != NULL && !ignoreWorldCollisions) {
sf::Vector2f renderObjectPos, renderObjectTile;
renderObjectPos.x = bounds.left + (bounds.width / 2.0f);
renderObjectPos.y = bounds.top + bounds.height;
renderObjectTile.x = floor(renderObjectPos.x / 32.0f);
renderObjectTile.y = floor(renderObjectPos.y / 32.0f);
// north
sf::FloatRect northTileRect((renderObjectTile.x - 1) * 32, (renderObjectTile.y - 1) * 32, 96, 32);
bool northTileFlag = tileWorld->getTileInBounds(sf::Vector2i(renderObjectTile.x, renderObjectTile.y - 1));
if (northTileRect.contains(nextPos) && !northTileFlag) {
if (vel.y < 0) { // we're checking a position, not a rect, and the player can glue themselves to the wall
doYCollision = true;
}
}
// south
sf::FloatRect southTileRect((renderObjectTile.x - 1) * 32, (renderObjectTile.y + 1) * 32, 96, 32);
bool southTileFlag = tileWorld->getTileInBounds(sf::Vector2i(renderObjectTile.x, renderObjectTile.y + 1));
if (southTileRect.intersects(nextYBounds) && !southTileFlag) {
doYCollision = true;
}
// east
sf::FloatRect eastTileRect((renderObjectTile.x + 1) * 32, (renderObjectTile.y - 1) * 32, 32, 96);
bool eastTileFlag = tileWorld->getTileInBounds(sf::Vector2i(renderObjectTile.x + 1, renderObjectTile.y));
if (eastTileRect.intersects(nextXBounds) && !eastTileFlag) {
doXCollision = true;
}
// west
sf::FloatRect westTileRect((renderObjectTile.x - 1) * 32, (renderObjectTile.y - 1) * 32, 32, 96);
bool westTileFlag = tileWorld->getTileInBounds(sf::Vector2i(renderObjectTile.x - 1, renderObjectTile.y));
if (westTileRect.intersects(nextXBounds) && !westTileFlag) {
doXCollision = true;
}
}
Essentially, that peice of code is the reason that, if you download the GitHub files and compile them into a project, it won't work. I'm sure I made other changes to the base engine to get my game's objects to work, because frankly, seperating game and engine was
always a bad idea and always is. I hate to say this, but in this instance, VOLTILES 1 did it better. At least that engine knew what it wanted to be, and what sort of game was supposed to run in it. VOLT 2D had
no idea.
Let's go back to "Make games, not engines" for a minute, because I feel this example encapsulates its idea best. When I made VOLTILES 1, I made it for the delvinator prototype, and only that. When I made VOLT, I was just making a game engine for the sake of making a game engine. It was far into development when I actually came up with the idea of remaking delvinator. And I'll stress, right now; don't do that.
If you're making a game engine, the key thing I've learned is know what your vision needs and build the engine like that. That way, you'll make decisions based on game design, rather than engine design. You'll sometimes end up with something messy, yes, but at least you won't end up with VOLT - an engine I scrapped because working with it was a nightmare on so many levels. Inheritance levels. Yeah. I made that joke.
Memory management in VOLT was sloppy, too. I created new instances constantly when I could have saved time and memory with pointers, causing the engine to run notably slower during draw cycles and especially when rendering loading screens. As in, not having a loading screen render when loading something would make it load 5x faster.
Conventions- Camel case.
- It was acceptable to register multiple object types in one header file when I made VOLT. I never did this before VOLT or did it again afterwards.
- I used default arguments wherever possible, something I never did in VOLTILES 1.
- I used sfml vectors instead of x and y to avoid the VOLTILES fiasco I forgot to mention where all positions had to be stored as sfml vectors yet split into "int x, int y" to be passed around.
- Destructors were never used unless they were absolutely needed.
AssetsVOLT implemented (slightly weird) concept I came up with for assets.
The asset management in VOLT (i.e. the "res" namespace, because I had a real thing for namespaces back in the day) consisted of two classes. One was the resourceManager, which was a basic class that simply loaded in, as sfml primitives,
every image and sound it found in the game's folders. All of them, into the memory. Until the program was closed. Even the ones that might not ever actually be used by the game. No caching in VOLT. It also loaded a single font. Once this was done, the actual
assetManager would then process them.
The actual assets, however, were not processed based on a file this time. Since I was smarter now, and had already made the decision to write both engine and game in C++, assets were now loaded into the game like this:
// required
assetMngr->registerSprite("gameLogo", "delvinator.png");
assetMngr->registerSprite("loading0", "loading0.png");
assetMngr->registerSprite("loading1", "loading1.png");
assetMngr->registerSprite("loading2", "loading2.png");
// GUI
assetMngr->registerSprite("beginGUI", "menuBegin.png", sf::Vector2i(256, 64), sf::Vector2i(1, 3));
assetMngr->registerSprite("resumeGUI", "menuResume.png", sf::Vector2i(256, 64), sf::Vector2i(1, 3));
assetMngr->registerSprite("optionsGUI", "menuOptions.png", sf::Vector2i(192, 48), sf::Vector2i(1, 3));
assetMngr->registerSprite("creditsGUI", "menuCredits.png", sf::Vector2i(192, 48), sf::Vector2i(1, 3));
assetMngr->registerSprite("wikiGUI", "menuWiki.png", sf::Vector2i(192, 48), sf::Vector2i(1, 3));
assetMngr->registerSprite("quitGUI", "menuQuit.png", sf::Vector2i(192, 48), sf::Vector2i(1, 3));
assetMngr->registerSprite("backGUI", "menuBack.png", sf::Vector2i(128, 32), sf::Vector2i(1, 3));
assetMngr->registerSprite("spGUI", "menuSingleplayer.png", sf::Vector2i(342, 48), sf::Vector2i(1, 3));
assetMngr->registerSprite("mpGUI", "menuMultiplayer.png", sf::Vector2i(342, 48), sf::Vector2i(1, 3));
assetMngr->registerSprite("joinGUI", "menuJoin.png", sf::Vector2i(342, 48), sf::Vector2i(1, 3));
// Tile
assetMngr->registerSprite("topTiles", "floors.png", sf::Vector2i(16, 16), sf::Vector2i(8, 8));
assetMngr->registerSprite("rimTiles", "rims.png", sf::Vector2i(16, 16), sf::Vector2i(4, 4));
assetMngr->registerSprite("sideTiles", "walls.png", sf::Vector2i(16, 8), sf::Vector2i(4, 4));
// Object
assetMngr->registerSprite("playerBase", "playerBase.png", sf::Vector2i(24, 24), sf::Vector2i(9, 3));
assetMngr->registerSprite("portalNorth", "northwalk.png", sf::Vector2i(16, 16), sf::Vector2i(1, 2));
assetMngr->registerSprite("portalSouth", "southwalk.png", sf::Vector2i(16, 16), sf::Vector2i(1, 2));
assetMngr->registerSprite("portalEast", "eastwalk.png", sf::Vector2i(16, 16), sf::Vector2i(1, 2));
assetMngr->registerSprite("portalWest", "westwalk.png", sf::Vector2i(16, 16), sf::Vector2i(1, 2));
// Misc
assetMngr->registerSprite("menuBG0", "menubg0.png");
assetMngr->registerSprite("menuBG1", "menubg1.png");
assetMngr->registerSprite("menuBG2", "menubg2.png");
// Music
assetMngr->registerSFX("menuLoop", "AllThis.wav", false, true);
// SFX
assetMngr->buildExplicitSprite("loading0"); // do this first because it's needed for the loading screen
if (serverMode) {
assetMngr->buildAll(NULL, "loading0"); // this will draw a loading bar for us automatically
}
else {
assetMngr->buildAll(window, "loading0"); // this will draw a loading bar for us automatically
}
Notice how, while this new design has the same, register first, load all at once later idea as VOLTILES' asset manager, we can now force the assetManager to initialize (build and split into spritesheets) a specific sprite before any others. Since VOLT had a built in class just for loading screens, in this case we're loading in the sprite that the asset manager will display when it automatically shows one.
There is still a filesystem based aspect to this, however. See, the sprites have certain metadata that would have been kind of difficult, and messy, to add into the engine. VOLT, unlike VOLTILES, had its own sprite class capable of displaying animations at specific framerates so that the object didn't have to contain extremely consise render code again (because we weren't about to repeat THAT fiasco) So, for storing the animations of sprites, I created the .anim file format:
IDLE2 1_0
IDLE1 1_3
IDLE0 1_6
IDLE7 1_9
IDLE6 1_12
IDLE3 1_15
IDLE4 1_18
IDLE5 1_21
MOVE2 4_0_1_2_1
MOVE1 4_3_4_5_4
MOVE0 4_6_7_8_7
MOVE7 4_9_10_11_10
MOVE6 4_12_13_14_13
MOVE3 4_15_16_17_16
MOVE4 4_18_19_20_19
MOVE5 4_21_22_23_22
DEAD 1_24
DIE 4_24_25_26
Notice how it's string based, with both " " and "_" serving as delimiters. A lot of stuff in VOLT was string based - including certain things that really shouldn't have been. But we'll get to that later.
SFX in volt also had its own class. It wrapped the SFML classes required for audio into a single class and even added support for some things that were incredibly fiddly without it, including looping and cool pitch randomization each time the sound was played.
VOLT, however, still didn't give any real love to fonts. The only game font (which could be changed only by swapping the file in the "base/data" directory) was always loaded from its incredibly specific place where it had to be (the game would do a controlled crash if it couldn't find it) and held in the resourceManager. Which meant that every object that wanted to display text had to reference the resourceManager instead of the assetManager. Which was stupid.
RenderingIn VOLT, rendering was handled neither by any dedicated or "renderer" class, or the objects themselves. Rendering was handled by sprites - the object would "update" its sprite, and then when the time came to draw the sprite would pass down the texture that was to be displayed, already constructed and ready to display in the window, so that the object could do so. While I think this was a pretty good way to do it, I later found that component-based design had an even better answer. But we'll get to that another time.
Game ObjectsI've probably mentioned enough on this already in my design section, but I'll elaborate a little more.
In VOLTILES, I didn't really think Game Objects through. I just sort of built what I needed when I needed it. In VOLT, however, they absolutely
had to follow a very specific set of rules:
- They always had to have a position, a rotation, bounds, and a string that defined what the object was, so that it could be identified. Yes, that was how bad the inheritance model made things. Objects had a string attached, by default, just so you could tell what type they were and therefore what you could do with them.
- They always had to have the functions draw, update, and setupExternal, where the majority of init was done.
setupExternal was probably a weird/bad decision primarily because it meant I initialized the object BEFORE letting it know where to find its friends. It caused a few debug issues in the early days, too.
The interface, on the other hand, was a lot more abstract this time. Here's the full class definition; notice how few virtuals there are this time, compared to VOLTILES 1's fiasco:
class gameObject {
public:
gameObject(std::string namea, sf::Vector2f pos = sf::Vector2f(0, 0), int rot = 0);
void setPosition(sf::Vector2f pos);
void setRotation(int rot);
void setBoundingBox(sf::FloatRect Nbounds);
sf::FloatRect getBoundingBox();
sf::Vector2f getPosition();
float getRotation();
// used only for net
float getLastRotation();
sf::Vector2f getLastPosition();
bool hasCollision(sf::FloatRect checkBounds);
std::string getName();
void rename(std::string newName);
void passParent(voltScene* scn);
voltScene* getParent();
std::string getType();
// overload functions for gameObject
virtual void draw(sf::RenderWindow* toWindow, sf::Vector2f offset) = 0;
virtual void update() = 0;
virtual void setupExternal() {} // called when the object is added to the game scene.
void selfDestruct();
protected:
sf::Vector2f position;
sf::Vector2f lastPosition;
sf::FloatRect bounds;
float rotation;
float lastRotation;
std::string type = "N/A";
private:
std::string name;
voltScene* parent;
};
WorldAs I said before, the world in VOLT was stored by the scene class, which could be derived to add extra functionality. The scene class belonged to the gameManager class, which belonged to the core class. Inheritance was something I really nailed in VOLT.
Notably, there was no mass-sprite creating fiasco this time. The world was rendered as a single texture which was constructed beforehand - nothing that wasn't onscreen was drawn. In fact, the drawing was so optimized in Delvinator's tileScene class that only the parts of the array the camera was over were even iterated through. Maths always helps.
Continued on next page...