Separation of graphics representation and game logic

Started by
5 comments, last by turch 12 years, 2 months ago
I've been coding a while and have been experimenting with ways to separate my code from devices that do not belong. In doing so, I've also tossed the idea around to create a game-logic framework that is independent from media libraries.

The idea is that I can write a game, reference a graphical device, and simply write adapter classes to interact with the commands given.

For instance, I'm currently using a node-based system to represent graphics. Initially, we have a sprite. We know that this sprite is going to draw somehow but we're not using a library to truly determine anything.

Instead, classes using this graphics will use Nodes that contain manipulative data that can be interpreted later:


Sprite *ghost; // Graphical representation of a ghost
Graphics_Device->insert(ghost); // Add it to the rendering list
....
class Ghost_Simple : public Ghost
{
Graphics::Drawable::Node *my_sprite = ghost->makeNode<PositionData>();
...
void update()
{
(PositionData*)my_sprite->getData()->setImgPos( getX(), getY() );
}
}
...


Logically, graphics are represented and the appropriate data (can be defined easily by users!) will define how these graphics will be used later:


// Visitor pattern
class GraphicsAdapterDevice : public GraphicsDevice
{
private:

SomeAPI::APIGraphics::device *device;

void onDraw(Sprite *spr, DrawData* data)
{
TransformableData* d = dynamic_cast<TransformableData*>(data);

if(d!=0)
{
device->drawImage(d->getImg(), d->getX(), d->getY()); // drawImage is part of some API
}
ColorData* c = dynamic_cast<ColorData*>(data);
if(c != 0)
{
device->drawImageColored(c->getColorRGB() ); // drawImageColored is part of some API...
}
}
void onDraw(Polygon *poly, DrawData* data)
....
}


Quickly you should be able to spot the smell. For one, a lot of dynamic casting to figure out what new data is being sent for these graphics to be interpreted. Secondly, every new image type needs to have a new function to draw it.

Another idea I had was to allow the class itself to handle how it was drawn, but it needed to be visited by the GraphicsDevice to know how to handle it. Then that would violate the ability for these classes to be logically represented and completely unusable in case the user decides to switch APIs.

In summary, I love the idea of representing graphics in such a manner that it exists almost as logic, yet separated from game logic, but it can be easily tied to a Graphics API. This code would be virtually portable anywhere and the only portions the coder would need to worry about is writing the adapter graphics device to render graphics correctly based on some API. (but have all the information they'll need to do so!)

I would appreciate the help getting this model represented correctly. Or perhaps I'm looking at it the wrong way.

Thanks in advanced.
I'm that imaginary number in the parabola of life.
Advertisement
...

Why don't you write a visitor that extracts all the Graphics data you need for drawing and pass the list of those objects to the renderer. In that way all you renderer would ever see would be a RenderObject or DrawableObject with the information it needs, like material, textures, vbs and ibs or sprites.
This way you only expose logical render data to the renderer without having to deal with a specific API.
It also removes the need of dynamic casting as the render object should contain all the data the renderer can handle, it's the responsibilitie of the model layer on top of it to just pass the data the renderer needs.
Its then the responsibility of the model to interact with the attached game object, or the game object has one of these models attached to it. This allows you to seperate render information from game logic and even from model format and model loading.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion


Why don't you write a visitor that extracts all the Graphics data you need for drawing and pass the list of those objects to the renderer. In that way all you renderer would ever see would be a RenderObject or DrawableObject with the information it needs, like material, textures, vbs and ibs or sprites.


^^

At a low level, everything you can draw boils down to vertex / index information, shaders, and shader parameters. Each subsystem (geometry, sprites, particles, etc.) can ectract this information in an api-independent way and pass it to the renderer.

Assuming you aren't using the fixed function pipeline. And if you are, why? smile.png

[quote name='KanonBaum' timestamp='1328254309' post='4909050']...

Why don't you write a visitor that extracts all the Graphics data you need for drawing and pass the list of those objects to the renderer. In that way all you renderer would ever see would be a RenderObject or DrawableObject with the information it needs, like material, textures, vbs and ibs or sprites.
This way you only expose logical render data to the renderer without having to deal with a specific API.
It also removes the need of dynamic casting as the render object should contain all the data the renderer can handle, it's the responsibilitie of the model layer on top of it to just pass the data the renderer needs.
Its then the responsibility of the model to interact with the attached game object, or the game object has one of these models attached to it. This allows you to seperate render information from game logic and even from model format and model loading.
[/quote]

I agree with turch. Great idea. So to make sure I'm understanding this 100%

1) No more "DrawData". The class in question will have the functions it needs to represent a graphical structure.
2) This graphical structure will consist of low-level data (vertices and the alike)
3) This will allow the Renderer to extract specific data
4) If a user needs a class with a tad more functionality, they could simply extend the class

New question: Say I get this all sorted out and have this better model working. How will the renderer know what image we are rendering for some Image (representation) class? Some APIs use integer IDs to tell what image to draw, others have an Image class that is bundle with some API.

Should these Drawable classes have templated data? i.e. Sprite<int> *ghost = new Sprite(3); or Sprite<string> *ghost = new Sprite("ghost_sprite");

So that way the renderer:


void onDraw(Sprite<int> *spr)
{
API::getImage(spr->getData() )->draw ( all the data ) // returns integer 3
....
}


or


void onDraw(Sprite<string> *spr)
{
if(spr->getData() == "ghost_sprite")
API::StaticImage("path to sprite")->draw( all the data )
}


This is awful as well and hopefully you can see what I'm trying to get at. Thanks for the help from both of you so far!
I'm that imaginary number in the parabola of life.
I'm not sure what the visitor pattern is giving you over simple C-style compile time polymorphism in which you have one .h file with different .cpp implementations for each output graphics library. You know, you have protypes like void RenderSprite(SpriteType ... ) and then have implementations for OpenGl and DirectX or whatever in separate cpp's.


Or if that's too old-fashioned, the same idea done with templates.

New question: Say I get this all sorted out and have this better model working. How will the renderer know what image we are rendering for some Image (representation) class? Some APIs use integer IDs to tell what image to draw, others have an Image class that is bundle with some API.

Should these Drawable classes have templated data? i.e. Sprite<int> *ghost = new Sprite(3); or Sprite<string> *ghost = new Sprite("ghost_sprite");


I use no templated data in my classes, all the texture assignments and materials are already stored with the shader parameters they will be set on. So they are stored as a list of pairs in the render data in my case.

To manage textures I have a texture manager that maintains only one copy of the requested texture, stored by a pair of a hash of the texture name and the texture itself.

This management is all up to your own personal liking same as the way that I store a direct link to the shader parameter and it's data.



I'm not sure what the visitor pattern is giving you over simple C-style compile time polymorphism in which you have one .h file with different .cpp implementations for each output graphics library. You know, you have protypes like void RenderSprite(SpriteType ... ) and then have implementations for OpenGl and DirectX or whatever in separate cpp's.


Or if that's too old-fashioned, the same idea done with templates.


I see sprites as models to be honest, they just consist of a on screen quad, and a texture atlas with a current page from that atlas. So i break that down into render data and submit that.
The visitor pattern is there so you can actually pass more visitors over your scenegraph before submitting your final list to the renderer. Think previs checks to remove objects that arent in the frustum.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

I use no templated data in my classes, all the texture assignments and materials are already stored with the shader parameters they will be set on. So they are stored as a list of pairs in the render data in my case.

To manage textures I have a texture manager that maintains only one copy of the requested texture, stored by a pair of a hash of the texture name and the texture itself.

This management is all up to your own personal liking same as the way that I store a direct link to the shader parameter and it's data.
[/quote]

That's pretty much exactly what I do. DrawOperation is the class that contains all data needed for rendering: pointer to vb / ib, pointers to a vertex / index shader pair, pointer to a data structure that stores shader information (constants and textures), and some other data. I have a special draw op pool allocator that creates them in preallocated space and deletes all of them at the end of the frame - when traversing the scene multiple drawops are created for each mesh (i.e. multipass rendering) and passed into a draw queue class which sorts them into buckets based on priority, shader, texture, distance, etc.

I see sprites as models to be honest, they just consist of a on screen quad, and a texture atlas with a current page from that atlas[/quote]

Exactly what I was saying, at the lowest level all render operations are just a vb / ib, shader, and shader data.

I'm not sure what the visitor pattern is giving you over simple C-style compile time polymorphism in which you have one .h file with different .cpp implementations for each output graphics library. You know, you have protypes like void RenderSprite(SpriteType ... ) and then have implementations for OpenGl and DirectX or whatever in separate cpp's.[/quote]

The visitor solves a different problem. What you describe is how I support multiple rendering apis. The visitor pattern makes it so that the renderer doesn't need to care about wether its drawing a sprite, a static model, and animated model, a particle system, or anything else. It just draws "DrawOperations". You can have an octree, a bsp, a list of sprites, and each has and enqueue(DrawQueue dq) method, which simply generates a list of DrawOperations from whatever data it manages. Its not a true visitor but that's how I think of it :)

This topic is closed to new replies.

Advertisement