The Case for Message Passing Architectures

advertisement
The Case for Message Passing
Architectures
Dave Weinstein
Red Storm Entertainment
Ubisoft
Introduction
Message passing architectures have been used in
game development for over a decade, and while
many developers assume that networking needs
drive this style, it is my contention that the
benefits in terms of overall code clarity, and
reduction of critical path dependencies make
message passing architectures useful even for
single player games.
Message Passing for Network
Traffic
Message passing architectures lend themselves
particularly well to network games.
One of the biggest risks for any multiplayer game
is the degree to which the single player component
and the multiplayer component differ. The more
the code paths diverge, the more likely it is that
fixes in one area will break another, and the less
benefit either gets from QA testing.
Message Passing for Network
Traffic: Dividing Logical Tasks
In single player games, it has been common for
the code that determines whether an action is
desired, whether an action is valid, and what the
results are to be intertwined.
These are three discreet steps that any networked
game requires separated, and message passing
architectures are no different. However, if the
game messages are divided into request, confirm,
and report steps, the code path of the single player
game and the code path of the multiplayer game
can be almost identical.
Message Passing for Network
Traffic: Fail Soft Solutions
If game flow is broken up into multiplayer friendly
discreet steps, however, multiplayer gameplay for
new features becomes almost automatic.
A generic “fail soft” enables any new feature to
work by default in a multiplayer environment, even
if the network traffic involved is higher than
optimal. By keeping the networking engineers from
having to play catch-up with the rest of the
engineering team, the amount of time that new
features during development breaks the multiplayer
component is greatly reduced.
Message Passing for Network
Traffic: Optimizations
The generic case for any message
(regardless of implementation) will almost
always be less efficient in terms of
bandwidth than a specialized version. The
more that can be assumed about the
contents of a message, the tighter the actual
network traffic can be.
Message Passing for Network
Traffic: Example, Tom
Clancy’s Ghost Recon
void
IkeMsg_GunshotMissed::WriteBin( std::ostream& outStream )const
{
ASSERT( mGameMessage );
mGameMessage->WritePayloadWithoutType( 0, outStream );
// GameObjectID
mGameMessage->WritePayloadWithoutType( 1, outStream );
// InventoryItemIndex
// compress direction vector
RSFloat2* compVec = RSFloat2::Create();
RSPayloadUtils::CompressUnitVector( (RSFloat3*)mGameMessage->GetPayloadObject( 2 ), compVec );
compVec->WriteBin( outStream );
compVec->Release();
}
Source, Tom Clancy’s Ghost Recon, copyright Red Storm Entertainment/Ubisoft,
used with permission
Message Passing as Abstraction
Layer
With the introduction of Object Oriented
programming methodologies, developers
became used to deferring the actual
implementation of a logical concept.
Message passing architectures allow us to
use the same basic principles for the flow of
the application.
Message Passing as Abstraction
Layer: Genericity
Example: C++ object oriented application
pUnit = gObjectManager.GetObject( unitID );
if( pUnit )
{
pUnit->ApplyDamage( damageAmount );
}
Example: Message passing interface
Send( kMsgID_UnitDamaged, unitID, damageAmount );
Message Passing as Abstraction
Layer: Genericity
Note that these two approaches are not
contradictory. It would be entirely
reasonable to see the object oriented
approach as the implementation of the
response to the damage message at the
simulation level.
Message Passing as Abstraction
Layer: Genericity
The message passing system works, even in a
single player architecture, for the same reasons
that we use virtual function interfaces at the class
level.
It provides a generic interface that provides a
significant abstraction layer between modules, and
from a project management standpoint, between
developers.
Message Passing as Abstraction
Layer
In the following example source code, taken
from Force 21, the User Interface is
handling the damage message, and using the
information in it to adjust the display.
Message Passing as Abstraction
Layer: Example, Force 21
case kDamageMsg:
{
for( int i=0; i < Unit::kMaxVehicles; i++ )
{
SetVehicleDamage( i );
}
// Find unit damaged and blink that button.
GameObject* obj = NULL;
if ( target.Type() == MessageArg::kMAObject )
{
obj = target;
}
else if ( target.Type() == MessageArg::kMAUnsignedLong )
{
obj = theGame.mObjDB->FindObject( ObjId(target) );
}
if ( obj && ISA(obj, Actor) )
{
ObjId unitId = ((Actor*)obj)->GetUnitId();
Unit *pU = theGame.mObjDB->FindUnit( unitId );
Force* force = theGame.mRulesEngine->GetForce( theGame.mRulesEngine->WhoDoIControl() );
if ( pU && force && unitId == force->mUnits[ (int)pU->GetForceIndex() ] )
{
if ( pU == theGame.mArbiter->GetDisplayedUnit() )
{
int damagedVehicle = (int)pU->GetVehicleIndex( obj->GetId() );
if ( mVehicles[ damagedVehicle ] )
mVehicles[ damagedVehicle ]->Blink();
}
}
}
Source, Force 21, copyright Red Storm Entertainment/Ubisoft, used with permission
Message Passing as Abstraction
Layer
By using messages as a generic abstract
interface between modules, the architecture
allows for:
• Removal of other modules from the critical
path
• Clean separation of implementation from
semantics
• Easier integration of automated testbeds
Mechanisms of Message
Passing
Message passing mechanisms vary, ranging
from a limited number of arguments
(possibly with type information inferred
entirely from the message type), to flexible
and extensible systems with an almost
unlimited set of possible arguments
(including the ability to change type from
message call to message call).
Mechanisms of Message
Passing: Example, Tom
Clancy’s Ghost Recon
// compose and send a message to the server
newMsg = RSGameMessage::Create( kMsgID_GunshotMissed );
newMsg->AddPayload( id );
newMsg->AddPayload( index );
newMsg->AddPayload( direction );
// game object id
// inventory item that fired
// direction for tracer
IRSGameMessageMgr::SendMaster( kSystemChannel_Sim, newMsg );
newMsg->Release();
// note: we have to release it since we 'Create' it here
Source, Tom Clancy’s Ghost Recon, copyright Red Storm Entertainment/Ubisoft,
used with permission
Mechanisms of Message
Passing: Example, Force 21
void
ClientRouter::SendMessage( F21MessageTarget targetType,
const MessageArg& target,
F21MessageType message,
const MessageArg& arg1,
const MessageArg& arg2 )
{
// Now, handle anything for which we do default networking
if( ShouldSendToNetwork( targetType, target, message, arg1, arg2 ) )
{
TransmitMessage( targetType, target, message, arg1, arg2 );
}
// And finally, do any processing which we need to do on this
// machine.
if( ShouldSendToRecipient( targetType, target, message ) )
{
ProcessMessage( targetType, target, message, arg1, arg2 );
}
}
Source, Force 21, copyright Red Storm Entertainment/Ubisoft, used with permission
Mechanisms of Message
Passing: Tradeoffs
Rules based systems like Force 21 provide a
simple and effective mechanism, and guarantee
that the same message is always handled in the
same way.
Use based systems like Tom Clancy’s Ghost
Recon allow for more flexibility in how a message
is used, and allow for the reuse of the same
message, at the cost of increased code complexity
and the risk of human error.
Mechanisms of Message
Passing: Tradeoffs
Fixed argument systems allow for a simpler
calling convention and for smaller base cases,
subject to the limitations of the fixed number of
arguments.
Variable argument systems increase the
complexity of the calling convention (although
varargs like helpers can help mask this), and
require more complex message handlers.
Summary: Message Passing
Architecture Benefits
• Basic networking as the normal case, rather
than a special case
• Journaling/Replays as the normal case,
rather than a special case
• Obvious integration point for unit testing or
automated test beds
• Reduction of critical-path conflicts and
inter-module dependencies
Message Passing Architectures
Risks: Performance
• There is no such thing as an “inline message”.
There is an overhead to any message traffic.
• Increased function calls result in greater stack
depth and complexity
• Optimal network usage will require special cases
for the most used messages, however, once those
cases exist, any change to a message needs to be
duplicated in the optimized version.
Message Passing Architectures
Risks: Team Management
• Team understanding and “buy-in”. If the
team does not understand the message
passing heuristics, or is not convinced of the
efficacy, the result will likely be code that is
only partly message based, resulting in the
worst of both worlds.
Message Passing Architectures
Risks: Team Management
• Lack of documented heuristics on what
should be sent as a message, how messages
should be broken up, and, depending on the
architecture, what options for sending
messages are valid when. Many of the
strengths of the system are lost if it is used
inconsistently, and bugs will result if it is
used improperly.
Message Passing Architectures
Risks: Engineering
• Sending a message should be as lightweight
as a function call to the programmer. The
more difficult it is to construct and send a
message, the more likely it is that an
engineer will construct helper functions to
build the message, obscuring the use of
messages and making the code more
difficult to follow.
Message Passing Architectures
Risks: Engineering
• Over-engineering can be a significant risk.
If there are too many possible options,
especially if they are incompletely
understood by the team, it can cause
confusion and result in the wrong options
being used (or more likely, an engineer
using the options they are familiar with
regardless of how appropriate the option is
for the task at hand)
Message Passing Architectures
Risks: Engineering
• Because messages act as virtualized
functions, it is often necessary for an
engineer to search the code base to find all
the places that use a message to determine
the full semantics of the message.
Message Passing Architectures
Risks: Engineering
• The order in which different components
react to messages may need to vary based
on the message. This presents risks as to
timing issues on a message by message
basis, and can remove many of the benefits
of message passing systems as far as
removing dependencies on other modules,
depending on how message priorities are
handled.
Message Passing Architectures:
Next Generation
Based on the categorized risks, and in order
to maximize the benefits we get from
moving to a message passing system, we
can set some requirements for what we want
out of the next generation of message
passing systems.
Message Passing Architectures:
Next Generation Requirements
• The full semantics of a message, including
information on message payload, and the order of
processing, must be in a single location, such that
any use of the message will always use the same
logic.
• Heuristics for what should be sent as a message,
how messages should be broken up, and how the
rules for messages should be determined must be
set and documented at the project level.
Message Passing Architectures:
Next Generation Requirements
• Searchable documentation should be kept
synchronized with the source code, to enable any
engineer to determine how a message should be
used and why.
• Enforcement of message semantics should be
performed, as much as possible, by the compiler,
and otherwise at run time by the code, rather than
being enforced by manual programming.
Message Passing Architectures:
Next Generation Requirements
• Sending a message must be as simple as a
normal function call.
• The engineer using a message must be able
to assume that the message definition and
messaging engine will determine how the
message should be sent and handled,
regardless of single player or multiplayer
game mode.
Message Passing Architectures:
The Next Generation
One approach to meeting the requirements of a
next generation system is to draw heavily from
Bertrand Meyer’s DesignByContract™, and apply
that model to our messaging core.
Additionally, we know that it is highly unlikely,
even if messages were fully documented before
creation, that the documentation will be manually
kept in sync throughout the development life of a
project. To meet our documentation requirements,
we will want to draw off the source/view model
used in Eiffel.
Message Passing Architectures:
The Next Generation
What follows is a sketch of what a next generation
message definition could look like. There is
currently no code to take the hypothetical .m file
and turn it into the combination of source code,
documentation, and database needed, nor is there a
messaging engine to use that information.
The example, however, is real. The sequence of a
claiming a gunshot, displaying a gunshot, and
reporting the effects, is drawn from the way in
which Red Storm has traditionally built tactical
shooters.
Message Passing Architectures:
RequestFireGunshot.m
// A human (either AI or player) has requested to fire their gun
Message: RequestFireGunshot
Descriptors: Combat, Simulation, Action, Request, Input, AI
Modes: Action::Action
Order: Simulation > DontCare
Payload:
UInt32
mShooterID
is ActorID
UInt8
mWeaponID
is WeaponIndex
IsValid:
Reception:
{ return false; }
IsNotValid:
Processing:
{ return( !SystemRulesManager::IsOwnedByLocalSystem( mShooterID ) ); }
Perform:
Transmission:
{ return false; }
Message Passing Architectures:
ClaimGunshot.m
// Claim a gunshot, and associated results.
Message: ClaimGunshot
Descriptors: Combat, Simulation, Action, Claim
Modes: Action::Action
Order: Simulation > DontCare
Payload:
UInt32
mShooterID
is ActorID
UInt8
mWeaponID
is WeaponIndex
ImpactType mImpactType
is [kImpactNone,kImpactHuman,kImpactObject,kImpactWorld]
UInt32
is ActorID if ((mImpactType == kImpactHuman) || (mImpactType == kImpactObject))
mImpactID
otherwise is ObjectManager::kInvalidActorID
Vector3
mImpactPoint
is WorldCoordinate if (mImpactType == kImpactWorld)
is LocalCoordinate if ((mImpactType == kImpactHuman) || (mImpactType == kImpactObject))
otherwise is NullCoordinate
Vector3
mImpactNormal is NullVector if (mImpactType == kImpactNone)
otherwise is NormalizedVector
IsValid:
Reception:
{ return( SystemRulesManager::IsOwnedBySystem( SenderID(), mShooterID ); }
IsNotValid:
Reception:
{ return( !SystemRulesManager::IsMaster() ); }
Perform:
Transmission:
{ return( SystemRulesManager::IsOwnedByLocalSystem( mShooterID ) ); }
Processing:
{ return( SystemRulesManager::IsMaster() ); }
{ return( SystemRulesManager::IsOwnedByLocalSystem( mShooterID ) ); }
DoNotPerform:
Transmission:
{ return( SystemRulesManager::IsMaster() ); }
Message Passing Architectures:
ReportGunshot.m
// Report that a gunshot has been fired by the specified player with the specified weapon.
Message: ReportGunshot
Descriptors: Combat, Simulation, Action, Report
Modes: Action::Action
Order: Simulation > DontCare > Sound > UI
Payload:
UInt32
mShooterID
is ActorID
UInt32
mWeaponID
is WeaponIndex
Valid:
Reception:
{ return( SenderID() == SystemRulesManager::kMasterSystemID ); }
IsNotValid:
Reception:
{ return( SystemRulesManager::IsOwnedByLocalSystem( mShooterID ) ); }
{ return( SystemRulesManager::IsMaster() ); }
Perform:
Transmission:
{ return( SystemRulesManager::IsMaster(); }
Processing:
{ return( SenderID() == SystemRulesManager::kMasterSystemID ); }
{ return( SystemRulesManager::IsOwnedByLocalSystem( mShooterID ) ); }
DoNotPerform:
Transmission:
{ return( SystemRulesManager::IsOwnedBySystem( RecipientID(), mShooterID ); }
Message Passing Architectures:
ReportGunshotHitWorld.m
// Report a bullet impact with the world geometry.
Message: ReportGunshotHitWorld
Descriptors: Combat, Simulation, Action, Report
Modes: Action::Action
Order: DontCare > Sound > UI
Payload:
WeaponClass mWeaponClass
Vector3
mImpactPoint is WorldCoordinate
Vector3
mImpactNormal is NormalizedVector
IsValid:
Reception:
{ return( SenderID() == SystemRulesManager::kMasterSystemID ); }
Perform:
Transmission:
{ return( SystemRulesManager::IsMaster(); }
Processing:
{ return( SenderID() == SystemRulesManager::kMasterSystemID ); }
Message Passing Architectures:
Example Uses
void
Simulation::FireWeapon( const SimHuman& shooter )
{
...
[Combat calculations]
...
switch( impactType )
{
case kImpactNone:
Send( ClaimGunshotMsg( shooter.GetID(), shooter.GetActiveWeaponIndex(), impactType ) );
break;
case kImpactHuman:
case kImpactObject:
Send( ClaimGunshotMsg( shooter.GetID(), shooter.GetActiveWeaponIndex(), impactType,
impactObject, impactPoint, impactNormal ) );
break;
case kImpactWorld:
Send( ClaimGunshotMsg( shooter.GetID(), shooter.GetActiveWeaponIndex(), impactType,
impactPoint, impactNormal ) );
break;
default
ASSERT( false && "Invalid impact type returned by CalculateGunshotImpact()" );
}
}
Message Passing Architectures:
Example Uses
HANDLER( Simulation, ClaimGunshotMsg )
{
// If the shooter isn’t in a position to fire this weapon, nothing will happen anyway
if( CanShoot( msg.mShooterID, msg.mWeaponID ) )
{
// Ok, they could fire, so let’s report that
Send( ReportGunshotMsg( msg.mShooterID, msg.mWeaponID ) );
// And then let’s see if we believe the results, and report those as well
if( ValidateGunshot( msg.mShooterID,
msg.mWeaponID,
msg.mImpactType,
msg.mImpactPoint,
msg.mImpactNormal ) )
{
...
[Results Calculations
...
case kImpactWorld:
Send( ReportGunshotHitWorldMsg( GetWeaponClass( msg.mShooterID, msg.mShooterWeaponID ),
msg.mImpactPoint,
msg.mImpactNormal );
break;
...
[More Results Processing
...
}
}
}
Questions?
Download