Networking: Examples of Implementation¶
Sending and Receiving P2P Packets¶
User Experience¶
During an online multiplayer game, it is necessary for the players to share the information on the game and state of the game objects, so that every player can see the actions of others in real time.
Solution¶
The transfer of information about the game and/or game objects is accomplished by sending and reading P2P packets containing this information between players in the same lobby. It is important to send packets with an adequate frequency, so the players don’t experience any lag (at least until the connection itself is not overloaded with other data). In this demo, we implemented an online multiplayer game mode for 2 players using peer-to-peer connection. We send one P2P packet containing the positions of all pool balls (ballPacketBuffer
) every 0.015 s (which is around 60 packets per second) using the SendPacketCoroutine()
. A packet containing the information about the last shot made (switchPacketBuffer
) is only sent when needed, right after a finished shot.
Variables¶
Variable | Description |
---|---|
balls |
Array of GameObjects representing pool balls |
receiverID |
GalaxyID of the user who will receive packets from this GalaxyPeer |
switchPacketBuffer |
Array containing bytes with the information on the last shot made, namely last foul type (if there was any), the expected color of the ball for the next shot, and the shot score |
ballPacketBuffer |
Array containing the x and z coordinates of all balls (y is constant) translated into bytes, together with a byte containing the information whether a ball GameObject is active in the game scene |
Methods and Usage¶
Networking.OnEnable¶
void OnEnable()
{
ListenersInit();
balls = GameObject.Find("PoolBalls").GetComponent<PoolBalls>().poolBalls;
if (receiverID == null)
{
receiverID = GalaxyManager.Instance.Matchmaking.GetSecondPlayerID();
Debug.Log("receiverID was null. ReceiverID set to: " + receiverID);
}
sendPacketCoroutine = SendPacketCoroutine(waitTime);
StartCoroutine(sendPacketCoroutine);
}
When the script is enabled, NetworkingListener() is initialized, GameManager.poolBalls array is assigned to the balls
variable and GalaxyID of the second player in the lobby is retrieved and assigned to the receiverID
variable.
Networking.SendP2PPacket¶
public void SendP2PPacket(GalaxyID galaxyID, byte[] data, P2PSendType sendType = P2PSendType.P2P_SEND_RELIABLE, byte channel = 0)
{
uint dataSize = (uint)data.Length;
try
{
GalaxyInstance.Networking().SendP2PPacket(galaxyID, data, dataSize, sendType, channel);
}
catch (GalaxyInstance.Error e)
{
Debug.LogError("Couldn't send packet for reason: " + e);
}
}
This method is a wrapper for the GOG GALAXY SDK SendP2PPacket()
method, which allows us to send a P2P packet to a user or a lobby with a specified GalaxyID. Please note that communication is possible only among users connected to the same lobby.
The type of a P2P packet is specified by P2PSendType
; here we use P2PSendType.P2P_SEND_RELIABLE type.
Since there is only a single connection between two GOG GALAXY Peers, using channels is a way of separating communication layers between them while using the existing connection. Here, we use two different channels (0 and 1) to separate packets with the positions of the balls from those with the information on the most recent shot. When using channels, it is important to also specify them in the GOG GALAXY Peer receiver when reading or peeking a packet.
Networking.SendPacketWithPlayerShot¶
public void SendPacketWithPlayerShot(GameManager.FoulEnum foul, GameManager.BallColorEnum ballOn, int shotScore)
{
Debug.Log("Packet switch sent");
SendPacketWithBallPositions();
GetBytesFromInt((int)foul, 0, ref switchPacketBuffer);
GetBytesFromInt((int)ballOn, 4, ref switchPacketBuffer);
GetBytesFromInt(shotScore, 8, ref switchPacketBuffer);
SendP2PPacket(receiverID, switchPacketBuffer, P2PSendType.P2P_SEND_RELIABLE, (byte)1);
}
This method sends the switchPacketBuffer
packet with the information on the last shot made (foul type, the expected color of the ball for the next shot, and the shot score). It uses the GetBytesFromInt()
helper function to translate the information mentioned above from a byte array into an integer.
Those packets are sent over the channel 1.
Networking.SendPacketWithBallPositions¶
void SendPacketWithBallPositions()
{
Debug.Log("Packet position sent");
int startIndex = 0;
for (int i = 0; i < balls.Length; i++)
{
startIndex = i * 9;
GetByteFromBool(balls[i].activeInHierarchy, startIndex, ref ballPacketBuffer);
GetBytesFromFloat(balls[i].GetComponent<Rigidbody>().position.x, startIndex + 1, ref ballPacketBuffer);
GetBytesFromFloat(balls[i].GetComponent<Rigidbody>().position.z, startIndex + 5, ref ballPacketBuffer);
}
SendP2PPacket(receiverID, ballPacketBuffer);
}
This method sends the ballPacketBuffer
packet. It uses two helper functions:
GetByteFromBool()
to translate the information on a ball GameObject stateGetBytesFromFloat()
to translate the ball position.
Those packets are sent over the default channel 0.
Networking.ReadPacketWithPlayerShot¶
void ReadPacketWithPlayerShot(byte[] switchPacketReceived)
{
Debug.Log("Packet switch received");
GameManager.FoulEnum foul;
GameManager.BallColorEnum ballOn;
int shotScore;
if (switchPacketReceived != null)
{
foul = (GameManager.FoulEnum)GetIntFromBytes(0, ref switchPacketReceived);
ballOn = (GameManager.BallColorEnum)GetIntFromBytes(4, ref switchPacketReceived);
shotScore = GetIntFromBytes(8, ref switchPacketReceived);
GameManager.Instance.foul = foul;
GameManager.Instance.ballOn = ballOn;
GameManager.Instance.shotScore = shotScore;
GameManager.Instance.ShotShort();
}
}
This method is used inside the OnP2PPacketAvailable()
method of NetworkingListener()
when the received packet comes to channel 1. Two enumerator variables, foul
and ballOn
, are instantiated, together with the shotScore
integer variable.
The GetIntFromBytes()
helper function is used to translate information about the last shot from the received packet. Translated information on the foul type and the next ball color is cast to GameManager.FoulEnum and GameManager.BallColorEnum, and assigned to the foul
and ballOn
variables respectively. The integer with the actual score is assigned to shotScore
. Then, all three variables are assigned to their counterparts in GameManager.Instance, which sets their actual in-game values.
Networking.ReadPacketWithBallPositions¶
void ReadPacketWithBallPositions(byte[] positionPacketReceived)
{
Debug.Log("Packet position received");
int startIndex = 0;
float x;
float z;
bool active;
if (positionPacketReceived != null)
{
for (int i = 0; i < balls.Length; i++)
{
startIndex = i * 9;
active = GetBoolFromByte(startIndex, ref positionPacketReceived);
x = GetFloatFromBytes(startIndex + 1, ref positionPacketReceived);
z = GetFloatFromBytes(startIndex + 5, ref positionPacketReceived);
balls[i].SetActive(active);
balls[i].GetComponent<Rigidbody>().MovePosition(new Vector3(x, balls[i].GetComponent<Rigidbody>().position.y, z));
}
}
}
This method is used inside the OnP2PPacketAvailable()
method of NetworkingListener()
when the received packet comes to channel 0. The for
loop goes through all of the balls and assigns proper values to their positions and to the flag on whether they are active in the scene at that time.
It uses GetBoolFromByte()
method to translate the byte received in a packet containing the information on the ball status in the game scene (1 when the ball is active and hasn’t been put in a pocket yet) into the active
Boolean variable. Then, this variable is used as a parameter for the Unity SetActive()
method to set the ball as active or not.
The GetFloatFromBytes()
helper function is used to translate the bytes from the received packet containing the information on the x and z coordinates of the ball position. The x
and z
variables are then used to change the position of the ball with the Unity MovePosition(Vector3)
method.
Helpers Functions¶
Before sending the packet info, we manually encode each piece of information into byte arrays and form a packet from a collection of those. We have created methods for transforming the information from their default data types to byte arrays and adding them to the packet byte array at a specific index. You can find these methods at the very bottom of this class, in the Helpers region.