A Better Way

Started by
21 comments, last by EmrldDrgn 15 years, 9 months ago
Quote:Original post by EmrldDrgn
Um... nuh-uh. Fire tile changed to ice tile, both ways:

My way - Make new img file, change path/index to it, change applicable parameters. Run.
Factory - Make new img file, change path/index to it in code, change applicable parameters in code. Change file to new "token". Add new token to Factory method (so it knows what to do with it). Recompile. Run.

Or am I completely off-base here? I'm very confused...
The factory is capable of taking the path from a/the file, or accepting it as a parameter; it doesn't need to be hard coded.

There are a number of different ways to create factories depending on what you want them to do, but essentially their job is this: They move the job of creating new instances away the point where you need an instance to be made.

So your script reading function is parsing this file, identifies that a tile needs to be made. You can either put the code for creating new tiles/sprites directly into that script reading function or move that job out to a factory and instead have it make a call to the factory.
Either way your script reading function still needs to collect up the parameters from the file (the file structure needn't change at all) but instead of it then figuring out how they all work together to create a tile, it just chucks all the parameters at a factory and gets back a newly constructed tile.

Quote:Should have mentioned - I meant example in code (wow, same tag, same emphasis, totally different point).
Hmm, I'll let someone else get that. Unless you had something in mind? I'm not sure I understand how showing you a renderer will help demonstrate its usefulness over simply explaining why it's useful as a concept - code is just an implementation of a concept.

The difference is essentially between:

me.draw();

and:

draw( me );

Or, in point of fact, often:

draw( meta_description_of_me );

This is because the renderer doesn't actually need to know me to draw me, all it needs is a visual description of me. There are many more facets to me than just what I look like (phew!) but the renderer is shallow and only cares about my looks.

Quote:Light doesn't know anything in that situation... it hits the apple and is scattered. The apple does the action. Fail. Lol.
The light is the one being scattered, not the apple, so light is the one doing the action. When the light hits the surface of an object (an apple or otherwise), it inspects the surface properties (angle, opacity, diffuse colour, specular colour, etc) and makes a decision about what to do based on that - Light is making a decision about itself! If you consider light to be an object (in the OO sense) then there's no reason why the apple object should be making behavioural decisions for the light object.

Quote:Plus that'd be sub-optimal anyway, since the Light class would need to define different behaviors for each possible Object. Much better to use a virtual metrhod in the Object base class. But I digress...
Re the bit in bold: Many renderers are somewhat similar to that:

renderer.drawPlayer(..);
renderer.drawMonster(..);
renderer.drawTile(..);

It's not necessarily the best way but it still has the benefits over having the player, monster and tile all drawing themselves. The downside is the need to modify the renderer whenever you want to add a new type of renderable object - although you'd still be having to implement a new render function if you had objects draw themselves anyway.

A more generalised renderer is possible, one that doesn't have to know about different object types (which actually violates the open-closed principle anyway)...

Quote:In this theoretical implementation, how does one tell the renderer about all the objects that need to be rendered, and how they need to be rendered, so that it can sort them and achieve this oh-so-magical optimization?
I gave a crude example previously in this post with the renderer that only cares about my visual description.
The premise is to find a generalised - data driven - way to describe the visual appearance of an object you want rendererd. In the case of a tile engine, that's easy, it's just the name/index of a sprite to render with along with some way to represent the position is needs to be drawn; that's all the renderer needs to know (and this information could be kept in the sprite_set).

In the case of a more sophisticated renderer you need to generalise a lot more and so it will need to know a lot more information: like its geometry, its translation/rotation/skew, which shader to use, how many lights there are affecting the object, whether it emits light itself, how it reflects incoming light (diffuse, specular, reflective?, etc), its opacity, its subsurface properties for refraction, its texture(s), whether this object is big or small, whether it's moving quickly or slowly, the distance from the camera, how old the object is, and so on. All these things are abstracted away in different ways (like Material and LightModel classes) but at some point they're accumulated and all come together to describe a renderable object and get fed to/retrieved by the renderer.

Quote:And doesn't the massive amount of data that you'd need to pass to this class sort of break encapsulation?
No because we know nothing about the internals of any class. All we know is that by using public interfaces we can build or retrieve enough information to describe the visual appearance of an object. Again for a tile engine, that's easy, example:

tile   t = field(x, y);                      // get the current tile from the mapsprite img = sprites[ t.name() ];            // get the sprite from the sprite_setvector pos(x * tile_width, y * tile_height); // calculate where to draw itenqueue(img, pos);                           // add to the render queue// later once all sprites are enqueued:optimise_states();                           // sort queue to optimise state changesdraw();                                      // draw everything enqueued
Advertisement
Quote:Original post by EmrldDrgn
But then I would have all these useless data parameters floating around. Taking your example, what about tiles that don't hurt you when you step on them? Their "do damage on touched" attribute would be to zero, and I'll probably have more complicated types than that (not to mention I'll be adding them as I go along, which is much easier to do with derived classes). It seems like an inelegant solution at best. But maybe there are no elegant solutions here.
Yep, it's slightly wasteful; but ultimately you may find it results in a nicer overall system than building behaviour that differs depending on whether the tile is of HurtingTile type. Instead of having to do any kind of RTTI, or polymorphic behavior, you just have a value that is often zero. It's an extra memory cost per tile in exchange for a big simplification in design and execution (and simplicity usually means speed).

Quote:Let's say, just as an example, that I have a file which defines a lava tile. This file contains a string for the path to the tile sheet, an integer for the index on the tile sheet, and a token of some kind (say a string) that identifies it as a "hurting tile". It would look something like this:

"data/tiles.tga" 4 "hurtingtile"

Now my program reads that in, and creates a new HurtingTile, passing the file path to the tile's Image property which loads the image.

Now suppose I wanna change it to a frost tile. I just change the index to, say, five, and I'm done. Obviously there would have to be a lot more properties in the file (damage dealt, etc.) in order to make this useful, but the point is I haven't touched code, and would only have to if I was adding an entirely new type of tile (say, a healing tile). With your way, I most certainly would have to, no matter what. Do you see what I mean?
That example is not the 'changing type of tile' that I referred to, nor is it relevant to the factory pattern. The point is that you have a factory that takes a string such as "hurtingtile" and returns a new HurtingTile() instance - the graphic that you use for the tile is irrelevant. As you say, you touch the code if you're adding "an entirely new type of tile" - that's what we're talking about here. The texture you use to render the tile is trivial and unrelated.

Quote:Light doesn't know anything in that situation... it hits the apple and is scattered. The apple does the action. Fail. Lol. Plus that'd be sub-optimal anyway, since the Light class would need to define different behaviors for each possible Object. Much better to use a virtual metrhod in the Object base class. But I digress...
You're both wrong. Neither the apple nor the light have any concept of reflection; it is the molecules - the representation of the apple in the context of the light - that do the reflecting. The way the apple looks has no bearing on how it tastes or smells, on its position or orientation.

Truth be told, there are only a very small set of things that your renderer ever needs to be able to render:


  • A subrectangle of a texture

  • ...?


That's all that the renderer ever needs to actually operate on. Instances of that object are the representatives of everything in the game in the context of the renderer.

You suggested implementing such a thing as a Draw() method on the game objects. Presumably to avoid repeating code, you propose to encapsulate this kind of drawing in the base class. But what if you want to draw two subrectangles? Maybe you've got an object that takes up 2 tiles, or 4 tiles, or is hovering in the air and you want to render its shadow. If your base Draw() method renders the object in its current position, you've now got to override and replace the whole of that method with something that does this custom behavior - and maybe you can explicitly call the base class to take care of one of your tiles, but then you have to implement the others yourself. That's horribly asymmetric, and if your base class's method is using state then it's potentially going to end up extremely messy.

Quote:In this theoretical implementation, how does one tell the renderer about all the objects that need to be rendered, and how they need to be rendered, so that it can sort them and achieve this oh-so-magical optimization?
Here's one possible pattern: When created, each object's constructor could take a reference to a Renderer object. The object has some graphical representation, so it gets a new Sprite() object, passing the Renderer to the Sprite() constructor. The Sprite() constructor does the work of registering itself with the renderer. The game object then sets the necessary texture and position on the Sprite object and stores it for updating later (in the event that the tile moves, or changes texture, or whatever). When it comes to drawing all the tiles, the Renderer has a list of all sprites that were registered with it, that it can just loop through, or sort, or whatever it wants.

Quote:And doesn't the massive amount of data that you'd need to pass to this class sort of break encapsulation?
No, the point is that the game object shouldn't be encapsulating this information; its physical representation (its 'avatar', if you will) encapsulates that. The avatar might know how to render itself, but it's part of the rendering system; that's up to it, and not relevant to the game object.

Also, please watch your tone. Making sure you don't take things blindly is one thing, but comments like "Fail. Lol." and "Um... nuh-uh." are obnoxious and should be avoided.

Richard "Superpig" Fine - saving pigs from untimely fates - Microsoft DirectX MVP 2006/2007/2008/2009
"Shaders are not meant to do everything. Of course you can try to use it for everything, but it's like playing football using cabbage." - MickeyMouse

Well, it seems like we're just going back and forth here, so I guess I'll call it. I've certainly got a lot to think about, but at least now I can make an informed decision. Thanks a lot, everybody. I appreciate it.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~I program in C++, on MSVC++ '05 Express, and on Windows. Most of my programs are also for windows.

This topic is closed to new replies.

Advertisement