Is my object-structure bad? [Related to Text RPG]

Started by
5 comments, last by haegarr 8 years, 7 months ago

I'm building a text-RPG and, while everything's working fine for now, I'm concerned that my structure sucks. The primary objects in play are:

  • Characters
  • Rooms
  • Items
  • Notifiers
  • Handlers (Attack, Item, and Movement handlers)
  • World (stores handlers, notifiers and events)

The basic process is something like this:

  1. Character.move() from Room A to Room B
  2. World registers a move-event
  3. MoveNotifier.notify()
  4. Registered handlers receive notification and do their thing (i.e. - MoveHandler takes the pointer to Character stored in a member of Room A and moves it to the respective member in Room B. DescriptionHandler prints out the appropriate Room B description, what items it contains, etc.)

(I know the notifier-handler thing may be a bit overkill for a single-player text-RPG, but this is all practice for the big picture—please think of this as early practice for creating a larger, multi-player game.)

Here's my worry: Many of these objects store pointers to other objects. Character has a pointer to the Room in which it stands, while Room has a vector of pointers to every Character contained within it. This doesn't feel right. Part of me thinks that Character should know what Room it stands in, but another part of me wants every object to be totally independent and self contained. If everything should be self-contained, then my question is:

Is there a standard line of containment? Should Room know what Character is in it? Should a Character know what Item it has equipped? Should an Item know what Enhancements it's infused with?

If nothing should be aware of anything else, then should everything be stored in a relational database? I've been looking into object storage w/ MySQL or some SQL variant anyway.

Advertisement

In the broad sense, your architecture sounds over-engineered (over-engineering is not neccessary for a "larger, multi-player game"). You haven't really provided enough specifics for me to comment in turn, so I'll offer some similarly broad suggestions:

  • in general, it is better for references to be one-way. That is, a character should know what room it is in, or a room should know about characters in it, but not both unless absolutely necessary. Having both complicates things, both in terms of potential API surface area (both interfaces can answer very similar questions of the current game state) and lifetime management (moving a character involves updating state in two places, so does releasing a character). Duplicate state also leads to fun bugs later in development. If you can do without, do so.
  • fight the urge to literally objectify every object in your OO design. Fight the urge to give everything verbs that create as-close-to-1:1 analogues with the real world as possible. Yes, in the real world a character (person) can move itself, but it sounds like in your architecture, the character just passes a message along to something else to actually move it so the existence of the move verb on the character class is questionable.
  • when you have trouble thinking about where to put some functionality, consider thinking about how you're going to use it. What state that does that functionality need to do its job, and where do you current have that state? What is involved from getting that state from where it is to where you're thinking about calling this new function, and can you put that function elsewhere -- closer to the state it will tend to use?
  • there is no standard formula for constraining state and knowledge of the world across interface boundaries. There are guidelines, though. Prefer writing interfaces that are easy to use properly and efficiently and hard to use improperly, for example. Prefer interfaces that allow the implementation to be as uncomplicated and direct as possible.
  • Forget relational databases exist for now. SQL and such is fine for persistent data that you'll need to query in a complex fashion slowly. It is absolutely not a good idea to use one in real-time; stick to data structures in local process memory optimized for the actions you're going to take on them.

I'm currently implementing an action system, and while not explicitly tailored to text adventures, it uses the same basics like Inform, TADS, and companions. Part of it are interrelations between objects (where "object" means the nouns in action sentences) where the information content is whether or not the interrelation exists. To avoid the bi-lateral (or even multi-lateral) referencing Josh has mentioned, interrelations are implemented in look-up tables with the ordered set of object IDs as composed key. For example:


Interrelation<2> presence;
bool playerIsInRoom = presence.exists( player.id, room.id );
bool itemIsInRoom = presence.exists( item.id, room.id );
id_t where = presence.resolve( character.id );

Excellent! Thank you, this was exactly the type of advice I was looking for (especially the one-way references).

It's totally doable to get rid of most of the Character verbs and group everything up in the handler. My purpose for having them was moreso for programmer-intuition. Something like: If the player types in "get item," then the resulting command sent to to the game would be Character->get(item). The Character.get method tries to associate the "item" string that the player typed in with an item-pointer in the inventory. If that is successful, it really does just pass along a message—and the handler does all the work of removing the item-pointer from the Room and putting it in Character->inventory_, printing out a message like "You get [item]," etc. Otherwise, we get something like "You don't have that item."

I can similarly get rid of the World class entirely in favor of the Character class directly creating events. Or even having the Character class itself do all the work that the handler would otherwise do (but I don't think this translates well into multi-player, and don't really like the idea of the main classes [Room, Character, Item] having direct access to one another).

Maybe the best way to go about it would be:

  • Rooms know what Character they contain (vector of pointers to const)
  • Characters know what items they have (vector of pointers to const)
  • Notifiers notify
  • Handlers do the dirty work
  • Characters directly create events (move events, item events, etc.)

The most direct way would be just for Character A to call a method in Character B, but I don't like this idea (nor do I think it scales well to multiple characters interacting). The World class existed to be the structure in which the notifiers/handlers existed, but there's nothing wrong with them just free-floating.

Maybe I'm just thinking too much about structure? (I love thinking about structure, but at the same time I want to progress!)

haegarr, that sounds pretty awesome, actually. Let me make sure I have the idea straight:

  • Define an Interrelation container which just holds pairs of unique IDs, and call an instance of this "presence," for example.
  • For a character to contain an item, the ID of the character and the ID of the item are combined and put in the table.
  • For a room to contain an item, the ID of the room and the ID of the item are combined and put in the table.
  • To check if a room contains a character, just see if the ID-ID key is in the table.

Is this correct?


[…] Let me make sure I have the idea straight:

Define an Interrelation container which just holds pairs of unique IDs, and call an instance of this "presence," for example.
For a character to contain an item, the ID of the character and the ID of the item are combined and put in the table.
For a room to contain an item, the ID of the room and the ID of the item are combined and put in the table.
To check if a room contains a character, just see if the ID-ID key is in the table.

Is this correct?

Yes, indeed. Doing so has (at least for me) many advantages, up to kind of knowledge representation and the ability to let the player ask questions about relations.

BTW: The mentioned way is not new either. It is used e.g. in relational databases all day. It resembles how terms in propositional / predicate logic can be notated, and how associations in topic maps can be modeled.

It's funny. The natural way I would model this in code would be just like you have, a room has a list of what it holds, NPC's have a list of what they hold, etc. But if I was modeling the same thing in SQL, I'd do the exact opposite. Since GameObjects can only be possessed by one character and a character can only be inside one room, I'd probably have a foreign key on GameObject called ContainedByID. When we model the data this way, the data doesn't allow us to mess up and have a copy of the pointer in two places.

But SQL also facilitates accessing the data this way. Writing a query to get everything contained by game object (#42) is trivial. In code, the naive solution would be to have a big list of ALL game objects, iterate through them all, and construct a list of everything with a ContainedByID == 42. Technically this works, but it won't scale. It might be perfectly sufficient for all your text-game needs, but if you had to put this logic in a render loop, I think it'd start choking.

EckTech Games - Games and Unity Assets I'm working on
Still Flying - My GameDev journal
The Shilwulf Dynasty - Campaign notes for my Rogue Trader RPG


But SQL also facilitates accessing the data this way. Writing a query to get everything contained by game object (#42) is trivial. In code, the naive solution would be to have a big list of ALL game objects, iterate through them all, and construct a list of everything with a ContainedByID == 42. Technically this works, but it won't scale. It might be perfectly sufficient for all your text-game needs, but if you had to put this logic in a render loop, I think it'd start choking.

Of course. Hence I'm not using it as a general purpose solution but especially in player control / AI (the said action sub-system) and dialog (for knowledge and interrogation). This either means that the size of tables is small (or made small in some pre-filtering steps) or their application is not time critical, at least in almost all use cases I've hit yet. Specialization could also help. E.g. instead of using a generic in( A, B ) you can split it into located( A, room ) and contained( item, container ). And with n-ary relations things get funny anyway, e.g. jobBearing( person, profession, location ).

As always: Use the tool that fits the problem.

This topic is closed to new replies.

Advertisement