It's pretty clear that we're all quibbling over the details of how to implement the Observer Pattern [1,2], where the spring engine is the Subject with various AIs that are the Observers.
Let's look at the different parts in more detail, and see if it sheds light on what we need to be thinking about.
Observer: AI
update()
The update() function is called when the Observer needs to know about a change in the Subject.
We've already discussed the nature of the update() aspect of the Observer; I think we now all agree to use the handleEvent() way of doing things. The reason it made sense for the update() to be a single function (handleEvent()), is that we have a
single point of notification -- the Observer only needs to observe things in one place, which keeps code tidy, and the Subject needs to update its observers in only one way. We've already discussed many of the other advantages (other than this conceptual bonus).
Subject: Engine
notify()
The notify() function is called when the Subject state has changed and the Observers need to be notified.
As I understand it, the notification occurs on a frame-by-frame basis and results in the UpdateEvent. It also occurs as a side-effect of in game engine simulation, for example, when a unit is damaged, created, enters LOS etc.
getState()
The getState() function returns the state of the Subject.
In our case, things are a little trickier, since we're not actually interested in the state of a single object, since the engine is made up of many other objects, and
those are the states we're interested in.
That's why we have different getState() functions, and why I totally agree with Tobi:
Tobi wrote:
Thats why my preference goes to separate functions, which return either a primitive types, or pointers to structures that have a lifetime that far exceeds the single callback call (ie. UnitDef).
But the decision we have left here is: what level of
granularity can we expect for structures to be returned in the callback?
I'm of the opinion that we should choose the finest level of granularity that is reasonable, whilst loosely coupling the AI with the engine's implementation details.
Effectively, we are trying to gain information about several different object states, like Unit and Map. For example, I'm guessing we are interested in some sort of object like:
Code: Select all
class Unit {
float health;
float maxHealth;
float speed;
...
}
One approach is to ask for this object state directly.
Advantage:
* The AI can request the state of a Unit and gets exactly what it wants.
We can use inheritance when units get more complicated: old AIs can still interface with the old Unit interface, and new engines can interface with MegaUnit which is derived from Unit.
Disadvantage:
* Too much data is passed to the AI, when all it wants is a specific part of the Unit state.
A different approach would be to go for something like this:
Code: Select all
float getUnitHealth(int unitID);
float getUnitMaxHealth(int unitID);
float getUnitSpeed(int unitID);
...
Advantage:
* Only the data the AI is actually interested in is returned.
Disadvantage:
* We need to be careful that we haven't strongly coupled the AI with the underlying unit state eg, what happens if we decide to change the format of the float3 type?
* More code maintainence for the engine developers as the number of functions increase or the underlying storage types for Units change.
In either case I think it makes sense to wrap the callback in class like this:
Code: Select all
class Callback {
UnitDef* getUnitDef(int unitID);
Map* getMap();
...
}
so that we need only look to one place to see what the engine is exposing.
setState()
The setState() function is the means by which the Observer can change the state of the Subject.
So far we haven't drawn the distinction between the getState() functionality and the setState() functionality of the callback. I think that there's an important distinction here because the way we interact with setState() is conceptually very different.
One way of implementing setState() is by having multiple functions:
Code: Select all
void setUnitAttackTarget(int unitID);
void setUnitMovePosition(float3 pos);
...
This would be a way of mirroring the getState() way of doing things.
Advantage:
* sending events to the engine is easy to understand.
Disadvantage:
* If a new version of the AI tries to play on an old engine that doesn't support a function, then things break.
I think a better way though, would be to use a sendCommand() method like this:
Code: Select all
int sendCommand(int commandID, Command c);
Where the return value is the success of the sent command and commands are things like:
Code: Select all
Command
UnitCommand
UnitMoveCommand
UnitAttackCommand
...
DrawCommand
DrawLineCommand
DrawCommentCommand
...
Advantage:
* We get all the advantages of the handleEvent() arguments, but on the engine side. In particular, we have
one point of interface to debug when things go wrong, making exception handling much easier.
* The engine can deal with Commands however it wishes -- if an unknown command is sent (because we're using a new AI version on an old spring engine), the engine can choose to ignore the command.
* Since we'll be using handleEvent(), it nicely mirrors the way the two parts interface.
* We're making use of the well known Command pattern [1,3].
* I hope this is how the user interface works already!
Disadvantage:
* Maybe more complex to understand at first?
In my opinion, the sendCommand way of doing things makes more sense, since it's more future-proof, and actually opens us up to other interesting ways of interacting with the engine: for example, we could start having text-based control, rather than mouse based (imagine ASCII spring!)
edit: I forgot to include the addObserver() and removeObserver() functions; this is obviously the addAI(), and I'm not sure we have a way of removing AIs, though I don't think this matters.
edit: typos.
[1] Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson and Vlissides
[2]
http://en.wikipedia.org/wiki/Observer_pattern
[3]
http://en.wikipedia.org/wiki/Command_pattern