![]() |
Choose your data
Creating your own packet types is just a matter of knowing what data you want and deciding on the best way to send it. Here's how to do it. Decide on what data you want to send over the network. For an 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 also lose the compression you can automatically perform with the bitstream class. 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 also write data out dynamically, writing certain values if certain conditions are true and not if others are true. 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. |
![]() |
Go to NetworkStructures.h There should be a big section in the middle like this: // -------------------------- // YOUR STRUCTURES BELOW HERE! // -------------------------- // -------------------------- // YOUR STRUCTURES HERE! // -------------------------- // -------------------------- // YOUR STRUCTURES ABOVE HERE! // -------------------------- It should be pretty obvious where to put your structures. There's two general forms to use for structures, one with timestamping and one without. Without Timestamping #pragma pack(1) struct structName { unsigned char typeId; // Your type here // Your data here }; With Timestamping #pragma pack(1) struct structName { unsigned char useTimeStamp; // Assign this to ID_TIMESTAMP unsigned long timeStamp; // Put the system time in here returned by timeGetTime() or some other method that returns a similar value unsigned char typeId; // Your type here // Your data here }; Fill out your packet. For our timed mine, we want the form that uses timestamping. So the end result is as follows #pragma pack(1) struct structName { unsigned char useTimeStamp; // Assign this to ID_TIMESTAMP unsigned long timeStamp; // Put the system time in here returned by getTime() unsigned char typeId; // This will be assigned to a type I add to PacketEnumerations.h , lets say ID_SET_TIMED_MINE float x,y,z; // Mine position ObjectID objectId; // ObjectID of the mine, used as a common method to refer to the mine on different computers PlayerID playerId; // The PlayerID of the player that owns the mine }; As I wrote in the comment above, we have to add the typeID to the list of enums so when the data stream arrives in a Receive call we know what we are looking at. So the last step is to go to PacketEnumerations.h and add ID_SET_TIMED_MINE (or whatever we feel like calling the enum) to the list. NOTE THAT YOU CANNOT INCLUDE POINTERS DIRECTLY OR INDIRECTLY IN THE STRUCTS. It seems to be a fairly common mistake that people include a pointer or a class with a pointer in the struct and think that the data pointed to by the pointer will be sent over the network. This is not the case - all it would send is the pointer address Usability comment: You'll notice that I called the ObjectID objectId, and the PlayerID playerId. Why not use more descriptive names, like mineId and mineOwnerId? I can tell you from experience that using descriptive names in this particular situation doesn't benefit you in any way because by the time you determine the packet type you know what those variables mean from the context; they can't mean anything else. The benefit of using generic names is that you can cut and paste code to quickly handle your packet without tediously going through and renaming stuff. When you have a lot of packets, as you will in a big game, this saves a lot of hassle. Nested Structures There is no problem with nesting structures. Just keep in mind that the first byte is always what determines the packet type. #pragma pack(1) struct A { unsigned char typeId; // ID_A }; #pragma pack(1) struct B { unsigned char typeId; // ID_A }; #pragma pack(1) struct C // Struct C is of type ID_A { A a; B b; } #pragma pack(1) struct D // Struct D is of type ID_B { B b; A a; } |
![]() |
Write less data with bitstreams
Lets take our mine example above and use a bitstream to write it out instead. We have all the same data as before. unsigned char useTimeStamp; // Assign this to ID_TIMESTAMP unsigned long timeStamp; // Put the system time in here returned by getTime() unsigned char typeId; // This will be assigned to a type I add to PacketEnumerations.h , lets say ID_SET_TIMED_MINE useTimeStamp = ID_TIMESTAMP; timeStamp = getTime(); typeId=ID_SET_TIMED_MINE; Bitstream myBitStream; myBitStream.Write(useTimeStamp); myBitStream.Write(timeStamp); myBitStream.Write(typeId); // Assume we have a Mine* mine object myBitStream.Write(mine->GetPosition().x); myBitStream.Write(mine->GetPosition().y); myBitStream.Write(mine->GetPosition().z); myBitStream.Write(mine->GetID()); // In the struct this is ObjectID objectId myBitStream.Write(mine->GetOwner()); // In the struct this is PlayerID playerId If we were to send myBitStream to RakClient::Send or RakServer::Send it would be idential internally to a casted struct at this point. Now lets try some improvements. Lets assume that a good deal of the time mines are at 0,0,0 for some reason. We can then do this instead: unsigned char useTimeStamp; // Assign this to ID_TIMESTAMP unsigned long timeStamp; // Put the system time in here returned by getTime() unsigned char typeId; // This will be assigned to a type I add to PacketEnumerations.h , lets say ID_SET_TIMED_MINE useTimeStamp = ID_TIMESTAMP; timeStamp = getTime(); typeId=ID_SET_TIMED_MINE; Bitstream myBitStream; myBitStream.Write(useTimeStamp); myBitStream.Write(timeStamp); myBitStream.Write(typeId); // Assume we have a Mine* mine object // If the mine is at 0,0,0, use 1 bit to represent this if (mine->GetPosition().x==0.0f && mine->GetPosition().y==0.0f && mine->GetPosition().z==0.0f) { myBitStream.Write(true); } else { myBitStream.Write(false); myBitStream.Write(mine->GetPosition().x); myBitStream.Write(mine->GetPosition().y); myBitStream.Write(mine->GetPosition().z); } myBitStream.Write(mine->GetID()); // In the struct this is ObjectID objectId myBitStream.Write(mine->GetOwner()); // In the struct this is PlayerID playerId This can potentially save us sending 3 floats over the network, at the cost of 1 bit. Writing strings It is possible to write strings using the array overload of the BitStream. One way to do it would be to write the length, then the data such as: void WriteStringToBitStream(char *myString, BitStream *output) { output->Write((unsigned short) strlen(myString)); output->Write(myString, strlen(myString); }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) { stringCompressor->EncodeString(myString, 256, 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) { stringCompressor->DecodeString(myString, 256, 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. Programmer's note: You can also write structs directly into a Bitstream simply by casting it to a (char*). It will copy your structs using memcpy. As with structs, it will not dereference pointers so you should not write pointers into the bitstream. |
![]() |
Index Sending Packets Receiving Packets Timestamping |