Skip to main content
gamedev.net
🔒 Locked

communicating between engine modules

Started by hick18 Mar 17, 2011 at 10:52 PM 2 replies 1.7k views
Original Post
hick18
hick18
I was hoping to get some ideas on how engine modules should communicate with each other. For example, if I had written a c++ "script" ( I dont use a scripting language yet ) for the implementation of some hand held weapon, maybe the fire() functionality I would need to process collision, sound, HUD, animation etc etc.

Whilst the collision can be queued to some "Collision Manager" for batch collison and then later responce, I may need the results there and then, say for updating the HUD to show when an enemy is locked on. The problem is that I need access to all these elements, and Im not sure how to go about it. I invision the script being something like


void OnUpdate()
{
RayCastResult = CollisionManager.RayCast( WeaponLineOfSight );

if( RayCastResult.Hit )
{
if( RayCastResult.Item == Enemy )
{
Hud.SetCrossHair(RED);
}
else if( RayCastResult.Item == Team )
{
Hud.SetCrossHair(GREEN);
}
}

if( Weapon.AmmoCount <= LOW )
{
Hud.SetItemImage( "LowAmmoImage" );
}
}

void OnFire()
{
RayCastResult = CollisionManager.RayCast( WeaponLineOfSight );

if( RayCastResult.Hit && RayCastResult != Team )
{
if( Weapon.AmmoCount )
{
Animation.SetCurrent( "Fire", Restart );
SoundManager.PlayClip( "SingleBurst" );
AIManager.DespatchMessage( WeaponSpecs ); //alert NPCs
ParticleManager.AddSystem( "SingleFire" );
}
else
{
if( Weapon.Clips )
{
Reload();
}
else
{
Animation.SetCurrent( "Empty", Restart );
}
}
}
}



For lack of better names, should thes Managers be global? or passed along?

Is there a better approach?

I see problems in the above system in making it scaleable in terms of multi-threading. If each script can access the managers like this then I wont be able to batch stuff unless I use a lot of locking. Yet I dont know how else to go about it.
Hodgman
Hodgman

For lack of better names, should thes Managers be global? or passed along?
Definately passed along.

Either pass the required systems into the gun's constructor, or pass them into the update. A handy pattern for this is "contexts":struct UpdateContext
{
CollisionManager& collision;
SoundManager& sound;
};
void Gun::OnUpdate( UpdateContext& context )
{ ...
I see problems in the above system in making it scaleable in terms of multi-threading. If each script can access the managers like this then I wont be able to batch stuff unless I use a lot of locking. Yet I dont know how else to go about it.[/quote]To make the managers accessible by multiple threads, make all of their public functions write messages into thread-safe queues, and return a future value. Later, the message will be executed and the future will contain an actual value. In my system, if you post a message that contains a future value as an argument, then that message won't be executed until the future becomes valid.
Your gun example would look something like:void Gun::OnUpdate()
{
Future<RayCastResult> result = CollisionManager.RayCast( WeaponLineOfSight );
OnLineOfSight( result );
}

template<class T>
void Gun::OnLineOfSight( T value )
{
if( IsFuture<T>::value )
m_ParentManager.DelayedMessage( this, &OnLineOfSight_, value );
else
m_ParentManager.Message( this, &OnLineOfSight_, value );
}

void Gun::OnLineOfSight_( RayCastResult )
{
if( RayCastResult.Hit )
{
if( RayCastResult.Item == Enemy )
{
Hud.SetCrossHair(RED);//n.b. these Hud calls are actually sending messages too
}
else if( RayCastResult.Item == Team )
{
Hud.SetCrossHair(GREEN);
}
}

if( Weapon.AmmoCount <= LOW )
{
Hud.SetItemImage( "LowAmmoImage" );
}
}
hick18
hick18
Ah, so in effect, its like a callback system. You request a value, and get called back when that value is avaliable?

What happens when you need more than one value though? Such as a query from your AI system and your collision system, that needs to be assessed before you can take another action. Do you have some kind of "WaitOnMultipleValues" implementation?

How did you implement the member function callbacks? Like this?

Having thought about it some more, I realised that the bulk of the queries that would be performed are imutable, in that using a RayCast query should be thread safe as you're only really reading data from whatever structure your collision system happens to use. Same with AI too. But then again, depending on the implemetations, there might be cache optimzations in use which would mutate the system on each call.
Gorbstein
Gorbstein
One way to attack this is to use the Service Locator pattern. >Linky<. I haven't tried it with threads but I imagine it would still be doable.

You have a Service Locator object which is created at application start. On initialisation of the app you register each game service with it.. ie: sound, graphics, physics, logging, etc.

Logger* log = new Logger();
MyGraphicsEngine* graphics = new MyGraphicsEngine(800,600,32......);
serviceLocator->registerGraphicsService(graphics);
serviceLocator->registerLogService(log);

A reference to the Locator passed to all entities in their contructor or Init function..

Baddie b = new Baddie(serviceLocator,type,position........);

..or you could pass it each Update if you prefer they didn't store the reference.

Entities can then request services from the locator like this:

serviceLocator->getGraphics()->drawMesh(....);

or

objectsInRange = serviceLocator->getWorld()->scanForEntities(...);

The advantages are:

- if you add a new feature/module/service to the game engine, such as a sound engine or a logger, you don't then need to go around all of your entities and alter the parameter lists to pass it in specifically. Everything will have automatic access to it through the serviceLocator.

- if you implement it properly by using interfaces/virtual classes for your services, you can create different versions of your services and swap them in really easily. So you could add in a null logger that ignores log writes, or a sound module that uses a different underlying sound engine. And the rest of your engine wouldn't change at all.

- you can have more than one service locator, so you could have one half of your game rendering in openGl and the rest in another window in directx for example...

The main disadvantage is that it becomes more difficult for services to return specific objects without them being defined and available to everyone. If your world service passes back a BladeOfGrass object then potentially your whole app knows what a BladeOfGrass is and can depend on it. Either that or you start using void* a lot...

D

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.