Kotlin 146: A Framework?
Implementation inheritance, or delegation? I’ve chosen the former in a fairly big way. Why? It seems that I’m evolving a sort of ‘framework’, and to make it do what I want, I think I need this inheritance. Some may disagree.
As I predicted, Hill objects to my new arrangement with inherited implementation, and says he thinks that all such designs go better with delegation. We might have to pair or something to reach real agreement.
As I sit at the BMW dealer with dollars draining audibly from my credit card1, I want to write up what I think I’m building here, and to think about other ways of doing it. I truly welcome ideas about how to do it better.
I think we’re working toward a sort of “framework” supporting our SpaceObjects. I might describe it this way, with a bit of poetic license and/or prediction of things not yet quite done:
The Game
The game — any game sufficiently like this one — consists of visible objects in “space”, supported by invisible objects that make up much of the game logic. The visible objects are called SolidObject and the invisible I often refer to as “special”. They all act much the same way:
The Game Loop
The game loop is a cycle that runs about every 1/60th of a second. That cycle has two main “phases”: first all the objects “in the mix” get a chance to update, and then they all get a chance to interact. In each “phase”, objects get a chance to add objects to the mix, or remove them. All such operations are done in between cycles: what’s in the mix is constant during the cycle. Removed objects will be sent the message finalize
from which they may return a Transaction.
Transaction
The Transaction object contains objects to be added to the mix and objects to be removed. It accepts messages add
and remove
, which record objects to be added or removed before the next cycle. Transactions also understand accumulate(otherTransaction)
, which amounts to +=
, causing the receiving Transaction to take in all the changes from the other transaction.
Update Phase
During “update”, visible objects typically adjust their position based on their velocity or the settings of their controls. During update, the object can return a transaction adding or removing objects. A ship might fire a missile, adding a missile object to the mix, for example.
Elapsed Time
Every space object includes a member variable elapsedTime
which is incremented automatically at the beginning of update
in each cycle. Objects may freely set that value to any value for their ow purposes, and it will continue to be incremented from whatever value they set. Want to do something six seconds from now? Set elapsedTime
to -6 and wait until it’s more than zero. Whatever.
Interaction Phase
During “interaction”, each pair of objects in the mix get a chance to interact. The interaction message is sent to one or the other of the pair, and there is provision for deciding which of the pair gets to make decisions. Generally visible objects do not care, but the invisible objects always want control, so that they can “see” every other object in the mix. During the interaction, the object may return a Transaction containing objects to be added to the mix, or removed.
The interaction phase itself has three stages. Each object can elect to see any or all of three messages:
beginInteraction
— sent at the beginning of the interaction process;interactWith
— sent once to one of the elements of every pair in the mix.finishInteraction
— sent after your last possible interaction has been completed.
The latter two of these messages allow the receiver to return a Transaction adding or removing objects to or from the mix.
Common uses of Transaction during interaction include:
- detecting that the pair have collided, and returning objects to be destroyed and new objects to be added. For example, a ship-asteroid collision destroys the ship and the asteroid and adds two new smaller asteroids.
- counting items. For example, the chance of surviving a trip to hyperspace depends on how many asteroids there are. The object handling this counts them, setting the count to zero in “begin”, tallying in “interactWith”, and takes necessary action in “finish”.
- detecting presence or absence of items. When the ship is “destroyed”, it is removed from the mix. A “ShipChecker” periodically uses the interaction logic to detect this situation and create a new ship if appropriate.
Objects intended to be part of the game should derive from SpaceObject and can override any or all of update
, beginInteraction
interactWith
, interactWithObject
, and finishInteraction
. At present you probably need to implement both of the interactWith
functions if you implement either, but this requirement may be relaxed in the future. Additional classes from which to subclass may be provided for better support of common needs.
The mechanism for all this of necessity includes support for your not overriding methods you do not need in your space objects. Default operations in the superclass simply return empty results. There is never a need to invoke the super
method, though an option to returning an empty Transaction of your own might be to call super
. We advise against that pattern.
Techniques and Approaches
Since there is no central control program in the game, other than the update/interact cycle, everything that happens is up to individual objects in the mix. We’ll describe some examples here.
ShipChecker
A ShipChecker object is created with a pointer to the current ship. It uses the interaction cycle to check to see if the ship is present. If not, the checker returns a Transaction adding a ShipMaker object, and removing itself.
ShipMaker
ShipMaker uses elapsedTime
to wait some amount of time before inserting a new ship. (In our case, we actually insert the same ship as there is no need to actually destroy one and create another.) ShipMaker uses the interaction cycle to ensure that there are no objects near where the ship will emerge, so that you get a fair shot at surviving.
ShipMaker also determines whether or not the ship emerges successfully if it has been in hyperspace. If a hyperspace emergence fails, the ship is created, but also a ShipDestroyer object right on top of it, so that the ship immediately explodes.
ShipMaker always adds a new ShipChecker back into the mix, so that the next time the ship is removed, that will be dealt with as described above. ShipMaker deletes itself.
ShipChecker looks for the ship and if it is missing, creates a ShipMaker and deletes itself. ShipMaker makes a ship and deletes itself. Optionally, it also creates a ShipDestroyer to explode the ship on emergence.
LifetimeClock
LifetimeClock checks all objects to see if their elapsed time has exceeded their lifetime. (Most objects have an infinite lifetime: missiles do not.) If the object has exceeded its lifetime, LifetimeClock removes it from the mix. The object will be allowed to finalize in case it wants to make fireworks or something.
Score
Some objects may wish to add to the game score, such as when an Asteroid is destroyed. They add to the score by adding a Score
object to the mix, containing the number of points to be awarded.
ScoreKeeper
During interaction, the object Scorekeeper checks all objects to see whether they are a Score and if they are, adds their score to the total. In update
, the ScoreKeeper displays the current score on the screen.
Other Objects
There can be any number of other objects, all working more or less like the ones above. There is a WaveChecker, for example, that checks to see whether there are any asteroids left, and if not, uses WaveMaker to create new ones. These objects work quite similarly to ShipChecker and ShipMaker.
The Implementation
From the viewpoint of the game’s main and nearly trivial loop, there are just all these “space objects” that are sequentially updated and interacted. The game makes no determination of what they are: it just sends them all the standard messages.
But I want each space object to include only the code that it really needs, in order to do whatever it does. All that a space object can really do is update itself, and observe all the other objects as they go by, During that updating and observation process, each space object has several opportunities to add new objects to the mix or remove them. If all it needs to do is update, then I’d like it to be able to implement only update
and not even to implement empty versions of the other functions that it might have used but did not.
That means, as far as I can see, that there needs to be a default function “higher up” for each object that can be in the mix, doing nothing and returning as close to nothing as we can provide. I see how to do that with a superclass / subclass arrangement, and at this writing, do not see a cleaner way to do things. Some of my friends are looking askance at this implementation.2
We’re not perfect … yet.
There are still discernible flaws in what we have.
-
There are two central interaction methods,
interactWith
andinteractWithOther
, with a double dispatch in play to make sure that the special objects always get first crack at the interaction. Right now, we don’t have a default for either of those that would allow the developer to implement only one of the two. To fix this we may need an intermediate superclass for the special objects: I just don’t know yet. -
The so-called Solid objects are a single class encompassing asteroids, ship, missiles, and a few other specialized objects like the ship destroyer. It seems likely but not certain that we’d be happier with these broken out into separate classes, one per type of Solid object.
-
The interplay between objects can be a bit hard to understand, even if the individual objects are easy to read. For example, the ping-pong game between ShipChecker and ShipMaker, or WaveChecker and WaveMaker, while rather elegant (I feel) is not explicit anywhere in the code. Oh, you can read the Checker and see that it makes a Maker and vice versa … but what’s going on? It’s a bit like eating pizza. You have no idea what goes on inside, you just discover that you are somehow nourished and are feeling a bit of heartburn.
-
There is still some duplication in the space objects and in some areas perhaps too many if statements. That tells me that there’s opportunity for improvement of some kind.
But it’s interesting …
I suspect that it would be easy to use this same framework to implement Spacewar!, a game where there are two ships shooting at each other, rather than one ship shooting at asteroids. We’ll sort of see that when we implement the Asteroids Saucer, which amounts to a second ship that shoots at yours. It’ll just come down to adding in a couple of new SpaceObjects. All the game logic will be embedded in those objects and (I predict) none of it in the core game logic itself.
I didn’t intend this …
I didn’t set out with a framework in mind. I did make the decision to try to push as much of the game logic into the individual objects as possible, without requiring specific code in the “game” to deal with aspects of pay. So far, that’s almost entirely the case. The game code just gives everyone a chance to update and interact. The individuals and interactions3 get the job done.
Is there a “better” way to build objects that work as these do? Could it be done without inheritance, while keeping the individual objects this simple? Send me ideas, we’ll explore them.