Game object management
BitCruncher

I have defined a game object class; a general class that handles everything that any visible, interactive game object would have such as a position, bounding box system, an image and so on. No special attributes or functions that a certain object would have like jumping, walking, attacking, defending etcetera.

There are times, though, that I would like to define an object that has multiple frames that represent that object depending on which direction it is facing. The object is nonetheless part of the said game object class because it still has a position and a bounding box but it handles images differently and has functions that the general class doesn't handle.

I know that I can have a special object inherit from the game object class or I can try to stuff everything in that one interface. What's your opinion guys?

I can post the interface if needed.

Mark Oates

Personally, I would inherit. The key seems to be creating a game object class so that it is meant for inheriting, something that contains the basic groundwork properties that are shared by all objects. This is generally a better approach than stuffing everything into one object, only later to ignore half the components in most instantiations.

[edit]but you know, programs are crazy little things. You should just go with what makes the most sense to you.

ReyBrujo

Inheritance or composition myself, either seems fine in your model. However, if you want everything to be hung in the hierarchy, you would prefer composition, since some objects will have drawing routines and some (like status changes if you use them as attachable objects) won't.

OnlineCop

Will all objects that use this [base] object class have multiple frames? Without exception?

If so, put that into the base class, since it's something that they all share.

If any of the objects don't, you want to use inheritance, like mentioned before. I would also wonder about even having the image:

position, bounding box system, an image and so on

If you use frames, you won't actually use that, since you would use whichever frame is currently "active".

Personally, I provide an interface for my child classes to implement themselves.

class IGameObject
{
public:
  void draw(BITMAP* dest) = 0;
  void update_logic(const double& delta_time) = 0;

  void set_position(const unsigned int& x, const unsigned int& y) = 0;
  void get_position(unsigned int& x, unsigned int& y) = 0;

  bool check_collision(const IGameObject* rhs) = 0;

  void set_bounding_area(const unsigned int& x1, const unsigned int& y1, const unsigned int& x2, const unsigned int& y2) = 0;
  void get_bounding_area(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2) = 0;
};

From here, you may make two children classes: non-animated (which use a single image), and animated (which use frames, or some other implementation). The draw() function will still be able to be called for either of the types since they have both defined these functions internally.

ReyBrujo

I wouldn't make the objects inherit there, then. Give the object properties to draw, collision detection, physics, etc, and set them to NULL (or dummy classes) if you don't want the object to react with something:

#SelectExpand
1class IDraw { 2 public: 3 virtual void draw(BITMAP *) = 0; 4}; 5 6class ICollision { 7 public: 8 virtual bool collide(ICollision *) = 0; 9}; 10 11class CSprite : public IDraw { 12 private: 13 BITMAP *m_pSprite; 14 15 public: 16 virtual void draw(BITMAP *target) { 17 blit(...); 18 } 19}; 20 21class CAnimation : public IDraw { 22 private: 23 BITMAP *m_pAnimation[12]; 24 long m_lIndex; 25 26 public: 27 virtual void draw(BITMAP *target) { 28 blit(m_pAnimation[m_lIndex++], ...); 29 m_lIndex %= 12; 30 } 31}; 32 33class CObject { 34 private: 35 IDraw *m_pDraw; 36 ICollision *m_pCollision; 37 38 public : 39 IDraw *draw() { 40 return m_pDraw; 41 } 42 43 ICollision *collision() { 44 return m_pCollision; 45 } 46};

In this case, the timer could be an object that has no collision (you could pass through it, like in Super Mario Bros when you used the teleporting pipes and passed through the score).

Tobias Dammers

Even if some objects aren't animated, you could still provide animation functionality in the base class, and treat still images as a special case (with a single frame animation).
Otherwise, I'd vote for composition (i.e., an abstract ObjectDrawer base class from which StillImage and Animation derive, and an Object base class that has a member of type ObjectDrawer*, which is initialized to either StillImage* or Animation*).

BitCruncher

So, when you guys say composition, you mean using classes as data of other classes? I think I that have heard of this technique; is this also known as the composite technique? Does anyone know any useful articles?

bamccaig

I've found that trying to apply "dependency injection" has helped me with my designs. The speaker from the Google Tech Talk on global state and Singletons said that there are basically two types of objects. Services, that do things, and "value" objects, that contain the data necessary to do those things. His example was a CreditCard (as a "value" object) and a CreditCardProcessor as a service object. Should a credit card know how to charge itself? Of course not, that's not what a credit card is for. It needs something else to do the actual work. It just holds the data necessary to complete the operation.

Applying it to this situation, I don't think it makes sense for a game object to have a draw method. Why would a monster or a UI button, for example, know how to draw themselves? Does a drawing draw itself? No, a drawer (or artist or animator or painter or what have you) draws it. Rather than a draw method, you might have an IDrawable abstract class and implement it; perhaps all it does is return a const IImage & from a getCurrentImage(const GameTime &) method. A Drawer (which seems ambiguous, so substitute whatever term you prefer... I'm using drawer for this post so it corresponds with the draw method) would have the draw method and would accept perhaps an ICanvas & (which would be a drawing destination), an const IDrawable &, and perhaps a const IObject * const (for other data that might be needed for any particular object; for example, GameTime).

I'm still pretty new to a lot of these concepts, but I'm trying to make sense of them and figure out what works and what doesn't. C# has shown me that having abstract bases (or interfaces, in C#/Java) helps to separate the design from the implementation; allowing you to focus first on design and then on implementation (i.e., when you're designing it you first identify what and then you can address how).

A rough example of what I'm currently thinking...

#SelectExpand
1// Forward declarations. 2class ICanvas; 3class IDrawable; 4class IDrawer; 5class IImage; 6class IObject; 7class IServiceObject; 8class IValueObject; 9 10// Abstract bases. 11class IObject 12{ 13 // ... 14public: 15 virtual ~IObject(void) = 0; 16 17 // ... 18}; 19 20class IServiceObject: 21 public IObject 22{ 23 // ... 24public: 25 virtual ~IServiceObject(void) = 0; 26 27 // ... 28}; 29 30class IValueObject: 31 public IObject 32{ 33 // ... 34public: 35 virtual ~IValueObject(void) = 0; 36 37 // ... 38}; 39 40class IImage: 41 public IDrawable, 42 public IValueObject 43{ 44 // ... 45public: 46 virtual ~IImage(void) = 0; 47 48 // ... 49 50 virtual const IImage & getCurrentImage(const IObject * const) = 0; 51}; 52 53class ICanvas: 54 public IValueObject 55{ 56public: 57 virtual ~ICanvas(void) = 0; 58 59 // ... 60}; 61 62class IDrawable: 63 public IValueObject 64{ 65 // ... 66public: 67 virtual ~IDrawable(void) = 0; 68 69 // ... 70 71 virtual const IImage & getCurrentImage(const IObject * const) = 0; 72}; 73 74class IDrawer: 75 public IServiceObject 76{ 77 // ... 78public: 79 virtual ~IDrawer(void) = 0; 80 81 // ... 82 83 virtual void draw(ICanvas &, const IDrawable &, 84 /*...other parameters to describe where and how to draw the 85 object...,*/ const IObject * const) const = 0; 86}; 87 88// C++ requires the pure virtual destructors be defined. 89ICanvas ::~ICanvas (void) {} 90IDrawable ::~IDrawable (void) {} 91IDrawer ::~IDrawer (void) {} 92IImage ::~IImage (void) {} 93IObject ::~IObject (void) {} 94IServiceObject ::~IServiceObject (void) {} 95IValueObject ::~IValueObject (void) {}

With something like that, your animation could go something like...

#SelectExpand
1class IAnimation; 2class GameTime; 3 4class GameTime: 5 public IValueObject 6{ 7 // ... 8public: 9 // ... 10 ~GameTime(void); 11 12 // ... 13}; 14 15class IAnimation: 16 public IDrawable, 17 public IServiceObject // Service or value? Hmm... 18{ 19 // ... 20public: 21 virtual void ~IAnimation(void) = 0; 22 23 // ... 24 25 virtual const IImage & getCurrentImage(const IObject * const) = 0; 26 virtual const IImage & getCurrentImage(const GameTime * const) = 0; 27}; 28 29class Animation: 30 public IAnimation 31{ 32 // ... 33public: 34 // ... 35 virtual ~Animation(void); 36 37 // ... 38}; 39 40IAnimation::~IAnimation(void) {} 41 42GameTime::~GameTime(void) 43{ 44 // ... 45} 46 47Animation::~Animation(void) 48{ 49 // ... 50} 51 52const IImage & Animation::getCurrentImage(const IObject * const) 53{ 54 /* 55 * Always throws. A more context-specific exception would be more appropriate, 56 * but you get the idea. 57 */ 58 throw std::runtime_error("Animations require GameTime to get the current " 59 "image."); 60} 61 62const IImage & Animation::getCurrentImage(const GameTime * const) 63{ 64 /* 65 * Determines the current frame based on the GameTime and returns a reference. 66 */ 67}

(I'm not 100% sure if always throwing is a design error or a valid design... WIP.)

This way, a statically drawn object can ignore the parameter to getCurrentImage method (and an overload with a void parameter list can be called). An animation will work when passed game time, but explode otherwise. The actual drawing is handled in centralized containers specifically meant to draw. Everything else just provides an interface to what needs to be drawn.

WIP... :-/

Audric
Bamccaig said:

I don't think it makes sense for a game object to have a draw method. Why would a monster or a UI button, for example, know how to draw themselves?

Because it works well. Look at any object-oriented UI, and you'll see a Draw() method at the Widget level, re-implemented for each derived Widget (button, listbox, anything). The "it's self-evident" argument doesn't work for me. If you try to apply this design method to a classic UI system, you'll see you're only doubling the number of classes, for no benefit since there's no 'mutual reference' issue to solve. So this design is not universally better.

BitCruncher

@ bamccaig

At first, I was on the same idea. I asked myself whether I wanted my objects to draw themselves or not. If I implemented a 'drawer' method which did all of the drawing, I could overload the functions but then if each object knew how to draw themselves, they could act accordingly depending on what type of object it is.

bamccaig

They still can be drawn accordingly depending on which type of object they are. Note that the individual game objects tell the drawer what to draw (AKA data). It's just that the drawing itself is offloaded onto a separate service object. With a fully thought out design there will likely be savings in code. Honestly, a lot of objects are going to draw themselves in much the same way. There's no need to individually write the necessary Allegro interface for each one when they could all share the same through an abstraction. I think you'll find it makes more sense to have a separate object doing the drawing too. I know it certainly makes more sense in my designs.

I'm by no means an expert on this, but I'm trying to learn as I go. I've built functionality into data objects before and after trying it the other way it makes much more sense to separate tasks like this (and it's also been much less code to write). Of course, I have little practical experience applying it (even less with respect to games) so it's possible I'm wrong. Just trying to get feedback to uncover more of the picture. :)

Tobias Dammers
Audric said:

Because it works well. Look at any object-oriented UI, and you'll see a Draw() method at the Widget level, re-implemented for each derived Widget (button, listbox, anything). The "it's self-evident" argument doesn't work for me. If you try to apply this design method to a classic UI system, you'll see you're only doubling the number of classes, for no benefit since there's no 'mutual reference' issue to solve. So this design is not universally better.

It's not as bad as you think. You don't need a dedicated drawer class for each widget; instead, create a drawer interface that contains a number of methods to draw widgets (e.g. DrawButton, DrawBorder, DrawText, DrawRadioButton, etc.).
The big advantage of this is that the widgets' functionality is separated from its presentation, and you can swap the drawer class for something different (e.g., a hard-coded one for a skinned one) with relative ease. This approach also allows for exchanging the graphics engine that does the drawing, e.g. switch from plain allegro to allegrogl, without changing the widgets themselves.
If the widgets drawthemselves directly, you tie yourself into one drawing algorithm, which is probably OK for a simple GUI, but not for one you intend to drag along for a few years and reuse in various projects.

BitCruncher

Thanks so much you guys for your input. From the responses I am getting, it sounds like doing a 'drawing' class will be most beneficial in the long run in terms of code reuse. I will think on this one. I should be getting a project up on the Depot pretty soon. And

- I have finally found an avatar that I like for the moment.

OnlineCop

- I have finally found an avatar that I like for the moment.

An alien with two eyes, oriented vertically from each other (instead of horizontally like we have), staring off to the left while a missile barely misses him/her/it/them as it farts and leaves big dust clouds to kill whoever shot at him/her/it/them in the first place? :P

Pretty neat! ;)

Thread #601959. Printed from Allegro.cc