前言
隨著Unity版本的更新,新版的網絡系統Multiplayer也漸漸地越來越被重視,5.3.4版本測試很好用。使用這套網絡系統可以輕松開發聯機網絡游戲,而且其中封裝的API也針對于開發者的層次作了區分。HighLevelAPI(簡稱HLAPI)針對于簡單的網絡系統搭建,封裝的比較嚴重,只需輕松幾部即可完成網絡環境的搭建;LowLevelAPI(簡稱LLAPI)偏向底層,網絡環境的搭建,需要依靠底層類層層搭建,但較為靈活。根據不同的需求,可以選擇不同的API,當然通常HLAPI和LLAPI是混合起來一起用的。本文會簡單講解Multiplayer的API架構,主要通過項目將所有內容串聯。
-
HLAPI架構圖
首先給大家看一下UnityAPI中提供的一張Multiplayer的HLAPI架構圖,清晰的了解我們常用的類的層次。
HLAPI架構圖- Transport/Configuration — 底層API類
- Connection/Reader/Writer — 消息發送類、序列化與反序列化類
- NetworkClient/NetworkServer — 網絡環境搭建類
- NetworkIDentity/NetworkBehaviour — 網絡對象狀態同步
- NetworkManager — 網絡游戲控制(一個組件搞定一個網絡)
- NetworkLobbyManager — 集成了網絡游戲大廳功能
- NetworkTransform/NetworkAnimator — 引擎繼承的狀態同步組件
-
使用基礎類(NetworkServer/NetworkClient)搭建網絡環境
服務器端
NetworkServer.Listen(7777);//創建服務器監聽本機網卡7777端口客戶端
NetworkClient client;//創建客戶端對象
client.Connect("127.0.0.1",7777);//連接服務器
-
使用基礎類(NetworkServer/NetworkClient)創建網絡游戲對象
服務器創建網絡對象卵生到客戶端- 客戶端
ClientScene.Ready(msg.conn); //通知服務器已準備完畢
ClientScene.RegisterPrefab(playerPrefab); //注冊網絡預設體
ClientScene.AddPlayer(0); //通知服務器實例化預設體 - 服務器(只有服務器才能創建網絡對象)
GameObject player = (GameObject)Instantiate(playerPrefab);
//給予該客戶端該對象的權限
NetworkServer.AddPlayerForConnection(netMsg.conn, player, 0);
//卵生[同步到其他客戶端]
NetworkServer.Spawn(player);
- 客戶端
-
遠程過程調用(RPC)
網絡環境下的遠程消息發送Command:由客戶端發送給服務器[在服務器執行方法]
ClientRPC:由服務器發送給客戶端[在客戶端執行方法]
客戶端調服務器方法(Command方法名必須以Cmd開頭)
[Command]
/// <summary>
/// 發射炮彈
/// </summary>
void CmdFire ()
{
GameObject bullet = (GameObject)Instantiate (MyLobbyManager.instance.spawnPrefabs [0],firePoint.position, firePoint.rotation);
bullet.GetComponent<Rigidbody> ().velocity = bullet.transform.forward * 20;
NetworkServer.Spawn (bullet);
}服務器調客戶端方法(ClientRPC方法必須以Rpc開頭)
[ClientRpc]
/// <summary>
/// 播放特效稍后銷毀
/// </summary>
/// <param name="eff">Eff.</param>
void RpcStopEffect (GameObject eff)
{
eff.GetComponent<ParticleSystem> ().Play ();
Destroy (eff, 1.05f);
}
當然是用NetworkManager/NetworkLobbyManager組件同樣可以搭建網絡環境,且更為方便,這里不再贅述,詳見項目。
-
實戰項目坦克大戰
坦克大戰游戲大廳
坦克大戰主場景 挑幾個重點腳本看看
1.大廳管理
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
public class MyLobbyManager : NetworkLobbyManager
{
//單例
public static MyLobbyManager instance;
//是否開啟切換場景標志位
public bool beginChange = false;
//背景音樂
public GameObject backAud;
//玩家位置編號
private int playerPositionIndex = 0;
void Awake ()
{
instance = this;
}
void Start ()
{
DontDestroyOnLoad (backAud);
}
/// <summary>
/// 當所有玩家都已準備完畢
/// </summary>
public override void OnLobbyServerPlayersReady ()
{
//啟動協程等待動畫播放完畢
StartCoroutine (PlayProgress ());
//遍歷所有客戶端發送播放指令
foreach (NetworkLobbyPlayer item in lobbySlots) {
if (item) {
(item as MyLobbyPlayer).RpcBeginPlay ();
}
}
}
IEnumerator PlayProgress ()
{
//如果還沒有開始切換場景,繼續播放動畫,保持等待
while (!beginChange) {
yield return null;
}
base.OnLobbyServerPlayersReady ();
}
//當服務器添加玩家對象時調用
public override void OnServerAddPlayer (NetworkConnection conn, short playerControllerId)
{
base.OnServerAddPlayer (conn, playerControllerId);
//判斷是否在游戲場景而非游戲大廳
if (beginChange) {
//創建坦克
GameObject player = Instantiate (gamePlayerPrefab) as GameObject;
//通過新場景的NetworkStartPosition確定坦克的創建位置
player.transform.position = startPositions [playerPositionIndex++].position;
//設置坦克腳本中的網絡變量--坦克編號
player.GetComponent<MyPlayer> ().tankNum = playerPositionIndex - 1;
//給予客戶端該坦克的使用權限
NetworkServer.AddPlayerForConnection (conn, player, playerControllerId);
//卵生坦克
NetworkServer.Spawn (player);
}
}
}
2.大廳玩家
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.UI;
public class MyLobbyPlayer : NetworkLobbyPlayer
{
//玩家大廳名稱顯示
private Transform content;
//玩家大廳準備按鈕
private Button readyButton;
//單例LobbyManager
private MyLobbyManager manager;
//倒計時進度條
private GameObject progress;
void Awake ()
{
manager = MyLobbyManager.instance;
content = GameController.instance.content.transform;
readyButton = transform.GetChild (0).GetComponent<Button> ();
}
void Start ()
{
//設置當前對象到UI中顯示
GameController.instance.SetParent (this.transform);
//獲取進度條
progress = transform.parent.parent.parent.GetChild (3).gameObject;
//重置縮放
transform.localScale = Vector3.one;
//非主機客戶端更新玩家名稱
if (!isServer) {
CmdUpdateItemName ();
}
}
[Command]
public void CmdUpdateItemName ()
{
//服務器開始下發指令
RpcUpdateItemName ();
}
[ClientRpc]
public void RpcUpdateItemName ()
{
//非主機客戶端設置字體顏色
content.GetChild (1).GetComponent<Image> ().color = Color.red;
//非主機客戶端設置玩家名稱
content.GetChild (1).GetChild (1).GetComponent<Text> ().text = "Player2";
}
/// <summary>
/// 本地玩家執行
/// </summary>
public override void OnStartLocalPlayer ()
{
base.OnStartLocalPlayer ();
//設置準備按鈕可用
readyButton.interactable = true;
//移除所有監聽
readyButton.onClick.RemoveAllListeners ();
//設置準備按鈕事件監聽
readyButton.onClick.AddListener (OnReadyButtonClick);
}
/// <summary>
/// 玩家準備按鈕點擊事件
/// </summary>
public void OnReadyButtonClick ()
{
//向服務器發送準備指令
SendReadyToBeginMessage ();
//移除該按鈕所有事件監聽
readyButton.onClick.RemoveAllListeners ();
//設置該按鈕取消準備的事件監聽
readyButton.onClick.AddListener (OnNotReadyButtonClick);
}
/// <summary>
/// 玩家取消準備按鈕點擊事件
/// </summary>
public void OnNotReadyButtonClick ()
{
//向服務器發送取消準備的指令
SendNotReadyToBeginMessage ();
//取消該按鈕的所有事件監聽
readyButton.onClick.RemoveAllListeners ();
//添加該按鈕準備的事件監聽
readyButton.onClick.AddListener (OnReadyButtonClick);
}
/// <summary>
/// 當客戶端主播完畢后調用
/// </summary>
/// <param name="readyState">If set to <c>true</c> ready state.</param>
public override void OnClientReady (bool readyState)
{
base.OnClientReady (readyState);
//如果準備好了
if (readyState) {
//按鈕文字顯示為Done
readyButton.GetComponentInChildren<Text> ().text = "Done";
} else {
//否則顯示Ready
readyButton.GetComponentInChildren<Text> ().text = "Ready";
}
}
[ClientRpc]
/// <summary>
/// 客戶端開始播放切換場景動畫
/// </summary>
public void RpcBeginPlay ()
{
progress.SetActive (true);
}
}
3.主場景玩家(坦克)
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.UI;
public class MyPlayer : NetworkBehaviour
{
[SyncVar]
//坦克編號
public int tankNum = 0;
[SyncVar]
//坦克血量
public int health = 100;
//坦克移動速度
public float tankMoveSpeed = 3f;
//坦克旋轉速度
public float tankTurnSpeed = 10f;
//坦克發射的炮彈飛行速度
public float fireSpeed = 20f;
//聲音片段
public AudioClip idle;
public AudioClip run;
private Rigidbody rig;
//觀察點
private Transform targetPoint;
//坦克炮頭
private Transform gun;
//發射點
private Transform firePoint;
//操縱軸
private float hor, ver, gunDir;
//坦克血條顏色
private Color[] colors = new Color[]{ Color.red, Color.green };
//坦克血條背景圖片
private Image healthColor;
//坦克血條
private Slider healthSlider;
//結果UI
private GameObject resultUI;
//聲音片段
private AudioSource aud;
void Awake ()
{
rig = GetComponent<Rigidbody> ();
aud = GetComponent<AudioSource> ();
targetPoint = transform.Find ("TargetPoint");
gun = transform.Find ("TankTurret");
firePoint = transform.Find ("TankTurret/FirePoint");
healthColor = transform.Find ("HealthCanvas/Slider/Fill Area/Fill").GetComponent<Image> ();
healthSlider = transform.Find ("HealthCanvas/Slider").GetComponent<Slider> ();
resultUI = GameObject.FindWithTag ("UI");
}
/// <summary>
/// 本地玩家Start觸發
/// </summary>
public override void OnStartLocalPlayer ()
{
//如果是本地玩家
if (isLocalPlayer) {
//設置攝像機跟蹤點
Camera.main.GetComponent<MyCameraFollow> ().SetTarget (targetPoint);
}
}
[ClientCallback]
void Update ()
{
//設置血條背景顏色
healthColor.color = colors [tankNum];
//設置血條值
healthSlider.value = health;
//如果是本地玩家
if (isLocalPlayer) {
//操縱坦克
hor = Input.GetAxis ("Horizontal");
ver = Input.GetAxis ("Vertical");
gunDir = Input.GetAxis ("GunDirection");
rig.MovePosition (transform.position + transform.forward * ver * Time.deltaTime * tankMoveSpeed);
transform.eulerAngles += Vector3.up * hor * tankTurnSpeed;
gun.transform.eulerAngles += Vector3.up * gunDir * tankTurnSpeed;
//如果坦克移動
if (hor != 0 || ver != 0) {
if (aud.clip == idle) {
aud.Stop ();
aud.clip = run;
} else {
if (!aud.isPlaying) {
aud.Play ();
}
}
} else {
if (aud.clip == run) {
aud.Stop ();
aud.clip = idle;
} else {
if (!aud.isPlaying) {
aud.Play ();
}
}
}
//發射炮彈
if (Input.GetKeyDown (KeyCode.Space)) {
CmdFire ();
}
}
//如果血量見底
if (health <= 0) {
//本地玩家失敗
if (isLocalPlayer) {
resultUI.transform.GetChild (0).gameObject.SetActive (true);
resultUI.transform.GetChild (0).GetChild (0).GetComponent<Text> ().text = "GameOver";
resultUI.transform.GetChild (0).GetChild (1).GetComponent<Text> ().text = "GameOver";
}
//非本地玩家勝利
else {
resultUI.transform.GetChild (0).gameObject.SetActive (true);
resultUI.transform.GetChild (0).GetChild (0).GetComponent<Text> ().text = "Victory";
resultUI.transform.GetChild (0).GetChild (1).GetComponent<Text> ().text = "Victory";
}
}
}
[Command]
/// <summary>
/// 發射炮彈
/// </summary>
void CmdFire ()
{
GameObject bullet = (GameObject)Instantiate (MyLobbyManager.instance.spawnPrefabs [0],
firePoint.position, firePoint.rotation);
bullet.GetComponent<Rigidbody> ().velocity = bullet.transform.forward * 20;
NetworkServer.Spawn (bullet);
}
}
結束語
相比從前的老網絡系統,新版網絡解決了很多Bug,也進一步做了優化,沒有出現老網絡的尷尬問題,只是新網絡同步幀速率有些低,有時候會出現延遲較大的情況,這方面還有待改進。新網絡類多內容也多,感興趣的同學還需要多去看API,關于新網絡今后還會有續集喔,敬請期待。本次項目鏈接:https://pan.baidu.com/s/1hRC7C-diHdGeEtFnnMJQ-Q 密碼:rhjh