Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411489 Posts in 69371 Topics- by 58428 Members - Latest Member: shelton786

April 24, 2024, 03:23:44 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Let's make game engines - the thread
Pages: [1] 2
Print
Author Topic: Let's make game engines - the thread  (Read 5388 times)
Sixmorphugus
Level 0
***


._.


View Profile
« on: May 13, 2015, 08:28:46 AM »

This will also be a blog post on my site, www.memorymelt.net, when I get wordpress up and running. Give me some hits!

So I've realized that in my time as an (fairly novice) indie developer (and creator of 2 unfinished games now! gosh!) I've put together a lot of technology for my own use. I thought I'd share it with you.

I should mention that while I have heard of the "make games, not engines" philosophy, I've never been one to adhere to it. THE HARD WAY, DAMN IT.

Being entirely self-taught (I hope to get into a university where they teach C++ soon, even if I have to completely relearn the language to get rid of my own bad habits) and often having only my own concepts and ideas to rely on - as well as theories from the internet, including here - my designs and ideas have evolved pretty predictably. I only just started implementing component-based design, for example, as well as an outer scripting interface and package-based asset management.

So yeah. This is a thread where not only am I going to share with you not only past mistakes engines I have made, but also my newest developments and designs. As often as I can I'll post new progress and discoveries and would love to hear your ideas and feedback. For each engine, I will detail how they manage assets, rendering, game objects, the world and networking. I will also refer to the design of each.

I'll start by posting the first engine I ever made, at the age of 15.

VOLTILES 1


It is terrible.

I don't know why I put the "1" in the name. It implies I intend to make another in its image, which, while possibly true at the time, most certainly isn't now.

Github link: https://github.com/Sixsmorphugus/Old-Delvinator-Engine

Design
VOLTILES 1 was made for a small, experimental game called Delvinator. Despite possible interpretations of what I said before, I do tend to go by the "make games not engines" philosophy. To a point. I think I tried to make VOLTILES 1 reusable in most parts - I made asset loading dynamic, even though you have to hard code all object asset usage, and I tried to make it fancy. I tried to break new ground before I'd broken anything - the closest thing I'd made to an engine in the past was the dodgy state machine used by my first entirely-C++ game, Battle Blimps.

What resulted was a bit of a mess. In fact, what resulted was a lot of a mess. Just how much of a mess Delvinator's engine (I call it that instead of VOLTILES 1 because it can only really be used for the only game that can be produced with it) was, I will happily go into detail on.

Assets
For all the flak I give it, VOLTILES' asset management is pretty good. With one problem. But we'll get to that later.

VOLTILES loads everything on startup, based off the contents of a file that looks like this:
Quote
ASSET DATA FILE
DO NOT EDIT!

FONTS
minecraftia
minecraftia

SPRITEGRIDS BEGIN
TitleSprite
122
41
1
1
SPRITE
PixelSprite
1
1
1
1
SPRITE
WizardPlayer
24
24
9
5
SPRITE
...etc
SPRITEGRIDS END

SOUNDS BEGIN
startupSound
0
projectileHitWall
1
projectileSpawn
1
SOUNDS END
NOTE: edited down because original was freakishly long

Why I made VOLTILES 1 load assets like this remains a mystery to me, primarily because there was NEVER any point in adding dynamic asset loading to an engine where you have to basically hard code every object and the named sprites it uses. It's like if a restaurant let customers ask for any burger they want - any burger at all - and then served everyone the same burger. Hey, I'm not good at metaphors.

There's also the small matter of caching in VOLTILES 1. VOLTILES 1 holds every asset in the RAM for the entirety of runtime. So no caching. Yeah.

Rendering
VOLTILES 1 has no object that itself acts as a renderer. Nor does it have sprite assignment for objects. It especially doesn't have any kind of fixed pipeline for rendering.

So how does VOLTILES 1 render? Simple. All objects render themselves. With raw SFML code. Using the RenderWindow object the Core exposes.
Code:
rootObject->window->draw(mySprite);

Since SFML is such a simple library, this is... tolerable. In fact, most indie-made game engines use a method similar to this - though usually, they'll handle rendering in a way more gracious than:
- An engine that must supply the level offset (NOT camera offset, there's no camera!) to the object so that it can etch itself directly onto the window canvas depending on draw order (which is just creation order!).
- Objects that, in order to show animations, usually need hard-coded "tickers" to know when to change sprite, because there's no hard coded sprite manager to handle animation
- A system that doesn't allow any visual effects to be applied (glow, tint, etc) for more than a single frame without a lot of hard coding.

And then there's that problem I mentioned earlier. The problem that, during runtime, EVERY asset is held in the RAM, permanently. No caching, no grouping, no optimization, no package system.

Game Objects
As well as having to fully render themselves, game objects in VOLTILES were not component based. This perfectly exemplifies one of the main pitfalls I've learned of traditional OOP game design. Everything started fine. The player had move functions, could update, etcetera. But then disaster struck. Real disaster. Because as I added more to the game, the object itself had to get bigger, like the blob from that 80s horror flick. Was it the 80s? I think it was the 80s.

Play sounds? Modify the object to play the sounds held in the asset manager. Collision? Modify the object to read world tiles from the world object. Health? Modify the object. AI? Pathfinding? Add it all to the object. NONE of this will ever all be used at once, but it's all there, in the update function. Which is around 300 lines long alone.

Not to mention that I started with the idea of abstract game objects, but ended up with a blob of code that was basically just interface. Look at this:
Code:
class gameObject
{
public:
gameObject(void);
~gameObject(void);

// virtual stuff
// shared
virtual void drawAreaObject(int levelOffsetX, int levelOffsetY) {}
virtual void drawObject() {}
virtual void Update() {}

virtual void GoTo(int x, int y) {}
virtual void Move(int dir) {}
virtual void MoveCenter(int x, int y) {}

virtual sf::Vector2i GetPosition() { return sf::Vector2i(0, 0); }
virtual sf::Vector2i GetDrawPosition() { return sf::Vector2i(0, 0); }
virtual sf::IntRect GetBoundingBox() { return sf::IntRect(0, 0, 0, 0); }

virtual int GetSpeed(void) { return 0; }
virtual void SetSpeed(int speed) { }

virtual int getControlType(void) { return 0; }
virtual int getDrawType(void) { return 0; }
virtual int getUpdateType(void) { return 0; }

// players and other physical objects
virtual int getFacingDirection(void) { return 0; }
virtual void respawn(void) {}
virtual void setSpawnPos(int x, int y) {}

virtual int getHealth() { return 0; }
virtual void setHealth(int healthToSet) {}
virtual void incHealth(int healthToAdd) {}

// camera object
virtual void GoToCenter(int x, int y) {}
virtual void setMoveLimits(int x, int y) {}
virtual sf::Vector2i GetTileOffset() { return sf::Vector2i(0, 0); }
virtual sf::IntRect GetTileBounds() { return sf::IntRect(0, 0, 0, 0); }

// GUI
virtual bool pressedRecently(void) { return 0; }
virtual bool getInert(void) { return 0; }
virtual bool animating(void) { return 0; }
virtual void setInert(bool inert) {}

virtual void setPercentFilled(int percentFilledSet) {}
virtual void setPercentFilledNoAnimation(int percentFilledSet) {}

virtual void addText(std::string text, int offsetx, int offsety, int charSize, sf::Font* font, sf::Color colorwhenclicked) {}

// gameObject stuff
void passRootObject(rootObject* obj);
void passStringName(std::string name);

std::string getStringName();
protected:
rootObject* rootObject;
std::string stringName;
};


World
The world class in VOLTILES 1 was alright. It was based off an internet tutorial I found at the time. Did I mention making this engine was how I learned the SFML libary? As in, I started with no SFML knowledge? Just gonna mention that.

It was a fairly ok implementation of world rendering, with a few minor issues. And by minor issues, I mean huge, glaring, dominating flaws.

Take a look at this:
Code:
//A 2D array of Tile Sprites
std::vector<std::vector<sf::Sprite*>> mapTiles;
For those of you who aren't femilliar with both C++ and SFML, let me explain. When VOLTILES 1 loaded maps, it did not grab textures from the memory, print them onto a larger texture (like my later, better engines I'll post about here at some point did) and render that whole texture to the screen. What it did was render hundreds of spritesheet tiles manually to the window. One at a time. Individually. In sequence. Single file. The only optimization (made so badly that you can occasionally see it failing at the edges of the screen) is that only the sprites that the "camera" is over (i.e. only the sprites on the screen) are actually rendered to the window, and everything else is ignored.

But still. Let's say you're a modern basement dweller and you have a 1080p monitor. Say 1980x1080. Map tiles were drawn at 16x16 in Delvinator and scaled up to 64x64. 1980/64 is roughly 31, 1080/64 is roughly 17. 31x17 is 527. VOLTILES 1 is sending 527 unique, unpackaged, unoptimized draw calls to the SFML window every frame. From my point of view, this is bad design. Imagine the original 16x16 sprites were used. That's 8432 draw calls per frame.

The other problem is the same blob problem from before. I originally made the world (area) object just to hold the tile array and the (completely unneeded) collection of uncombined sprites that make it up. But did I stick to that functionality? Oh no. I added pathfinding to the world object. I added shadowing to the world object. I added whatever I wanted because none of you can stop me I was slighly misguided in my attempts to program dynamically with a static language.

Networking
VOLTILES 1 didn't have networking. I found a way to screw that up later. But that's another post...

I'm writing these little rants blogs not only for my own amusement, but also to help other people - prospective programmers and developers of engines alike - not only understand the technical limitations and ideas I have worked with, but also to help them not fall into the same pits as me. Not that they shouldn't. Making mistakes while coding is part of being a creator. It can be fun, and it can be infuriating. Occasionally, it's inspiring. Sometimes, it's hilarious.

That's all from me now. Expect more posts in the near future!
« Last Edit: August 16, 2015, 10:04:41 AM by Chrisbot6 » Logged

There's no place like 127.0.0.1.
Cheezmeister
Level 3
***



View Profile
« Reply #1 on: May 13, 2015, 08:51:14 PM »

While it's true the "make games, not engines" philosophy is not for everyone, the alternative should be "make games and engines", not "make engines nobody uses". Unless, and it's a big unless, unless this is what truly brings you joy; and if it is, I won't judge. For most of us, though, actually finishing a thing, no matter how crappy, is like landing the last blow on that piñata. Think of the candy! Don't give up!

I look forward to seeing this thread evolve.
Logged

෴Me෴ @chzmstr | www.luchenlabs.com ቒMadeቓ RA | Nextris | Chromathud   ᙍMakingᙌCheezus II (Devlog)
InfiniteStateMachine
Level 10
*****



View Profile
« Reply #2 on: May 13, 2015, 09:37:33 PM »

While it's true the "make games, not engines" philosophy is not for everyone, the alternative should be "make games and engines", not "make engines nobody uses". 
I look forward to seeing this thread evolve.


This. I felt I grow as a developer the most when I keep a careful balance between making my own tech and using external tech/engines.

Looking forward to it too

I do have to ask though, did you actually end up overriding all those virtual functions? Or were some of virtual declarations perhaps too presumptuous?
Logged

Sixmorphugus
Level 0
***


._.


View Profile
« Reply #3 on: May 13, 2015, 10:28:04 PM »

While it's true the "make games, not engines" philosophy is not for everyone, the alternative should be "make games and engines", not "make engines nobody uses". 
I look forward to seeing this thread evolve.


This. I felt I grow as a developer the most when I keep a careful balance between making my own tech and using external tech/engines.

Looking forward to it too

I do have to ask though, did you actually end up overriding all those virtual functions? Or were some of virtual declarations perhaps too presumptuous?
Thanks!

And yeah, all of those were overridden. It was a classic case of functionality bloat, and definitely a learning experience.
Logged

There's no place like 127.0.0.1.
oahda
Level 10
*****



View Profile
« Reply #4 on: May 13, 2015, 11:35:42 PM »

Saying you were 15 when you made the first one gives us no sense of perspective unless you tell us how many years it's been since. Tongue

Anyway, I highly recommend codeveloping a game alongside the engine and you'll really catch where things work well and when they do not along the way instead of later.
Logged

Sixmorphugus
Level 0
***


._.


View Profile
« Reply #5 on: May 13, 2015, 11:57:03 PM »

Saying you were 15 when you made the first one gives us no sense of perspective unless you tell us how many years it's been since. Tongue

Anyway, I highly recommend codeveloping a game alongside the engine and you'll really catch where things work well and when they do not along the way instead of later.
I'm 17 now.

I usually develop engines with a specific game in mind. There's an exception to this, but I'll post about that soonish  Tongue
Logged

There's no place like 127.0.0.1.
kamac
Level 10
*****


Notoriously edits his posts


View Profile
« Reply #6 on: May 14, 2015, 12:06:44 PM »

I hope you follow some formatting/naming convention since then (because you haven't mentioned it explicitly, or I'm just this tired Angry). You name some of the functions starting with capital letter, others lowercase.

Not only this, but it varies from one ruleset to another. I myself follow this one:
-) Method/Function names start with uppercase, _ is forbidden
-) Start each class/struct name with an uppercase letter, _ is forbidden
-) Start each argument variable name with the "a" prefix
-) Start each pointer variable name with the "p" prefix
-) Start each constant variable name with the "c" prefix
-) Start each member (protected or private) variable name with the "m_" prefix
-) Start each static variable name with the "s_" prefix
-) Start each global variable name with the "g_" prefix

So, for example:

Code:
class MyClass {
public:
MyClass();
~MyClass();

void SomeMethod(int aCount, MyClass* apOtherClass);

private:
MyClass* m_pOtherClass;
int m_count;
}

You could also look at the way CryEngine / Unreal Engine / Other Mayor Engine name their stuff. (I don't recommend UE's naming convention though!)

Sorry if you already know this.
Logged

J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #7 on: May 14, 2015, 12:30:16 PM »

Start each member (protected or private) variable name with the "m_" prefix
Why that overhead?
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
kamac
Level 10
*****


Notoriously edits his posts


View Profile
« Reply #8 on: May 14, 2015, 12:42:08 PM »

Because when intellisense gives you a list of member variables, you can tell which ones to access and which ones not to access without having to look at and memorize the icons (which vary between IDEs). Besides that, it's mostly convenience and the code is more clear for somebody who reads your class, and isn't familiar with your class' variables.
Logged

Allen Chou
Level 0
**



View Profile WWW
« Reply #9 on: May 14, 2015, 02:37:22 PM »

Start each member (protected or private) variable name with the "m_" prefix
Why that overhead?

The m_ (member), s_ (static), g_ (global), k_ (constant) prefixes (or variations) are code-hint-friendly and are adopted by many.
By typing the prefixes, you essentially filter the entries shown by code hint. This allows you to quickly locate the desired entry in the list.

Also, this makes your code more readable by letting people easily understand whether a variable is a member, static, global, or constant. Variables without prefixes are local (or some other exceptions that I can't think of off the top of my head).

P.S. Don't ask me why the prefix for constant is k_ instead of c_. I have no idea. Shrug
Logged

J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #10 on: May 15, 2015, 03:34:12 AM »

Ok, mind sharing what convention you use at Naughty Dog?
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
edmundmk
Level 0
**


View Profile
« Reply #11 on: May 15, 2015, 03:37:29 AM »

Good topic!  Great to see some technical details of your projects - very interesting.  I look forward to your grapples with networking.  Wink

My advice would be that the naming convention is the least important part of programming.  If we're recommending 'make games, not engines', then I would add 'make engines, not coding standards documents'.  It's nice to be consistent, but for personal projects just pick a naming convention that suits you.  Mine has changed with each project.

If you're writing game engines in C++ with inheritance and dynamic loading at 15 then you'll probably find that if you go to University, you'll be way ahead on a lot of things, 'bad habits' or no.

Myself, I am another one with the engine disease.  I have to compromise with other people's code constantly in my day job.  So in my free time, I indulge my not-invented-here syndrome, probably too much!  Years of effort making nice code and only one released game...

Shrug

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.  Grin

Logged
InfiniteStateMachine
Level 10
*****



View Profile
« Reply #12 on: May 15, 2015, 07:55:31 AM »

I'm a big fan of m/s member prefixing in C++ because unlike java/c# you can have data that doesn't reside in a class.

In C# I dont find the need to do it though.
Logged

Layl
Level 3
***

professional jerkface


View Profile WWW
« Reply #13 on: May 15, 2015, 07:56:47 AM »

In C# I tend to use _ instead of m_ because C# doesn't reserve names starting with _. Lately I've been using Rust more though, where all members can only be accessed through "self" anyways.
Logged
Allen Chou
Level 0
**



View Profile WWW
« Reply #14 on: May 15, 2015, 03:51:45 PM »

Ok, mind sharing what convention you use at Naughty Dog?

We use:
m_ (member), s_ (static), g_ (global), k (constant).
Logged

J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #15 on: May 15, 2015, 04:27:03 PM »

Since a class usually consists of members what about the following change in convention: locals get a prefix but members don't.
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
Boreal
Level 6
*


Reinventing the wheel


View Profile
« Reply #16 on: May 16, 2015, 07:37:45 AM »

I like to think of it in terms of classifying side effects and their "blast radius".  No prefix (local)?  No problems there.  m?  Probably not that bad.  s?  I don't like this.  g?  Avoid at all costs.

Not using prefixes is preferable, so it subtly encourages you to write non-effectful functions Smiley
Logged

"In software, the only numbers of significance are 0, 1, and N." - Josh Barczak

magma - Reconstructed Mantle API
oahda
Level 10
*****



View Profile
« Reply #17 on: May 16, 2015, 08:29:09 AM »

I only use prefixes because I've adopted a jQuery-style interface as I've mentioned before, where setters and getters have the same name with a different overload for each, which is generally simply the name of the attribute. m_ for members and c_ for class variables (statics, for which Allen Chou mentioned s_).

I don't prefix anything else and I didn't prefix stuff back when I still used a different naming convention for the interfaces.

I generally don't prefix stuff for small projects either (my jam code always looks like a mess). Or for Unity games where I'm not making the interface myself anyway, I guess? I probably need to clean my Unity game up...
Logged

RandyGaul
Level 1
*

~~~


View Profile WWW
« Reply #18 on: May 16, 2015, 09:51:09 AM »

Ok, mind sharing what convention you use at Naughty Dog?

We use:
m_ (member), s_ (static), g_ (global), k (constant).

This seems to be pretty popular around the Santa Monica area Smiley
Logged
Sixmorphugus
Level 0
***


._.


View Profile
« Reply #19 on: May 17, 2015, 10:29:35 AM »

Been busy a couple of days. Really happy with the response this has got!  Gentleman

I hope you follow some formatting/naming convention since then (because you haven't mentioned it explicitly, or I'm just this tired Angry).
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.  Cheesy

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.  Grin
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/VoltEngine

Despite 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.

Design
VOLT 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):
Code:
                // 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.

Assets
VOLT 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:
Code:
// 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:
Code:
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.

Rendering
In 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 Objects
I'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:
Code:
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;
};

World
As 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...
Logged

There's no place like 127.0.0.1.
Pages: [1] 2
Print
Jump to:  

Theme orange-lt created by panic