Interesting design question
I have an interesting design question for people to mull over.
In my masters thesis I proposed a software architecture for use in electronic games (I just submitted it for review, so I'll publish it soon for people to read - and flame :) It's 90 pages + 250 pages of appendix so it should make good kindling) . At its highest level it is a data-centered architecture where game data is in a central data store, and technology component attach and operate on that data.
A Simple example:
Game Object Contains:
- Position
- Orientation.
AI tech component Requires:
- Object Position
Graphics tech component requires:
- Object Position
- Object Orientation
Both tech components will attach to the data component and request position data from an object.
Now for the design question:
The tech components are designed and developed independently from each other and the data component. One may be expecting position as x,y "int", while the other wants x,y,z as "double". A tech component might even want a "location" and not "position" (i.e. wording is different).
How can the tech component get the data it wants from the object?
Sol'n 1:
Objects implement an interface for each tech component. E.g. the object implements a "getLocation()" function that returns 2 integers so that tech component can call that function to get the data it needs.
Pros:
- Fast execution. Data access is a function call (as opposed to an SQL-like query).
Cons:
- Lots of duplication. For each component that requires position data, the object must implement a seperate interface specific to that component.
- Not easy to use, everytime you add a new component, new interfaces must be implemented.
I'm looking for other possible design solutions that people can think of for a follow-up paper.
Any ideas would be appreciated.
-Thanks,
Jeff
This problem creeps up on me all the time. My method of handling it is pretty cheesed-up and definitely a hack, but... why not implement the position/orientation data in its own helper-struct, with all public members. Then the object could implement a single method that passes out a copy of the data to the techs to be used as needed.
I'd love to hear about some ideas for this myself =)
I'd love to hear about some ideas for this myself =)
The game object must be implemented with only limited regard for the components that may depend on it. The reason is that there is no way for the implementer of a game object to anticipate its every possible use or every possible context in which it will be used.
One alternative to your solution is to put all the responsibility for compatibility in the component. That is, if the component needs a 2D integer position, then it must implement a way to convert from the game object's representation of its position. If the component needs a "location" rather than a "position", it is up to the component to convert a request for a "location" into a request for a "position". This design is especially appropriate if you anticipate creating new components.
What if the component is also fixed? In that case you would implement a mediator between the component and game object.
One alternative to your solution is to put all the responsibility for compatibility in the component. That is, if the component needs a 2D integer position, then it must implement a way to convert from the game object's representation of its position. If the component needs a "location" rather than a "position", it is up to the component to convert a request for a "location" into a request for a "position". This design is especially appropriate if you anticipate creating new components.
What if the component is also fixed? In that case you would implement a mediator between the component and game object.
I'm currently working on a design for discovery at component connection time. i.e. When the components are loaded, they handshake and figure out how to get the data.
Easier said than done though. Hard to keep things simple enough that a game developer can implement it easy enough so they won't say "I know what graphics engine I'm using, its easier to just tie the two together"
Fun stuff.
Easier said than done though. Hard to keep things simple enough that a game developer can implement it easy enough so they won't say "I know what graphics engine I'm using, its easier to just tie the two together"
Fun stuff.
You could consider using a form of dynamic typing, instead of directly using int or whatever, return a variant type which can automatically convert to whatever type the client wants. Performance wise, its only a couple of extra tests. Alternatively you could implement a pre-process that could automatically add type conversions where needed and do everything at compile time (this would be a lot of work though).
You could then implement a simple key or property system:
Variant getProperty(const string& property);
Again, you'll have to consider performance, it might be possible instead to get the property only once and use that,
(presuming C++, it's much easier in other languages):
class Client
{
Property& m_MyProp;
Client( Data& data ) : m_MyProp( data.getProperty("location" )
{
}
void foo()
{
Vector2 location = m_MyProp.Vector2();
}
};
You could then implement a simple key or property system:
Variant getProperty(const string& property);
Again, you'll have to consider performance, it might be possible instead to get the property only once and use that,
(presuming C++, it's much easier in other languages):
class Client
{
Property& m_MyProp;
Client( Data& data ) : m_MyProp( data.getProperty("location" )
{
}
void foo()
{
Vector2 location = m_MyProp.Vector2();
}
};
The way that I've solved this problem is as follows:
Each subsystem in the engine provides an interface which can be implemented to add its functionality to an object. For instance, we have IInteractable, IRenderable, IAudible, etc.
Each of these interfaces declares pure virtual functions which must be overridden to return the proper data needed by the subsystems. There is some overlap here, as IInteractable::GetPhysicsPosition() and IAudible::GetAudioPosition() most likely end up doing the same thing in the child object. However, this allows the child object to store its position and orientation data however it sees fit. It is reponsible for returning it in the formats necessary for each of the subsystems.
The one exception is IRenderable, which actually stores the position and orientation information in its associated scene graph node. This is a result of these interfaces being added to the system late in development, after our previous solutions were found to be inadequate. Refactoring the Graphics system to pull the data from the IRenderable objects would have been a nightmare, and was deemed unnecessary.
Also, I think the Variant return type may be a bit overkill. In all situations data such as position and orientation can be stored as floating point values. If it needs to be integral, it can be casted at the site of retrieval and then stored appropriately. If the casting to integral data is too expensive, then another interface could be required which returns the data in the correct format. Although, chances are, the data will be internally stored as floating point and just converted before it is returned... defeating the purpose.
I'd also like to point out that the mechanism which was presented by JuNC for retrieving properties moves variable name typo errors from compile-time to run-time. That is definitely a Bad Thing.
Each subsystem in the engine provides an interface which can be implemented to add its functionality to an object. For instance, we have IInteractable, IRenderable, IAudible, etc.
Each of these interfaces declares pure virtual functions which must be overridden to return the proper data needed by the subsystems. There is some overlap here, as IInteractable::GetPhysicsPosition() and IAudible::GetAudioPosition() most likely end up doing the same thing in the child object. However, this allows the child object to store its position and orientation data however it sees fit. It is reponsible for returning it in the formats necessary for each of the subsystems.
The one exception is IRenderable, which actually stores the position and orientation information in its associated scene graph node. This is a result of these interfaces being added to the system late in development, after our previous solutions were found to be inadequate. Refactoring the Graphics system to pull the data from the IRenderable objects would have been a nightmare, and was deemed unnecessary.
Also, I think the Variant return type may be a bit overkill. In all situations data such as position and orientation can be stored as floating point values. If it needs to be integral, it can be casted at the site of retrieval and then stored appropriately. If the casting to integral data is too expensive, then another interface could be required which returns the data in the correct format. Although, chances are, the data will be internally stored as floating point and just converted before it is returned... defeating the purpose.
I'd also like to point out that the mechanism which was presented by JuNC for retrieving properties moves variable name typo errors from compile-time to run-time. That is definitely a Bad Thing.
Shaft, you've got the right idea. Typically, all major components/packages of an application should be logically seperated via an interface or adapter. The reason for this is that you may want to change a component later on in production. Since everything is accessing one interface, all you have to do is modify the way the interface interacts with the new component. Granted, you may need to create multiple interfaces but it's better than having to change every reference in the application if there's a major design change later on.
Quote:
I'd also like to point out that the mechanism which was presented by JuNC for retrieving properties moves variable name typo errors from compile-time to run-time. That is definitely a Bad Thing.
True, but it could also move *correcting* the errors to runtime as well :)
And if you really wanted you could just
#define LOCATION "location"
and use that instead. The idea was to give each data object to present a dynamic interface which is resolved at runtime (so for instance the data object could be dynamically loaded, or distributed over a network).
Just thought I'd post this to people reading this topic. My thesis is currently in draft form, and not ready to be publicly published till next week sometime. But for those interested, you can read the draft at:
http://www.jeffplummer.com/Writings/Writings.htm
The topic is:
"A flexible and expandable architecture for electronic games"
http://www.jeffplummer.com/Writings/Writings.htm
The topic is:
"A flexible and expandable architecture for electronic games"
I wrote two pages and accidentally touched the touchpad on my laptop, which was oddly hovering over the X in my browser tab. It's done that a few times before... *suspicious glare*
In any event, a protocol I think you might like to consider would be to have your basic object request, but instead of an interface, just pass the db a lambda function describing exactly how you would like your objects served up. e.g.
Pros:
Flexible -each call can define how it wants its object.
Don't ever need to change db code except to reflect bugfixes.
Distributable over a network.
Cons:
C++ can't really do this.
Distribution of code makes compile time checking more difficult, though you can write unit tests to parse your lisp/scheme and verify calls to the db which is a.)not very difficult. and b.) just as good if not better (it can even run queries).
Issues:
Security is a factor since the db might run unsafe code unless you screen it somehow.
Social acceptance of S-Expressions may force you to use Water.
In any event, a protocol I think you might like to consider would be to have your basic object request, but instead of an interface, just pass the db a lambda function describing exactly how you would like your objects served up. e.g.
(let ((s (make-xy-coord) (id 345))(db-obj-request id s 'xy-coord 'not-strict ;not-strict will allow xyz coords too (lambda (x y z-notused) (set-xy-coord-x s x) (set-xy-coord-y s y))))
Pros:
Flexible -each call can define how it wants its object.
Don't ever need to change db code except to reflect bugfixes.
Distributable over a network.
Cons:
C++ can't really do this.
Distribution of code makes compile time checking more difficult, though you can write unit tests to parse your lisp/scheme and verify calls to the db which is a.)not very difficult. and b.) just as good if not better (it can even run queries).
Issues:
Security is a factor since the db might run unsafe code unless you screen it somehow.
Social acceptance of S-Expressions may force you to use Water.
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement