• Examples (iOS)
• Examples (Java/Android)
• Examples (C++)
Server API Documentation

 

» Shooter

» Overview

The Shooter example shows how to develop a full multiplayer first/third person combat game with Unity and SmartFoxServer 2X. The game utilizes SmartFoxServer 2X's ability to mix TCP and UDP based messaging, and makes full use of SmartFoxServer's Lag Monitor.

TCP is the most common protocol deployed on the Internet. Its dominance is explained by the fact that TCP performs error correction. When TCP is used, there is an assurance of delivery. This is accomplished through a number of characteristics including ordered data transfer, retransmission, flow control and congestion control. During the delivery process, packet data may collide and be lost, however TCP ensures that all packet data is received by re-sending requests until the complete package is successfully delivered.

UDP is also commonly found on the Internet. However UDP is not used to deliver critical information, as it forgoes the data checking and flow control found in TCP. For this reason, UDP is significantly faster and more efficient, although, it cannot be relied on to reach its destination.

In this example, all critical game information between the server and client are sent via TCP for reliable delivery. This could be shooting for example, or animation sync, damage messages, etc. On the other hand, sending updates to the players Transform, which is continuously sent by and to all clients, is executed using UDP.

The example scene features a simple lobby and multiple games which can run at the same time as individual arenas. We have also implemented several of Unity's features that are included in Unity 2021.3 and above, like:

You can find further details on each of these components on the Unity Documentation website, but they are easily configured using the included scripts and the editor's inspector window.

>> DOWNLOAD the source files <<

» Setup & run

In order to setup and run the example, follow these steps:

  1. unzip the examples package;
  2. launch the Unity Hub, click on the Open button and navigate to the Shooter folder;
  3. if prompted, select the Unity Editor version to use (v2021.3 or later is recommended);
  4. click on the SmartFoxServer → Demo Project Setup menu item in the Unity UI, and follow the additional instructions provided in the appeared Editor window.

The client's C# code controlling the Login and Lobby scenes is contained in the Unity project's /Assets/Scripts folder, while all scripts related to the actual game logic are in the /Assets/Game/GameScripts folder; the SmartFoxServer 2X client API DLLs are in the /Assets/Plugins folder. Read the introduction to understand why multiple DLLs are used.

» Server-side Extension

This example features a server-side Java Extension. Its source code is contained in the /Assets/SFS2X-Shooter.zip file. Create and setup a new project in your Java IDE of choice as described in the Writing the first Java Extension document of the Java Extensions Development section. Copy the content of the /Extension-Java/src folder to your Java project' source folder.

» Introduction to code

» Client code

This example is not meant to be a tutorial on how to write a shooter in Unity. It is designed to showcase the features of SmartFoxServer, and how to utilize TCP and UDP in the same project. However, this example is extensive and could be used as the basis of a 1st or 3rd person multiplayer game and as such, we will cover the main components of the client code, but more specifically, how they relate to the server-side Extension.

The main components in the client project are found in the GameScripts folder. They are as follows:

» Server code

The Java Extension includes handlers to communicate with the client, and simulation code to handle the player transforms and positions within the world simulation. This means that the world simulation runs on the server side, and all clients have a representative simulation running on their system. Clients communicate with the game code on the server side via Extension requests and gets data back via Extension response. The data that is sent and received using SFSObjects, which are highly optimized data objects that can be nested. The main sections in the server code are as follows:

» Client code details

The code for this example utilizes the Login and Lobby classes from the Lobby:Basic example. As such, it is divided into multiple classes contained in the /Assets/Scripts folder. The Controllers subfolder contains two SceneController scripts which are attached to the empty Controller game object in their respective scenes. All controllers extend the BaseSceneController abstract class, which in turn extends MonoBehavior. However, unlike the Lobby:Basics, this example does not implement the chat features.

This example will run without any changes. However, it is easily configured for whatever form your game may take. Most changes to the Unity client can be done with a drag and drop in the Unity Inspector panel via the provided Editor Script. Each section can be expanded or collapsed, Character Prefabs, Random Colors, Camera Angles, UI settings and Audio can all be changed without playing with the code.

The main components, as they relate to the server-side Extension and why each is required, are detailed below. Further information on how to configure this example, using custom characters, custom weapons, etc, are explained in the video tutorials linked at the end of this tutorial.

» SF2X_GameManager

Once the player has logged in and either started or joined a game, the Game Manager calls the method SendSpawnRequest. This method selects a random Character Prefab from however many have been added to the Game Manager in the Unity Inspector panel. A random color is also selected, and this will be applied to the first material on the model, which in our case is the shirt. The prefab number (int) and selected color (int) are placed in an SFSObject, and sent to the Extension as a spawnMe request.

	public void SendSpawnRequest()
	{
		int colors1 = Random.Range(0, colorarray.Length);
		int prefab1 = Random.Range(0, playerPrefab.Length);
		Room room = smartFox.LastJoinedRoom;
		ISFSObject data = new SFSObject();
		data.PutInt("prefab", prefab1);
		data.PutInt("color", colors1);
		ExtensionRequest request = new ExtensionRequest("spawnMe", data, room);
		smartFox.Send(request);
	}

A similar format and structure is used for all other client requests sent to the Server Extension, SendShot, SendReload, SendAnimationState, etc. Once the request has been processed by the Extension (discussed in the Server code details section below), an Extension response is broadcast to all players and is handled by the following method in the Game Manager:

	private void OnExtensionResponse(BaseEvent evt)
	{
		string cmd = (string)evt.Params["cmd"];
		ISFSObject sfsobject = (SFSObject)evt.Params["params"];
		switch (cmd)
		{
			case "spawnPlayer":
			{
				HandleInstantiatePlayer(sfsobject);
			}
			break;
			
			...

This will then call the corresponding method, in this case we call HandleInstantiatePlayer and pass the SFSObject. This method will expand the SFSObject, which includes the userid, current score, prefab identifier, and color. If the userid is the local player, we instantiate a character, adding the Character Controller, Cinemachine Camera, AimIk, and Colliders.

	private void HandleInstantiatePlayer(ISFSObject sfsobject)
	{
		ISFSObject playerData = sfsobject.GetSFSObject("player");
		int userId = playerData.GetInt("id");
		int score = playerData.GetInt("score");
		int prefab = playerData.GetInt("prefab");
		int colors = playerData.GetInt("color");
		
		SF2X_CharacterTransform chtransform = SF2X_CharacterTransform.FromSFSObject(playerData);
		User user = smartFox.UserManager.GetUserById(userId);
		string name = user.Name;
		if (userId == smartFox.MySelf.Id)
		{
			if (playerObj == null)
			{
				playerObj = GameObject.Instantiate(playerPrefab[prefab].playerPrefab) as GameObject;
				collidercenter = new Vector3(0, playerPrefab[prefab].colliderCenter, 0);
				colliderheight = playerPrefab[prefab].colliderHeight;
				playerObj.transform.position = chtransform.Position;
				playerObj.transform.localEulerAngles = chtransform.AngleRotationFPS;
				playerObj.name = user.Name;
				playerCollider = playerObj.AddComponent<CapsuleCollider>();
				
				...

If the userid is not the player's one, the Game Manager will instantiate a prefab as a remote character, attaching the Character Controller but not the Cinemachine. It the NetworkSyncMode chosen is Complex in the SF2X_GameManager, we also add the SF2X_SyncManager for Interpolation of the position and rotation.

Each of the actions are handled in a similar way. For example, when the player shoots, a SendShot request containing the Target is forwarded to the Extension. The response received calls HandShotFired, which processes the animation and audio. The target is raycast by the client, but the other calculations such as ammunition count, wounding, kill, etc, are done by the Extension.

NOTE
A targeting system could be built for the server-side Extension, but it would to take into account all GameObjects in the scene, including buildings and props. This is currently outside the scope of this example.

» SF2X_CharacterTransform

This script is used to build an SFSObject containing the Character Position, Rotation, and Spine Rotation Vectors. The x,y,z from each Vector3 is extracted into a Double, and a time stamp is added. This script also acts in the reverse, building Vector3's from the SFSObject. These are then used for Character positioning in the world simulation, as well as the Interpolation code.

	public void ToSFSObject(ISFSObject data)
	{
		ISFSObject tr = new SFSObject();
		tr.PutDouble("x", Convert.ToDouble(this.position.x));
		tr.PutDouble("y", Convert.ToDouble(this.position.y));
		tr.PutDouble("z", Convert.ToDouble(this.position.z));
		tr.PutDouble("rx", Convert.ToDouble(this.angleRotation.x));
		tr.PutDouble("ry", Convert.ToDouble(this.angleRotation.y));
		tr.PutDouble("rz", Convert.ToDouble(this.angleRotation.z));
		tr.PutDouble("srx", Convert.ToDouble(this.spineRotation.x));
		tr.PutDouble("sry", Convert.ToDouble(this.spineRotation.y));
		tr.PutDouble("srz", Convert.ToDouble(this.spineRotation.z));
		tr.PutLong("t", Convert.ToInt64(this.timeStamp));
		data.PutSFSObject("transform", tr);
	}

This is actioned every frame when the SF2X_CharacterController's Update loop calls SendTransform. However, it does not send a transform to the server Extension every frame, as it checks the time the last is update is sent (sendingPeriod). This is currently set to 0.03, which results in 30 updates per second, or every second frame on a game running at 60 frames per second. While this may seem a lot to some, these updates are sent using UDP.

	public void SendTransform()
	{
		if (isPlayer)
		{
			if (timeLastSending >= sendingPeriod)
			{
				lastState = SF2X_CharacterTransform.FromTransform(this.transform, this.spineTransform);
				SF2X_GameManager.Instance.SendTransform(lastState);
				timeLastSending = 0;
				return;
			}
			timeLastSending += Time.deltaTime;
		}
	}

If the timing is correct, we pass both the Character Transform (position and rotation) and the Spine Transform (rotation) to the SendTransform method in the SF2X_GameManager.

	public void SendTransform(SF2X_CharacterTransform chtransform)
	{
		Room room = smartFox.LastJoinedRoom;
		ISFSObject data = new SFSObject();
		chtransform.ToSFSObject(data);
		ExtensionRequest request = new ExtensionRequest("sendTransform", data, room, true); // True flag = UDP
		smartFox.Send(request);
	}

As shown above, UPD is set by coding true on the Extension send request in the SendTransform method.

» SF2X_SyncManager

If the NetworkSyncMode:Complex is selected in the Game Manager, this script is attached to all remote characters when they are instantiated. As describe in the code comments, we buffer 30 copies of the transform received (position and rotation) and using the SFS Lag Monitor, we calculate how far back we go within the buffer to compensate for any network lag.

	double currentTime = NetworkTime;
	double interpolationTime = currentTime - interpolationBackTime;
	if (bufferedStates[0].TimeStamp > interpolationTime)
	{
	    for (int i = 0; i < statesCount; i++)
	    {
	        if (bufferedStates[i].TimeStamp <= interpolationTime || i == statesCount - 1)
	        {
	            SF2X_CharacterTransform rhs = bufferedStates[Mathf.Max(i - 1, 0)];
	            SF2X_CharacterTransform lhs = bufferedStates[i];
	            double length = rhs.TimeStamp - lhs.TimeStamp;
	            float t = 0.0F;
	            if (length > 0.0001)
	            {
	                t = (float)((interpolationTime - lhs.TimeStamp) / length);
	            }
	            thisTransform.isMoving = true;
				
				this.transform.position = Vector3.Lerp(lhs.Position, rhs.Position, t);
				this.transform.rotation = Quaternion.Slerp(lhs.Rotation, rhs.Rotation, t);
				this.spineTransform.localRotation = Quaternion.Slerp(lhs.RotationSpine, rhs.RotationSpine, t);
				this.spineRotation = rhs.RotationSpine;
				return;
			}
		}
	}

For a description on network lag, see the documentation for the SpaceWar2 example under the heading The lag problem. We do not use Extrapolate in this example as Unity only uses this for objects with Rigid Body attached. If the NetworkSyncMode:Simple is selected, we utilize a Simple Lerp to position the player. This is good for when the server is running on the local machine, and no lag is apparent (see SF2X_CharacterController class).

	public void checkPosition()
	{
		if (!isPlayer)
		{
			if (simpleLerp)
			{
				isMoving = true;
				if (this.transform.position != newState.Position)
				{
					this.transform.position = Vector3.Lerp(this.transform.position, newState.Position, Time.deltaTime * 100.0f);
				}
				if (spineTransform.localRotation != Quaternion.Euler(newState.SpineRotationFPS))
				{
					spineTransform.localRotation = Quaternion.Euler(newState.SpineRotationFPS);
				}
				if (this.transform.localEulerAngles != newState.AngleRotationFPS)
				{
					this.transform.localEulerAngles = newState.AngleRotationFPS;
				}
			}
			else
			{
				spineTransform.localRotation = syncManager.spineRotation;
			}
		}
	}

» SF2X_CharacterController

This script is added to both local player character and to each remote character. It controls movement, animation, and if the character is the local player, it accepts the input from the input asset, sending the Extension request to the Game Manager. For the local player character, this script also controls the Cinemachine camera position. The script is structured in a way that it is simple to see what does what. The code is commented and regions are used to expand each section.

Most Input Keys are set to Toggle. For example, pressing F will activate the Aiming animation, and pressing it again, will end the Aiming animation. Pressing V will cycle through the Camera views, as pressing C will cycle through the added crosshairs. The H key is currently configured to show you these on the Help Panel.

» Server code details

SFS2X supports extension communication using both UDP and TCP. For a shooter it is best to have the fastest possible transformation synchronization possible, while at the same time not missing displaying animations or having an up to date health status. UDP while fast and small is also unreliable, and thus one has to use it selectively.

UDP is used for sending transforms, and TCP for sending everything else (animation synchronizations, health updates, shot messages, etc). Missing a few transforms using UDP will not be vital for the game, as they are being sent many times per second and the game clients will interpolate movement to smooth out anything that is missed.

The sending of UDP messages is very simple. When sending the ExtensionRequest, you simply add a “true” boolean as parameter, and the given request is sent via UDP, as shown in the Client code above.

» World simulation model

The primary part of the server extension is the world simulation. Shown below is a data object representation of what is going on inside the game world. The objects and their relationships can be seen in the following diagram. It also contains some of the primary attributes and methods on each class.

The World itself contains a list of players and a list of the items that are spawned in the world. Each Item has a type (HealthPack or Ammo) as well as a Transform for its position in the world. Each CombatPlayer is linked to a SFS2X User, so we can relate a given network command to the specific CombatPlayer in the simulation. Each player has a Weapon that can be fired, a Transform for where they are located in the simulation, as well as attributes for score, health and weapons containing ammo counts and methods to reload, shoot etc.

The Transform class has 2 primary methods to transform itself to and from an SFSObject. These are the SFSObjects sent back and forth between the clients and the server. Basically any extension request (e.g. a player spawn) will change this world simulation model.

» Extension communication

As discussed above in the Client code details section, clients communicate with the server-side Extension by sending requests and get data back via Extension responses. This model is simple but flexible. The data that is sent or received uses SFSObjects. This is best illustrated with an example.

Shooting: when the player in the game client wants to shoot, in this example they press the left mouse button (although this can be changed in the Input Asset). The SF2X_CharacterController calls OnShoot, and does some initial checks.

NOTE
Previous versions of this code used the Hit Collider in the world simulation for ascertaining if the enemy was shot, but this doesn't work once you add buildings and objects where the other players can hide, therefore we have used a simple Raycast looking for an object Tag of Player. A more secure way of Raycasting on the client, would be to use the PlayerIdGenerator class so that a potential hacker could not randomly guess the PlayerId and add that to the Raycast Target. However, this is outside the scope of this example and would be the topic of a more advanced example.

For this example, we RayCast from the centre of the screen on the client, looking for a Game Object with a Tag of Player. If the RaycastHit returns the value of another player, their userid is passed as a parameter. If not, we place a random 99999 in the Target value. This could in fact be any number that would be used as a userid, but it cannot be null.

	public void OnShoot(InputValue value)
	{
		if (!dead && !died)
		{
			UnityEngine.Cursor.visible = false;
			UnityEngine.Cursor.lockState = CursorLockMode.Locked;
			int target;
			target = 99999;
			Ray ray = cam.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
			RaycastHit hit;
			if (Physics.Raycast(ray, out hit))
			{
				string tag = hit.collider.tag;
				if (tag == "Player")
				{
					target = hit.collider.gameObject.GetComponent<SF2X_CharacterController>().userid;
					if (!reloading && aim)
						SF2X_GameManager.Instance.SendShot(target);
				}
				
				...

If the player is not reloading the weapon, and is aiming, the a request is passed to SF2X_GameManager to SendShot to the server Extension.

	public void SendShot(int target)
	{
		Room room = smartFox.LastJoinedRoom;
		ISFSObject data = new SFSObject();
		data.PutInt("target", target);
		ExtensionRequest request = new ExtensionRequest("shot", data, room);
		smartFox.Send(request);
	}

On the server side we will receive this request in the ShooterExtension class. This is the main entry point for all client communication on the server side. When this class was initialized, it registered to listen for shot commands. When any are received, they are handled in the ShotHandler class, which tells the world that a given user requested to shoot his weapon

	public class ShotHandler extends BaseClientRequestHandler
	{
		@Override
		public void handleClientRequest(User u, ISFSObject data)
		{
			int enemyHit = data.getInt("target").intValue();
			RoomHelper.getWorld(this).processShot(u, enemyHit);
		}
	}

The code in the World class now converts the SFSUser to the CombatPlayer object associated with it. It does checks to see if the given CombatPlayer is dead, has ammo, has ammo loaded. Then it tells the weapon to shoot. The method now handles the result of the shooting. We need to send information back to the user who fired the weapon that the ammo count changed and notify other players in the simulation that the given player fired the weapon so they can run animations and play gun shot sfx.

	public void processShot(User fromUser, int enemyHit)
	{
		CombatPlayer player = getPlayer(fromUser);
		if (player.isDead())
			return; 
		if (player.getWeapon().getAmmoCount() <= 0)
			return; 
		if (!player.getWeapon().isReadyToFire())
			return; 
		player.getWeapon().shoot();
		this.extension.clientUpdateAmmo(player);
		this.extension.clientEnemyShotFired(player);
		for (CombatPlayer pl : this.players)
		{
			if (pl != player)
				if (pl.getSfsUser().getId() == enemyHit)
				{
					playerHit(player, pl);
					return;
				}  
		}
	}

If the enemyHit parameter matches a userid, we can process the playerHit code, by passing the Player object and the player that was hit to the playerHit method. We update Ammo on the Player and Health on the player hit. If the player that was has no more health, we process the kill.

	private void playerHit(CombatPlayer fromPlayer, CombatPlayer pl)
	{
		pl.removeHealth(1);
		if (pl.isDead())
		{
			fromPlayer.addKillToScore();   // Adding frag to the player if he killed the enemy
			this.extension.updatePlayerScore(fromPlayer);
			this.extension.clientKillPlayer(pl, fromPlayer);
		}
		else
		{
			this.extension.clientUpdateHealth(pl);  // Updating the health of the hit enemy
		} 
	}

The send() method on the Extension class sends back a response command and data to be send to client(s). In this case, we only send the ammo count back to the client that is connected to the given CombatPlayer that shot.

	public void clientUpdateAmmo(CombatPlayer player)
	{
		SFSObject sFSObject = new SFSObject();
		sFSObject.putInt("id", player.getSfsUser().getId());
		sFSObject.putInt("ammo", player.getWeapon().getAmmoCount());
		sFSObject.putInt("maxAmmo", 6);
		sFSObject.putInt("unloadedAmmo", player.getAmmoReserve());
		send("ammo", (ISFSObject)sFSObject, player.getSfsUser());
 	}

Back at the client, the OnExtensionResponse method will receive the SFSObject and process what it received. For the ammo it will call HandleAmmoCountChange which will in turn update the local variables and in our example, update the UI.

	private void HandleAmmoCountChange(ISFSObject sfsobject)
	{
		int userId = sfsobject.GetInt("id");
		if (userId != smartFox.MySelf.Id) return;
		int loadedAmmo = sfsobject.GetInt("ammo");
		int maxAmmo = sfsobject.GetInt("maxAmmo");
		int ammo = sfsobject.GetInt("unloadedAmmo");
		for (int i = 0; i < loadedBullets.Length; i++)
		{
			loadedBullets[i].SetActive(false);
		}
		for (int i = 0; i < loadedAmmo; i++)
		{
			loadedBullets[i].SetActive(true);
		}
		for (int i = 0; i < unloadedBullets.Length; i++)
		{
			unloadedBullets[i].SetActive(false);
		}
		for (int i = 0; i < ammo; i++)
		{
			unloadedBullets[i].SetActive(true);
		}
	}

All other sent and received SFSObjects are basically handled in the same way. Everything from the Character Transform (position and rotation) to Items such as Health and Ammo boxes which are spawned, are all controlled by the server Extension, and data is sent to each client for processing.

» Hardcoded variables

Some variables must be changed on the server (hardcoded variables) if the client scene is changed. An example of this would be Spawn Vectors if the Game Objects such as Buildings and other solid objects are moved. Spawn points (number of and Vector3) for both characters and items are in the Transform Java class. Each array has 8 spawn points, but this can be changed depending on your needs.

	private static Transform[] getSpawnPoints()
	{
		Transform[] spawnPoints = new Transform[8];
		spawnPoints[0] = new Transform(-30.0D, 1.0D, 22.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		spawnPoints[1] = new Transform(13.0D, 1.0D, 27.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		spawnPoints[2] = new Transform(1.0D, 1.0D, -17.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		spawnPoints[3] = new Transform(1.0D, 1.0D, 8.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		spawnPoints[4] = new Transform(6.0D, 1.0D, 47.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		spawnPoints[5] = new Transform(-5.0D, 1.0D, 47.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		spawnPoints[6] = new Transform(0.0D, 1.0D, 24.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		spawnPoints[7] = new Transform(0.0D, 1.0D, 37.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
		return spawnPoints;
	}

Other variables that are hard coded in the server Extension that may need to be changed depending on your Weapon, Health, and UI are as follows.

» Possible enhancements

This example has several actions controlled and validated by the server Extension, but not all actions are. As mentioned earlier, the raycast of the aim and shoot is carried out on the client, and models such as buildings and props are not represented at all on the server side. The game server could account for everything in the Extension, and the client would only send the input commands and receive the resulting actions. This example is only suggests how all of this can be implemented. Future enhancements could be:

» More resources

You can learn more about the SmartFoxServer concepts discussed in this example by consulting the following resources:

Also check the video tutorial available for this example: