Saturday, February 18, 2012

Game Object Models

Right, so, continuing on from where we left off last time...

So, we discussed polymorphism.
The Game Object Model exploits polymorphism. In this model, each game object is a member of its own class. However, each discrete class shares a common ancestor, the Entity class. An Entity is any discrete object that is drawn to the screen and does per-frame logic. So, the Entity class provides virtual methods for Update and Draw. The Draw method can be written once in the Entity class, based on the needs of the game (a 2D game would wrap Sprite.Draw, a 3D game would wrap Model.Render). Update can be overwritten in each subclass, so Enemy objects would do collision detection on the Player's bullets, and the Player object would check if it's colliding with any Enemy objects. Subclass-specific logic and variables would be tracked by the subclasses, so the Player would keep track of its firingCoolDown.
Then, to easily organize every game object, we have an EntityList class that stores every entity in a scene. We can keep a single list of all the entities (I would recommend either a linked list or hash table, for the linear-time traversal and the constant time addition/removal) which we iterate over every frame, and call whatever per-frame methods we need (note: simplification; more on this later). Having a base Entity class lets us store everything in a Container, and calling Update and Draw methods (and possibly others) in sequence on them.
That is an extremely clean interface, and we owe it to the Game Object Model. The Game Object Model is built on top of understanding OOP principles.

That said...
Having every discrete object be its own class is a first-level understanding of OOP principles.
One of the key tenets of OOP involve not repeating yourself, but if Player and Enemy both inherit from Entity, we'll need to reproduce a lot of functionality between the two. Say both of them react to colliding with tiles on the map in an identical fashion. We could write the TileCollision function twice, which is uncomfortable. We could realize that a Player has a lot of variables in common with an Enemy, (they both have health, an enemy drops numCoins on death while a player uses their numCoins to purchase from the shop, and so on) so we think we can inherit the player from Enemy instead of directly from Entity. But this violates one of the major principles of deciding when to inherit classes, specifically when the two have an "is-a" relationship. A player is not an Enemy, so this is another uncomfortable solution. Then we decide to move the TileCollision function into the Entity class itself, because it is a fairly common functionality and we can use it to have dropped Coin objects bounce off as well. This is also non-ideal because soon the Entity class will be swollen with shared functionality, and possibly many variables that some subclasses simply don't need.

The solution? The Game Object Component Model.
More Object-Oriented Programming principles include include making classes that have one and only one responsibility. So, the GOC model would abstract the TileCollision function into a TileCollider class, which does nothing but detect and handle collision with tiles. Then an Entity can be created by aggregating the various components. We can build an Enemy with SpriteDrawer, TileCollider, CoinDropper, ShotCollider, and EnemyPattern components. Each component, then, would handle its one responsibility. The various components can be reused as appropriate, even from project-to-project.

There's many details that need to be considered when using the GOC model, mind. Firstly, there needs to be some mechanism to synchronize the components; a ShotCollider and PhysicsSimulator and ModelRenderer all need to agree on what the current position of the object is. This synchronization is commonly accomplished through message passing and common parameters. 
Secondly, there is an overhead to heirarchizing code, always. Putting code into a function invokes an overhead whenever the function is called. This is rarely an issue with large or infrequently called functions, but for very small functions called repeatedly, the function call overhead can be very noticeable (this is why SDL doesn't have a putpixel function, preferring to have you manually access pixels[x + y*stride] each time; 800x600 = 560,000 function overheads, per frame, which is very slow).

Implementation details can be referenced elsewhere; I just felt like describing the object-oriented thought process behind the evolution of the component model.
... I also wrote all this up so I could say the following: How did I just now learn about this? It's taken me this long to internalize OOP practices to even realize that I could do better than the Game Object model. I never learned this in class. Turns out the GOC model is somewhat industry standard now. There should be a course offered that teaches standard techniques in production code like this. Make it an elective because "we're not a vocational school", but offer it because it should vastly improve the quality of code that recent graduates write.

No comments:

Post a Comment