What's your preferred way of letting objects affect the game?

Started by
19 comments, last by darkhaven3 11 years, 1 month ago

I try to program as atomically as possible, following the Single Responsibility Principle as best I can (but I don't go extreme nazi on things, in case you panic). This works pretty well in a lot of my work.

But games are complicated. Objects interact with each other and the entire game's state regularly throughout the game. Units collide, one unit attacks another, a building makes units, a unit can create projectiles, and those projectiles can affect a large number of other units, etc. In other words, when programming this, you need a way for one object to be able to affect another object (or the game state itself, possibly by creating other units or projectiles). But it's not just a one-time case; many different units have different affects and interactions, which can result in messy code when trying to make all these possible interactions and events possible.

What's your preferred way of letting various objects (units) affect the game (by creating other units, buildings, projectiles, hurting/healing other units, etc.)?

I'm trying to think of some good ways to let these interactions occur that allows for (generally) clean code. My current solution is to have a Game class that actually runs a single game (by updating units (units update themselves as much as possible, but the Game tells them when to), enforcing game rules (so units can't walk up cliffs, for example), updating scores, etc.), and having each unit (or any other object that needs to interact with the Game itself) hold a reference to the Game to which it belongs. If a unit wants to do something that affects another unit or the Game, it calls the corresponding method on the Game object.

For example, if unit A wants to create a building at x, y, it calls Game's makeBuilding(owning_player, x, y), and the Game either creates the building, or it returns false indicating that was an illegal move (perhaps there's already a building there). Or, for another example, unit B wants to launch a rocket that's aimed at target_x, target_y. Unit B then calls Game's createProjectile(owning_player, unit_pos_x, unit_pos_y, target_x, target_y), and the Game creates the projectile and takes ownership of it. Or, another example, unit C performs a melee attack at position x, y in direction dir, so it calls Game's meleeAttack(owning_player, x, y, dir) and the Game can decide if another (enemy) unit was hit and deal damage as needed.

Is this a sane solution? Is there a better one? I'm afraid Game will grow into a monstrous class and have too much responsibility, but then again I'm not sure this can be avoided, and this solution will hopefully prevent a lot of spaghetti code and unnecessary coupling.

I want to try and explore as many (good) solutions as I can before I get too far down the road with this game I'm working on.

[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]
Advertisement

This should be a cool thread. Looking forward to responses.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

though i basically went for the same system you are using in my own engine, so i cant say for sure that it is better, but in retrospect i wish i had implemented it as a event driven system, where game objects can register callbacks for an event. that way the interface stays 'clean', and you have a clearly defined connection point, namely the event message dispatcher/processing. also, when done properly, you get the 'only update when necessary' for free. needless to say, this variant is usually very performance effective (of course it always depends)

however, like i said, i haven't actually done it, so there could be some serious pitfalls i cant think of just now, i'm just learning this stuff myself.

The key realization is that representing a "game" or "unit" or "building" might still be violating SRP.

Yes, on some level, those are "single" groups of responsibility. But they're really just God Classes of slightly more tame dimensions.

At some point you have to decide how much complexity is actually too much; in some games, like PacMan, representing a single "character" in a class is totally fine; in, say, an RPG, it's a mistake. This is entirely situational and dependent on the nature of the game you're writing, which is why I resist the idea that there's value in a "preferred way" or some kind of ruleset for making design decisions absent the relevant contextual information.

On one extreme, I find no sin in writing a game like TicTacToe in a couple of modules/units of organization (classes, if your language of choice believes in them, are just one unit of organization).

At the other, let's look at how a rich RPG might be implemented. I would consider each of the following to merit its own unit of organization in a complex game:

  • Individual items[/*]

  • Groups of items - think bags, containers, etc.[/*]
  • Inventories[/*]
  • Health bars[/*]
  • Individual skills (swing sword, cast fireball)[/*]
  • Skill groupings (all skills granted to sword wielders)[/*]
  • Movement controller (can I walk? Run? Swim? Climb trees?)[/*]
  • High level action management (coordination between using items, skills, movement)[/*]
  • Combat engine (coordinates usage of skills/items and how they interact with health and movement)[/*]
  • Status engine (handles things like temporary status ailments, buffs, etc.)[/*]
  • Movement controllers interact with the physics system to determine where units move. The status engine might be consulted to see if we are paralyzed or slowed for some reason. Skill groupings may selectively permit certain skills in various contextual scenarios. Action management might be wired to player input or an AI system. And so on.

    Component architectures tend to be a popular contemporary solution for building up complex logic from all this sort of decomposition of responsibilities. They're just one option, though; and not necessarily the best.

    Wielder of the Sacred Wands
    [Work - ArenaNet] [Epoch Language] [Scribblings]

    I've been implementing a Entity Component System and have found it to be very clean and easy to control.

    Entity's store data. Controllers are attached to entities and add or alter the entities data(usually after reading it) they also handle events. Observers act on data changes and fire events.

    I currently have an explicit controller, that takes the user input and changes the entitys state.
    An implicit controller that takes movement states and turns it into actual movement.

    An observer that watches entity movement and fires collision events.
    and an observer that watches entity positions and animations and sends updated data to the renderer.

    Such a system can require some intermediate techniques to set up, but has many advantages. Controllers and observers each only have one task, and so are very simple. Game data is cleanly split from the renderer(and both can be moved to different threads much easier).
    Apoch has covered design nicely, but I think this is still worth calling out.

    But games are complicated. Objects interact with each other and the entire game's state regularly throughout the game. Units collide, one unit attacks another, a building makes units, a unit can create projectiles, and those projectiles can affect a large number of other units, etc. In other words, when programming this, you need a way for one object to be able to affect another object (or the game state itself, possibly by creating other units or projectiles). But it's not just a one-time case; many different units have different affects and interactions, which can result in messy code when trying to make all these possible interactions and events possible.

    The bold section, in particular the end, is key to this I feel; a single object in the game is not going to touch the whole game state. Not ever and certainly not directly. This is something you go on to basically say yourself with the list of interactions below and this is a key place to start when thinking about it - each object has a pre-defined interaction with the world. If you were going to make an RTS game, for example, your 'tank' super-object (super-object => collection of parts) isn't suddenly going to be marrying The Hulk any time soon ;)

    If we take a closer look at one of the examples you gave, "a building makes units", as a typical factory in an RTS game.
    While this might look complicated it isn't really, at the highest level the building does one thing 'creates a unit', at which point beyond throwing out a few notifications it is done.

    Its output might simply be a message (in the generic sense, not a 'message system' sense) which says "I've made vehicle <type> at location <here> for <player>". Other systems would be listening for that event and respond accordingly; a player's unit collection might update and the unit spawner might create a new object so it can be rendered.

    At which point the messages could ripple further afield (unit collection => UI to display message/icon; spawner to any AI and physics systems to insert it into their world view; and so on) but the key interaction is done.

    As to how you expose this... well, there is the erstwhile mentioned 'message system' which you could route all the messages to - however this global routing might not be what you want. Your 'factory' super-object might contain a list of delegates/functions to call when certain events happen and systems 'subscribe' to them or you could abstract the whole thing and come up with a processing graph which represents spawning in data; this would require your super-class still outputs events but the hookup of those input and output pins becomes data driven and potentially modified per level.

    The key thing, however, is that each unit has a predetermined level of interaction with the world so it can't mutate all the state itself nor does it have to deal with every possible thing that can happen. Even something as simple as 'collision' isn't really the units problem as it is a problem for the physics under the hood.

    i didn't mention this in my first post, but i've actually moved to something in between 'every object can affect everything else (within its class)' and an 'event system', im using a command queue of sorts.

    all objects interacting with the world issue a command to the queue (most prominently spawning of stuff, but also game logic triggers and such), and the command queue handles dispatching data to other objects that need it. so its basically an event system, but i will not shoehorn my collision into there, that seems performant enough, since i got a broadphase covering all objects anyway.

    a nice side effect of the queue was that i was easily able to prioritize and limit events per game loop iteration.

    even though a lot still gets calculated inside the object class itself, i managed to move most of it to the queue.

    deriving new objects from my game object class has become so clean and easy now, wish i had done it from day one (well actually i did, but limited to spawning at first)

    EDIT: seems from the answers sofar that event systems are the way to go....

    I think Apoch and phantom cover the bases well but phantom brings up the real problem: how to trigger event and apply effect. No offense meant but phantoms "clarification" was unfortunately lacking in "how" it works.

    It's all in the lists...

    Games are "turn based", just really damned fast turns. So, your object wants to create a projectile, you create the new projectile and send a message to the games main loop saying "add this to the world". You send a message because you want the creation of a new projectile "NOT" to effect current turn processing, the creation is the result of this turn and will affect the world next turn, "not" this turn. (This delayed reaction solves ungodly numbers of bugs, just do it, really.)

    Given we solved how to produce a projectile, how do you apply damage? Same basic deal, the projectile says "I hit" and posts a message for "do damage to xxx" because I hit it. Next frame xxx figures out if it is still alive after getting hit.

    Or, xxx could post a "I did x damage to area" to the world object and the message can be applied to all objects in the area. Those objects can be told to die or removed, depends how you want to deal with the details here.

    The key thing to remember is that even though the game is realtime the rules are basically turn based. If you try to apply rules interactively in a single frame, you are asking for all sorts of problems. OMG, he didn't die 1/60th of a second faster really isn't something a player will notice and not integrating everything every loop makes for a much more simplified coding problem. Network and all that, gets more complicated but still basically the same other than when to apply messages.

    
    typedef unsigned long coord_t;
    typedef unsigned char byte_t;
    typedef signed short mobjtype_t;
     
    typedef enum {
    MOBJ_CREATE=0,
    MOBJ_ACTIVE,
    MOBJ_ATTACK,
    MOBJ_PAIN,
    MOBJ_DEATH,
    maxmobjframes;
    } mobjstates_e;
     
    typedef struct {
    mobjtype_t type;
    frame_t* mobjframes[maxmobjframes];
    coord_t mapx,mapy;
    mobj_t* target;
    } mobj_t;

    ?

    mobj_t represents every possible object in the game world, including the player. mobj_t contains a list of pointers to a master "frame list" that defines things like what graphic should be displayed for this object at this frame, what the next and/or previous frames for the current one are, and a function pointer to describe what needs to be done by this object this frame. mobj_t frames only have access to certain functions in a master list which should, by convention, only interact with other mobj_t types, and what object that ends up being is dependent on mobj_t.target. Objects can do whatever the hell they want to each other after this point.

    ?
    mobj_t represents every possible object in the game world, including the player. mobj_t contains a list of pointers to a master "frame list" that defines things like what graphic should be displayed for this object at this frame, what the next and/or previous frames for the current one are, and a function pointer to describe what needs to be done by this object this frame. mobj_t frames only have access to certain functions in a master list which should, by convention, only interact with other mobj_t types, and what object that ends up being is dependent on mobj_t.target. Objects can do whatever the hell they want to each other after this point.

    I can see this being a legitimate approach if and only if your game logic is heavily data-driven and validated by external tools prior to loading into the engine.

    If you're hard-coding a non-trivial game in this style, it's going to turn into a Big Ball of Mud sooner rather than later.

    Of course, as I said before, for a certain class of games (and more accurately, for a certain degree of simulation simplicity) that's totally fine.

    I'd hate to see an RTS built that way, though ;-)

    Wielder of the Sacred Wands
    [Work - ArenaNet] [Epoch Language] [Scribblings]

    This topic is closed to new replies.

    Advertisement