I've run into a recurring problem trying to translate my game design(s) into code, and I need some help. Some bits of game design translate very easily, others don't seem to have a reasonable analogue. Pardon the length of this post.
Game objects
Game objects translate directly into classes (objects, structures, whatnot). By game object I mean something you can point to as a conceptual whole. That is a Bobo the Ogre (and here are his numbers), that is Marvin Gardens (and here is its rent), etc. There's a lot of information available on ways to code classes (and inheritence, and so forth), and I feel comfortable with the translation.
Game process
Game processes translate directly to functions (procedures, methods, etc). A series of steps to follow that may in turn trigger other processes, and so forth. There is a lot of information about writing and organizing functions, and I feel comfortable with these too.
Relationships between game objects
Here's where the game design and the code design diverge, and I need help.
A relationship defines a particular state between two game objects. Might be more than two, but solving for two gives me the rest.
The relationship itself does not necessarily contain any state information - the relationship is the state itself.
Example:
Game object: Bobo the Ogre (monster)
Game Object: Room
########################
#......................#
#..########............#
#..# #............#
#..# Ogre #............#
#..# +............#
#..########............#
#......................#
########################
The Ogre is in the Room.
The Room contains the Ogre.
Ogre <----relationship----> Room
I didn't include the exact location of the Ogre since it's not necessary for the relationship.
A Relationship:
a) depends on the existence of two game objects. If either of the game objects is removed, then the relationship no longer exists.
b) doesn't necessarily exist at any point in the game. 'Permanent' relationships can be simply hard-coded into the game objects or procedures. Relationships are necessarily transitory.
c) is generally bi-directional, or can be described starting from either object. A relationship between game object A and B may not be described the same going from A to B as from B to A, but it can be described starting from either object. See the example above.
d) does not necessarily contain any data values.
There's another non-obvious feature. Relationships must have a significant impact on the game. At a minimum, some process might need to check if a certain relationship exists, or what the other end of the relationship affects. Sometimes the presence, absence, creation, or destruction of a relationship changes what code is run. So:
e) must have an impact on the gameplay or code process. Sounds vague, I've got some examples later on.
There just doesn't seem to be an easy, elegant way of expressing them in code. Some approaches I have seen:
1) Overcode the objects. Create a bunch of extra pointers (or lists) in each of the game objects. Bobo the Ogre would have a Room pointer, which is NULL when he walks around outside, and each Room has a list of pointers to all the monsters it contains.
This doesn't seem to scale well. The objects quickly bloat with a bunch of NULL pointers and other artifacts.
2) A separate 'Relationship' object. This would be the OOP approach. Create a separate, abstract Relationship class with several different types. The game objects then each have a list of pointers to the relationship objects they are part of, and each relationship has pointers to the two game objects they connect (as well as type information).
I'm not quite comfortable with this approach. It seems to require a lot time spent identifying, sorting, managing, and comparing relationships and types rather than actually doing the effects. Abstraction makes the objects very ignorant.
3) A lot of runtime checks. Everytime you run through a process (procedure, function), you include a lot of conditional checks. If this relationship exists then do that, if that relationship exists, do this other thing.
This results in a lot of spaghetti code. Maintenance is a nightmare.
4) A very complex set of function pointers or function objects. The function or procedure that creates the Relationship also changes one or more other functions directly as a consequence. Creating, destroying, or altering a Relationship means doThis() becomes doThat(), so the function pointer whatDoIDoNext changes from one to the other. This is a sort of LISP approach.
This creates a tangled weave of behavior, not unlike the runtime checks in (3) above. It gets worse when different instances of the same object type need to use different routines because different Relationships exist for one instance than another. It's somewhat elegant, but potentially slow because of the indirections, and I'm not confident about using it.
Maybe each of these is a good approach under different circumstances? If so, what are some good guidelines? Or is there another approach? Or a language where relationships are built-in? I find it frustrating that something so simple and obvious to my brain (Bobo is in the Room) doesn't translate easily to code.
Before you respond, I've included some examples below, to give you an idea of why (and where) it matters.
The three object diagram:
Game Object <-- Relationship --> Game Object <-- Relationship --> Game Object
^ ^
| |
----------Rule Change ---------
This is a pretty common setup. When A has a particular relationship to B, and B has a particular relationship to C, then do X.
Example:
Game object: Bobo the Ogre (monster)
Game Object: Room
Game Object: Hero
Rule: If the hero is near a room, then update the contents of that room (monster locations) each turn. If not, then just represent the monster's movements by a small chance he appears at the door.
########################
#......................#
#..########............#
#..# #............#
#..# Ogre #............#
#..# +...@........#
#..########............#
#......................#
########################
becomes
########################
#......................#
#..########............#
#..#......#............#
#..#.O....#............#
#..#......+...@........#
#..########............#
#......................#
########################
Hero <---- is Near ---> a Room <--- Contains --> Bobo the Ogre
^ ^
| |
----Update Ogre Position---
The first trick is that these two relationships exist independently of each other. The Hero may be near the Room, but the Ogre may be outside. Or the inverse.
Tracking a false state for all of the possible relationships is impossible, but the code needs to be able to evaluate what the changes to make with reasonable speed. In this case, there is a common object that is in both relationships (the Room), so maybe the decision making should be closely tied to the middle object.
The four object diagram:
Object <----relationship-----> Object
^
|
Rule to Follow
|
v
Object <----relationship-----> Object
This is a bit trickier.
Example: If the player is holding one or more evil artifacts, and there is a graveyard on the map, then every (time interval), each graveyard spawns a number of zombies equal to the number of evil artifacts held.
Hero <---- holding (some) ----> evil artifact(s)
^
|
Spawn (number of) Zombies
|
v
Map <------ contains (some) -----> Graveyard(s)
Assume there is a condition that can convert existing map areas to graveyards, and that the hero has a means to destroy graveyards (in addition to picking up and dropping evil artifacts), so at any time any of the Relationships may come into existence, or stop existing, or change numeric values.
Further assume that this a fairly rare set of conditions. If the hero isn't holding any evil artifacts, or the map doesn't contain any graveyards, then there is no need to spawn zombies; there doesn't even need to be a check (ie, you shouldn't need to calculate spawning zero zombies). If the code solution needs to spend time checking for these conditions every time interval, then it needs to check for other conditions as well - the whole process soon slows down looking for non-existent conditions (spawn zombies? no spawn rats? no rising water level? no ...etc)
It's frustrating because I can express the game rules very simply, define what the conditions are very simply, but turning them into good code is much more difficult. Is there a good generic solution, or set of approaches? If there are mulitple approaches, then what are some good criteria to decide between them? Any links on teh subject? Books? When I run into this I can hack out working code, but I'd like something better.
JSwing
edit:spacing
[edited by - JSwing on May 17, 2004 5:54:53 PM]