• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
noizex

Network message solution for client/server architecture

4 posts in this topic

Hello,

 

What are most common approaches for solving network messaging issue for client/server architecture (more towards online RPG game than first person shooter, so amount of messages is much bigger because there is a lot more going on in the world)? By message I mean communication between client and server (back and forth).

 

First method - Message classes

 

So far I explored Ryzom and Planeshift MMOs sources and they seem to solve this by having a shared code between client and server that has a Message class that is then inherited by every message (ClientLoginMsg, CharacterSelectMsg, GetInventoryMsg). Obviously each message will be used in two ways - the sender will serialize message into bitstream thats sent over the network, and receiver will deserialize it into something usable (class members, data structures etc.). So it looks a bit like this in pseudo-code:

 

class Message
{
}

class LoginAuthorizeMessgae: Message
{
    string user;
    string pass;
    BitStream out;

    // Assume that BitStream has already MESSAGE_ID read from it, so all thats left is message content
    // Because we read MESSAGE_ID, dispatcher knows which message to instantiate and it can be sent to
    // proper handlers
    LoginAuthorizeMessage(BitStream* in)
    {
       in.read(&user);
       in.read(&pass);
    }

    LoginAuthorizeMessage(string username, string password): user(username), pass(password);
    {
       out.write(MSG_AUTHORIZE_ID);  // write message id
       out.write(user);
       out.write(pass);
    }
}

// Then, client side:

void SendAuthRequest(login, pass)
{
    LoginAuthorizeMessage msg(login, pass);
    dispatcher->Send(msg);
}

// Server side:

void ReceiveAuthRequestHandler(Message* msg)
{
    LoginAuthorizeMessage* msg = (LoginAuthorizeMessage*)msg;
    if (db->auth(msg->user, msg->pass)
    {
       // ...
    }
}

 


Problem with this approach seems to be insane number of classes that need to be maintained. Just creating a simple response message with one text field requires declaring class with 2 constructors, then its .cpp file etc. Pros of this method is that same messages can be used by both, receiver and sender, so the code can be shared between client and server. All the serialize/deserialize logic is in one place, so its easier to maintain and harder to make some mistake because you see code for both, serialization and deserialization.

 

I came to even more advanced solution than the one above - I have a factory class that can create an instance of a correct class based on MESSAGE_ID, then I have an event dispatcher that is able to send that exact instance to receivers. A bit of a code snippet to demonstrate:

 
// =================
// NetworkManager
// =================

NetworkManager()
{
   m_MsgFactory->Register<SampleMessage>(NetMessageType::MSG_SAMPLE);
}

NetworkManager::HandlePacket(Packet* packet)
{
   msgID = GetPacketMsgId(packet);
 
   // Create a class thats registered for a given msgID
   // - for example for MSG_SAMPLE it will instantiate SampleMessage class and pass
   //   bitstream from packet.data into its constructor

   NetMessage* msg = m_MsgFactory->Create(msgID, packet.data);

   // This call sends that message to methods that are registered to handle it
   // I register methods instead of msgID, so the method is called with exact
   // class that it needs (so SampleMessage, instead of Message). It doesn't 
   // require explicit casting in handler method)

   m_MsgDispatcher->DispatchEvent(msg);
}

// =================
// SampleMessage
// =================

class SampleMessage: public NetMessage
{
public:
   int a;
   float b; 
 
  SampleMessage(RakNet::BitStream& msg): SampleMessage()
   {
      msg.Read(a);
      msg.Read(b);
   }

   SampleMessage(): NetMessage(NetMessageType::MSG_SAMPLE)
   {
      // This sets MSG_SAMPLE id for this message, so we 
      // dont have to care about it later
   }
};

// ====================
// SampleMessageHandler
// ====================
bool Init(NetMessageDispatcher& dispatcher)
{
    dispatcher.Subscribe(this, &SampleMessageHandler::HandleSampleMsg);
    return true;
}

void HandleSampleMsg(const SampleMessage* msg)
{
    std::cout << "Received sample message: " << msg.a << "\n";
} 

This works really nicely, but its probably a bit slower than some direct method (that clever event dispatcher that sends exact instance instead of passing Message* which allows me to register methods that receive directly what they want is probably doing some casting that could be avoided, although these are static_casts). Also it doesn't help with a problem of enormous amount of very small classes that may be hard to keep in order. 

 

Second method - sendXX methods to serialize some data to bitstream, and directly reading from message in receiver

 

This is method I've seen in some SWG emulator, but it was emulator and I only saw server code, so don't know how well it would work with client and if it was shared in any way or it was separate. In that code, there was a huge MsgLib.h file that defined a lot of methods like:

 


void sendPlayerPosition(Player*, Client*)
void sendServerTime(double time, Client*)
void sendInventory(Inventory*, Client*)
void sendChatMessage(message, channel, Client*)

and so on. Each of this methods serialized required data into a network message (msg.addUint32, msg.addString, msg.addFloat etc.) and sent it directly using provided Client.

 

void HandlePlayerPosition(Message* msg)
{
   msg.getFloat(&player.x);
   msg.getFloat(&player.y);
   msg.getFloat(&player.z);
}

 

This means that writing and reading happens in totally different places (server or client) so code is not really shared, all thats shared is opcodes or message ids because both sides need to be aware of them. 

 

 

The question

So, the question is, are there other methods? Are methods I described good enough and what can be done better? Am I doing something really wrong here? I find it hard to gather any knowledge on this topic, there seem to be no books on this, and only help were source codes of these three MMOs I mentioned at the beginning.

 

Thanks for any input! It would be very interesting to see how others solve this.

 

0

Share this post


Link to post
Share on other sites

There are two different things here.

 

The first aspect is the serialized data you send across the wire.  In both of the examples you sent the data across the wire is basically identical, in the form {ID, content}.  It doesn't matter what the code looks like as long as it follows the protocol.

 

The second aspect is the code organization.  Your first example calls the functions directly and the second example uses an event broadcaster/listener pattern.  Those are the usual ways of doing it.  Both patterns are widely used, and both patterns have their own benefits and drawbacks.  You can even mix-and-match the two.  Use whatever fits your design best.

0

Share this post


Link to post
Share on other sites

Yeah, I'm interested more in the second aspect - code organization and design. The "internal" message format which is what's sent through the wire doesn't concern me - I use RakNet's BitStream for this, someone can use his own class or other lib. What I'm interested in is way to encapsulate this "raw" data into some higher level Message like the ones I shown above.

 

These benefits and drawbacks is what I want to discuss.

0

Share this post


Link to post
Share on other sites

First, note that I mainly use C# together with XNA and Unity3D, so maybe the rules for games and engines using C++ are different. Anyway, I strongly prefer the first option. While there are quite a few classes that needs to be created, I do not see this as a major drawback. It creates a very clear separation of "data that is put on the wire" and the other code which deals with this data. Everything gets encapsulated into one neat little package. And I suspect that even if you use something which is more akin to your second example (the SWG emulator one), you would still end up with some sort of encapsulation similar to an "Message" or equivalent to be able to do reliable sends, sequencing, etc. 

 

Just as an example, this is a "Message" (my networking lib calls them "Events") which is used to send the click position and target object in a physics based puzzle game:

 

public class ShootBallEvent : NetEvent
{
    public static readonly ushort Id = 0;

    public NetObject Target;
    public Vector3 Position;

    public ShootBallEvent()
        : base(Id, NetEventDelivery.Unreliable, NetEventDirection.ClientToServer)
    {

    }

    public override void Pack(NetConnection conn, NetPacket packet)
    {
        packet.WriteVector3(Position);
        packet.WriteNetObject(Target);
    }

    public override void Unpack(NetConnection conn, NetPacket packet)
    {
        Position = packet.ReadVector3();
        Target = packet.ReadNetObject();
    }

    public override void Process(NetConnection conn)
    {
        if (Target == null)
            return;

        if (!ReferenceEquals(Target.Owner, conn))
            return;

        // ... do stuff to Target and Position
    }
}

 

I find this encapsulation to be very clear and easy to work with.

1

Share this post


Link to post
Share on other sites

I use RakNet with BitStream's as well, and I fire "events" with messages, but I just go raw BitStream & Packet handling instead of wrapping them up in a formal message object. There are so many messages in a game that making an object for each one I just felt wasn't worth it. I'd rather just have a function be mapped to a message and then read the BitStream directly in there. The Event.h file I use is below. I do this so 1 message gets mapped to 1 function and I don't have to worry about messing with some switch control. Instead it's always just 1 mapping line of code. 

 

Events are stored like:

 

 

map<MessageID, Event2<Packet&, BitStream&>> _events;

 

 

I have 1 area that captures packets coming in from the RakNet loop to dispatch to member functions that are registered to the message:

 

 

 

for(_packet = _peer->Receive(); _packet; _peer->DeallocatePacket(_packet), _packet = _peer->Receive())
{
RakNet::MessageID msg = _packet->data[0];


// make sure we mapped to the event
if(_events.find(msg) != _events.end())
{
// create bitstream and ignore the msg
RakNet::BitStream reader(_packet->data, _packet->length, false);
reader.IgnoreBytes(sizeof(RakNet::MessageID));


// raise the event
_events[msg].Raise((*_packet), reader);
}
}

 

And I register the functions like:

 

 

_events[(MessageID)ID_NEW_INCOMING_CONNECTION].Bind(this, &Server::OnNewIncomingConnection);

 

The function using the data would be defined as:

 

 

 

void Server::OnNewIncomingConnection(Packet& p, BitStream& reader)
{
    int age;


   reader.Read(age);


   _peer->Send(..., p.address);
}

 

To me the BitStream is basically the "message" object. It just isn't wrapped in a type. The plus is it doesn't get the overhead wrapping hit that a type does, if the message changes you don't have to go 2 places for that change (the message object, then in here to use new field), & you won't have potentially hundreds of little classes laying around (although if you have multiple people on a team this might help separate duties). The only negative is you don't get a pretty type that defines all the fields that the object has. You have to do that yourself. I'm not sure of the speed hit that would occur with the message object, but it'll be "slowER" than this, but it might not matter.

 

 

 

Event.h. This has EventX objects where X is 0 - 3 which is the number of parameters to the class function.

#pragma once
#include <list>


using namespace std;




class TFunctor0
{
public:
virtual void Call()=0;
};


template <class TClass>
class TSpecificFunctor0 : public TFunctor0
{
private:
void (TClass::*fpt)();
TClass* pt2Object;         
public:
TSpecificFunctor0(TClass* _pt2Object, void(TClass::*_fpt)())
{ 
pt2Object = _pt2Object;  
fpt=_fpt; 
}


virtual void Call()
{ (*pt2Object.*fpt)(); }
};


class Event0
{
public:
list<TFunctor0*>  mCaller;


template<class Target>
void Bind(Target* t, void (Target::*fnPtr)())
{ 
mCaller.push_back(new TSpecificFunctor0<Target>(t,fnPtr));
}


void Clear()
{ mCaller.clear(); }
       
void Raise()
{
list<TFunctor0*>::reverse_iterator iter;


for (iter = mCaller.rbegin(); iter!= mCaller.rend(); iter++)
{
(*iter)->Call();
}
}
};


//===============================================================================


template<class P>
class TFunctor1
{
public:
virtual void Call(P var1)=0;
};


template <class TClass, class param1>
class TSpecificFunctor1 : public TFunctor1<param1>
{
private:
void (TClass::*fpt)(param1);
TClass* pt2Object;         
public:
TSpecificFunctor1(TClass* _pt2Object, void(TClass::*_fpt)(param1))
{ pt2Object = _pt2Object;  fpt=_fpt; }


virtual void Call(param1 var1)
{ (*pt2Object.*fpt)(var1); }
};


template<class T1>
class Event1
{
public:
list<TFunctor1<T1>* >  mCaller;


template<class Target>
void Bind(Target* t, void (Target::*fnPtr)(T1))
{ mCaller.push_back(new TSpecificFunctor1<Target, T1>(t,fnPtr)); }
       
void Raise(T1 V1)
{
list<TFunctor1<T1>*>::reverse_iterator iter;




for (iter = mCaller.rbegin(); iter!= mCaller.rend(); iter++)
{
(*iter)->Call(V1);
}
}


void Clear()
{
mCaller.clear();
}
};


//===============================================================================


template<class P, class Q>
class TFunctor2
{
public:
virtual void Call(P var1, Q var2)=0;
};


template <class TClass, class param1, class param2>
class TSpecificFunctor2 : public TFunctor2<param1, param2>
{
private:
void (TClass::*fpt)(param1, param2);
TClass* pt2Object;         
public:
TSpecificFunctor2(TClass* _pt2Object, void(TClass::*_fpt)(param1, param2))
{ pt2Object = _pt2Object;  fpt=_fpt; }


virtual void Call(param1 var1, param2 var2)
{ (*pt2Object.*fpt)(var1, var2); }
};


template<class T1, class T2>
class Event2
{
public:
list<TFunctor2<T1, T2>* >  mCaller;


template<class Target>
Event2(Target* t, void (Target::*fnPtr)(T1, T2))
{ mCaller.push_back(new TSpecificFunctor2<Target, T1, T2>(t,fnPtr)); }


Event2(){}


template<class Target>
void Bind(Target* t, void (Target::*fnPtr)(T1, T2))
{ mCaller.push_back(new TSpecificFunctor2<Target, T1, T2>(t,fnPtr)); }
       
void Raise(T1 V1, T2 V2)
{
list<TFunctor2<T1, T2>*>::reverse_iterator iter;




for (iter = mCaller.rbegin(); iter!= mCaller.rend(); iter++)
{
(*iter)->Call(V1, V2);
}
}


void Clear()
{
mCaller.clear();
}
};




//===============================================================================


template<class P, class Q, class Y>
class TFunctor3
{
public:
virtual void Call(P var1, Q var2, Y var3)=0;
};


template <class TClass, class param1, class param2, class param3>
class TSpecificFunctor3 : public TFunctor3<param1, param2, param3>
{
private:
void (TClass::*fpt)(param1, param2, param3);
TClass* pt2Object;         
public:
TSpecificFunctor3(TClass* _pt2Object, void(TClass::*_fpt)(param1, param2, param3))
{ pt2Object = _pt2Object;  fpt=_fpt; }


virtual void Call(param1 var1, param2 var2, param3 var3)
{ (*pt2Object.*fpt)(var1, var2, var3); }
};


template<class T1, class T2, class T3>
class Event3
{
public:
list<TFunctor3<T1, T2, T3>* >  mCaller;


template<class Target>
void Bind(Target* t, void (Target::*fnPtr)(T1, T2, T3))
{ mCaller.push_back(new TSpecificFunctor3<Target, T1, T2, T3>(t,fnPtr)); }
       
void Raise(T1 V1, T2 V2, T3 V3)
{
list<TFunctor3<T1, T2, T3>*>::reverse_iterator iter;




for (iter = mCaller.rbegin(); iter!= mCaller.rend(); iter++)
{
(*iter)->Call(V1, V2, V3);
}
}


void Clear()
{
mCaller.clear();
}
};
Edited by rpiller
0

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0