Unity Networking開發多人聯機射擊遊戲
UNet開發多人聯機射擊遊戲
引言: Networking作為Unity官方的用於開發多人線上遊戲的網路模組,開發者可以不用自己搭建網路模組的底層,通過使用Unity提供的一些相關元件,可以輕鬆實現簡單的多人線上遊戲。本片部落格為泰課線上賈老師的《Unity多人網路系統講解》的學習筆記,連結地址在文末。
開發版本: Unity 2017.2
文章目錄
1. 網路管理器
建立空物件,新增Network Manager和Network Manager HUD元件,如下圖所示:
2. 建立Player預製體
玩家可以分為LocalPlayer和RemotePlayer:
LocalPlayer指本地玩家控制的物件
RemotePlayer指多人遊戲中其他玩家控制的物件
為提供的坦克Player新增Network Identity元件,勾選Local Player Authority,表示該物件由本地玩家控制,而不是伺服器。並將該物件製作為預製體。
Network Identity:網路物體最基本的元件,客戶端與伺服器確認是否是一個物體(netID),也用來表示各個狀態,比如判斷是否是伺服器,是否是客戶端,是否有許可權,是否是本地玩家等。舉一個簡單的栗子,A是Host(又是伺服器,又是客戶端),B是一個Client(客戶端),A與B分別有一個玩家PlayA與PlayB。在機器A上,playA與playB的isServer為true,isClent為true,其中playA有許可權,是本地玩家,B沒許可權,也不是本地玩家。在機器B上,playA與playB的isServer為false,isClent為true,其中playB有許可權,是本地玩家,A沒許可權,也不是本地玩家。機器A與機器B上的PlayA的netID相同,機器A與機器B上的PlayB的netID也相同,其中netID用來表示他們是在不同機器上的同一網路物件。
3. 註冊Player
將Player預製體新增到Network Manager元件中的Player Prefab中,並將場景中的Player刪除,如下所示:
運行遊戲,點選左上角的LAN Host按鈕,將其作為伺服器,又作為客戶端使用,如下所示:
然後,Network Manager會自動在原點生成一個LocalPlayer,左上角表示客戶端連線的IP為本地IP,埠號為7777
4. 控制玩家移動
為Player新增指令碼PlayerController,可以實現WASD鍵或者方向鍵控制塔克移動旋轉,指令碼如下:
public float rotateSpeed = 150;
public float moveSpeed = 6;
private void Update()
{
var x = Input.GetAxis("Horizontal") * Time.deltaTime * rotateSpeed;
var z = Input.GetAxis("Vertical") * Time.deltaTime * moveSpeed;
transform.Rotate(0, x, 0);
transform.Translate(0, 0, z);
}
打包一個PC端用於測試多人線上,編輯器點選LAN Host,打包的點選LAN Client按鈕,效果如下所示:
我們發現如下問題:
- 無論在Host端或者Client端,進行移動或者旋轉操作,兩個Player都會有響應。
- 一方有位移或者角度變化,並一方不會保持相同變化
修改程式碼如下,isLocalPlayer用於判斷是否是本地玩家,只有本地玩家才可以做出響應
using UnityEngine.Networking;
public class PlayerController : NetworkBehaviour
{
public float rotateSpeed = 150;
public float moveSpeed = 6;
private void Update()
{
if (isLocalPlayer == false) return;
var x = Input.GetAxis("Horizontal") * Time.deltaTime * rotateSpeed;
var z = Input.GetAxis("Vertical") * Time.deltaTime * moveSpeed;
transform.Rotate(0, x, 0);
transform.Translate(0, 0, z);
}
}
為Player新增Network Transform元件,用於網路間同步Transform資料,其中Network Send Rate(Seconds)表示網路資料同步的頻率,如果同步頻率太頻繁會導致網路延遲等問題,而頻率太低又會影響使用者的體驗。
5. 初始化LocalPlayer顏色
為PlayerController指令碼新增如下方法
//用於本地玩家初始化
public override void OnStartLocalPlayer()
{
MeshRenderer[] renderers = gameObject.GetComponentsInChildren<MeshRenderer>();
foreach (var render in renderers)
{
render.material.color = Color.blue;
}
}
6. 新增射擊功能
建立一個球體,根據坦克炮筒口徑,調整大小,勾選Collider的isTrigger,為其新增Rigidbody元件,並取消勾選UseGravity。新增NetworkIdentity、NetworkTransform元件,將NetworkSendRate調整為0,因為在子彈生成的時候,我們規定了其位置和發射方向,可以由本地計運算元彈接下來的位置,而不用網路同步來調整子彈位置,可以減少網路同步資料的壓力。最後,將其作為預製體儲存。
為PlayerController添加發射子彈的方法
using UnityEngine.Networking;
public class PlayerController : NetworkBehaviour
{
public float rotateSpeed = 150;
public float moveSpeed = 6;
public GameObject bulletPrefab;
public Transform bulletSpawnPos;
private void Update()
{
if (isLocalPlayer == false) return;
var x = Input.GetAxis("Horizontal") * Time.deltaTime * rotateSpeed;
var z = Input.GetAxis("Vertical") * Time.deltaTime * moveSpeed;
transform.Rotate(0, x, 0);
transform.Translate(0, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
}
//用於本地玩家初始化
public override void OnStartLocalPlayer()
{
MeshRenderer[] renderers = gameObject.GetComponentsInChildren<MeshRenderer>();
foreach (var render in renderers)
{
render.material.color = Color.blue;
}
}
private void Fire()
{
GameObject bullet = (GameObject)Instantiate(bulletPrefab, bulletSpawnPos.position, bulletSpawnPos.rotation);
bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 20;
Destroy(bullet, 2);
}
}
在坦克炮口位置建立一個空物體,作為子彈生成的位置
將子彈預製體和BulletSpawnPos物件賦值到PlayerController上,如下所示:
此時,打包測試,會發現一方發射子彈,另一方不會同步,如下所示:
解決該問題,需要先將子彈在Network Manager中註冊為可生成預製體,如下:
然後將Fire方法修改為Command方法,並且將生成的Bullet物件,放到伺服器的管理生成物件的集合中,如果後面有個客戶端連線進來,可以保證生成的預製體一致。
[Command]
private void CmdFire()
{
GameObject bullet = (GameObject)Instantiate(bulletPrefab, bulletSpawnPos.position, bulletSpawnPos.rotation);
bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 20;
NetworkServer.Spawn(bullet);
Destroy(bullet, 2);
}
Command:在客戶端呼叫,伺服器端執行。客戶端呼叫的引數必須要UNet可以序列化,這樣伺服器在執行時才能把引數反序列化。需要注意,在客戶端需要有許可權的NetworkIdentity元件才能呼叫Command命令。
NetworkServer:主要持有一個NetworkScene並且做一些只有在伺服器上才能對網路服務做的事,如spawn, destory等。以及維護所有客戶端連線。
打包測試效果如下:
7. 顯示玩家生命值
為Player新增Helath指令碼
public class Health : MonoBehaviour
{
public const int maxHealth = 100;
public int currentHealth = maxHealth;
public RectTransform bloodNum;
public void TakeDamage(int count)
{
currentHealth -= count;
if (currentHealth <= 0)
{
currentHealth = 0;
}
bloodNum.sizeDelta = new Vector2(currentHealth, bloodNum.sizeDelta.y);
}
}
為bullet新增Bullet的指令碼
public class Bullet : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
Health health = other.gameObject.GetComponent<Health>();
if (health != null)
health.TakeDamage(10);
Destroy(gameObject);
}
}
建立血條UI,設定為World Space模式,如下:
需要將BloodNum圖片的錨點設定在左側,然後將其賦值給Health中的bloodNum,如下:
為了讓HealthBar永遠朝向攝像機,新增BillBoard指令碼
public class BillBoard : MonoBehaviour
{
void Update ()
{
transform.LookAt(Camera.main.transform);
}
}
經打包測試,發現已經可以子彈打中後掉血的功能,但目前掉血是由於兩方的子彈打中坦克後,都觸發TakeDamage方法。如果一方的子彈已經打中對方並銷燬,由於網路延遲,另一方的子彈還沒打中物件,由於子彈是伺服器統一管理,所以子彈還沒打中物件就直接銷燬子彈了,這樣就會導致兩方的資料不一致現象。
如何解決這個問題呢,需要使用SyncVar特性
SyncVar:伺服器的值能自動同步到客戶端,保持客戶端的值與伺服器一致。客戶端值改變並不會影響伺服器的值。
修改Health指令碼,TakeDamage方法只在伺服器執行,即資料邏輯在伺服器處理,其他客戶端的資料均以伺服器為準,當currentHealth的值發生變化時,自動同步到所有客戶端,並呼叫OnChangeHealth方法,currentHealth作為方法形參傳入。
using UnityEngine.Networking;
public class Health : NetworkBehaviour
{
public const int maxHealth = 100;
public RectTransform bloodNum;
[SyncVar(hook = "OnChangeHealth")]
public int currentHealth = maxHealth;
public void TakeDamage(int count)
{
if (isServer == false) return;
currentHealth -= count;
if (currentHealth <= 0)
{
currentHealth = 0;
}
}
public void OnChangeHealth(int currentHealth)
{
bloodNum.sizeDelta = new Vector2(currentHealth, bloodNum.sizeDelta.y);
}
}
打包測試,血條可以正常同步,如下所示:
8. 處理死亡
ClientRpc:服務端呼叫,客戶端執行。服務端的引數序列化到客戶端執行,一般來說,服務端會找到上面的NetworkIdentity元件,確定那些客戶端在監視這個NetworkIdentity,Rpc命令會發送給所有的監視客戶端。注意方法名要以“Rpc”開頭。
using UnityEngine.Networking;
public class Health : NetworkBehaviour
{
public const int maxHealth = 100;
public RectTransform bloodNum;
public bool destroyOnDeath;
[SyncVar(hook = "OnChangeHealth")]
public int currentHealth = maxHealth;
public void TakeDamage(int count)
{
if (isServer == false) return;
currentHealth -= count;
if (currentHealth <= 0)
{
if (destroyOnDeath)
{
Destroy(gameObject);
}else
{
currentHealth = maxHealth;
RpcRespawn();
}
}
}
public void OnChangeHealth(int currentHealth)
{
bloodNum.sizeDelta = new Vector2(currentHealth, bloodNum.sizeDelta.y);
}
[ClientRpc]
private void RpcRespawn()
{
if (isLocalPlayer)
transform.position = Vector3.zero;
}
}
9. 新增敵人
伺服器端生成非玩家物件,首先建立一個空物件,命名為EnemySpawner,新增NetworkIdentity元件,勾選Server Only,新增EnemySpawner指令碼。
public class EnemySpawner : NetworkBehaviour
{
public GameObject enemyPrefab;
public int numOfEnemy;
//用於伺服器的初始化操作
public override void OnStartServer()
{
for (int i = 0; i < numOfEnemy; i++)
{
Vector3 spawnPos = new Vector3(Random.Range(-15, 15), 0, Random.Range(-15, 15));
Quaternion spawnRotation = Quaternion.Euler(0, Random.Range(0, 180), 0);
GameObject enemy = (GameObject)Instantiate(enemyPrefab, spawnPos, spawnRotation);
NetworkServer.Spawn(enemy);
}
}
}
複製一個Player預製體,修改為Enemy預製體,並刪除PlayerController元件,需要勾選Health元件中的DestroyOnDeath。然後將其註冊到NetworkManager中的RegisteredSpawnablePrefabs中。執行後如下:
10. 修改出生位置
建立空的預製體,新增Network Start Position元件
將Network Manager中的Player Spawn Method修改為Round Robin,表示按生成點順序一個一個生成
修改Health指令碼,修改其生成位置
using UnityEngine.Networking;
public class Health : NetworkBehaviour
{
public const int maxHealth = 100;
public RectTransform bloodNum;
public bool destroyOnDeath;
[SyncVar(hook = "OnChangeHealth")]
public int currentHealth = maxHealth;
private NetworkStartPosition[] spawnPoints;
private void Start()
{
OnChangeHealth(currentHealth);
if (isLocalPlayer)
{
spawnPoints = FindObjectsOfType<NetworkStartPosition>();
}
}
public void TakeDamage(int count)
{
if (isServer == false) return;
currentHealth -= count;
if (currentHealth <= 0)
{
if (destroyOnDeath)
{
Destroy(gameObject);
}else
{
currentHealth = maxHealth;
RpcRespawn();
}
}
}
public void OnChangeHealth(int currentHealth)
{
bloodNum.sizeDelta = new Vector2(currentHealth, bloodNum.sizeDelta.y);
}
[ClientRpc]
private void RpcRespawn()
{
if (isLocalPlayer)
{
Vector3 spawnPoint = Vector3.zero;
if (spawnPoints != null && spawnPoints.Length > 0)
{
spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)].transform.position;
}
transform.position = spawnPoint;
}
}
}
打包測試,實現了修改生成位置的功能。
自此,簡單的多人線上射擊遊戲開發完成,每天學習一點,至少比昨天的自己進步了一點!
參考資源:
Unity多人網路系統講解-實踐篇
Unity3D網路元件UNet詳解
Networking API文件翻譯