Unity Networking Fundamentals
Unity Networking Fundamentals
Khagendra Kumar
Katihar, Bihar, India
Standard Apress
Trademarked names, logos, and images may appear in this book. Rather
than use a trademark symbol with every occurrence of a trademarked name,
logo, or image we use the names, logos, and images only in an editorial
fashion and to the benefit of the trademark owner, with no intention of
infringement of the trademark. The use in this publication of trade names,
trademarks, service marks, and similar terms, even if they are not identified
as such, is not to be taken as an expression of opinion as to whether or not
they are subject to proprietary rights.
The publisher, the authors and the editors are safe to assume that the advice
and information in this book are believed to be true and accurate at the date
of publication. Neither the publisher nor the authors or the editors give a
warranty, express or implied, with respect to the material contained herein
or for any errors or omissions that may have been made. The publisher
remains neutral with regard to jurisdictional claims in published maps and
institutional affiliations.
if (player.IsLoggedIn)
{
print("Player is logged in");
server.PlayerAttached(player.ID);
}
This book contains a list of some tools that come with your operating
system to help you. These all run from the command line, also known as the
terminal or DOS prompt depending, on your operating system. Command
lines are written in the following style. The $ at the beginning of the line
should not be typed:
$ ls -al
Note This is a call out and will alert you to any information that is
important.
Any source code or other supplementary material referenced by the author
in this book is available to readers on GitHub via the book’s product page,
located at www.apress.com/978-1-4842-7357-9. For more detailed
information, please visit http://www.apress.com/source-code.
Table of Contents
Chapter 1: Networking Concepts
Client-Server Model
Connected vs. Connectionless Services
Packets
Connection-Oriented Service
Connectionless-Oriented Service
Physical Network Devices
Network Addressing
Media Access Control (MAC) Address
IP Address
Domain Name System
Sockets and Ports
What Is a Port Number?
What Is a Socket?
Open Systems Interconnection (OSI) Model
Command-Line Tools
Opening a Command Prompt
Hostname
Ping
IP Configuration
Address Resolution Protocol Cache
Network Status
Tracing the Route to the Server
Summary
Chapter 2: Serialization
Serialization Basics
JSON
Simple JSON Serialization/Deserialization
Binary
Simple Binary Serialization/Deserialization
The Network Library NetLib
Summary
Chapter 3: RESTful APIs
What Is a RESTful API?
RESTful Requests
RESTful Responses
Authentication and Restrictions
The UnityWebRequest Class
Fetching Text
Fetching Images
The Weather Application
Registering and Getting an API Key
The User Interface
Creating the Project
The OpenWeather Daily Forecast Endpoint
Fetching the Data
Running the Weather Application
Generic RESTful API Client
Summary
Chapter 4: TCP Connections
The TCP Three-Way Handshake
TCP Client-Server Connections
Socket Connections
Establishing a Socket Connection
Accepting a Socket Connection
Sending Data
Receiving Data
Hello World Using TCP Sockets
Simple Network Copier
TcpClient Connections
Sockets vs. TcpClient and TcpListener
Connecting to a Server Using TcpClient
Sending Data Using TcpClient
Reading Data Using a TcpClient
TcpListener: Accepting a TcpClient Connection
Hello World Example Using TcpClient and TcpListener
Tic-Tac-Toe
Starter Files
The Game Architecture
NetLib Classes
Client and Server Classes
Running the Game
Summary
Chapter 5: Networking Issues
Authoritative Servers
Synchronous or Asynchronous
Planning Multiplayer Games
Game Lag
Client-Side Prediction and Server Reconciliation
Client-Side Predictions
Synchronization Issues
Server Reconciliation
Further Steps
Getting Ping from Unity
Summary
Chapter 6: Develop a Maze Shooter
Lobby
Matchmaking
Spawn/Respawn
Spawn Point
Introducing the RPG Game
The Game’s Story
Game Prerequisites
Section 1: Creating the Project and Setting Up Unity
Section 2: Downloading and Installing MLAPI
Section 3: Programming with MLAPI
MLAPI Event Messaging
Using Remote Procedural Calls (RPCs)
Working with Bullets in Multiplayer Games
Summary
Chapter 7: LAN Networking
How VPN Works
What Is Hamachi?
Using Hamachi
LAN Party in Games
Summary
Chapter 8: Servers
What Is a Server?
Dedicated Servers
Who Should Get a Dedicated Server?
Dedicated Servers in Gaming
Headless Server, AKA Listen Server
Why a Headless Server?
Headless Servers in Games
Peer-to-Peer Networks
Peer-to-Peer Networks in Games
Benefits of a Peer-to-Peer Network
Load Balancers
Hardware-Based Load Balancers
Software-Based Load Balancers
Summary
Index
About the Authors
Sloan Kelly
has worked in the games industry for more than 13 years. He worked on a
number of AAA and indie titles and currently works for an educational
game company. He lives in Ontario, Canada with his wife and children.
Sloan is on Twitter @codehoose and makes YouTube videos in his spare
time.
Khagendra Kumar
has worked with a number of educational institutions and game studios for
training and solutions. He lives in Bihar, India and spends most of his time
working with game AI. He can be reached via LinkedIn at
/itskhagendra and on Instagram @Khagendra_Developer.
About the Technical Reviewer
Simon Jackson
is a long-time software engineer and
architect with many years of Unity game
development experience, as well as an
author of several Unity game development
titles. He both loves to both create Unity
projects as well as lend a hand to help
educate others, whether it’s via a blog,
vlog, user group, or major speaking event.
His primary focus at the moment is
with the XRTK (Mixed Reality Toolkit)
project, which is aimed at building a cross-
platform Mixed Reality framework to
enable both VR and AR developers to build efficient solutions in Unity and
then build/distribute them to as many platforms as possible.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Kelly, K. Kumar, Unity Networking Fundamentals
https://doi.org/10.1007/978-1-4842-7358-6_1
1. Networking Concepts
Sloan Kelly1 and Khagendra Kumar2
(1) Niagara Falls, ON, Canada
(2) Katihar, Bihar, India
This chapter covers the very basics of networking and introduces some
tools that are beneficial when you need to debug your application. This
chapter includes a brief overview of the client-server model, discusses how
we will build the games in this book, and covers networking fundamentals
that will help you understand and debug your games when developing the
networking components.
By the end of this chapter, you will be familiar with the devices used to
connect your PC to the outside world, how Internet addressing works, and
what the client-server model is.
If you have an email account, surf the web, use social media, or play
online games, you have used networking components. The modern Internet
runs on a suite of protocols based on Transportation Control
Protocol/Internet Protocol (TCP/IP).
Internet browsers like Chrome and Firefox use HTTP and HTTPS
(HyperText Transport Protocol and HyperText Transport Protocol Secure,
respectively) to communicate with remote servers. As shown in Figure 1-1,
an encyclopedia is just a click away!
Figure 1-1 A Firefox browser containing the wikipedia.org home page
https://www.wikipedia.org
This statement says “Fetch me the index.html page using the HTTP
version 1.1. protocol.” The server will interpret this message and return the
document to the client, as shown in Figure 1-2.
Figure 1-2 The sequence of events fetching a document from a web server
Packets
Your message is split into small packets and routed through the network.
This is done for several reasons. It could be because a route to the server is
blocked or because it is more efficient to group the packets and send them
later when reaching a node in the network.
Your packets can arrive at the destination out of order, as shown in
Figure 1-3.
Figure 1-3 File split into smaller packets, packet loss and packets received out of sequence
In the example in Figure 1-3, the file is sent across the network in
smaller packets numbered 1, 2, 3, and 4. As they travel through the
network, Packet 2 is lost and Packet 4 arrives before Packet 3.
Sometimes packet loss is acceptable and other times it is not. It is up to
the application developer to decide if packet loss or packets received out of
sequence are acceptable side-effects. The developer will choose either a
connection-oriented or connectionless-oriented approach, depending on the
needs of the game. In a fast-moving game, some packet loss might be
acceptable for the sake of maintaining speed.
Connection-Oriented Service
If you need to guarantee that messages arrive at the remote device in the
correct order with no parts missing, you need to write a connection-oriented
service.
Anything that uses Transport Control Protocol (TCP) will guarantee that
packets arrive in order and the message you send will be intact. An example
of an application that uses TCP is the web. HTTP is built using TCP to
guarantee that messages arrive intact.
Using TCP, your client must establish a connection with a server to
allow communication to flow between them. The connection can last any
amount of time, from a couple of seconds to days. The connection is
required to ensure that data is sent and received.
TCP provides error-free data transmission. If packets are dropped or
corrupted, they are retransmitted. When packets arrive at their destination,
the sender is notified.
Connectionless-Oriented Service
There are times when you do not need to guarantee delivery of packets. If
they can arrive out of order, or not at all, you should consider creating a
connectionless-oriented service.
In a connectionless-oriented service, the client does not connect with a
server, it just sends the information. If the server cannot receive the packet,
then it’s lost. Because there is no connection, the number of messages sent
per packet transferred is always just one – the packet being transferred.
Connectionless-oriented services are used for things like:
Video streaming
Multiplayer games
A video stream sends a minimum of 24 frames per second. It must get
there very quickly (1/24th of a second) and so if one frame is lost there is
not enough time to ask for another.
In multiplayer games, it is often too much overhead to use a TCP
connection for game play. If the player input is sent at 60 frames per second
(fps), then the occasional dropped packet will not make much difference.
As you will see later in the book, there are ways around this.
The IP part of the TCP/IP offers a connectionless-oriented protocol
called User Datagram Protocol (UDP). UDP allows developers to send so-
called “fire and forget” messages to remote machines. There is no guarantee
that the packets will arrive on time, or in sequence. If that is a sacrifice
you’re willing to make for speed, then UDP is a perfect choice.
Physical Network Devices
Devices like your mobile phone, PC, and tablet need to connect to a
network . They do this using a network card. The formal name of this is a
network interface card (NIC). Your device will connect to a local device
called a router using either a WiFi (wireless) or an Ethernet (wired)
connection.
Your router might be part of a cable or ADSL modem or a separate
device altogether. The modem – short for modulator/demodulator – is a
device that turns the received zeros and ones into wavelengths that can be
passed down a wire and onto the Internet. Figure 1-5 shows a typical
network diagram for a home network and a connection to a remote server
like www.google.com.
Figure 1-5 Devices on your Local Area Network (LAN) connect through a router to the Internet
All the traffic from your local devices to a remote server travels through
the router. The router also provides another function that provides each
connected device with a unique address on the LAN. This function is called
Dynamic Host Control Protocol. This address will be used by other
machines to communicate with each other.
There are two Internet Protocol address formats: IPv4 and IPv6. Each
device will have an IPv4 and IPv6 address. This book uses IPv4 addresses.
We cover addressing in the next section.
IPv4 and IPv6 are logical addresses. Each network card is assigned a
physical address at the factory. This address is called the Media Access
Control (MAC) address.
The architects of the original Internet — called DARPANet, short for
Defense Advanced Research Projects Agency Network — used a mesh
network, as shown in Figure 1-6.
LANs are connected to the Wider Area Network (WAN) or to the
Internet using routers. The routers pass messages between each other until
the destination is reached. This is why they are called routers; they route
messages between nodes on the network. A node is a device connected to
the network and can be a router or computer or any other network-
accessible device.
Figure 1-6 Mesh network routing past disabled routers from Device A to B
IP Address
The IP protocol uses a logical address to access devices on the network.
This logical address is known as the IP address of the device. There are two
ways to assign an IP address to a device; static and dynamic.
Static IP Addresses
The IP address can be set on the machine. This is a static address. This is
usually only done for servers because these devices are known as endpoints
in the network.
Dynamic IP Addresses
Dynamic IP addresses are assigned to each device when they boot up. The
TCP/IP stack reaches out to the network to find a DHCP (Dynamic Host
Control Protocol) server. The DHCP server assigns an address to the client.
The dynamic addresses have a lease time, which means they expire and
need to be renewed.
On my LAN, my PC seems to be given the same IP address, but it might
not be the same on the network where your machine is connected.
IP Address Format
This book concentrates on IPv4 rather than IPv6. There are minor changes
to the code to get it to run for IPv6, a flag or two to set.
The IPv6 address is much longer than its v4 counterpart. It consists of
eight groups of four hexadecimal digits. An example IPv6 address is as
follows:
1234:5678:9abc:def0:1234:5678:9abc:def0
On the contrary, IPv4 uses only four bytes separated by a period (.),
such as:
192.168.1.1
Each digit in the IPv4 address is called an octet because it contains eight
bits (one byte). The address’s format is called dotted decimal because it
contains four decimals separated by full stops. Four bytes is the same
amount of space as an integer. This means that an IPv4 address can access
up to 2^32 or 4.3 billion devices. But wait – aren’t there more devices in
existence than that? What about all the IoT (Internet of Things) devices like
light bulbs, toasters, fridges, and the like?
It turns out that IPv4 was not enough and that is why we moved to IPv6.
IPv4 gets around its limited address space by using network segmentation .
Address Classification
If you look at your machine’s IP address using the ipconfig or
ifconfig command (depending on your operating system), it is probably
going to be something like 192.168.1.17 and your router is probably going
to be located at address 192.168.1.1. A magic trick? No – most routers
default to the Class C network, which is 192.168.1.x.
The IP addresses are split into several ranges. Each range represents the
number of networks and the number of hosts that each network can contain.
A host is just another name for a device. These ranges are called classes.
There are also special IP addresses that you cannot use for your machine.
There are five classes of networks in the available IPv4 address ranges,
called Classes A through E. Classes A to C are the ones most used because
D and E are reserved classes. Table 1-1 shows each classification and
describes what it means with respect to the available networks in that class
and the number of hosts allowed per network.
Table 1-1 Network Classes
More on this command later. There are two indications that the IPv4
address is a Class C address. The first is that the first octal is 192. The
second is that the subnetwork mask is 255.255.255.0. This number is
bitwise ANDed with the IP address on the local network to obtain the
network, which would be 192.168.1.0..255 in this case.
There are special addresses that you cannot assign to your machine and
have special meaning.
The first is the loopback address. This is 127.0.0.1. That address is the
machine you are using. If you start a service using that IP, it cannot be
accessed from outside your computer.
The second is the broadcast address. You can send a UDP message out
onto the local area network using the address 255.255.255.255. This
message will be sent to every device.
Note IP addresses are unique to the local area network.
However, they are not globally unique.
using System.Net;
using UnityEngine;
void Start()
{
System.Net.IPAddress[] addresses =
Dns.GetHostAddresses(url);
foreach (var address in addresses)
{
print(address);
}
}
}
Listing 1-1 Using Dns.GetHostAddresses() to Fetch the Addresses of the Google Server from
the Current DNS
When the game runs, you should see something like the following
output in the console:
172.217.1.164
UnityEngine.MonoBehaviour:print (object)
DnsLookup:Start () (at
Assets/Scripts/DnsLookup.cs:13)
Sockets and Ports
You will often hear people talking about network programming as socket
programming. This is because network sockets are used in the TCP/IP suite.
The combination of a port number and an IP address is called a socket
address. You now know what an IP address is, so let’s look at what a port
number so that you can fully understand what a socket does.
What Is a Socket?
Not to be confused with a socket address, the singular socket class is a
.NET representation of the Berkeley Socket. Let’s take a trip down memory
lane for this one. BSD is a flavor of the UNIX operating system and version
4.2 shipped with a programming interface that made network programming
a lot easier. It was called Berkeley Sockets.
How did it make networking programming easier? In UNIX, everything
is a file. When you open a file, a console, an input device like a keyboard,
you get a file descriptor. This is a unique integer number representing the
file you just opened. If you want to write to the file, you pass that number to
the write() function.
When designing Berkeley Sockets, they chose to use this paradigm for
their network programming interface. The socket() function returns a
number that represents a file descriptor to a socket address. This number
allows you to read and write to that socket address like you would a file.
The socket address represents a connection to a remote machine.
The .NET framework has its own version of the low-level file
descriptor, the socket class. This too references a socket address; an IP
address plus port number.
The socket class is low-level and for some situations it is useful. If you are
using a connection-based service with TCP, though, there is a much better
way in .NET, using network streams.
Open Systems Interconnection (OSI) Model
The TCP/IP suite is synonymous with the Internet. The suite dovetails quite
nicely into the conceptual stack of protocols, known as the OSI seven-layer
model, that allows communication between remote devices. These devices
can be your computer, a mobile phone, a tablet, etc. The model itself does
not describe how these devices talk to each other. Instead it focuses on the
purpose of each layer.
The OSI model was created by the International Organization for
Standardization (ISO) because during the early days of the Internet it was
common for a large network of computers to use a variety of protocols. This
led to network fragmentation. To clarify how a network should be set up,
the OSI model was created, as shown in Figure 1-7.
The model defines the Physical layer (the connections, voltages, etc.) to
the Application layer. Reading from bottom to the top, each layer builds on
the previous one and abstracts itself more and more. By the time you get to
the top, where you will be building your games, you don’t need to know
about how data is routed through the network, or how error detection at the
Data Link layer is handled. But it is nice to have a background in this
process.
On the right side of Figure 1-7 are groups of protocols or services that
use the accompanying layers. For example, the IP protocol sits at the
Network layer. TCP and UDP sit at the Transport layer. Examples of
services like POP (Post Office Protocol), DNS (Dynamic Name Service),
and HTTP (HyperText Transport Protocol) use the Application,
Presentation, and Session layers.
Now that we have explored some of the concepts, let’s take a look at
some command-line tools that will help you when you run into problems
creating your networked games.
Command-Line Tools
There are several command-line tools that you should familiarize yourself
with when creating a networked game, or indeed any application that uses
networking. The examples in this section were run using Windows 10, but
you can use the same commands on other operating systems too. There are
notes for changes to make when using Ubuntu/macOS.
Commands are entered using the DOS prompt in Windows, or using the
Terminal application in either Mac or Linux.
Hostname
The first command will display the name of your PC. This is handy if you
want to give this information to other people on your network. Type
hostname at the command prompt. The output will be the name of your
host, which in my case is Sloan-PC:
$ hostname
Sloan-PC
Ping
The ping command is used to determine if your machine can “see”
another machine. It sends a small packet of data to the remote machine and
the time taken to reach the destination. If you’ve played multiplayer games,
you’re probably familiar with the name ping. Run the command with the
name of the remote machine. Use the -4 option in Windows/Linux to show
only the IPv4 addresses:
$ ping -4 www.google.com
Pinging www.google.com [172.16.1.86] with 32 bytes
of data:
Reply from 172.16.1.86: bytes=32 time=3ms TTL=128
Reply from 172.16.1.86: bytes=32 time=3ms TTL=128
Reply from 172.16.1.86: bytes=32 time=2ms TTL=128
Reply from 172.16.1.86: bytes=32 time=2ms TTL=128
If you are having issues connecting to a remote server, you can check
that your machine is connected to the network using ping with
127.0.0.1:
$ ping -4 127.0.0.1
Pinging 127.0.0.1 with 32 bytes of data:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
$ ping -4 172.16.1.86
Pinging 172.16.1.86 with 32 bytes of data:
Request timed out.
IP Configuration
To list the IP configuration for your machine, use the ipconfig
command. It will display the IP address, subnetwork mask, and default
gateway of your current network connection. For Ubuntu and macOS, use
ifconfig:
$ ipconfig
Windows IP Configuration
You can share the IPv4 address with others on your LAN to allow them
to connect to your PC. You will see a programmatic way to get this
information later.
$ arp -a
Interface: 192.168.1.149 --- 0xd
Internet Address Physical Address Type
172.16.1.1 2c-56-dc-55-c0-
c8 dynamic
172.16.1.19 f0-18-98-14-e4-
90 dynamic
172.16.1.31 20-c9-d0-c9-60-
53 dynamic
172.16.1.46 3c-2a-f4-01-bb-
c6 dynamic
172.16.1.59 00-90-a9-cf-8a-
b4 dynamic
172.16.1.86 24-0a-64-3a-86-
c5 dynamic
172.16.1.110 00-09-b0-47-b0-
df dynamic
172.16.1.117 c0-41-f6-5b-cb-
e5 dynamic
172.16.1.164 60-6d-3c-23-0b-
74 dynamic
172.16.1.173 f0-f0-a4-2a-ed-
f4 dynamic
172.16.1.230 b0-72-bf-4a-8d-
02 dynamic
172.16.1.243 00-a0-96-e8-fc-
54 dynamic
172.16.1.250 a0-ce-c8-d3-c6-
46 dynamic
172.16.1.255 ff-ff-ff-ff-ff-
ff static
224.0.0.2 01-00-5e-00-00-
02 static
224.0.0.22 01-00-5e-00-00-
16 static
224.0.0.251 01-00-5e-00-00-
fb static
224.0.0.252 01-00-5e-00-00-
fc static
239.0.0.250 01-00-5e-00-00-
fa static
239.255.255.250 01-00-5e-7f-ff-
fa static
255.255.255.255 ff-ff-ff-ff-ff-
ff static
Network Status
The netstat command displays the active TCP/IP connections. If you are
having issues with your application while it is running, this application
might help you determine if you have connections. It is sometimes used
with the grep or findstr command to filter the results. grep is short
for get regular expression and, like findstr (find string), it can be used
to filter the output from a command to cut down on the information
displayed. Run the command without any options:
$ netstat
Active Connections
Proto Local Address Foreign
Address State
TCP 127.0.0.1:5354 Sloan-
PC:61997 ESTABLISHED
TCP 127.0.0.1:5354 Sloan-
PC:61998 ESTABLISHED
TCP 127.0.0.1:2701 Sloan-
PC:56361 ESTABLISHED
TCP 127.0.0.1:2701 Sloan-
PC:61994 ESTABLISHED
TCP 127.0.0.1:4966 Sloan-
PC:49670 ESTABLISHED
TCP 127.0.0.1:4967 Sloan-
PC:49669 ESTABLISHED
TCP 127.0.0.1:4969 Sloan-
PC:49693 ESTABLISHED
TCP 127.0.0.1:4969 Sloan-
PC:49692 ESTABLISHED
TCP 127.0.0.1:4993 Sloan-
PC:49935 ESTABLISHED
TCP 127.0.0.1:4993 Sloan-
PC:49934 ESTABLISHED
The columns, from left to right, show the protocol used, the local
address as a socket address (the IP and port number), the foreign (remote)
address, which is also a socket address but, as shown here, can show names
as well as IP addresses. The last column shows the state of the connection.
This will take some time to complete; it is usually a very long list. If
you are using Windows, you can filter these results using the findstr
command. For Ubuntu and macOS, use the grep command instead. To
find all the HTTP connections, run the following:
$ tracert www.google.com
Tracing route to www.google.com [172.217.165.4]
over a maximum of 30 hops:
1 <1 ms <1 ms <1 ms router.asus.com
[172.16.1.1]
2 1 ms 1 ms 1 ms 192.168.0.1
3 10 ms 11 ms 7 ms 10.91.64.1
4 15 ms 12 ms 11 ms 10.0.75.209
5 19 ms 14 ms 11 ms 10.0.18.73
6 14 ms 15 ms 13 ms 209.85.173.40
7 14 ms 15 ms 13 ms 74.125.244.145
8 17 ms 13 ms 14 ms 216.239.40.255
9 14 ms 14 ms 11 ms yyz12s06-in-f4.1e100.net
[172.217.165.4]
This shows the journey through the mesh network that the packet took.
Remember that mesh networks are robust and the devices on the network
will route packets a different way if nodes are not available. If you run this
command multiple times, you might get multiple different routes.
From left to right, the columns are:
The hop number, which represents the next node in the route. The first
node is the local network’s router. The last node (number 9) is the
destination.
At each hop, the tracert command makes three attempts to contact
that node. These three numbers in milliseconds (ms) are the response
times for each attempt.
The last column is the IPv4 address of the node, or the name if it can be
resolved.
Sometimes tracert can’t determine the response time and you might
see an asterisk (*) in one or more of the response time columns. This is
usually okay and might just be an issue with the node. However, if your
route does not get traced and you continually see a “Request Timed Out”
message, there might be an issue with that node. It could be as simple as
that particular node doesn’t respond to pings.
Summary
There are many different parts to networking; this introductory chapter
covered the basics that you need in order to understand how networking
works “under the hood.”
The Internet and by extension your local area network and your devices
use the TCP/IP suite: Transport Control Protocol/Internet Protocol. This
protocol is part of the OSI seven-layer conceptual networking model and
describe how data is routed through the Internet.
There are two ways to send data across the network using the TCP/IP
suite. One uses connection-based TCP and the other uses connectionless
UDP.
We use IPv4 addressing in this text rather than the newer IPv6. IPv4
addresses are made up of four octals (bytes).
Every device has a physical address (MAC) and a logical address (IP).
Each machine can be given a name that is exposed through the Domain
Name System (DNS), allowing you to use words rather than IP addresses to
find remote servers.
Services running on a host use another type of address, called a socket
address, that contains both the IP address of the host and the port number. A
socket is also a name given to a low-level object that can be used to send
and receive data to and from the socket address.
There is a set of useful command-line tools available on all modern
operating systems to help you debug your application if you run into
problems.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Kelly, K. Kumar, Unity Networking Fundamentals
https://doi.org/10.1007/978-1-4842-7358-6_2
2. Serialization
Sloan Kelly1 and Khagendra Kumar2
(1) Niagara Falls, ON, Canada
(2) Katihar, Bihar, India
{
"name": "El Player",
"lives": 3,
"completed_levels": [1, 2, 3, 4],
"last_pos": {
"x": 10,
"y": 15
}
}
Listing 2-1 JSON Data Containing Character Information
using System;
using UnityEngine;
[Serializable]
public class BasicObject
{
public Vector3 position;
public string name;
public int health;
public int shield;
}
Listing 2-2 The BasicObject That Will Be Serialized and Deserialized to and from JSON
The class does not have a constructor. Having a constructor would cause
problems when an attempt is made to deserialize the object. The
deserializer would try to create an instance of the object but wouldn’t be
able to find a non-parameterized constructor. You can get around this by
creating two constructors—one with parameters to set the initial state and a
parameterless constructor for serialization. Because it is a simple data
object, I have chosen to omit constructors completely.
Any class that’s used to serialize data should have the Serializable
attribute (located in the System namespace) applied to it.
The class also contains the Vector3 Unity class. The JSON serializer will
have no problem serializing this field because it is part of the Unity Engine.
In general, though, class instances you want to serialize should extend from
MonoBehaviour or ScriptableObject, or plain class or structs with
the [Serializable] attribute.
Save the file and open the JsonSerializationExample script
file. Change the JsonSerializationExample.cs file to match
Listing 2-3.
using UnityEngine;
string json =
JsonUtility.ToJson(basicObject);
Debug.Log(json);
BasicObject copy =
JsonUtility.FromJson<BasicObject>(json);
Vector3 pos = copy.position;
Debug.Log($"{copy.name} at {pos.x},
{pos.y}, {pos.z}");
}
}
Listing 2-3 The JsonSerializationExample Script that Will Serialize and Deserialize an Object
to and from JSON
When the game runs, you should see the following output in the
console.
{"position":{"x":1.0,"y":2.0,"z":3.0},"name":"Sven
The Explorer","health":50,"shield":100}
UnityEngine.Debug:Log (object)
JsonSerializationExample:Start () (at
Assets/Scripts/JsonSerializationExample.cs:17)
Figure 2-1 The output of the two Debug.Log() functions in the JsonSerializationExample script
using System.Text;
using UnityEngine;
byte[] bytes =
Encoding.ASCII.GetBytes(json);
Debug.Log($"{bytes[0]:x} {bytes[1]:x}
{bytes[2]:x} {bytes[3]:x}");
string jsonFromBytes =
Encoding.ASCII.GetString(bytes);
BasicObject copy =
JsonUtility.FromJson<BasicObject>(jsonFromBytes);
Vector3 pos = copy.position;
Debug.Log($"{copy.name} at {pos.x},
{pos.y}, {pos.z}");
}
}
Listing 2-4 The Binary Version of the JSON Serializer. Additional Lines Shown in Bold
The output from the console window should look similar to this:
{"position":{"x":1.0,"y":2.0,"z":3.0},"name":"Sven
The Explorer","health":50,"shield":100}
UnityEngine.Debug:Log (object)
JsonSerializationExample:Start () (at
Assets/Scripts/JsonSerializationExample.cs:17)
7b 22 70 6f
UnityEngine.Debug:Log (object)
JsonSerializationExample:Start () (at
Assets/Scripts/JsonSerializationExample.cs:20)
Sven The Explorer at 1, 2, 3
UnityEngine.Debug:Log (object)
JsonSerializationExample:Start () (at
Assets/Scripts/JsonSerializationExample.cs:25)
The JSON string is converted to a byte array. This byte array is what
you send across the network. In this example, though, the additional print
statement displays the first four bytes of the array to show you its contents.
I used the :x option to format the numbers in hexadecimal.
If you convert those numbers to ASCII values, as shown in Table 2-1,
you can see that they represent the characters {"po, which are the first four
characters of the JSON string.
Table 2-1 The First Four Bytes of the Message with their ASCII Values Shown as Hexadecimal and
Decimal Values
using System;
using System.Runtime.InteropServices;
using UnityEngine;
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct MyData
{
public Vector3 position;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst
= 128)]
public string name;
public int health;
public int shield;
The contents of the struct are almost the same as the data from the
JSON example. There are a couple of additional attributes, though:
StructLayout and MarshalAs. These are part of the interoperation
services (interop services) between .NET and COM (Common Object
Model) used by Windows. To use these services, you must include the
System.Runtime.InteropServices namespace.
The StructLayout attribute is required because .NET determines
the most efficient way to pack data. You might assume that the data will be
stored in the order that you write it in code, but that might not be the case.
To force .NET to keep the order that you used, you must apply the
StructLayout attribute with the LayoutKind.Sequential option.
The Pack parameter specifies the padding value. By setting it to 1, you
make sure that the struct is not padded and takes up the number of bytes
specified, i.e., an integer takes four bytes.
The MarshalAs attribute is used on the string field name because the
marshaler needs to know the size of the strings. Always specify the least
amount of storage for this. If it is possible to compress the name into a
smaller size, try to do that to minimize the overall size of the structure.
Save this file.
Listing 2-6 shows the contents of the .cs file. Open this file and
change its contents to the following.
using System.Runtime.InteropServices;
using UnityEngine;
Debug.Log($"Original: {data}");
Debug.Log($"Copy: {copy}");
}
/// <summary>
/// Deserialize an array of bytes and return
an
/// instance of object type T with the
serialized data.
/// </summary>
/// <typeparam name="T">Class or Struct type
to be created</typeparam>
/// <param name="data">Array of bytes
containing serialized data</param>
/// <returns>An instance of object type
T</returns>
private T ToObject<T>(byte[] data)
{
// Create an area of memory to store the
byte array and
// then copy it to memory
var size = Marshal.SizeOf(typeof(T));
var ptr = Marshal.AllocHGlobal(size);
Marshal.Copy(data, 0, ptr, size);
/// <summary>
/// Serialize an object to an array of bytes.
/// </summary>
/// <param name="data">The object to be
serialized</param>
/// <returns>The serialized object as an array
of bytes</returns>
private byte[] ToBytes(object data)
{
// Create a pointer in memory and allocate
the size of the structure
var size = Marshal.SizeOf(data);
byte[] buf = new byte[size];
var ptr = Marshal.AllocHGlobal(size);
The two methods of interest in this example are the ToBytes() and
ToObject() methods.
using System.Text;
using UnityEngine;
These static methods use similar code that was created for the
JsonSerializationExample script. ToJsonBinary() is an
extension method for objects to help serialize them to a byte array
containing JSON data. FromJsonBinary() is an extension method for
byte arrays to convert the contents to an instance of an object. Save the file.
Open the StructExtensions script file and replace it with the code
in Listing 2-8. These are the same methods, slightly renamed, that were
used in the binary serialization example.
using System.Runtime.InteropServices;
var copyData =
(T)Marshal.PtrToStructure(ptr, typeof(T));
Marshal.FreeHGlobal(ptr);
return copyData;
}
public static byte[] ToArray (this object
data)
{
var size = Marshal.SizeOf(data);
byte[] buf = new byte[size];
var ptr = Marshal.AllocHGlobal(size);
Marshal.FreeHGlobal(ptr);
return buf;
}
}
Listing 2-8 The StructExtensions Class
using UnityEngine;
Each of the listings that follow will be added one at a time to the
Start() method. Listing 2-10 creates the instance of the MyData class
and assigns some values to the instance. A debug statement outputs the
contents to the console.
MyData data = new MyData
{
shield = 100,
health = 50,
name = "Sven The Destroyer",
position = new Vector3(1, 2, 3)
};
Because you’re using extension methods, the code looks a lot cleaner
and you don’t need to place additional functions in each class that requires
it. Static helper methods could be used, but extension methods are cleaner
because they are attached to the object. This elegance can be seen in Listing
2-11, which shows how the structure is marshaled into a byte array using
the ToArray() extension method and re-created using the ToStruct()
extension method. To demonstrate that it worked, a debug print statement
outputs the contents of the copy.
Debug.Log($"Copy: {copy}");
Listing 2-11 Performing a Binary Serialization/Deserialization
Last but not least, Listing 2-12 uses the extension methods
ToJsonBinary() and FromJsonBinary() to serialize/deserialize an
object to and from the binary JSON format. A debug print displays the
contents of the copy.
Figure 2-4 The output showing the original, the binary copy, and the JSON copy
using UnityEngine;
Debug.Log($"Original: {data}");
Debug.Log($"Copy: {copy}");
Up to this point, you have been learning about low-level concepts when it comes to networking. For the most part,
though, network programming is done at the Application layer. This is the top-most layer of the OSI model, as
shown in Figure 3-1, where your game sits.
Your game might utilize some form of web service to get data for your game, such as leaderboards, friend’s
lists, etc.
The UnityWebRequest class can be used to connect to a web server and perform web requests. It is
possible to write your own code to do this using Sockets or TcpClient, but because this class exists and provides a
lot of the functionality that you need already, it’s best to use it instead. Among other features, it can be used to get
data from web pages using what is known as RESTful APIs.
What Is a RESTful API?
In its simplest form, a RESTful API is a way for clients to request data using a web URL (Uniform Resource
Locator) and the request can be given a response in a known format from the server. The client can use HTTP
verbs like GET, PUT, and DELETE to perform actions on the data. This matches up with typical CRUD (Create,
Read, Update and Delete) operations like getting leaderboard data, saving or replacing player state, and deleting a
save slot, for example. This means that the client does not have to be written in the same language as the server.
Because the requests use URLs and standard HTTP messages, there is no need to open additional ports. The
developer just needs to be able to request a web page to read or write information.
REST is short for REpresentational State Transfer . API is short for Application Programming Interface . As
long as the client and the server agree on the messages – the format of the requests and their responses – and how
those requests are made using HTTP, both the client and the server will understand the messages passed between
them.
Note REST is a concept; it does not define the API. APIs are different for each service.
As shown in Figure 3-2, a request is made to a website using a RESTful call. The response that returns is a JSON
string.
Figure 3-2 A RESTful request and response showing a GET request for a leaderboard and the JSON response that’s returned
RESTful Requests
Each URL that’s used to access a RESTful API is called a request and it is made up of four things:
An endpoint
A method
Headers
The data, also known as the body
The Endpoint
This is the URL that forms the request for the data you need. It is somewhat arbitrary and depends on the API
creator, but it follows the structure:
https://server/path/feature/sub-feature
For example:
https://api.example.com/games/leaderboard
There is also a root endpoint. This is the starting point of the API and usually includes the protocol. For
example:
https://api.example.com
A Method
The path is what comes after the root endpoint. For example:
/games/leaderboard
You need to look at the service’s API documentation to see the paths that it offers. For example, there is
extensive documentation for the Steam API at
https://developer.valvesoftware.com/wiki/Steam_Web_API. You could think of paths as
being synonymous with functions and methods in a normal program.
Sometimes a path in an API document will specify a colon. This means that you should put in a value there.
For example, let’s say your service has leaderboards for lots of games. You might see a path defined in a document
as follows:
/games/:gamename/leaderboard
To access the leaderboard of your game called “Jump Game” for example, you might have a path like so:
/games/jump-game/leaderboard
The Headers
The headers provide extra information to the client and server. Headers can be used for a number of reasons,
including authentication and providing additional metadata to the receiving device.
HTTP headers are property-value pairs that are separated by a colon. The following example shows a header
that identifies the content as a JSON string:
Content-Type: application/json
The Data
The data is sometimes called the body or the message. It contains information that you want to send to the server.
This option is only used with POST, PUT, PATCH, and DELETE requests. Examples are shown in Table 3-1.
Table 3-1 HTTP Verbs and Their Uses
RESTful Responses
The format of the response varies from service to service but it is typically formatted as a JSON string. This string
can be easily converted into a class using a JSON parser like the one provided by Unity’s JsonUtility class.
Responses can also contain additional HTTP headers. These are the same HTTP headers as for the client/server
requests. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers for more
information.
Authentication and Restrictions
You should be respectful of the limits placed by the API provider. They usually limit you to a few hundred calls
per minute. More calls than that and you could find yourself banned from the service after repeat offenses.
RESTful API providers also want to ensure that only authorized users have access to their services. This means
that you will most likely have to go through some authentication process like OAuth or provide a token as part of
the URL, typically called an API token, that uniquely identifies your application. Tokens usually take the form of a
large hexadecimal number:
42dca02c33584aa783280d83d5e01d04
The major difference is who owns the authority to use the website. With OAuth, the user must validate
themselves to a provider like Google, Facebook, Twitter, or OpenID using a username and password. The result is
that they receive a token to use with the remote server. The token is unique to the user.
In the case of the client application owning the authorization, the API key becomes the token. The token is
unique to the application.
Note Always keep tokens and passwords safe! Do not let your token fall into the wrong hands!
In the weather application detailed later in this chapter, you will be using the simpler API token method for
OpenWeatherMap rather than OAuth.
The UnityWebRequest Class
Reading and writing data to a remote website is made possible through the UnityWebRequest class . You do
not create a UnityWebRequest class directly. Instead you use the Get() method and pass in the URL of the
RESTful API endpoint.
The UnityWebRequest class is typically used in a co-routine because of its asynchronous nature. A request
is made and at some point in the future that request is given a response.
A normal function in Unity must execute completely before anything else can complete. If there is a function
that takes a long time, this will affect the performance of your game. To combat this, Unity created co-routines.
These are functions that yield control back to Unity when they need to wait longer than a frame to complete. When
the function is called again, it picks up where it left off. See
https://docs.unity3d.com/Manual/Coroutines.html for more details.
Errors are handled by checking the isNetworkError and isHttpError properties once the request
operation has completed. The text of the error is contained in the error property of the request instance.
The response is located in the text property of the web request’s downloadHandler. This is just C# code
and no further processing is required. The response will usually be in the JSON format, so creating classes from it
is a simple matter of using JsonUtility.FromJson<T>().
Fetching Text
Listing 3-1 illustrates how to fetch a web page. For this example, it’s the Google home page. The script can be
attached to a game object like the Main Camera in a blank project and run.
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log(request.downloadHandler.text);
}
else
{
Debug.Log(request.error);
}
}
}
Listing 3-1 Fetch the Google Home Page from the Web Using UnityWebRequest
The Start() method is a co-routine that will not stall out the game while the web request is waiting for a
response. The SendWebRequest() method returns an enumerator that exits when either an error occurs or
when the data has been received.
If an error occurs in this example, it is printed to the console. If data is returned it is printed to the console, as
shown in Figure 3-3. This is the HTML of the Google home page—the one with the search box in the middle of
the screen.
Figure 3-3 The output from the FetchGoogle script showing the contents of the Google home page’s HTML
Note A request is required for each resource. If you were building a web server, you would have to make
several requests for the HTML page, each image, each stylesheet, and each JavaScript file needed to display the
page completely.
Fetching Images
The UnityWebRequest class can be used to return an image. The following example fetches the Unity Logo
from Wikimedia Commons, as shown in Figure 3-4.
Figure 3-4 The Unity logo as shown on the Wikimedia Commons website
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
SpriteRenderer = GetComponent<SpriteRenderer>();
var rect = new Rect(0, 0, texture.width, texture.height);
spriteRenderer.sprite = Sprite.Create(texture, rect,
Vector2.zero);
}
else
{
Debug.Log(request.error);
}
}
}
Listing 3-2 Fetching an Image from a Website Using UnityWebRequestTexture
Instead of using the Get() method, as you did for the text, the GetTexture() method is used. This returns
an instance of UnityWebRequestTexture, but, the actual return is an object of type UnityWebRequest.
You must do some casting to get the objects back to the type that you need in order to get the texture.
The download handler that is returned from a UnityWebRequest is a standard DownloadHandler
instance. However, after casting the UnityWebRequestTexture’s downloadHandler property to the
DownloadHandlerTexture class, we can access the downloadHandler’s texture property.
Then it is a simple matter of creating a Sprite object and passing the downloaded texture. When run, the image
is downloaded and the sprite is constructed, as shown in Figure 3-6.
Figure 3-6 The Unity logo is displayed in the game after downloading it from the remote site
Note It can take upwards of two hours to get a confirmation back while OpenWeather creates a valid key.
Figure 3-8 The OpenWeather sign-up page
Once logged in, you will be taken to your account home page. Click the API Keys link shown in Figure 3-9.
By default, an API key is defined for you with the name default. You can edit this to be any name you like:
for example, the name of your application. The most important part, though, is the key itself. This is what will be
used to verify your application when you make a call to an API. Figure 3-10 shows my API key that I have
renamed to Unity Net Book. The name is more of a mnemonic for you and is not required when making an API
call.
Figure 3-10 The default API key has been renamed Unity Net Book
The resources for this project can be obtained by clicking the Download Source Code button located at
www.apress.com/9781484273579. This includes the images for the weather icons, the prefabs for each
day, and the fonts.
https://api.openweathermap.org/data/2.5/forecast/daily
The endpoint takes in three parameters that will be passed to the method using an HTTP query string, which
you need to provide in order to obtain the data.
A query string is set of key-value pairs that appear after the question mark (?) character in a web address. The
key and the value are separated by an equals sign (=). Query strings are not secure because they are part of the web
address and are sent in plain text.
The three parameters are:
q – The city and country code of the location.
cnt – The number of days to return. You will set this to 5 in your example.
appid – The API key for the application.
To request the forecast in Boston for the next five days, the URL would look like the following – API key
truncated:
https://api.openweathermap.org/data/2.5/forecast/daily?
q=Boston&cnt=5&appid=dc54fac
The API can be tested using the CURL command. Curl stands for client URL and is used to download
resources from websites on the command line. It’s perfect for testing APIs because of this. Open a terminal
window or DOS prompt. At the prompt, type in the command like so. Don’t forget to change your appid to your
application’s ID:
$ curl "https://api.openweathermap.org/data/2.5/forecast/daily?
q=Boston&cnt=5&appid=dc54fac"
The query (q) must be URL-encoded. This means that the string will have whitespace characters trimmed out
and problematic characters replaced with HTML entities. For example, a space becomes a plus symbol (+). The
example shows two lines—the normal text followed by the URL-encoded version:
The .NET framework’s HttpUtility class has a method called UrlEncode() that will take a string and
return the URL-encoded version:
Serialization Classes
The serialization classes in Scripts/Serialization will just be plain classes with public fields that will be
populated with data.
Note Unity’s JsonUtility can only serialize/deserialize public fields. Do not use properties for your
serialization classes!
The shape of the data (i.e. the names of the fields and the class structure) is dictated by the application. In the case
of the daily forecast from OpenWeather, this is defined at
https://openweathermap.org/forecast16#JSON and a portion of it is shown in Figure 3-14.
Figure 3-14 Example showing the shape of the data returned from the API endpoint for daily forecasting using the OpenWeather API
The date (dt), sunrise, and sunset fields are 10 digits long. These values are the number of seconds since
midnight on 1/1/1970. You’ll learn how to create a function to convert them to something human readable later in
the UnixTimeToDateTime() function .
Taking this hierarchy, you can draw a diagram to represent the parent-child relationships of each class as
shown in Figure 3-15.
Figure 3-15 The parent-child relationships of the response message from OpenWeather’s API call
The ResponseContainer is the message received from the server. Each ResponseItem is a day of the
week.
The leaf node in this hierarchy is what I called the WeatherItem and it is defined in Listing 3-3.
using System;
[Serializable]
public class WeatherItem
{
public int id;
public string main;
public string description;
public string icon;
}
Listing 3-3 The WeatherItem Script
This class contains the icon used to visually represent the weather as well as a description of the weather itself.
Paired with the WeatherItem class is the ResponseTemperature class, shown in Listing 3-4. It contains,
unsurprisingly, the temperature for the parts of the day. The temperatures returned from the API are in Kelvin. You
will write a function called ToHumanTemperature() that will convert the temperature from Kelvin to Celsius
or Fahrenheit.
using System;
[Serializable]
public class ResponseTemperature
{
public float day;
public float night;
public float min;
public float max;
public float eve;
public float morn;
}
Listing 3-4 The ResponseTemperature Script
Daily temperatures, icons, and descriptions are contained in the ResponseItem class , as shown in Listing
3-5. This represents a single day’s results in addition to the sunrise and sunset times for that day.
using System;
[Serializable]
public class ResponseItem
{
public long dt;
public ResponseTemperature temp;
public WeatherItem[] weather;
public long sunrise;
public long sunset;
}
Listing 3-5 The ResponseItem Script
The dt, sunrise, and sunset fields are not in the DateTime format. This is because the
OpenWeatherMap API returns times in what is called UNIX Epoch time. This is the number of seconds since
midnight on 1/1/1970.
Lastly, the actual message itself is represented in code as ResponseContainer, as shown in Listing 3-6. It
contains a collection of ResponseItem instances as well as the count of the number of days requested.
using System;
[Serializable]
public class ResponseContainer
{
public string cod;
public float message;
public int cnt;
public ResponseItem[] list;
}
Listing 3-6 The ResponseContainer Script
Now that the serializable classes have been defined, you can take a look at creating a MonoBehaviour that
queries the OpenWeather endpoint for a particular location and returns the result to the caller.
using System.Collections;
using System.Web;
using UnityEngine;
using UnityEngine.Networking;
if (webRequest.result == UnityWebRequest.Result.Success)
{
string json = webRequest.downloadHandler.text;
Response = JsonUtility.FromJson<ResponseContainer>(json);
}
else
{
Debug.Log(webRequest.error);
}
}
}
Listing 3-7 The OpenWeatherMapAPI MonoBehaviour Script
Notice that the endpoint is stored as a constant called ApiBaseUrl and the string.Format() method is
used to place the query and the API key in the query. Also of note is the UrlEncode() method, which is used to
encode the query string.
This MonoBehaviour is used by the final script that you write, which acts as a controller for the whole
application. If you were writing an application that uses a lot of API calls, it would be rather inefficient to rewrite
this over and over again. At the end of this chapter, you’ll take a look at making this more generic.
The FetchResults class, as shown in Listing 3-8, adds a click event handler to the button to which it is
attached. The event handler calls the FetchData() method and that in turn calls the OpenWeatherMapAPI’s
GetForecast() method. On a successful response, the day prefabs are filled.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
void Awake()
{
button = GetComponent<Button>();
api = GetComponent<OpenWeatherMapAPI>();
// Create the dictionary that maps the name of the sprite to its
image
foreach (Sprite s in spriteIcons)
{
sprites[s.name] = s;
}
button.onClick.AddListener(delegate
{
if (!string.IsNullOrEmpty(cityInputField.text.Trim()) &&
!isRunningQuery)
{
StartCoroutine(FetchData(cityInputField.text));
}
});
}
if (api.Response != null)
{
FillDays(api.Response);
panel.alpha = 1;
}
}
Sprite = sprites[icon];
DayCardModel day = new DayCardModel(response.list[i], sprite);
DayCard = dayCards[i];
dayCard.SetModel(day);
}
}
}
Listing 3-8 The FetchResults MonoBehaviour script
The final thing to do is to enter the API token on the OpenWeatherMapAPI script in the Unity Editor. At
this point, you can test the application—everything is complete!
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
if (webRequest.isNetworkError || webRequest.isHttpError)
{
onError?.Invoke(webRequest.error);
}
else
{
string json = webRequest.downloadHandler.text;
onSuccess?.Invoke(JsonUtility.FromJson<T>(json));
}
}
}
}
Listing 3-9 The RestfulHelper Class
The OpenWeatherMapAPI class can now be rewritten (see Listing 3-10) to take advantage of this new
helper class. Any additional endpoint calls can be defined in this class and called each from a separate method.
Because you’re calling only one endpoint, you use GetForecast(), but you could add other methods, like
GetDailyForecast(), GetGlobalAlerts(), etc.
using System;
using System.Collections;
using System.Web;
using UnityEngine;
The bold code line is the one that performs the API call to get the forecast data. The FillDays() method
will be called automatically once the remote call has completed successfully.
Summary
Your game can act as a client to a remote service that exposes various methods using a RESTful interface. This
will allow you to perform read and write operations on remote data. These are performed by requesting a particular
URL or endpoint.
There are two methods of authentication: one is per user, the other is per application. OAuth requires users to
sign in to a service like Google, Twitter, etc. and obtain a token. The alternative is that the application provides the
token. The token is then passed via each call to the remote server to validate the request. Out of date or invalid
tokens will get rejected.
Results from the remote service are usually returned in a JSON format. Classes can be easily created using the
JSON function built into Unity.
If you are making multiple calls to different endpoints, it is a good idea to create a generic function to perform
the remote calls.
The weather application uses a high-level client server architecture. It’s now time to switch gears and look at
the lower-level socket programming that is provided as part of the .NET framework, as this will allow you to
create your own protocols.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Kelly, K. Kumar, Unity Networking Fundamentals
https://doi.org/10.1007/978-1-4842-7358-6_4
4. TCP Connections
Sloan Kelly1 and Khagendra Kumar2
(1) Niagara Falls, ON, Canada
(2) Katihar, Bihar, India
In Chapter 3, you used the UnityWebRequest class as a client to access data stored on a website (the server).
The UnityWebRequest class handled all the underlying communication with the web server. This included
the use of HTTP (HyperText Transport Protocol) to format messages and deconstruct the data returned from the
server.
HTTP is quite a heavy protocol and is not recommended for games beyond its use at fetching leaderboards,
or for updating a player’s save game to a cloud service. This chapter looks at how you can create your own
protocol that will provide a client/server game; you will use Transport Control Protocol (TCP) to do that.
Remember that TCP is a connection-oriented protocol. This means that the messages that are passed to and
from the client and server are in order and complete. All of this is handled at the TCP level; how it achieves the
connection and maintains the integrity of the data is determined by the way the client and server synchronize and
acknowledge data.
The TCP Three-Way Handshake
TCP is a connection-oriented protocol and, as such, it has a way of tracking and ensuring that data that’s sent is
received by the remote party. It does this using a system called sequence and acknowledgement numbers .
The client establishes a connection with the remote machine. It sends a synchronization message (SYN) with a
sequence number and lets the remote machine know that the client is ready.
The server responds to the client with a synchronization/acknowledgement (SYN-ACK). The ACK contains the
next number in the client’s SYN message and the SYN holds the sequence number to start subsequent
messages.
The client acknowledges (ACK) the response of the server with the next number in the server’s SYN message.
The three-way handshake is complete, so the local machine (client) and the remote machine (server) can
begin the actual data transfer process. This is illustrated in Figure 4-1.
Figure 4-1 The TCP three-way handshake illustrating the SYN/ACK sequence numbers
TCP Client-Server Connections
The TcpListener class in the System.Net.Sockets namespace provides you with a simple way to allow
incoming TCP connections to be established in your game. The TcpListener can accept an incoming socket
or TcpClient. It provides both synchronous and asynchronous methods to accept connections.
Once a server is created, a remote machine can try to connect to it. These requests will be placed in a queue
that is handled by the underlying networking framework. This isn’t something you have to worry about!
However, be careful when using synchronous calls. When there is nothing in the queue, the main thread will be
blocked, waiting for clients to connect or messages to be received.
Note Synchronous method calls are blocking calls. This means that the main thread cannot do anything
while waiting for the method to return a value or complete the task. On the other hand, asynchronous calls
allow you to provide a callback (another method to call) when something happens. Think of them as event-
driven network programming. Asynchronous calls allow the main thread to keep processing—moving
characters around, updating animations, and so on. The downside is that messages received are on another
thread. This will be covered later.
Socket Connections
Remember that Berkeley’s networking suite abstracted network programming using the file descriptor paradigm.
A socket is a file descriptor for networking and it represents an IP address and a port number. Any
communication to and from the remote server can be made through the Socket class.
A connection to a remote server can be made using the low-level Socket class. Because this class is
IDisposable, I recommend wrapping it inside a using(), as shown in Listing 4-1.
The socket type and protocol are passed to the socket in the constructor. The socket type is Stream. This
represents a connection-oriented service and the protocol type is TCP because that is the protocol you are using
to establish the connection.
try
{
socket.Connect(IPAddress.Parse("127.0.0.1"), 9021);
}
catch (SocketException e)
{
print(e);
}
Listing 4-2 Connecting to a Local Service Running on Port 9021
Because the Connect() method can throw an exception, it is best to wrap it inside a try/catch block,
as shown in this example.
The better option is to use the BeginAcceptSocket() method , which takes two parameters:
The callback method
The state object
The state object can be anything or null, but I recommend passing in the listener. It will be accessible through
the callback’s IAsyncResult parameter’s AsyncState property.
Listing 4-4 shows how to create a TcpListener instance listening on port 9021 of the local machine using
any IP and to signal when a connection from a remote machine has been established.
Listing 4-5 shows the code for the Socket_Connected event. Notice that the AsyncState is accessed
as a TcpListener because that is the state object that you passed into the BeginAcceptSocket() call.
Sending Data
Data is sent to the remote server using Send() or BeginSend(). The Send() method is the synchronous
call and BeginSend() is the asynchronous call.
As with all data transferred via sockets, it takes the form of a byte array. Byte arrays will form part of your
serialization/deserialization routines. For now, though, these examples will just use plain ASCII text converted to
and from a byte array.
Synchronous Send
Listing 4-6 illustrates how to send a simple ASCII message to the remote server using a byte[] array.
The number of bytes sent is returned from the Send() method. If the number of bytes sent is less than the
total number of bytes in your message, you will have to advance the pointer and send again. A simple while
loop can be used, as shown in Listing 4-7. The receiver will also have a buffer to accept the incoming data. The
receiver will have to maintain a count of received bytes to make sure that the complete message has been
received. The buffer can be periodically written out to disk or some other storage—a memory stream for
example.
Asynchronous Send
Sending data asynchronously is achieved through the BeginSend() method. The asynchronous callback takes
an IAsyncResult object that contains an AsyncState property. This property can be filled by passing the
state parameter to BeginState(). I recommend that you pass in the socket, as shown in Listing 4-8.
void Start()
{
var socket = new Socket(SocketType.Stream,
ProtocolType.Tcp);
socket.Connect(IPAddress.Parse("127.0.0.1"), 9021);
var msg = Encoding.ASCII.GetBytes("Hello, from Client!");
socket.BeginSend(msg,
0,
msg.Length,
SocketFlags.None,
Send_Complete,
socket);
}
Receiving Data
Data is received by both the client and the server; the client when receiving a result from the server and the
server when receiving a request from the client. In order to receive data from a remote machine, you must have a
place to store the incoming messages. In C#, this is a byte array.
As with sending, you can receive synchronously with the Receive() method and asynchronously with the
BeginReceive() method.
Synchronous Receive
Listing 4-9 illustrates how to receive a message into a buffer using a socket. The code assumes that socket is a
valid instance of Socket.
Receiving a larger file into a smaller buffer is possible. It is good practice for the sender to send information
about the data being transferred, including the size of the file. If you were going to write a file transfer program,
you might want to send the size of the file as the first part of the transmission and then the contents of the file as
the remainder.
The receiver would then read the first four bytes and use this as a counter against the number of bytes
received. Listing 4-10 contains code that receives a byte array. The first four bytes represent an integer indicating
the size of the transfer. BitConverter.ToInt32() can convert bytes to an integer easily and the remaining
bytes of the message are a matter of arithmetic.
byte[] superBuffer;
var buffer = new byte[1024];
recv -= 4;
int sbOffset = recv;
int bytesRemaining = length - recv;
Array.Copy(buffer, 4, superBuffer, 0, sbOffset);
while (bytesRemaining > 0)
{
Array.Clear(buffer, 0, buffer.Length);
recv = client.Receive(buffer);
bytesRemaining -= recv;
This is part of a program that receives a file sent over the network. The first four bytes of a received file is
the length of the file. These four bytes are converted into an integer and the remaining bytes are added to a buffer
that will contain the whole file.
Messages are received into a temporary byte array that’s 1024 bytes in size, called buffer. The superBuffer is
created with the length of the actual file—the first four bytes of the received data. The contents of buffer are
copied to superBuffer. This process is repeated until there are no more bytes to copy, i.e., bytesRemaining is
zero.
Asynchronous Receive
As with the synchronous Receive(), any messages received will go into a buffer. Because there is a callback
involved, it would be difficult to access this buffer if it was created locally. Therefore, I recommend creating a
state object that can be used to hold not only the socket that sent the message but also the buffer. Listing 4-11
shows such a class.
using System.Net.Sockets;
The StateObject can be used to pass information to the callback through the
IAsyncResult.AsyncState property . For example, when a socket connects, you can start to receive on
that socket, as shown in Listing 4-12.
Once you have completed a receive, you must call BeginReceive() again if you want to allow the client
to send you multiple messages or if you have not received the correct number of bytes in a larger message. The
number of bytes received is returned in bytesIn. Check this variable. You may have to go through a loop like
in Listing 4-10.
You should now have a project hierarchy like the one shown in Figure 4-2.
Figure 4-2 The hierarchy of the Async Sockets project
Open the StateObject script file in your IDE and change the text to the contents of Listing 4-14. Make
sure you save this file when you’re done.
using System.Net.Sockets;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
[HideInInspector]
public bool _isReady;
void Start()
{
_listener = new TcpListener(IPAddress.Any, _port);
_listener.Start();
_listener.BeginAcceptSocket(Socket_Connected,
_listener);
_isReady = true;
}
private void OnDestroy()
{
_listener?.Stop();
_listener = null;
}
socket.BeginReceive(state.Buffer,
0,
state.Buffer.Length,
SocketFlags.None,
Socket_Received,
state);
}
}
if (bytesIn > 0)
{
var msg = Encoding.ASCII
.GetString(state.Buffer,
0,
bytesIn);
print($"From client: {msg}");
}
using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
[RequireComponent(typeof(TcpListenSocketBehaviour))]
public class TcpSocketAsyncBehaviour : MonoBehaviour
{
private Socket _socket;
IEnumerator Start()
{
var listener =
GetComponent<TcpListenSocketBehaviour>();
while (!listener._isReady)
{
yield return null;
}
_socket = new Socket(SocketType.Stream,
ProtocolType.Tcp);
_socket.Connect(IPAddress.Parse("127.0.0.1"), _port);
var msg = Encoding.ASCII.GetBytes("Hello, from Client!");
_socket.BeginSend(msg,
0,
msg.Length,
SocketFlags.None,
Send_Complete,
_socket);
}
Figure 4-3 The output from the Async Sockets Unity project
Along with Program.cs, you should now have the same solution hierarchy as shown in Figure 4-4. The
names of the solution file and project might be different; the ones in the figure are NetCopy and ncp
respectively, but that won’t matter.
This is a command-line tool. You have to open a DOS/command prompt to run these. To run the program as
a server (receiving a file), use the following command-line format. The filename is required. This is the
destination filename that the file will be copied into.
The port option can be omitted. It defaults to the 9021 port if not specified:
$ ncp mypicture.png
The IP is the IP address of the server. The port is the port number the service is running on. The last
parameter is the local file to copy to the remote server.
The first class you will look at is the Config class . This class parses the command-line arguments for the
following parameters:
Filename
Port (default is 9021)
IsServer (default is True)
Debug (Boolean)
ServerIP (default is IPAddress.Any)
AskForHelp (Boolean)
Open the Config.cs file and enter the code in Listing 4-17; save the file.
using System.Linq;
using System.Net;
namespace SloanKelly.Networking.NetCopy
{
class Config
{
public string Filename { get; }
if (args.Length ==0)
{
return;
}
int index = 0;
while (index < args.Length)
{
if (IsMatch(args[index], "-ip", "/ip", "ip"))
{
index++;
ServerIP = IPAddress.Parse(args[index]);
IsServer = false;
} else if (IsMatch(args[index], "-p", "/p", "port"))
{
index++;
Port = int.Parse(args[index]);
}
else if (IsMatch(args[index], "-h", "/h", "/help", "help"))
{
AskForHelp = true;
return;
}
else if(IsMatch(args[index], "-d", "/d"))
{
Debug = true;
}
else
{
Filename = args[index];
}
index++;
}
}
The Sender class sends the file to the remote machine. It does this by reading in the contents of a file as a
byte array and sending that byte array to the server. Before it sends the contents of the file, it sends an integer
(four bytes). This integer contains the length of the file. This is important because the server has no idea how
large the payload is until you tell it.
Note Protocols are all about setting up rules, such as “The first four bytes represent the size of the file being
sent.”
Open the Sender.cs file and enter the code in Listing 4-18. Save the file when you’re done.
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
namespace SloanKelly.Networking.NetCopy
{
class Sender
{
private IPAddress _serverIP;
private int _port;
private string _filename;
private bool _debug;
socket.Send(BitConverter.GetBytes(contents.Length));
Console.WriteLine("Finished!");
socket.Close();
socket.Dispose();
}
return null;
}
}
}
Listing 4-18 The Sender Class Used to Send Data to a Remote Server
The Receiver class receives the data from the remote client. As discussed, it uses two buffers. The first is
the main buffer used to store the entire file. The second is a smaller receive buffer, which is 1KB (1024 bytes) in
size.
Open the Receiver.cs file in the IDE and enter the code from Listing 4-19. Save the file when you’re
done.
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
namespace SloanKelly.Networking.NetCopy
{
class Receiver
{
private IPAddress _serverIP;
private int _port;
private string _fileName;
private bool _debug;
public Receiver(IPAddress serverIP, int port, string fileName, bool
debug)
{
_serverIP = serverIP;
_port = port;
_fileName = fileName;
_debug = debug;
}
if (_debug)
Console.WriteLine($"Received {recv} byte(s)");
socket.Close();
socket.Dispose();
listener.Stop();
return superBuffer;
}
}
}
Listing 4-19 The Receiver Class Is Used to Receive the Data
Lastly is the Program class. This is the entry point into the application. It creates the classes that will be
used, depending on the contents of the arguments passed on the command line. These are parsed out using the
Config class. Help is given if the user requests it or if they make an error in the arguments passed.
Open the Program.cs file and enter the code in Listing 4-20. Save the file when you’re done.
using System;
namespace SloanKelly.Networking.NetCopy
{
class Program
{
static void Main(string[] args)
{
var config = new Config(args);
if (config.AskForHelp || string.IsNullOrEmpty(config.Filename))
{
Console.WriteLine("NetCopy - Sloan Kelly 2020");
Console.WriteLine("Provides a simple peer to peer copy from
one machine to another");
Console.WriteLine("Usage");
Console.WriteLine("\tSend\tncp [-ip serverIP] [-p port]
filename");
Console.WriteLine("\tReceive\tncp [-p port] filename");
}
else if (config.IsServer)
{
var server = new Receiver(config.ServerIP,
config.Port,
config.Filename,
config.Debug);
server.Run();
}
else
{
var sender = new Sender(config.ServerIP,
config.Port,
config.Filename,
config.Debug);
sender.Run();
}
}
}
}
Listing 4-20 Program Is the Entry Point to the Application
Before you can run the program, you have to change the name of the executable. Follow these steps to do so:
1. Right-click the Project file in the Solution Explorer.
2. Click Properties. This will open the Project Properties window.
3. Click the Application tab,
4. Change the Assembly Name to ncp.
5. Click the Save All icon.
To try this program, you have to run it from the command line. Two command prompt windows are required
—one for the server and the other for the client.
To receive a picture, on one command prompt window, the output might look like this after an image has
been sent:
$ ncp troncopy2.jpg
Listening for connection
Size of file received is 2330429 byte(s)
The TcpListener and Socket classes makes things easier, but to make things really easy, the .Net
framework also has another class called TcpClient. This class wraps the Socket class into a neat package.
You’ll look at how the TcpClient can be used to talk to a TcpListener next.
TcpClient Connections
The TcpClient class connects to a remote server and is used on the server to refer to a remote connection
from another machine. The TcpClient class exposes a NetworkStream that is used to read and write data.
While it is certainly possible to use synchronous methods with TcpClient, this book will be avoiding
them in favor of the asynchronous versions.
The TcpClient wraps the Socket class and contains an internal buffer. Because you’re using the
asynchronous methods, this buffer is returned when the read operation is completed.
The state object is contained in the AsyncState property of the IAsyncResult. Because you passed in
the TcpClient, it can be used directly here to obtain the NetworkStream used to end the write transaction.
using System.Net.Sockets;
Because the buffer is integral to the state, it is created in the constructor. Listing 4-26 shows the
Client_Received callback where the StateObject is used.
if (bytesIn > 0)
{
var msg = Encoding.ASCII
.GetString(state.Buffer,
0,
bytesIn);
print($"From client: {msg}");
}
The state object is read from the AsyncState property of the IAsyncResult. The network stream read
is completed by calling the EndRead() method and this returns the number of bytes received.
Similar to the socket example earlier, this might not be the complete message and you will have to listen for
more. This is why there is a call to BeginRead() at the very end of this method.
Listing 4-28 illustrates an example callback when a client is connected. At the end of the method there is a
further call to accept an incoming connection. Without this, no other client could connect to your service.
listener.BeginAcceptTcpClient(Socket_Connected,
listener);
}
}
Listing 4-28 Example Callback to Accept a TCP Client
Note If you want to accept more than one connection, you have to re-call BeginAcceptTcpClient()
when a client connects.
Don’t worry about timing. Clients are buffered in a queue as they connect and are presented to you one at a time.
This is handled automatically by the operating system too.
Hello World Example Using TcpClient and TcpListener
In this section, you see how to create a Hello World example using the TcpClient and TcpListener. You
will revisit the Unity project from earlier in this chapter. Follow these steps:
1. Create a new scene in the project.
2. In the Scripts folder, create a new C# script file called TcpListenClientBehaviour.
3. In the Scripts folder, create a new C# script file called TcpClientAsyncBehaviour.
4. In the Scripts folder, create a new C# script file called ClientStateObject.
5. Drag and drop the TcpClientAsyncBehaviour script file onto the MainCamera in the scene.
6. Drag and drop the TcpListenClientBehaviour script file onto the MainCamera in the scene.
7. Save the scene as AsyncClients.
You should now have the project hierarchy shown in Figure 4-6.
Figure 4-6 The project hierarchy after completing the steps to add the TcpClient script files
With those steps complete, you will now fill in the script files that were just created. Listing 4-29 is the full
script of the ClientStateObject class. This class is the state object used when receiving data from the
client. It contains a reference to the TcpClient as well as the buffer that data is received into. Replace the
current contents of the ClientStateObject script with Listing 4-29.
using System.Net.Sockets;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
[HideInInspector]
public bool _isReady;
void Start()
{
_listener = new TcpListener(IPAddress.Any,
_port);
_listener.Start();
_listener.BeginAcceptTcpClient(Socket_Connected,
_listener);
_isReady = true;
}
client.GetStream()
.BeginRead(state.Buffer,
0,
state.Buffer.Length,
Client_Received,
state);
}
}
if (bytesIn > 0)
{
var msg = Encoding.ASCII
.GetString(state.Buffer,
0,
bytesIn);
print($"From client: {msg}");
}
Save the file. Finally, Listing 4-31 contains the contents of TcpClientAsyncBehaviour. Replace the
current contents of the TcpClientAsyncBehaviour script file with the code in Listing 4-31. The
assumption is that the client is connecting to a server running locally on port 9031, but those values can be
changed in the code or through the Unity Editor’s Inspector. Don’t forget to save the file.
using System;
using System.Collections;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
[RequireComponent(typeof(TcpListenClientBehaviour))]
public class TcpClientAsyncBehaviour : MonoBehaviour
{
private TcpClient _client;
IEnumerator Start()
{
var listener = GetComponent<TcpListenClientBehaviour>();
while (!listener._isReady)
{
yield return null;
}
_client = new TcpClient();
_client.Connect(_ipAddress, _port);
var msg = Encoding.ASCII.GetBytes("Hello, from TcpClient!");
_client.GetStream()
.BeginWrite(msg,
0,
msg.Length,
Send_Complete,
_client);
}
When you run the program in Unity, the console should, as shown in Figure 4-7, show the message received
from the client.
Figure 4-7 The console output after running the scene containing the TcpClient example
Tic-Tac-Toe
Tic-Tac-Toe—also known as Naughts and Crosses or Xs and Os—is a game played on a 3x3 grid. The goal is to
be the first to place three tokens in a line on the grid. Each player takes turn placing their token—an X or an O
on a blank grid square. The game is popular because the rules are simple and you only need a flat surface and
something to draw the shapes on. Figure 4-8 shows a typical game in progress.
Figure 4-8 An in-progress game of Tic-Tac-Toe played on a piece of paper. X is about to win
Games can end in one of two ways; X or O can win by creating a row of tokens horizontally, vertically, or
diagonally. The game can also end in a tie, sometimes referred to as a “cat’s game.” This is when there is no
winner and no more moves are available, i.e., the board is full.
In the remaining pages of this chapter, you are going to create a client-server networked version of Tic-Tac-
Toe that you can play with someone on their device. Figure 4-9 shows the same game as earlier, but in progress
on the finished Tic-Tac-Toe client-server version you will build.
Figure 4-9 An in-progress game of Tic-Tac-Toe played in the networked game. X is about to win
This project will use the TcpClient and TcpListener classes to build the client-server application. To
add more functionality, these classes will be wrapped inside some custom classes, located in the NetLib folder
. These classes will help you maintain the list of clients connected to the server, messaging events and
serialization/deserialization. You could just as easily not write these additional classes and code everything into
the tic-tac-toe game itself. Abstraction of the network layer provides a better way to separate the game from the
networking classes.
To pass data between the client and the server, you will use a simple serialization protocol. The messages
passed will be simple text messages. The first line will contain a header with the size of the payload. The
payload is the actual message being sent. The payload contains the state of the board and the current player as
well as a command to inform the client application what state to be in.
The client and server will use a small finite state machine (FSM) to control the current state of the program.
See https://en.wikipedia.org/wiki/Finite-state_machine for more details on finite state
machines.
The Tic-Tac-Toe game will allow one player to act as a server and allow another to connect. The player
running the server will also connect via the network, but this will be transparent to the player.
When the game starts, the players are shown the title page, as shown in Figure 4-10. It contains two options
—start or join a server.
Figure 4-10 The title screen showing the Start Server, Join, and IP address controls
When the server starts, a client is automatically created and joins the server. The first client to join is always
X. The second client will be another player running on another device. When the second client joins, the game
starts and X gets to make their first turn. Then O, and so on, until the game ends. It isn’t possible to click a
square when another player is choosing their grid square.
Each cell or button click on the client sends a message to the server. This will result in a message being sent
back to all the clients.
Clients can only send commands to the server and the server can only send updates to the command. The
cycle is shown in Figure 4-11 with one client, but in the actual application, the updates will be sent to both
clients.
Figure 4-11 The client sends a command and the server broadcasts the updated board, game state, and current player to each client
Starter Files
A project of this size requires some work up front. The source code for this and other chapters is available on
GitHub via the book’s product page, located at
https://www.apress.com/us/book/9781484273579.
Getting Started
The project uses the Canvas and UI controls to build the game and its various states. To create the starter project,
follow these steps:
1. Create a new 2D project in Unity called Tic-Tac-Toe.
2. Download the tictactoe-starter.unitypackage file from the book’s GitHub repo.
3. Double-click the package to install it into the open project.
You should now have a scene called StartHere and the list of assets, as shown in Figure 4-12.
The Client
The tic-tac-toe game has been built using the client-server model. Figure 4-13 shows some of the classes used on
the client side of the game and how they communicate internally and with the server.
Figure 4-13 The TicTacToeClientBehaviour class and how it communicates internally with the UI and to and from the server via the TcpClient
The TicTacToeClientBehaviour provides the glue code between the UI—the play grid and the
buttons—and the network communications.
Even though the client handles events, it does not directly respond to them and instead waits for confirmation
of the action from the server. This ensures the integrity of the game state.
Each time the player clicks a button in the UI, it is translated into an event. The event handler on
TicTacToeClientBehaviour calls a method on the TicTacToeClient, which in turn sends a message
to the server. When messages are received from the server, they are placed on a queue and processed one at a
time in the Update() method of the TicTacToeClientBehaviour. These messages update the visual
state of the game and the circle is complete.
Note You should never trust the client! Confirm all actions on the server to ensure that the game is being
played fairly.
Note The entire application will use standard .Net events to decouple the classes. These will be used on the
networking side to inform subscribers when a message has been received.
The TicTacToeClient
The TicTacToeClient provides several services for the TicTacToeClientBehaviour:
Handles sending messages to the server. Methods are exposed to TicTacToeClientBehaviour to send
those messages without it having to explicitly know how to format them.
Handles incoming messages from the server.
Provides event handlers that the TicTacToeClientBehaviour can subscribe to for starting the game,
toggling the active player, showing the winner screen, and quitting to return to the title.
The TicTacToeClient contains an instance of the NetworkClient class. This will provide the
underlying network access.
NetworkClient
The NetworkClient class wraps the .Net TcpClient class and a buffer into a single class. This makes the
reception and transmission of data to and from a client easier. Internally, there is a MessageBuffer class that
will be used to store large messages received from the server. Once a complete message has been received,
anyone subscribing to the MessageReceived event will be notified.
It should be noted that any messages received will be on a thread that is not the main thread. There must be a
way to send the message from the remote machine to the main thread. In the game, this will be implemented
using a very simple message queue.
Each time there is an interaction with the queue, you must lock it. This prevents other threads from accessing
the message queue while an action is enqueued/dequeued.
Note Use the lock() pattern to ensure that other threads cannot access the data at the same time.
The Server
TicTacToeServer is based on the NetworkServer<T> class that contains a TcpListener instance.
The TicTacToeServer class handles the network traffic and the TicTacToeGameServer class acts as a
bridge between the network traffic (in and out bound) and the game logic. Figure 4-14 shows how the server
classes pass messages internally and externally to the connected clients.
Figure 4-14 The relationship between the classes used in the server part of the game
The AppController class extends MonoBehaviour and has links to the PanelController; it
handles the user interaction with the title screen, which includes event handlers for the Join and Start Server
buttons.
The NetworkServer<T> class contains a TcpListener instance and maintains a list of clients using
the NetworkClientCollection class. Each time a client connects, an event is fired. The class can also
limit the maximum number of clients that can connect. You will be using this feature in this application because
only two people can play tic-tac-toe. The class is generic because, when a message is received, it is deserialized
into the correct class/struct instance.
Serialization
To minimize the errors in serialization/deserialization, a factory class called GameSerialization will be
created. This will contain functions to generate the messages in the correct format. It will also provide a way to
deserialize a byte array into a message.
The message class is GameMessage and contains three fields:
messageType – An enumeration that represents the type of message being passed
playerId (int) – The currently active player
boardState (int[]) – An array of integer values that represents the current state of the board. This
means that there will always be nine values in this array; one for each grid square
The boardState will contain one of three possible values for each grid square:
0 – The grid square is unoccupied
1 – An X is placed in the grid square
2 – An O is placed in the grid square
Messages passed from server to client will always contain the current state of the game.
Messages passed from client to server will provide the server with the player ID (which could be validated
on the server side); the boardState value contains an array with a single integer representing the move if the
messageType is MessageType.ClientMakeMove. All other messages from the client to the server will
have an array with a single 0 (zero) in them, because those commands do not need extra information.
The payload format uses a simple colon-separated list in this format:
messageType:playerId:gridState
Where messageType is the type of message being transmitted, playerId is the ID of the player (1 – first
player, 2 – second player) who has control of the board, and the gridState is a comma-separated list of
values representing the state of the board. For example, the following message will let the client know whose
turn it is while indicating the state of the board. The top left of the board is occupied with an X (player 1):
Size: 38
ServerTogglePlayer:2:1,0,0,0,0,0,0,0,0
Notice that the entire message includes the header. The header will be stripped out and the payload will be
read separately by the deserializer.
NetLib Classes
To abstract the networking code from the main logic of the Tic-Tac-Toe game, you will create some wrapper
classes around TcpClient and TcpListener. These classes will provide you with ways to handle passing
messages across the network as well as maintain client connections. Follow these instructions to create the script
files for the new NetLib classes :
1. Create a new C# script file in the Scripts/NetLib folder called MessageBuffer.
2. Create a new C# script file in the Scripts/NetLib folder called NetworkClient.
3. Create a new C# script file in the Scripts/NetLib folder called NetworkClientConnection.
4. Create a new C# script file in the Scripts/NetLib folder called NetworkServer.
5. Create a new folder called Events inside the NetLib folder.
6. Create a new C# script file in the Scripts/NetLib/Events folder called
MessageReceivedEventArgs.
7. Create a new C# script file in the Scripts/NetLib/Events folder called
NetworkClientEventArgs.
8. Create a new C# script file in the Scripts/NetLib/Events folder called PayloadEventArgs.
With all those files created, you should have a hierarchy inside your NetLib folder that looks like Figure 4-
15.
Figure 4-15 The NetLib script hierarchy after completing the tasks
With all those files created, it’s time to start adding the code. The classes have been separated into logical
breaks to give a little insight to their purpose and how they can be used outside of this project.
MessageBuffer Class
The MessageBuffer class is located in the NetLib folder. It is used to store incoming large messages from a
remote device. On completion, the IsComplete property is set. Listing 4-33 shows the contents of the
MessageBuffer class. Open the MessageBuffer.cs file and replace the contents with the code in Listing
4-33.
using System;
When a message has been received, subscribers should be notified. The MessageReceivedEventArgs
will be used with an event handler to provide subscribers with the needed information. Listing 4-34 contains the
contents of the MessageReceivedEventArgs class. Open the
NetLib/Events/MessageReceivedEventArgs script file and replace the contents with the code in
Listing 4-34.
using System;
NetworkClient Class
The NetworkClient class is used by the client (unsurprisingly!) and the server to allow for two-way
communication between the client and server. It contains an instance of TcpClient to provide the networking and
a MessageBuffer to hold larger messages.
It is based on a simple text-based protocol that has a header describing the size of the payload and the
payload itself. The header and payload are separated by a single \n (newline) character.
The format of the header is as follows:
Size: size-of-header
Where size-of-header is an integer that is the length of the payload in bytes. It is assumed that the
messages passed in are in the correct format. For simplicity, there is no error checking, which can be added later.
When a message has been received in full, the MessageReceived event is triggered. This uses the
MessageReceivedEventArgs class that you created earlier.
Listing 4-35 contains the full listing for NetworkClient. Open the NetworkClient script file in the
NetLib folder and replace it with the following listing.
using System;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
_client.GetStream().BeginWrite(fullMessage,
0,
fullMessage.Length,
Write_Callback,
null);
}
Array.Clear(_buffer, 0, _buffer.Length);
_client.GetStream()
.BeginRead(_buffer,
0,
_buffer.Length,
Remote_ReceivedMessage,
null);
}
}
}
On the server, the clients are stored inside a NetworkClientCollection class. This class keeps the clients
together and provides a single MessageReceived event handler that subscribers can hook into. The
subscribers also receive the NetworkClient instance that sent the message.
Open NetworkClientCollection in the NetLib folder and replace it with the code in Listing 4-36.
using System;
using System.Collections;
using System.Collections.Generic;
_clients.Clear();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Listing 4-36 The NetworkClientCollection Class
NetworkServer
The NetworkServer class is located in the NetLib folder; it handles the incoming connections and
maintains a list of connected clients. The maximum number of clients allowed to connect can be set at runtime.
The NetworkServer is a wrapper around the TcpListener class. It provides four events for subscribers:
ClientConnected – Subscribers are informed each time a client connects.
ConnectionOverflow – Subscribers are informed when a client tries to connect but the maximum
number of connections has been exceeded.
PayloadReceived – Subscribers are informed when a payload has been received from one of the clients.
ClientListFull – Subscribers are informed when the maximum number of clients have connected. This
fires when the current client connects and the number of connected clients equals the maximum allowed.
The NetworkServer class is an abstract class. Each game needs to provide its own subclass and
implement a single method, CreatePayload(). Optionally, subclasses can override the
OnClientConnected() method.
Before you create the NetworkServer class, you need to create the two remaining event argument classes
—NetworkClientEventArgs and PayloadEventArgs.
Open the NetworkClientEventArgs script file in NetLib/Events folder. Replace the existing code
with the contents in Listing 4-37. Save the file.
using System;
Now open the PayloadEventArgs script file in the NetLib/Events folder. Replace the existing code
with the contents of Listing 4-38. Save the file.
With those two classes complete, you can now finish the NetworkServer class. It is a generic class that
takes a type that represents the payload. For this game, that will be the GameMessage class. As you will see
later, the TicTacToeServer extends NetworkServer and provides this generic parameter.
Open the NetworkServer script file located in the NetLib folder. Replace the contents of this file with
the code in Listing 4-39 and save the file.
using System;
using System.Net;
using System.Net.Sockets;
if (_clients.Count == _maxConnections)
{
ConnectionOverflow?.Invoke(this,
new NetworkClientEventArgs(networkClient));
}
else
{
_clients.Add(networkClient);
OnClientConnected(networkClient);
if (_clients.Count == _maxConnections)
{
ClientListFull?.Invoke(this,
EventArgs.Empty);
}
else
{
_listener.BeginAcceptTcpClient(Listener_ClientConnected,
null);
}
}
}
}
Messages
The game messages are sent between the classes using standard .Net event handlers. Each GameMessage
consists of three elements:
The message type, which is an enum value from MessageType
The player ID
The state of the tic-tac-toe grid as an array of integers
Open the MessageType script file. Replace the contents of this file with Listing 4-40 and save the file.
Open the GameMessage script file. Replace the contents of the GameMessage script file with Listing 4-
41 and save the file.
The GameMessage instances are passed around using an EventHandler that emits a
GameMessageEventArgs instance. Open the GameMessageEventArgs script file. Replace the contents
of the file with Listing 4-42.
using System;
To make it easier to serialize game messages to a byte array and from a byte array to a GameMessage
instance, a factory class called GameSerialization needs to be created. Open the GameSerialization
script file and replace the contents of that file with Listing 4-43. Don’t forget to save what you have done so far.
using System;
using System.Linq;
using System.Text;
return ToBytes(message);
}
Client Classes
There are two client classes used by the game—TicTacToeClient and TicTacToClientBehaviour.
TicTacToeClient is a wrapper around NetworkClient and exposes a number of events that
subscribers can hook into. These events are fired when a message is received from the server:
StartGame – The server has indicated that the game is starting
TogglePlayer – The active player has changed
ShowPodium – Show the winners podium
ReturnToTitle – The game should return to the title screen
The TicTacToeClientBehaviour is created when the player that is hosting the game starts the server
or when a remote player joins a server. It uses the methods on the TicTacToeClient to send messages to the
server.
Open the TicTacToeClient script file and replace the contents with the code shown in Listing 4-44.
using System;
Open the TicTacToeClientBehaviour script file and replace the contents with the code shown in
Listing 4-45. This class will be instantiated in the AppController class.
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;
void Awake()
{
_actions = new Queue<Action>();
}
void Update()
{
lock(_actions)
{
while (_actions.Count > 0)
{
_actions.Dequeue().Invoke();
}
}
}
_client.ShowPodium -= Client_ShowPodium;
_client.StartGame -= Client_StartGame;
_client.TogglePlayer -= Client_TogglePlayer;
_client.ReturnToTitle -= Client_ReturnToTitle;
_board.CellClicked -= BoardCell_Clicked;
_board.PlayAgainClicked -= PlayAgain_Clicked;
_board.ReturnToTitleClicked -= ReturnToTitle_Clicked;
_client.Cleanup();
}
_board.CellClicked += BoardCell_Clicked;
_board.PlayAgainClicked += PlayAgain_Clicked;
_board.ReturnToTitleClicked += ReturnToTitle_Clicked;
}
lock (_actions)
{
_actions.Enqueue(action);
}
}
lock (_actions)
{
_actions.Enqueue(action);
}
}
}
Listing 4-45 TicTacToeClientBehaviour Class
Note Because TicTacToeClientBehaviour provides the link between the network messages and UI,
a message queue has to be used to process the messages on the main thread.
Server Classes
There are two server classes—TicTacToeServer is the networking component and
TicTacToeGameServer is the part that interfaces the networking code with the tic-tac-toe game engine. The
TicTacToeGameEngine class does not need to know that it is running a networked game of tic-tac-toe. The
TicTacToeGameServer provides some code to bridge the actions coming from the client and the game.
The TicTacToeServer extends NetworkServer and provides it with the generic argument—
GameMessage, which is the struct you declared earlier. Open the TicTacToeServer script file and replace
it with the code in Listing 4-46.
using System.Collections.Generic;
_players[i].Send(message);
}
}
As you have noticed, these are methods that wrap around network calls. The TicTacToeGameServer
uses these methods to interact with the clients. Open the TicTacToeGameServer script file and replace the
contents with Listing 4-47. Save the file.
using System;
switch (_engine.State)
{
case TicTacToeGameState.Playing:
if (_server.IsCurrentPlayer(client, _engine.CurrentPlayer))
{
if (e.Payload.messageType ==
MessageType.ClientMakeMove)
{
MakeMove(e.Payload.boardState);
}
}
break;
case TicTacToeGameState.Podium:
if (e.Payload.messageType == MessageType.ClientPlayAgain)
{
StartTheRound(this, EventArgs.Empty);
}
else if (e.Payload.messageType == MessageType.ClientQuit)
{
_server.QuitToTitle();
}
break;
}
}
}
Listing 4-47 TicTacToeGameServer Class
AppController
The last class is the AppController class . The class provides a link between the UI panels used to create the
game’s visuals as well as instantiation of the server and client. Its main function is to wait for the player to click
the Start Server or Join buttons. This action will set off a series of events that will start the server and connect a
client, or, in the case of Join, will attempt to join a server.
Open the AppController script file and replace the contents with Listing 4-48. Save the file.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
void Start()
{
_clients = new List<TicTacToeClientBehaviour>();
_panels.GetPanel<MainTitlePanel>().StartServerClicked +=
StartServer;
_panels.GetPanel<MainTitlePanel>().JoinServerClicked += JoinServer;
_panels.GetPanel<StartServerPanel>().CancelClicked += CancelServer;
}
CreateClient(true);
_panels.ShowPanel(PanelType.StartServer);
}
_panels.ShowPanel(PanelType.Title);
ClearClients();
}
if (!local)
{
var userEnteredAddress = _panels.GetPanel<MainTitlePanel>
().ServerAddress;
address = GetAddress(userEnteredAddress);
}
if (address == IPAddress.None)
{
ShowError("Invalid IP Address!");
}
else
{
var tcpClient = new TcpClient();
tcpClient.Connect(address, _port);
client.Connect(tcpClient);
}
_clients.Add(client);
}
The CreateClient() method is called if the player is running the server locally or if they are connecting
remotely. You need to know the IP address of the server. If you are running the server locally, the default
callback of 127.0.0.1 is used.
Now that you have the code for this class, it should be added to the scene. Follow these instructions to add
AppController to the scene:
1. In the scene hierarchy, create a new GameObject called AppController.
2. Drag and drop the AppController script file to the AppController game object.
3. With the AppController object selected, drag and drop the TicTacToeUI game object into the
AppController’s Panels field in the Inspector.
You should now have the object hierarchy shown in Figure 4-16.
Figure 4-16 The scene hierarchy after creating the AppController object
When selected, the Inspector should look like Figure 4-17 for the AppController object. Your positional
data may be different, but you should have an AppController script attached to it.
Figure 4-17 The Inspector showing the components attached to the AppController GameObject
Now that everything has been written, it’s time to play a game of tic-tac-toe!
Select the PC, Mac, and Linux Standalone platform and click the Build button. You will be prompted to
specify a location. The build will then start.
Run the editor first. This will act as the server for the game. The application you just built will act as the
client. Run the client. You may have to use Alt+Tab (also known as Cool Switch) to shift focus between the
running game and the Unity Editor.
Figure 4-19 shows the layout of the game being played on a dual monitor. The left side is the client and the
right side is the server running in the Unity Editor. Once the applications have been placed side by side, it is easy
to see what is happening and play the game.
Figure 4-19 The client and server running on the same machine in side-by-side windows. The client on the left is a standalone executable and the
right shows the server running inside the Unity Editor
Notice that it doesn’t take long for the screens to update on a grid click, even though the updates are
happening over a network connection. This is because the client and the server are both running on the same
machine using the loopback 127.0.0.1 address. Even when playing across the local area network, you shouldn’t
see much if any delay.
If you run into any problems, there are a couple of troubleshooting points you can try:
Change the port number of the server. This can be done by changing the port number field on the
AppController GameObject. The default is 9021.
Ensure that your firewall is not blocking the port you selected.
Check the classes and ensure that everything is typed correctly.
There isn’t a quick way to change the port number on the client, but I leave that as an exercise for you, dear
reader! In the program given, you would need to change the port number and recompile.
Summary
The .Net framework provides a low-level socket connection class in the form of Socket. It can be used to
establish a connection using a variety of socket types (datagram and stream, for example) and protocols,
including TCP. It is easier, however, when using TCP, to use the higher-level classes TcpClient and
TcpListener.
Both synchronous (blocking) and asynchronous (non-blocking) methods are provided to read, write, and wait
for connections. It is always better to use the non-blocking methods. This does mean that, in order to complete
the operation, you must also code a callback. This is a small price to pay to avoid your game from stuttering or
freezing all together.
The TcpClient uses a NetworkStream to read and write data. It is possible to use a BinaryWriter,
for example, to write data and a BinaryReader to read data from this stream. It is often easier using the
BeginWrite() and BeginRead() methods. These methods allow a state object to be passed to allow
context to be established in the callback. For example, when ending a write the state object would be the client
performing the write operation.
Reading or writing large amounts of data might require multiple operations. This can be done by having a
smaller reception buffer and filling a larger buffer. You will need to include some way in your protocol to inform
the receiver what size of payload you intend to send.
At the end of this chapter, you built on your knowledge to create a TCP version of Tic-Tac-Toe for two
players, utilizing the TcpClient and TcpListener classes. You used wrappers to abstract the client
connections and the server to make it easier for the game code to interact with the underlying networking code.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Kelly, K. Kumar, Unity Networking Fundamentals
https://doi.org/10.1007/978-1-4842-7358-6_5
5. Networking Issues
Sloan Kelly1 and Khagendra Kumar2
(1) Niagara Falls, ON, Canada
(2) Katihar, Bihar, India
Game Lag
Anyone developing an application that uses the Internet or that has
networking capabilities must consider two key things that can affect the
performance of the application. If the application relies completely on the
Internet, it might become unusable because of these two issues:
Bandwidth
Latency
Bandwidth
Bandwidth is the maximum amount of data that can pass through the
network at a given time. Consider it like lanes on the highway, whereby
only a certain number of vehicles can drive on them at one time.
Bandwidth should not be confused with the speed of the Internet,
although it does affect the speed indirectly. The more available bandwidth
there is, the more data can be downloaded synchronously.
In other words, bandwidth is the volume of data moving at any instant
in time.
Even though bandwidth measures data volume over a network, it does
not speculate about how data is moving from one node of the network to
another. As data travels through the network (via Ethernet, coax, fiber, or
any other connection media), bandwidth turns a blind eye toward the
network speed.
For bandwidth, it can be helpful to imagine a hose connected via a pipe
or a tube to a water tank. This water tank can upload data at an infinite
speed and has an unlimited bandwidth (which is realistically not possible).
Speed at which the water is flowing is the Internet speed and the volume of
water that is coming out of the hose at any given instance is the bandwidth.
If you increase the diameter of hose, the volume of water coming out will
increase, but the speed of water might not be affected.
Data often moves through multiple networks or computers, and the
terminating points of these are usually the personal devices, such as
computer, phone, or laptop. If we backtrack the connections, we will find
that bandwidth is very high for backbone systems of the Internet. For
example, bandwidth available via India’s TIER-I provider is more than
20+Tbps. Because of this, you commonly don’t get slow data if you have
more bandwidth available with your ISP.
How Bandwidth Works
The more bandwidth there is, the more data can be sent and received at
once. The wider the hose, the more water can pass through. Likewise, the
higher the capacity of the connection or pipeline, the more data can pass
through in a second.
Most of these expenses are paid by the end consumers. The higher the
bandwidth you want for your connection, the higher the charges for that
connection.
Bandwidth vs. Speed
Many people confuse bandwidth and speed. The main part of the confusion
is due to the way they are portrayed in advertisements for high speed, when
they actually mean high bandwidth. In fact, speed means how fast data can
be sent and received and bandwidth means how much data can be sent and
received. Fiber-optic based connections are close to the speed of light, so
the bandwidth will define how your experience is.
[8 bit = 1 Byte]
Demand on Demand
Demand on demand is a marketing term that can be understood by VOD
(Video on Demand). As the name suggests, video is available whenever
there is a demand for it. It may be via OTT platforms or streaming websites.
Corporate Internet connections are dedicated lines and are sold at fixed
prices. In the case of domestic connections, most people are unaware of
their bandwidth requirements. How much bandwidth do they need? They
often opt for plans according to their budget or based on the sales
representative’s recommendation. In many situations, they buy more than
they need. Even if they need it, they may not be saturating the bandwidth
24*7, allowing the bandwidth to be shared by other people with the same
plans.
According to my research, ISPs opt for a 1:50 ratio. This means that for
every 1Mbps of bandwidth, they will have 50 people on it, as not all of
them will be using it at one instance. Whenever that happens, you get a
slow experience.
Latency
What is network latency ? Is it important? Why does it differ a lot? Figure
5-1 is a graphical representation of latency.
Latency can simply be considered a delay. Delay and latency are used
interchangeably in literature but not in networking terminology. Latency in
networking is measured as the roundtrip delay, i.e., the time it takes the data
packet to travel from the source to the destination and to come back.
This roundtrip delay is an important measure, as computers use the
TCP/IP protocol, whereby the computer sends data to its destination and
then waits for its acknowledgement before sending new data. Thus,
roundtrip delay can have a significant impact on network performance.
Latency is the time taken by the system/network on the users’ action
over the network for a resulting response. Network delays refer to the
delays directly in the network. I general terms, latency is the time taken by
the website or service to respond over the Internet once the user clicks the
button and the appropriate result is shown to them.
Although data travels at the speed of light, the distance can still affect
the system and cause delays. Latency often comes from the infrastructure
required by the Internet itself, which cannot be completely eliminated.
However, this infrastructure can be reduced and optimized for better
latency.
Distribution Delay
This is the time required by the data to reach its destination, which is a
function of the speed at which the signal is being transmitted and the
distance it must travel.
Delivery Delay
The time required for a file/web page to collect the data packet from the
transmission. This is dependent on the data packet size and the transmission
speed.
Processing Delay
The time taken by the system to process the data packet for errors and
integrity, and then to forward that packet to the destination.
Line Delay
The delay caused by being in the queue for processing to happen.
A delay between the client and the server is the total sum of all the
calculated delays. Distribution time is defined by the distance and location
of the signal. As you will see, the speed of distribution is often within the
normal range of the speed of light, whereas the delivery delay is caused by
the availability of data at the relay server after processing.
As an example, say that you want to transfer a 10MB file over two
destinations: 1 Mbps and 100 Mbps. It will take 10 seconds to put the entire
file “on the phone” at the 1 Mbps link and only 0.1 seconds at the 100 Mbps
link.
The next step is to check the data packet for its next steps from its
information header. First the router will check the data integrity, then it will
look for the next step, information from header. Most of these things are
done via hardware processing, which is usually slower than the data
transmission rate. Therefore, the data packets start queuing up in the
incoming buffer. Time spent in that buffer is called a linear delay.
Every data packet will have multiple instances of these types of delays,
depending on the distance between the server and the client and the number
of routers or hops the data packet takes. The more devices or routers there
are, the longer the delay will be and the longer the line delay will be as
well.
Speed of Light and Latency
Nothing can travel faster than light and data packets carried through light
inside optical fibers are traveling at that same speed. This creates a
limitation on the rate that data can be transmitted.
However, light can travel at a speed of 299 million meters/sec or 186K
miles/hour. This speed is in a vacuum; light carrying data packets cannot
travel at these max speeds. The speed at which data can travel in copper
wire is much lower than in optical fiber. Table 5-1 outlines these speeds.
Table 5-1 Signal Latencies
The speed of light is fast but it is not instant; it still takes more than 150
milliseconds to travel from New York to Sydney and back. The data in
Table 5-1 assumes that packets travel through optical fiber in a large circle
between cities, which is rare. In most cases, the route taken by data packets
has a very large number of hops in between and can take longer to reach its
destination. Each hop will add delay, and the actual roundtrip time will
increase significantly. Considering everything, the roundtrip time can be
200-300ms, which is really good.
Humans are not very sensitive to delays in milliseconds; however,
research suggests that a delay of 100-200ms can be detected by the human
brain. When the 300ms delay mark is crossed, the human brain sees it as
lazy. If it reaches 1000ms, (1 second), the human brain will start questing
the connection speed, the system response, the connectivity, and so on.
The point of all this information is that you should try to make content
available for users as close to them as possible, in order to reduce the delay
and latency and keep it under 100ms. To be successful in reducing the
latency of the network, you must carefully manage it and provide a clear,
less congested path.
Last Mile Latency
You might be astonished to know that most latency is added when the
connection is about to reach your home office, not in underground cables or
when crossing continents. This is referred to as the last mile latency. To
connect to your home, your ISP will use wires with the router at its
exchange and might add several hops along the line to reduce installation
and maintenance costs. Sometime these can add up to hundreds of
milliseconds of latency.
According to the FCC, global-based broadband service has three
performance ratings:
10-20ms is good
15-40ms is moderate
30-65ms is a DSL connection
This latency of 10-65ms is to the closest server inside the main ISP
network, before the data packet is delivered to its final destination. The
FCC report mainly focuses on United States broadband. However, the last
mile connectivity is a challenge for all Internet service providers in all
geographic locations. Ironically, this the area where most Internet
bandwidth is affected as well.
If you want to know about latency in your network, you can run a
simple traceroute command in a shell. It will tell you how many hops
there are before reaching the destination. See Figure 5-2.
Figure 5-2 Traceroute maps for connecting servers
Client-Side Predictions
Almost all online games will have some cheaters playing the game and
most gaming servers are configured in a way to process valid requests.
(Assuming cheaters camouflage their requests as legit requests.) This means
that the input received by the server will be processed and the game will be
updated as expected. If a player was at (10,10), for example, and the right
arrow key is pressed, the player’s new position after a server update will be
(11,10). Since these behaviors are predictable, you can use this to your
advantage. Assume, for example, that you are playing a game and the
character animation plays for 100ms. You have a latency of 100ms, so you
have total latency of 200ms. See Figure 5-4.
This way, you don’t have a delay between the player’s actions onscreen
while the server is still in control (if there is a discrepancy in the data, the
server data will be considered valid).
Synchronization Issues
In the previous example, we carefully chose a number that reduced the
mathematical complexities. However, it is far from an ideal case. So now
let’s say that the delay between the server and the user is 250ms and the
animation plays for 100ms. If the player makes a move two times by
pressing the appropriate button, Figure 5-6 shows how this process might
proceed.
Figure 5-6 Predictive state mismatch with an authoritative server
--------------------------------------------------
------------
Summary
This chapter started by covering problems with conventional networking
that you have to handle before you can focus on multiplayer game
development. The chapter then discussed synchronous and asynchronous
games, which are types of multiplayer games that buffer a lot when
implementing the system.
Lastly, the chapter covered bandwidth, latency, and ping and how to
handle these shortcomings with server reconciliation and client prediction.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Kelly, K. Kumar, Unity Networking Fundamentals
https://doi.org/10.1007/978-1-4842-7358-6_6
6. Develop a Maze Shooter
Sloan Kelly1 and Khagendra Kumar2
(1) Niagara Falls, ON, Canada
(2) Katihar, Bihar, India
In this chapter, you will learn how to create a multiplayer game. The chapter goes through
the key steps required to create a multiplayer game. The process is divided into multiple
steps.
Before we jump right into multiplayer games, let’s look at some keywords used
frequently in multiplayer games.
Lobby
A lobby is a waiting area in any building. Similar to that concept, a lobby is the
matchmaking area of a multiplayer game.
Matchmaking
The room allocation or matchmaking process can be handled by allocating players to
different rooms using matchmaking algorithms.
One of the simplest ways is to allocate on a first-come/first-serve basis, but there are
many more sophisticated algorithms to ensure a better match. These algorithms are similar
to matchmaking algorithms , or algorithms used by dating websites. The key concept is to
match a player with another player of a similar level so that the match is fair. Most online
multiplayer service providers use multiple KPIs to match players, such as network speed,
past scores, game winning history, and so on. Combining these and more details helps
create a rank and using this rank, algorithms create a group of players who are suited to
play together.
Spawn/Respawn
The process of appearing or reappearing in the game, either at the beginning of the game
or after dying in the game.
Spawn Point
The room or the area of a game where players respawn after death.
Introducing the RPG Game
This section is a hands-on, step-by-step tutorial for creating a simple multiplayer game.
This game is a primitive representation of RPG shooting game that happens inside a
Maze environment. It’s created this way for the sake of simplicity and understanding and
can be extended to a complete multiplayer game.
Game Prerequisites
To create this game, you must have:
A basic understanding of Unity and game development in Unity.
A basic understanding of object-oriented programming using C#.
An understanding of the material covered in Chapters 1-5.
Downloaded the Unity Hub and at least one Unity Editor (preferably 2020.3 or higher).
Git Bash installed in your system, as it is utilized in the Unity multiplayer setup.
This tutorial project uses a mid-level networking library provided by Unity. Unity
provides this mid-level networking library with most of the methods and communications
system already programmed. As you learned in the previous chapters, creating a socket
connect is a complicated process.
Creating an entire system for a multiplayer game can be very tedious.
High-level multiplayer libraries can come in handy because they include code for
multiple subsystems such as:
Network management
Network behavior
Message handling
High-purpose deserialization of data
Object management
Game state synchronization
Server class, client class, connection class, and so on
After creating the project, create a plane by choosing Game Object ➤ 3D ➤ Plane.
You will then get the screen shown in Figure 6-2.
Now create a simple maze level using the simple cube primitives. For this reference,
apply the maze design texture on the Plane gameObject using a material. After you
do this, you’ll get the view shown in Figure 6-3.
Figure 6-3 Maze texture
After this step, you can use the cube to create the similar game level. There are
multiple ways to create the level:
Use a digital content creation (DCC) tool
Use pro-builder inside Unity
Use primitives
We will use primitives to create the level in this example. (For details on using pro-
builder, refer to the Unity documentation for Pro-Builder and for DCC tools, refer to the
Asset Import section of the Unity Documentation.)
To create a cube, choose Game Object ➤ 3D ➤ Cube and then use the Move and
Scale tools to place the object properly.
After creating the cube, you can remove the texture of maze from the plane and add a
grass texture; see Figure 6-4.
Figure 6-4 The maze with a grass texture
After you complete all the steps properly, the Inspector window of the Network
Manager should look like Figure 6-7.
Figure 6-7 The Inspector window of the Network Manager
Section 3.2
Once the Network Manager is ready, you can create your player using any prefab. Let’s
use a capsule as a player. To create a capsule, choose Game Object 3D ➤ Capsule.
Once the player is ready, you need to add some MLAPI components so that it can be
utilized properly. To do that, you will again add a network object component to this game
object. To add the component, select the player game object from the Inspector window
and choose Add Component ➤ MLAPI ➤ Network Object. After this, your Inspector
window should look like Figure 6-8.
Figure 6-8 The Inspector window
Player Movement
In order to add movement to your characters, you create a first-person controller for the
player. Since it’s an authoritative server, the player’s movement should be authorized by
the server. This movement should be part of the networking library.
To make the movement part of the network, the player prefab should have another
component called networkTransform.
Choose Add Component ➤ MLAPI ➤ Network Transform.
It should be noted that the player prefab will have two MLAPI components—
NetworkObject and NetworkTransform—and an FPS controller for the player
controls. After all these components, the Inspector panel should look Figure 6-9.
Figure 6-9 The updated Inspector panel
Now you need to configure the Network Manager, as it will be responsible for creating
a player once the game starts. The server will not be able to control things that are not
created by the Network Manager.
For this, you must save the player as a prefab first; drag the player from the Hierarchy
to the Project window to create a prefab. After creating the prefab, you can safely delete
the player from the hierarchy.
Section 3.3
To make the Game Manager work for this game, you need the UI to start the game. You
can create simple buttons called Host and Client using Unity Canvas.
To create a canvas, choose gameObject ➤ UI ➤ Canvas. To add buttons and text input
options, first select the Canvas and then choose Game Object ➤ UI ➤ Button. The button
will then be created as a child component of the Canvas. Inside this button, you will have
a text component. You can change the name of the button by selecting the text component
from the hierarchy. In the Inspector window, change the Button Text to HOST, which will
start the server (see Figure 6-10).
To manage the UI and the connections, you can create an empty game object and call
it connectionManager. In this connection manager, add a new component script to
add functionality to this UI. Create a script by choosing Add Component ➤ New Script ➤
ConnectionManager.
Section 3.4
To open the connectionManager script , double-click it. It should open by default in
the code editor. If not, you can configure the text editor by choosing Edit ➤ Preferences ➤
External Tools and finding External Script Editor. Select your favorite tool from the
dropdown or browse the Launcher Location. See Figure 6-12.
Figure 6-12 Use the External Script Editor
After opening the script in the text editor, the first thing you have to do is import the
MLAPI Library in the code. You will write this using MLAPI at the start of the file.
using MLAPI;
Now go back to the Unity Editor and add the Canvas to this public field. You do this
by dragging the Canvas GameObject to this field. Or you can click the circle-looking
symbol at the corner of the field and select Canvas from that dropdown. See Figure 6-14.
After that, you can control the canvas with the ConenctionPanelUI variable. To
create a button functionality for the Host button, create a public StartHost function as
follows.
transport =
NetworkManager.Singleton.GetComponent<UNetTransport>();
Once you get the reference to UNetTransport, you can set the IP for the client
connection.
}
[ServerRpc]
void ShootServerRPC()
{
ShootClientRpc();
}
[ClientRpc]
void ShootClientRpc()
{
var bullet = Instantiate(gunbullet, gun.position,
Quaternion.identity);
bullet.transform.position = gun.transform.position;
There is more than one player in a multiplayer game and since they will have the same
movement script, you need to determine which component is the local player. This is
because you will be playing the game locally and the other parts of the game are coming
from the server.
To achieve this, the code checks whether you are a local payer and calls
shootserverRPC, which will instruct the server to shoot a bullet using the RPC calls.
Summary
This chapter discussed common multiplayer gameplayer terms and dove into developing a
multiplayer game. You learned how multiplayer games work and how the data is shared
between the server and the players. To create this game, you used MLAPI of Unity, which
is a mid-level networking library.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Kelly, K. Kumar, Unity Networking Fundamentals
https://doi.org/10.1007/978-1-4842-7358-6_7
7. LAN Networking
Sloan Kelly1 and Khagendra Kumar2
(1) Niagara Falls, ON, Canada
(2) Katihar, Bihar, India
3. Log in with your credentials if you’re asked for them. After you do so,
you will be logged in and the Hamachi service will start, as shown in
Figure 7-5.
4. Note the IP address given to you next to the Start Service button.
5. If you are planning to host the game, click Create a New Network, as
shown in Figure 7-6.
Figure 7-6 Create a new network if you are hosting the game
6. You will be prompted to enter the network ID and password (see Figure
7-7). This network ID and password must also be shared with friends
who want to join the LAN party and play the game.
7. Your friends should click the Join an Existing Network button to join
your network. They will be prompted to enter the network ID and
password as well, as shown in Figure 7-8.
Figure 7-8 Other players also have to supply the network ID and password to join the network
8. Once you are successfully connected, open the game and enter the IP
address into the game’s Start screen to play the game.
LAN Party in Games
There are many ways that games leverage the LAN party concept. One very
simple way is by creating a server on your computer. Then you scan for a
local server, which usually scans for the ports that have been programmed
by developers to be used for LAN party servers across the local network.
The newer and more popular way to create a LAN party is by creating a
local WiFi network or by connecting all the devices to the same WiFi
network.
Summary
This chapter talked about how LAN parties work. It discussed the major
problems that arise when you try to connect to a LAN party from outside
the network. The chapter also looked into how you can rectify these
solutions using VPN.
You learned what a VPN is and how it solves these LAN party issues.
You also learned how to create a VPN-based LAN party using Hamachi.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Kelly, K. Kumar, Unity Networking Fundamentals
https://doi.org/10.1007/978-1-4842-7358-6_8
8. Servers
Sloan Kelly1 and Khagendra Kumar2
(1) Niagara Falls, ON, Canada
(2) Katihar, Bihar, India
Servers are like computers that sit at the other end of the network helping
you do things. Servers are used for multiple purposes, including as
webservers. They are used for data storage, loud storage, cloud computing,
research, and for games as well.
Games use servers in different ways, which means the way the servers
are developed is different as well (see Figure 8-1).
Dedicated servers
Listen servers
Peer-to-peer networks
Figure 8-1 Server and clients
What Is a Server?
A server is a computer that connects to different computers over the
network to serve data over LANs (Local Area Networks) or WANs (Wide
Area Networks).
You might have heard of different types of servers—email servers, web
servers, file servers—these servers have similar hardware components
whereas the software they run is unique.
Server software includes Apache, Nginx, and Microsoft IIS, which are
most predominantly used for website hosting. Some SMTP servers used for
email services include Exim, iMail, etc.
While any computer can be configured as a server, the major difference
is the hardware design. Servers are designed to run around the clock
without any hiccups. Many industries and big organizations don’t use
traditional computers for servers; they use especially designed cases for
servers named as 1U, 2U, 3U and so on, the sizes of which vary upon their
usage. These servers are typically mounted on hardware racks for storage.
Since these devices are capable of running with major interruptions and
can be left alone, most of these servers are configured in a way that they
can be controlled and managed remotely.
Dedicated Servers
Dedicated servers are exactly what they sound like. These servers are
dedicated to one specific purpose, which can be for video streaming, web
hosting, or for any other purpose. Most businesses prefer dedicated servers
because of their immense power and flexibility. Servers are mostly used by
websites over the Internet and most websites across the Internet are hosted
on shared servers.
Shared servers are servers that host hundreds or thousands of websites.
This makes renting server space a lot cheaper for owners who don’t require
a lot of storage and computational power. However, once they need to
upgrade their plan, they can migrate to dedicated servers, which are usually
known as VPSs (Virtual Private Servers).
With a VPS, you get a virtual computer, which you can configure
according to your needs. A VPS is as flexible as dedicated servers to an
extent. But this is still far from an actual dedicated server, which means you
have access to the actual server. The major downsides to this kind of
approach are cost and maintenance. As their devices are manufactured to
run continuously in isolation, they also can be extremely loud and might
require an ideal environment to operate at 100% capacity.
Who Should Get a Dedicated Server?
Consider these characteristics of dedicated servers:
Scalability: Dedicated servers are very easy to scale up, which is
beneficial for growth. For example, websites like Google and YouTube,
which serve billions of users daily and are expected to grow, would burn
a lot of money in renting servers.
Security: Companies that deal with very sensitive data may not want to
use dedicated servers. Keeping that data in a shared space is a big
concern. If any of the websites or services hosted on that same shared
space are compromised, the possibility of confidential data being leaked
is significant.
Any malicious web service could also rent the same server with bad
intentions to compromise the service. They could infect the service with
ransomware, virus-vulnerable code, or any other malicious activity.
Speed: Shared services can lead to instability, depending on a load of the
shared websites and this, in turn, can affect the performance of your
services.
Control: As discussed earlier, dedicated servers allow you to customize
your services as needed, which means you have the power to offer non-
traditional services, such as game builds, game streaming, etc.
Dedicated Servers in Gaming
Since the beginning of personal computers, private game servers/dedicated
game servers were always among the top choices for gaming platforms.
This was preferrable to depending on a multiplayer service provider.
Creating your dedicated game server has a lot of benefits, including
stability, customizability, and control. A lot of different multiplayer systems
use dedicated game servers, including PUBG, ARK: Survival Evolved,
Team Fortress 2, etc.
Most public game servers use the client-server model (discussed in
Chapter 5) or peer-to-peer (P2P) hosting, both of which have their
problems. Client servers are run by process owners, usually the publishers
or manufacturer, and as a consequence they can manage individual
connections. This model works in most cases, but it lags in terms of
customizability options.
P2P (peer-to-peer networking) is another very popular modern
multiplayer gaming service. Using P2P, one player can dynamically act as
host or master, which can facilitate connections from other devices or
players. This system is highly random, as anyone can be chosen as the host.
If the host’s network is poor, everyone will have a bad gaming experience.