Game Entity Organization

Started by
5 comments, last by hupsilardee 11 years, 5 months ago
I am returning to a bit of game development after about five years away. Back then, I was lucky enough to get a job without having a portfolio of samples. However, I always started something and never really saw it through. Now that I am an experienced developer (albeit, with the bulk of my career spent in C#) I decided I would try to complete something fairly simple. The first thing I am creating is Pong, probably the simplest thing that I could think of. I am using C++ and DirectX and trying to write much of it myself - while I am relying heavily on STL and Boost, I'm not using middleware for physics, etc.

Enough setup- the question I have is around organizing two types of entities: drawable entities and collidable entities. I have a renderer interface: IRenderer and then an implementation called DXRenderer which is for DirectX 10.1. This isn't for runtime late binding purposes (the projects in which they reside are static libs, not dlls) but for separation of concerns purposes. My Game class (from which I have a Pong subclass) accepts an IRenderer* and registers with it IDrawable*s. Per frame, IRenderer iterates through all of its IDrawables and - if they are not hidden - the renderer calls IDrawable::Draw(IRenderer* pRenderer), passing itself it.

Now, this achieves a fairly pleasant separation of concerns, but I'm now a bit worried about the amount of work I've just created for myself. So far, I have only two IDrawable implementations: StringDrawable and RectangleDrawable. IRenderer thus has two methods corresponding to these: IRenderer::DrawFIlledRectangle(...) and IRenderer::DrawString(...). The two issues here are: having to add another method almost per IDrawable interface, and having to open up each method sufficiently to support a plethora of rendering options (so far, you can't even choose what color you want to render the rectangle or string - it's just a black brush from d2d).

The second issue is pretty similar, in that it's about me painting myself into a bit of a designerly corner. The Game class has a CollisionDetector, into which games can register ICollidable objects. Per frame, the CollisionDetector iterates through all of the moving objects and checks if they have collided with something (ie: it iterates through everything else per object, this feels a wee bit heavy-handed, but is in no way slowing things down so far- still takes <1ms per frame). ICollidables only support bounding Rectangles at the moment, so one issue is how I would be able to support multiple types of bounding shape (well, for Pong I only really need rectangles and circles, but still...) The next issue is about how I go about responding to a collision. CollisionDetector retrieves the bounding rectangle of two ICollidables and finds their intersecting Rectangle, if one exists. I then pass the primary object the intersecting rectangle and a pointer to the ICollidable that it hit.

If we take the example of the player's paddle moving vertically in the playing area, I have created two Wall objects: _floor and _ceiling. These are ICollidables. Currently, Paddle::Collided(...) is called and the top of intersecting rectangle is the same as the top of the Paddle, then I "know" we're in the _ceiling and need to move down the same distance as the intersecting rectangle's height. Conversely, if Paddle::Collided(...) is called and the bottom of the intersection is equal to the bottom of the Paddle, the I "know" we're in the _floor and need to move the Paddle up the same distance as the intersecting rectangle's height. Clearly, this is not going to apply to many more scenarios. Eventually, I'm going to have to pass more context into the Collided(...) method, or request it from ICollidables.

Also, I'm not too happy about doing Euler integration on the velocity to get the position of the paddle, but something like RK4 is going to be massive overkill until I can justify it. I'm doing this on evenings and at weekends in between the myriad responsibilities I have, so each change I make can't really take more than an hour without seeing some tangible results. However, is this kind of 'corrective' collision detection still the norm? Ie: after the Game's Update method is called, objects have already intersected so enact some kind of restorative hack to make things look right. I far prefer the idea of predictively detecting collisions, but I guess that's also overkill for now. I do (eventually) wish to make more than just Pong with this framework, though, which is why I'm going to so many pains as to make things extensible and `neat`.

Thanks, and apologies for the rambling wall of text.

-Edit: Here's some code to illuminate what I'm talking about-


// Update loop in my Game base class
void Game::Loop(double time)
{
Update(time);
_collisionDetector.TestForCollisions();
_pRenderer->RenderFrame(time);
}

// Renderer iterates through IDrawable*s and asks them to draw themselves...
void IRenderer::RenderFrame(double time)
{
std::for_each(this->_vpDrawables.cbegin(), this->_vpDrawables.cend(), boost::bind(&DrawDrawable, _1, this, time));
}

void DrawDrawable(const IDrawable* pDrawable, IRenderer* pRenderer, double time)
{
pDrawable->Draw(pRenderer, time);
}

// I need an implementation of IDrawable for everything that can be drawn. At the moment, all I can do is render black filled rectangles...
void RectangleDrawable::Draw(IRenderer* pRenderer, double time) const
{
if(!Hidden)
pRenderer->FilledRectangle(_position.Left, _position.Top, _position.Right - _position.Left, _position.Bottom - _position.Top);
}

//...and black strings:
void StringDrawable::Draw(IRenderer* pRenderer, double time) const
{
if(!Hidden)
pRenderer->String(_position.X, _position.Y, _text);
}

//To use this in a game, I need to register something as drawable:
Pong::Pong(float width, float height, IRenderer* pRenderer) :
Game(pRenderer),
_screenExtents(width, height),
_playArea(width*.05f, height*.05f, width*.95f, height*.95f),
_floor(Math::Rectangle<float>(.0f, height*.95f, width, height)),
_ceiling(Math::Rectangle<float>(.0f, .0f, width, height*.05f)),
_playerLeft(width*.05f, height*.05f),
_playerRight(width*.95f, height*.05f)
{
AddDrawable(&_playerLeft);
AddDrawable(&_playerRight);
AddCollidable(&_playerLeft);
AddCollidable(&_playerRight);
AddCollidable(&_floor);
AddCollidable(&_ceiling);
}

//This does mean that Player is IDrawable, but it just delegates to its components, which will (in turn) delegate to either RectangleDrawable or StringDrawable, of which they are composed:
void Player::Draw(IRenderer* pRenderer, double time) const
{
_paddle.Draw(pRenderer, time);
_score.Draw(pRenderer, time);
}


Collision detection is much the same story:


// I'm sure this is going to be hugely inefficient at some point:
void CollisionDetector::TestForCollisions()
{
for(CollidableCollection::iterator i(_vpCollidables.begin()); i!=_vpCollidables.end(); ++i)
{
if((*i)->IsMoving())
{
for(CollidableCollection::iterator j(_vpCollidables.begin()); j!=_vpCollidables.end(); ++j)
{
if((*i) != (*j)) // don't test for collisions between ourselves
{
Rectangle<float> intersection;
if((*i)->GetBounds().GetIntersect((*j)->GetBounds(), intersection))
{
(*i)->Collided((*j), intersection);
}
}
}
}
}
}

// As we saw before, Player implements ICollidable, but again it delegates to components:
void Player::Collided(ICollidable* pCollidable, const Math::Rectangle<float>& intersection)
{
_paddle.Collided(pCollidable, intersection);
}

// Paddle handles the collision, but this also feels like I'm doing only what will work for this specific case - I've tried to design a general solution but there is still too much knowledge that is apropos of nothing:
void Paddle::Collided(ICollidable* pCollidable, const Math::Rectangle<float>& intersection)
{
const Rectangle<float>& position(_position.GetBounds());
if(intersection.Top == position.Top)
{
// we have intersected with the ceiling, move down...
_position.Move(.0f, intersection.Bottom - intersection.Top);
}
else if (intersection.Bottom == position.Bottom)
{
// we have intersected with the ceiling, move up...
_position.Move(.0f, intersection.Top - intersection.Bottom);
}
}
Advertisement
You should investigate component-based entities. The idea is to split up the responsibilities of an object into separate components that can be handled by different 'manager' objects. It's very powerful and flexible.

For your collision detection question, I advise against moving objects THEN detecting collisions. I would calculate the next-frame positions for all objects then look for collisions and adjust those positions accordingly.

You should also consider (re)writing your renderer for DX11, because this is backwards compatible with DX10 and 10.1 (being a superset of their functionality). When you create the device you can command it to only support DX10.1 functionality, so the game runs on DX10.1 hardware.
The reason for not targeting DX11 is that interoping DX11 with D2D looks like a bit of a nightmare. There was a couple hoops to jump through with 10.1, and I understand it is even worse with 11. However, I guess that once the initialization is done, I don't really need to think about it again.

Is the next-frame position of an object just assuming that the frame delta is going to be exactly the same as the current frame? That sounds... imprecise?

I have split up my entities into components - the Player is composed of a Paddle (as well as a score and other such stuff) and the Paddle is IColllidable and IDrawable. The IDrawable delegates to a RectangleDrawable, which basically adapts my Math::Rectangle to also implement IDrawable. I don't have too much problem with how they are separated, I just wonder if I've perhaps gone an indirection too far? Maybe RectangleDrawable doesn't need to be given an IRenderer to which it `describes` itself. Perhaps, instead, I could just pass in to the RectangleDrawable an ID2DRenderTarget. My obvious grip with this being that, while my game entities are unaware of DX, their components are now wedded to it. Might prevent me having to effective facade DX, though...
Compliment: I don't think you've gone too far. It's solid code.
Criticism: The whole idea of using components is to avoid having an inheritance chain. IE: no ICollidable, or IDrawable. Instead the game object is the glue for a bunch of components. That is, a paddle is a game object which contains the following components: Motor (Handles movement, and collision reaction), Collider (Fires off the OnCollision event, which motor can respond to), Renderer (Passes some information onto your game renderer. Not sure if that makes a lot of sense or not. I tried to explain it here: http://www.gamedev.net/topic/633218-howwho-create-the-gameobjects/page__p__4992951#entry4992951
Compliment: You're sexy.

Add some pickles, maybe a little mustard and you have a compliment sandwich :)
Ahhh, to me - that's a criticism sandwich! Still, tasty. Really appreciate that response, uglybdavis. I will read and digest that post. It seemed like a smell to have to implement those interfaces, so I'm glad my intuition was correct.
The way I prefer to do my renderables components is that each render operation essentially boils down to a material+geometry. The renderer really only needs to know what material (ie textures, blend settings, shaders, etc...) to bind and what geometry (ie quads, quad lists for a 2D game) to draw. As long as the renderable component can supply the correct material and geometry, the renderer should be able to draw it in a single method and just leave it up to the renderable component to do what it needs to do, be it calculating sprite frames and applying texture matrices to the material, setting blend settings, constructing quad-lists from glyph strings, etc...

uglybdavis: You made me chuckle. Have a +1 cookie to go with the sandwich. :D
I read bad things about the performance of D2D. To be honest, I never recognised the niche it occupies - want fast 2D graphics? Use D3D. I bet you could easily render an entire pong game in a single DrawIndexed with D3D11.

P.s. Pickles and mustard in the same sandwich? Madness

This topic is closed to new replies.

Advertisement