|
blundis
|
 |
« on: March 18, 2010, 01:46:05 AM » |
|
I tend to get an idea for a great game, and then immediately start throwing something together (code-wise). Most of the time I end up not finishing the game but that's another topic.. I thought I'd ask you guys how you usually arrange things and see if there's a better way than what I'm currently doing. I inevitably start up with the standard get-a-window-up main function à la: int main() { handle_events();
clear_screen(); draw_screen(); }
Then most of the times I'll move on to the player. The player gets his own class, with an update and draw method that I call from main. After this it's either time for the "world" or enemies. Most of the time it's the world, so I'll add the world class. Same thing as for player, update and draw methods that get called from the main function. But now I realize the player needs to know some data that's inside the world, like collision data, since the player is handling it's own movement inside it's update method, so I end up passing in a reference of the world to the player. This is where things start to go south.. Now I add enemies. Basically like the World class, except split into two classes, Enemy and EnemyManager. EnemyManager gets updated and drawn from main function and in turn updates all the enemies. But of course the player needs to be able to shoot the enemies. So a reference to the EnemyManager gets passed into player. After this I start refactoring (hmm, this code in player is replicated in enemy, should the inherit from a parent GameObject class?), most of the time end up breaking something bad or get bored with the whole thing and go watch some TV. I guess this is the way most people do it, but I don't know, keeping references to every single thing in the game with the player just seems dirty somehow. So tell me, is there a better way, a truer path? How do you guys normally set things up?
|
|
|
|
|
Logged
|
|
|
|
|
st33d
Guest
|
 |
« Reply #1 on: March 18, 2010, 02:52:09 AM » |
|
References to the player is normal. But it's a lot cleaner and flexible if you abstract what the player is.
The player is simply a set of coordinates in most cases. So if you have a class that is a set of coordinates, have the player extend from that class. Then your monster is reacting to a coordinate object.
Why is this good?
Because the monster can be sent to seek out any coordinate on the map, to do scripted actions or regroup with other monsters.
I currently have a Character class for the player, his minions and all the monsters. It's now simple to add a lot of behaviours to any of them, because they all follow the basic template. But they also inherit from the coordinate class. So when the camera is focusing on the player, it's just tracking a coordinate object - I could send it to track anything.
|
|
|
|
|
Logged
|
|
|
|
|
blundis
|
 |
« Reply #2 on: March 18, 2010, 03:22:44 AM » |
|
Abstracting, and by doing that being able to reuse code by treating all objects the same, is a great idea and something I try to do (although I normally bodge everything together first and then end up redoing it). But that doesn't change the fact that you need to pass references to all the different things you need access to, like level data and such. The other way that I thought of doing it (and have tried) is putting the main game loop in a class, and creating all objects from there. Then you pass along a back-pointer into the object so it can do stuff like (this example could be in the player class): parent->world->collision(x, y);
The problem then is you lose encapsulation. The Player object needs to know things about the internal state of the game class. A lot of people seem to be saying just do it, if it works it works and not care about breaking encapsulation and what not and I agree to a point, but I'm still interested in knowing if there are better ways of doing this. Anyone want to show examples of how you normally do it?
|
|
|
|
|
Logged
|
|
|
|
|
jotapeh
|
 |
« Reply #3 on: March 18, 2010, 04:38:18 AM » |
|
So tell me, is there a better way, a truer path? How do you guys normally set things up?
'Better' is subjective. I'll lend you a little insight into how I do it, you can judge for yourself what you think of the mess upstairs.Anyway, vague mind maps aside, the idea for me is generally that the Player object is an entity, just like Enemies. And in fact, a lot of things are entities that don't even move, like trees, doors, treasure chests, etc. In most cases, the Player object has no idea what is happening in the world. Nor does he need to. He knows what he 'wants' to do by user input. So within the 'behaviour component' of each entity, they basically mark down stuff they 'want' to do. AI can be given a reference to, for example, the Player coordinates (as steed said,) but do not give them free reign to the whole Player object unless you have a good reason. Usually for pathing all you need is Player coords and the relevant chunk of terrain. After that, the game loop iterates through each behaviour component and decides what is permitted. This includes the player, who may be mashing 'up' to walk into a wall, but he's still not allowed to. sorry! Handling collisions could be done by the World class, or you could do it with the current game loop. It's a comparison between two very different types of objects so yes, some encapsulation will seem to be flouted. For my entities, they all carry a 'collision component', which is their position and width/height. This allows me to create a generic function for collisions vs. terrain, and it applies to everything, enemies, the player, flying projectiles, etc. Obviously there's a lot of ways you can take this but if you're a stickler for encapsulation and DRY, that's one of the cleaner ways I've found. My 2¢
|
|
|
|
« Last Edit: March 18, 2010, 04:41:31 AM by jotapeh »
|
Logged
|
|
|
|
|
Average Software
|
 |
« Reply #4 on: March 18, 2010, 05:03:20 AM » |
|
My approach has always been to create a sort of state machine. Each state inherits from an abstract type called Logic, which has a abstract method called Process. Process does two things, it does whatever needs to happen in that state, and provides a reference to the next state (usually itself). I then create concrete types for all the major states of the game, things like TitleScreen, Game, Ending, Introduction, Credits... etc. The main loop just keeps calling Process and adjusts the current state accordingly. function Main_Loop return Integer is begin Tickables.Calculate_Delta;
Renderer.Draw;
declare Next_State: Logic_Access; begin Current_State.Process(Next_State);
if Next_State /= Current_State then Free(Current_State); Current_State := Next_State;
if Current_State = null then return 0; end if; end if; end;
return 1;
exception when Error: others => Front_End.Error_Box(New_String(Exception_Message(Error))); Front_End.Die;
return 0; end Main_Loop; Now, the game state actually "owns" everything in the game. The players, the enemies, the world, and so on. It's responsible for checking collisions, telling things they were hit, and so on. This way, everything is in one place and I don't have to pass references all over the damn place. This project is still very early, and I just have the players, but it ends up looking like this: package Logics.Game is type State is new Logic with private;
overriding procedure Initialize(This: in out State); overriding procedure Finalize(This: in out State); overriding procedure Process(This: in out State; Next: out Logic_Access);
private use FPS_Counters; use Font_Manager; use Heroes;
type State is new Logic with record FPS: FPS_Counter; FPS_Font: Font_ID; Torr: aliased Hero(Heroes.Torr); Tarra: aliased Hero(Heroes.Tarra); Current_Hero: access Hero; end record; end Logics.Game; You may be over object-orienting things, and that may be the source of your problems. Like someone else said, your in-game entities should basically know what they want to do, and the overarching system that owns them will tell them what actually happened, and they will react to that. The player ideally shouldn't know anything about enemies or items or the world or anything else. That responsibility lies elsewhere.
|
|
|
|
|
Logged
|
|
|
|
|
george
|
 |
« Reply #5 on: March 18, 2010, 07:20:43 AM » |
|
The problem then is you lose encapsulation. The Player object needs to know things about the internal state of the game class.
In my opinion the goal of encapsulation is not to prevent things from knowing about other thing's state, but simply to provide common interfaces with which to do so. So don't get so hung up on passing references or hiding private state, as long as you're creating coherent and common interfaces to get that information. Putting everything in a 'game' state is OK but it really isn't much different from making everything global to the program. Why have the game state at all? I think a game state is better off as its own thing along with other objects being their own thing. Remember that code objects and game objects need not map one to one -- there doesn't need to be a single Player object which is the same for all purposes as the player in game. Anyway that's basically what I've worked out for myself on the subject.
|
|
|
|
|
Logged
|
|
|
|
|
nikki
|
 |
« Reply #6 on: March 18, 2010, 07:47:40 AM » |
|
write to an interface instead of an implementation !!
Is something smart i heard once, considering the connecting of data.
|
|
|
|
|
Logged
|
|
|
|
|
jpetrie
|
 |
« Reply #7 on: March 18, 2010, 08:02:46 AM » |
|
There's a lot to say on this subject; entire books have been written about writing well-structured code (see Code Complete) or about the fundamental principles of OO (single responsibility, open-closed, liskov's substitution, et cetera).
At the end of the day though, probably the most important thing to learn is the ability to refactor your code: to be able to change, generally in small incremental steps, with the goal of improving the code while keeping it as functional as possible. Once you are comfortable with this technique, worrying about "good ways to structure X" become less of a hinderance to your ability to actually get things done, because you know that if you commit to a direction that turns out to be suboptimal a few hours in, you can always refactor it.
This is basically a fancy way to say "do it, and learn from your mistakes." The more you do that, the better you will become at conceptualizing mostly-correct designs in the first place. You also end up growing your reusable code base into something far more robust.
That said, some kind of 'state based' system for tracking gameplay is pretty common, and should be a reasonable starting point. How you implement that is largely up to what you feel most comfortable with, although I would personally caution against extensive use of globals and/or singletons in the implementation because they can make refactoring harder (especially since, due to their promiscuous nature, they can force you to touch more code at once than you might otherwise have to to impart some change, and you generally want to make small changes whenever possible).
|
|
|
|
|
Logged
|
|
|
|
|
st33d
Guest
|
 |
« Reply #8 on: March 18, 2010, 08:16:26 AM » |
|
I'd say the best way that works for me is to plow through making that hacky mess with loads of cross-referenced crap. Make sure it works. Then I start tidying up and encapsulating things that no longer need global access for debugging. It's not how I often do stuff at work, because it means a lot of rewriting that I don't have time for. But at home it means I'm developing a rather smart framework - which I can then take into work...
|
|
|
|
|
Logged
|
|
|
|
|
blundis
|
 |
« Reply #9 on: March 18, 2010, 09:02:12 AM » |
|
Thanks for some great input! It seems this is stuff that can't really be imparted until it's been experienced but still nice to have some input on it. In the end I think I'll just do it the way I've always done it (a.k.a. the st33d method), but I guess just thinking about these things might let me write better code. I'd say the best way that works for me is to plow through making that hacky mess with loads of cross-referenced crap. Make sure it works. Then I start tidying up and encapsulating things that no longer need global access for debugging.
|
|
|
|
|
Logged
|
|
|
|
|
David Pittman
|
 |
« Reply #10 on: March 18, 2010, 09:06:01 AM » |
|
At a very high level, my data is structured such that everything is accessible through a reference to the Game object. The Game class, besides containing the main loop logic and such, is pretty much just an encapsulation of global data. I don't really mind that--I don't believe it's a problem if globally-used data is globally accessible.
Game |-Clock |-Input devices |-Renderer |-Misc. other subsystems |-Player Entity* (special case) |-World |-Array<Mesh*> |-Array<Entity*> |-Array<Sector> |-Array<Mesh*> |-Array<Entity*> |-NavMesh |-kd-Tree |-Array<Mesh*>
Everything generally has a way to get to the Game object, and then everything else is reachable from there. The Player entity is the only entity that is specifically referenced by the Game, simply because there is so much player-centric code that it's faster to grab the player that way than by iterating the entity array to find a player.
I have a few redundant arrays, as depicted in that tree. Three separate classes maintain arrays of static geometry meshes, each with a different purpose: the world, which ultimately owns everything and is responsible for allocating and freeing the memory for those meshes; the sectors, which have a list of all their contained meshes for rendering; and the kd-trees, which store mesh pointers in their nodes for collision detection. Likewise, the world has a flat array of all the entities, while sectors also contain a subset of their local physical entities for collision detection.
|
|
|
|
|
Logged
|
|
|
|
|
Skomakar'n
|
 |
« Reply #11 on: March 18, 2010, 09:44:39 AM » |
|
I approach it a bit like st33d and you do, Blundis.
For example, I am currently working on a side-scrolling platformer. I think the first thing I did was a base class for things that should have a position, and from that, I derived a more advanced class for objects that should also be rotated, scaled or have their centers moved. Then, believe it or not, I actually made the class for one of the planned power-ups, healthbar and stuff like that. Before, I had obviously also created stuff for displaying images.
After that, I began to approach some of the more heavy stuff. In about a week's time, I had my trigonometric collision checking done, put in another superclass. After this, I was able to finally start working on the actual platformer engine, which I made in a cluttered manner inside of main(). When I had it working (character movement and ground collision as well as jumping and falling), I started wrapping it up and turning it into a proper framework with a couple of classes. Later on, I added the slope support, and I'm currently doing a lot of changes; I'm splitting up and abstracting stuff even better, such as creating a base class for physical objects, out of code that previously resided within the character class, I'm reworking the physics engine a lot, making it far more advanced, as well as generalising the code a lot more with new classes and type definitions in case we want to port it to a platform where we won't be able to use the same library.
Long story short; during the entire development, I use main.cpp as a testing ground and eventually move the code from there into new classes, and eventually, I will be able to make the file just a few lines long, in time for the final thing. I constantly update, move and refactor code to give it a better structure.
In one sentence, I start simple and ugly, and from time to time, I refine and enhance.
|
|
|
|
« Last Edit: March 18, 2010, 09:49:51 AM by Skomakar'n »
|
Logged
|
|
|
|
|
jotapeh
|
 |
« Reply #12 on: March 18, 2010, 09:56:06 AM » |
|
Thanks for some great input! It seems this is stuff that can't really be imparted until it's been experienced but still nice to have some input on it. In the end I think I'll just do it the way I've always done it (a.k.a. the st33d method), but I guess just thinking about these things might let me write better code.
That is pretty much the jist of it, it takes time and experience. I started a thread on game engine architecture and it kind of exploded into a "there's no best way to do things" argument, but just reading the insights from those who replied seriously was very helpful. It'll affect your process subconsciously and things will look much simpler over time.
|
|
|
|
|
Logged
|
|
|
|
|
BlueSweatshirt
|
 |
« Reply #13 on: March 18, 2010, 04:14:03 PM » |
|
At a very high level, my data is structured such that everything is accessible through a reference to the Game object. The Game class, besides containing the main loop logic and such, is pretty much just an encapsulation of global data. I don't really mind that--I don't believe it's a problem if globally-used data is globally accessible.
Game |-Clock [...]
This is exactly what I do. Along with this, I've started creating classes I call Delegates. Instead of interfacing with library-specific code, game objects interface with a Delegate class I write, which in turn interfaces with the library-specific code. It makes it much easier using something like SFML, and drawing sprites. I can simply tell my Delegate to draw a sprite with whatever parameters I want, then it will do all my dirty work. Although, I also use level states! Level states are just child-classes of a base class Level, which I use to easily separate different game states from each other. I keep global data in the Game class, which anything in my Level class can reference to by accessing it through my Game class reference. As mentioned, things like a player object are mostly just a set of coordinates. I'm shifting into a new kind of thought of programming-- You only need to represent what is there, not have a whole entity for what is there. IE: All the player object really does is display itself.
|
|
|
|
|
Logged
|
|
|
|
|
drChengele
|
 |
« Reply #14 on: March 18, 2010, 04:49:31 PM » |
|
I'm going to jump on the "start sloppy, refine later" bandwagon. The whole "absolute encapsulation" bit is only enforced because of large teams, basically you don't want someone who didn't write the class to have access to its guts. In case of the lone programmer, the only thing encapsulation can achieve is hide the code from the guy who wrote it, which doesn't sound like THAT good of an idea to me. I started my programming career in Visual Basic which used Public variables and was proud of it, and I still cant' shake that from my mindset. But I know my code and it works ok and debugging is easy. And that's all that matters. But I admit there is something beautiful in a compact, encapsulated class. Occasionally I get a craving to clean up the mess...
A trick I am using for my game entities is extensors. Almost everything in the game is an entity, and entity itself is a very light class. But it has a (growable) array of extensors. Extensor is a virtual class which has an update() and render() that are called when ever the entity's update and render are called. So every functionality that the entity needs (collision, target seeking, artificial intelligence, turrets, particle generators, player control processing) is derived from the base extensor class. An entity will have as many extensors as it needs, and can have multiple extensors of the same type. Indeed the majority of my game's functionality can be said to reside in these derived extensors classes.
Another thing that greatly eases the development for me is using what I believe is called property-based development. That is, I hardcode as little variables as I need into game classes (world, tile, entity etc.) and just provide them with a custom container of string-keyed integer PROPERTIES. I even put in a hash function to boost lookup times. This is insanely useful for fast game prototyping, as you can add new variables to any entity on the fly without changing the headers. Not to mention it eases game scripting tenfold. Another plus is that properties can always be replaced with in-code variables towards the end of the project if a speed boost is sorely needed (it usually isn't).
|
|
|
|
|
Logged
|
PraetorCurrently working on : tactical battles.
|
|
|
|