Creating Packets |
How
to encode your game data into packets Systems that run RakNet, and in fact all systems on the internet, communicate through what is commonly known as packets. Or more accurately in the case of UDP, datagrams. Each datagram is created by RakNet and contains one or more messages. Messages could be created by you, such as position or health, or sometimes are created by RakNet internally, such as pings. By convention, the first byte of messages contain a numerical identifier from 0 to 255 which is used to indicate what type of message it is. RakNet already has a large set of messages it uses internally, or for plugins. These can be viewed in the file MessageIdentifiers.h. For this example, lets set the position of a timed mine in the gameworld. We'll need the following data:
Ultimately, anytime you send data you will send a stream of characters. There are two easy ways to encode your data into this. One is to create a structure and cast it to a (char*) the other is to use the built-in BitStream class. The advantage of creating a structure and casting is that it is very easy to change the structure and to see what data you are actually sending. Since both the sender and the recipient can share the same source file defining the structure, you avoid casting mistakes. There is also no risk of getting the data out of order, or using the wrong types. The disadvantage of creating a structure is that you often have to change and recompile many files to do so. You lose the compression you can automatically perform with the bitstream class. And RakNet cannot automatically endian-swap the structure members. The advantage of using a bitstream is that you don't have to change any external files to use it. Simply create the bitstream, write the data you want in whatever order you want, and send it. You can use the 'compressed' version of the read and write methods to write using fewer bits and it will write bools using only one bit. You can write data out dynamically, writing certain values if certain conditions are true or false. BitStream will automatically endian-swap members written with Serialize(), Write(), or Read(). The disadvantage of a bitstream is you are now susceptible to make mistakes. You can read data in a way that does not complement how you wrote it - the wrong order, a wrong data type, or other mistakes. We will cover both ways of creating packets here. |
Creating Packets with structs |
#pragma pack(push, 1) Noticed the #pragma pack(push,1) and #pragma pack(pop) ? These force your compiler (in this case VC++), to pack the structure as byte-aligned. Check your compiler documentation to learn more. With Timestamping #pragma pack(push, 1) Note that when sending structures, RakNet assumes the timeStamp is in network order. You would have to use the function BitStream::EndianSwapBytes() on the timeStamp field to make this happen. To then read the timestamp on the receiving system, use if (bitStream->DoEndianSwap()) bitStream->ReverseBytes(timeStamp, sizeof(timeStamp). This step is not necessary if using BitStreams. Fill out your packet. For our timed mine, we want the form that uses timestamping. So the end result should look like the following... #pragma pack(push, 1)struct structName { unsigned char useTimeStamp; // Assign ID_TIMESTAMP to this RakNet::Time timeStamp; // Put the system time in here returned by RakNet::GetTime() unsigned char typeId; // You should put here an enum you defined after the last one defined in MessageIdentifiers.h, lets say ID_SET_TIMED_MINE float x,y,z; // Mine position NetworkID networkId; // NetworkID of the mine, used as a common method to refer to the mine on different computers SystemAddress systemAddress; // The SystenAddress of the player that owns the mine }; #pragma pack(pop) As I wrote in the comment above, we have to define enums for our own packets types, so when the data stream arrives in a Receive call we know what we are looking at. You should define your enums as starting at ID_USER_PACKET_ENUM, like this:
|
Creating Packets with Bitstreams |
Write
less data with bitstreams
Wrong: In the second case, RakNet will see the first byte is 0, which is reserved internally to ID_INTERNAL_PING, and you will never get it.
void WriteStringToBitStream(char *myString, BitStream *output) Decoding is similar. However, that is not very efficient. RakNet comes with a built in StringCompressor called... stringCompressor. It is a global instance. With it, WriteStringToBitStream becomes: void WriteStringToBitStream(char *myString, BitStream *output) Not only does it encode the string, so the string can not easily be read by packet sniffers, but it compresses it as well. To decode the string you would use: void WriteBitStreamToString(char *myString, BitStream *input) The 256 in this case is the maximum number of bytes to write and read. In EncodeString, if your string is less than 256 it will write the entire string. If it is greater than 256 characters it will truncate it such that it would decode to an array with 256 characters, including the null terminator. RakNet also has a string class, RakNet::RakString, found in RakString.h RakNet::RakString rakString("The value is %i", myInt); bitStream->write(rakString); Use RakWString for Unicode support. Programmer's notes: |
See Also |
Index Sending Packets Receiving Packets Timestamping |