-
Notifications
You must be signed in to change notification settings - Fork 8
Demo Tutorial: ExamplePawn
Welcome to the first part of the tutorial series where you will learn how to make a very basic game using TinyBirdNet.
For that I will be using the Demo game included, and will teach you what every part does and why it is necessary.
We will be using our own derived classes of:
In addition to a GameManager and SpawnPointManager, not related to networking tho.
You might be wondering if that is enough, but worry not as TinyBirdNet will take care of most things for you. From syncing scene changing, serialization/deserialization, authentication, you just worry about your game logic!
I would like to start with the cubes the players will move around, since the ExamplePawn is the script with less direct references to others.
public class ExamplePawn : TinyNetBehaviour {A TinyNetBehaviour is a MonoBehaviour who implements the interface ITinyNetObject, in addition, TinyBirdNet handles it's spawning, serialization, rpc, and mostly anything you need to create a new instance of it in a multiplayer game and have it automatically synced.
Most of the important network variables are declared here:
string _playerName;
[TinyNetSyncVar]
public string PlayerName { get { return _playerName; } set { _playerName = value; } }
Vector3 _networkPosition;
[TinyNetSyncVar]
float xPos { get { return _networkPosition.x; } set { _networkPosition.x = value; } }
[TinyNetSyncVar]
float zPos { get { return _networkPosition.z; } set { _networkPosition.z = value; } }
[TinyNetSyncVar]
byte netDir { get; set; }Why so many properties you ask? Well, the [TinyNetSyncVar] attribute only works with properties.
Why again? Mostly a whim I guess, but manly because that way you are assured to receive an event every time it is changed, and you get both the old and the new value assigned to it.
The [TinyNetSyncVar] attribute means that property will be automatically sent from the server to all clients whenever it is detected to be dirty (has changed) between network frames.
This mean that by just setting it, all clients will automatically sync their values.
PlayerName is used to display that text on top of the players, each client is responsible for designating their own name, but since replication can't be done client to client we first send it to the server and it is synced to all clients.
'xPos' and 'zPos' is just the current player's position, we route the property to a vector3 just to get some sugar from Unity.
Finally, netDir is a numerical representation of the player's facing direction. 1 is top and 4 is left, goes clockwise. (0 means there was an error)
Next about the methods, and again, I will be skipping ones not directly related to network.
private void Start() {
xPos = transform.position.x;
zPos = transform.position.z;
}This one just makes sure that as soon as this object is fully created, it's network position will be set.
public override void OnStartServer() {
base.OnStartServer();
timeForNextShoot = Time.time + 0.3f;
}OnStartServer() Is called on a Server when an object is network created, it is triggered after OnNetworkCreate and before OnStartClient() if the Server is also a Client.
public override void OnStartAuthority() {
base.OnStartAuthority();
controller = TinyNetClient.instance.connToHost.GetPlayerController<ExamplePlayerController>(ownerPlayerControllerId);
controller.GetPawn(this);
cameraTransform = GameObject.FindGameObjectWithTag("MainCamera").transform;
}This one is called when someone acquires Authority of this object. Authority is a fairly abstract concept tho, it don't really do anything besides being a marker for special privileges you might want to give to something.
In this case, it represents the client that controls that player.
Here we get our Player Controller, which I will explain later, by means of using our connection to the host and our ownerPlayerControllerId field that is given to us by the server.
Since we are using Authority here to mean the player you control, we also take the opportunity to grab the Main Camera and makes it follow us.
public override void OnStartClient() {
base.OnStartClient();
playerText.text = PlayerName;
}OnStartClient(), as you may have correctly guessed, is called on all Clients that receive this Object from the network. It is also called after all variables have been synced.
Here we are only displaying the player's name on our text mesh.
public override void OnNetworkDestroy() {
base.OnNetworkDestroy();
if (hasAuthority) {
controller.LosePawn();
controller = null;
}
}Called when an object is removed from the network simulation, at this one we just make the Player Controller known the player has died.
private void FixedUpdate() {
if (!hasAuthority) {
Vector3 pos = transform.position;
Vector3 result = Vector3.MoveTowards(pos, _networkPosition, movementSpeed * Time.fixedDeltaTime);
float dist = (result - _networkPosition).sqrMagnitude;
if (dist <= 0.1f || dist >= movespeedPow) {
result = _networkPosition;
}
FaceDir(netDir);
transform.position = result;
} else {
cameraTransform.position = new Vector3(transform.position.x, 10.0f, transform.position.z - 6f);
}
}This one have two modes, if we are not the owner of that player we take the information we received from the server and try to update our simulation of it.
Frankly this was really done poorly here as this was just the minimum example needed to work. You can see how the bullets don't really align when someone else is moving and shooting.
I recommend reading about interpolation or any other lag compensation methods.
If we are the owner tho, we just update the camera position, as everything else is controlled by the Player Controller.
public void Killed() {
TinyNetServer.instance.DestroyObject(gameObject);
}Called when our player is hit by an enemy bullet, it asks the Server to remove it from the network.
There are no safety checks here to see we are indeed the erver, but in our case we want errors to be thrown cos this is not to be called by any Client!
Next I want to explain the RPC (Remote Procedure Call) for the shooting mechanics. Firstly, RPC are methods which are called but not resolved on the same machine. This mean we can call methods at the Client which are executed on the Server and other combinations.
Sadly, this part ended up being a little bothersome since I tried to stay away from Weaving the Unet used, and reflection could only get me so far on it.
First we will declare our shoot method:
[TinyNetRPC(RPCTarget.Server, RPCCallers.ClientOwner)]
void ServerShoot(float xPos, float zPos, byte dir) {
if (!isServer) {
rpcRecycleWriter.Reset();
rpcRecycleWriter.Put(xPos);
rpcRecycleWriter.Put(zPos);
rpcRecycleWriter.Put(dir);
SendRPC(rpcRecycleWriter, "ServerShoot");
return;
}
ExampleBullet bullet = Instantiate(bulletPrefab, bulletSpawnPosition.position, transform.rotation).GetComponent<ExampleBullet>();
bullet.ownerNetworkId = NetIdentity.NetworkID;
bullet.direction = dir;
switch (dir) {
case 1:
bullet.transform.rotation = Quaternion.Euler(new Vector3(0f, 0f, 0f));
break;
//Right
case 2:
bullet.transform.rotation = Quaternion.Euler(new Vector3(0f, 90f, 0f));
break;
//Down
case 3:
bullet.transform.rotation = Quaternion.Euler(new Vector3(0f, 180f, 0f));
break;
//Left
case 4:
bullet.transform.rotation = Quaternion.Euler(new Vector3(0f, 270f, 0f));
break;
}
TinyNetServer.instance.SpawnObject(bullet.gameObject);
}The [TinyNetRPC(RPCTarget.Server, RPCCallers.ClientOwner)] declares that this method can only be called by the Client that owns (has Authority on) this object, and it will be executed at the Server.
Then we do a manual check to see if we are not the Server already, since in this game the Server is always a Client too, it could mean the owner of this object is already the target Server.
If we are not the Server, gather the parameters of this method at a reusable NetDataWritter that all TinyNetBehaviour have access to, and send an RPC with it, ending the method.
If we are the Server, by means of receiving the RPC or just initially being the owner, we proceed with the normal shooting. Create a new bullet, spawn it, done.
Now, going back to one network method we skipped...
public override void OnNetworkCreate() {
base.OnNetworkCreate();
RegisterRPCDelegate(ServerShootReceive, "ServerShoot");
}At this one, called when the object is added to the network, but before it have received any data, we register the ServerShootReceive method with the string id "ServerShoot".
This mean that if we ever receive an RPC with the "ServerShoot" id, we will call the method registered for it.
And finally:
void ServerShootReceive(NetDataReader reader) {
ServerShoot(reader.GetFloat(), reader.GetFloat(), reader.GetByte());
}The method registered, basically receives the data from the RPC and routes it to the original ServerShoot method.
A little cumbersome, if there is the opportunity to improve upon it I will do so, but for now it shall suffice.
At the next part we will take a look at the ExamplePlayerController script!