Saturday 7 August 2010

Game object rewrite

Placing a wrapper class Actor between the base and derived classes was not that clever after all. I wanted to give Opener and Container ability to some object types through Actor, but as usual when something may seem like a good solution it turns out to have unexpected problems.

The problem with Actor was that it didn't know about derived classes. In actions like opening something there is more involved than just the component data of Opener class. Different openers act different ways. So the rewrite is about removing Actor and moving Opener and Container classes as components of derived objects. With that approach you can't avoid copying code of virtual functions. Good thing is that the code is short, because of functionality of component classes. And more importantly you get to write individual opening code for each object type, checking out things that belong to the derived class.

Like for instance there are doors and sewers, both openers. But sewer is really a different thing, because the lid can be missing! So, using virtual Open() for both door and sewer have same type of code for Opener section, but there are special things that only certain object types have. Also, when you have virtual Open() it's going to be easier to handle, because you can use the base class handle for opening action.

I think the cost of using virtual functions is not that bad when compared to difficulties that follow with extra class in middle of the class hierarchy. I believe the rewrite can be done during this weekend, so it's not going be that hard, even though it is kind of a big change to the game object system.

4 comments:

  1. The only problem of virtual functions is that it's all-or-nothing: either have the default behavior, or do everything yourself. But you can get around it with a nice scheme. Make the main function non-virtual, with the default behavior that should happen all the time. It then calls a virtual function that, by default, doesn't do anything, but it can be overridden to do more stuff.

    You can also turn it around: the main function is virtual, and by default it simply calls the (non-virtual) function with the default behavior. The overridden function can call it or not. Actually this is probably better, since you can provide an array of different behaviors that can be turned on and off.

    I think the more you flatten your class hierarchy the less you'll be afflicted by these "who does what" problems. Composition is good because each instance is relatively independent (even if all components derive from one base class), they just ask if their buddy instances are there and call them.

    But for that to work you need a good internal communication system, instances need to be able to find out easily which other instances of a required type are around, and register to events so they're notified of changes. Safe_cast can help, or store the name of the derived class in the base class so it can be compared easily (I suppose that if you have a deep hierarchy, a list of the names of derived classes would be more helpful). Yes, your classes will be more tightly coupled -- but tiptoeing around that fact gives you an insane design requirement (I think it's not possible in a game at all).

    ReplyDelete
  2. I'm using virtual functions the way they were designed. I don't need to do anything else with them. I'm sorry, but I didn't understand most of your comment. Starting from the first assumption. It doesn't sound right, because in my experience the base class virtual function is called when there is no function written in a derived class. So it's not all-or-nothing.

    ReplyDelete
  3. Sorry, I was responding most specifically to this bit:

    "So, using virtual Open() for both door and sewer have same type of code for Opener section, but there are special things that only certain object types have."

    I meant that, if the default virtual function does something complicated, even if you want to override just a little bit, you have to re-write the whole function in the derived class. It's not a big issue most of the time, but sometimes it's nice to have a semi-standard recipe to deal with this and prevent the code duplication, while at the same time allowing the derived class to "plug in" some modifications to the standard behavior.

    An extreme example of this would be a class with a non-virtual Open function and a few virtual functions that may override some parts of the standard behavior. The non-virtual function calls them to allow the derived class to make some decisions or run some bits of code in the middle of the base class's function. Functions like "is locked", "on lockpick fail", "on open", etc. In a compositional architecture these would be implemented as events (a.k.a. signal/slots). I think it's a cool concept, and even if one doesn't implement it like this some of these ideas can make you think about the problems in different ways.

    ReplyDelete
  4. I want to be strict about virtual functions, because using virtual functions as modular and protected "switch-case" is the actual point of using them. If there is a lot of code copying then virtual function may be not the proper solution at all. It's just that with derived classes of game object types there are going to be those special things in each object type that benefit greatly from strict virtual function style.

    ReplyDelete