Handle Packets Globally, or Locally?

Started by
5 comments, last by hplus0603 15 years, 4 months ago
Is it alot more dynamic to handle my packets globally (through one static PacketProcessor class), or locally? (in the Player class) Right off the bat, one of the advantages to the local is that I can change my handlers (that are delegates) depending on the clients 'state' (in combat, idle, crafting, ect...). But this brings a bit of complexity and possibly a speed burden because each time i create a player class, it will have to register around 200 - 400 packet opcodes to handlers (some of which are stubs) One advantage I can see with a Global handler is simplicity. Just thought i would get you good peoples opinions. Thanks, Lordcorm
Advertisement
Quote:Original post by lordcorm
Is it alot more dynamic to handle my packets globally (through one static PacketProcessor class), or locally?


I don't know; what would it mean for one to be "more dynamic" than the other?
Quote:Original post by RDragon1
Quote:Original post by lordcorm
Is it alot more dynamic to handle my packets globally (through one static PacketProcessor class), or locally?


I don't know; what would it mean for one to be "more dynamic" than the other?


What I mean is, lets say we have 2 objects that are currently idle, then, out of the blue, one attacks the other.

Now, both objects are in the 'combat state', which means they should:
1) Not be able to craft
2) Not be able to trade
3) Not be able to talk to NPC's
4) ect...

With a GlobalHandler, I would have to do something along the lines of this (below), because I cant have a state on the global PacketHandler, because each object in the world may have a different state.

void HandleCraftingPacket(Player mPlayer, Packet mPacket){   if(mPlayer->State == COMBAT) // I have to check the state, but ill have to do it for ever object.      return;   if(mPlayer->State == COMMERSE)      return;   if(mPlayer->State == TALKING)      return;   // Horray! Crafting packet here!}


As opposed to locally, where when an object enters the combat state, it will set the handler for that packet to a stub, such as:

void PlayerStateCombat_OnCraftingPacket(Player mPlayer, CraftingPacket mPacket){   return; // stub, and possible output a message to the client.}
From a software engineering perspective your desired functionality can be achieved with a combination of two design patterns: "state"; and "subject + listener". And if you want to throw asynchronous functionality into it, add a "command" pattern.

But if you do have 1 single object interpreting data then its going to be VERY hard to perform proper unit testing on it, as its cohesion will be next to nothing and its coupling will be through the roof...
Quote:Original post by WXY

But if you do have 1 single object interpreting data then its going to be VERY hard to perform proper unit testing on it, as its cohesion will be next to nothing and its coupling will be through the roof...


How come?

The message resolver can be a global hash map, into which all other systems register their interest. For example:
struct MovementManager {  MovementManager(Dispatcher & d = Dispather::getInstace()) {    d.install(this, "move", &MovementManager::on_move);    d.install(this, "jump", &MovementManager::on_jump);  }};struct TradeManager {  TradeManager(Dispatcher & d = Dispather::getInstace()) {    d.install(this, "start_trade", &TradeManager::on_start_trade);    d.install(this, "end_trade", &TradeManager::on_end_trade);  }};struct MovementComponent {  MovementComponent(TradeManager & tm = MovementComponent::getInstance()) {    tm.install(this);    tm.moveto(this, "move", Vector3d(100, 100, 100));  }};


No coupling whatsoever, it's extensible from user side (component or manager describes what it wants), it's perfectly testable, and can optionally be completely global for simple case, or use specific instances as needed.

Quote:Now, both objects are in the 'combat state', which means they should:
1) Not be able to craft
2) Not be able to trade
3) Not be able to talk to NPC's
4) ect...


Not that this is not a networking problem, but design issue. Commands may come over network, or they may be in response to some timer or other event.

It's just a matter of how you design your logic. One simple way is to define a set of possible actions (enum or int or something similar), then for each action, define in which states it's allowable.

For simple problem, each entity than has a single state value. Whenever you perform an action, you can then check if action is allows (any action) without knowing the type of action.

If you have a context, then things get a bit trickier. For example, you can trade with any same-allied player, but not with two players currently in a duel, or if they are too far, or if they are currently in middle of action, unless they are on your friends list, and you also cannot trade with ignored players.

Trying to solve this problem generically will get result in some for of rule engine which will use some predicate logic to evaluate. Depending on the design, it may be a non-trivial task to design and implement in such a way to work reliably. This is a great source of exploits. Two players start trading through an invisible wall. Trade goes through, but complete trade message is deemed to be impossible since players can't see each other. Or similar.


By far the easiest way to start is to have each handler check the pre-requisites itself. Once you have a system running with actual logic, you will see which checks are common, and you can start abstracting those.

One way to do it is to define a set of per-action rules for simple tests (in range, same faction, in combat, etc...) for quick rejection. Then, in actual handler, check more complex conditions.

[Edited by - Antheus on November 22, 2008 9:28:58 AM]
The code you written there is an instance of the "subject + listener" GoF responsibility design pattern, what you've said about the *subject*'s potential to be unit tested is undeniable, however with all those static references lets see you perform unit testing on those listeners ;).

Also "No coupling whatsoever" I beg to differ. As you see with the "subject + listener" pattern you are simply pushing the responsibility of data processing to the referenced objects, which does not reduce your coupling, as all listeners has bidirectional dependencies to the subject which they are registered upon.

I would like to apologize as I've made a unstated assumption. Where he said "handle my packets globally" I assumed that by "handle" he meant receiving, interpreting, and reacting, which would be poor design.

The bottom line is, static methods... only use them for singletons' GetInstance() method and C library adaptions <_< (given a compiler that don't think a static method is not a regular function)
What are the static functions that would be hard to unit test?
Are you talking about Dispatcher::instance() as the default value for the constructor arguments?
First, it's perfectly possible to unit test the static function itself.
Second, it's perfectly possible to pass something non-static to those constructors instead.

When it comes to coupling, there is a real difference between coupling in interface, and coupling in implementation. Coupling in interface is necessary and desired for any computer system; interfaces is how different parts can talk to each other without being physically coupled.

It sounds to me as if you've read the GoF book recently, and are a little too emotionally attached to the nomenclature they present. When the book was new, and I read it, the reaction I had was "great, now the industry will have agreed-on names for all the things we've been doing all along." However, what I've found over the years, is that any programmer who does not already have a lot of experience when reading the book, should be kept away from writing code for six months after reading the book, because it tends to have too much influence (in the sense of "if I have a hammer, everything is a nail").

A good book to describe the difference between interface coupling and physical coupling is Lakos "Large-scale C++ Software Design." Again, it describes a lot of practices that most people have already been doing for years, but it's a good text on practices in software design that are necessary, but not necessarily taught in school. And what I call "interface coupling," I think he calls "dependency injection" or "dependency inversion," so there's still a bit of nomenclature problem...

That being said, having a central function that dispatches AND handles all messages isn't going to work well, because that function would need to be physically coupled to all objects (handling == physical coupling). However, having a central manager that maps from object ID to handler, or perhaps object ID and action ID to handler, is common and expected -- and eminently unit testable, because you can configure it with dummy inputs, dummy outputs, and make sure that the right outputs get called when you push pre-determined inputs into it.

At the lowest level, game packets typically look something like:

FRAMING (packet id, sequence number, timing, authentication, etc)LIST-OF  TARGET-OBJECT  ACTION  PARAMETERS


By necessity, the representation of TARGET-OBJECT needs to be globally understood by the networking system. That's where dispatch happens. The target-object could then, separately, dispatch ACTION, and interpret PARAMETERS in the context of that ACTION. However, it would be just as reasonable to make the format (but not necessarily semantics) of ACTION be globally understood, and part of the top-level dispatch. That neither increases nor reduces coupling; it just formulates the necessary work in a different way.

In the first case, there would be one known interface from network to object, where the object of the given ID would receive notification of the packet. Each object would then have to dispatch ACTION, which generally is class dependent. This means that the action dispatching is replicated in each class, which isn't necessarily great design -- it allows different objects to dispatch actions differently, which may confuse future attempts to change or optimize the protocol. On the other hand, registering each action for each object may mean more traffic to the global registry when new objects are created, and old objects removed, which may or may not be a performance problem. My feeling is that it won't be a performance problem, because creating/destroying objects is limited by network bandwidth, and thus can't be that frequent.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement