Unity5.联机笔记
參考教程譯文:
https://blog.csdn.net/swt369/article/details/78320900
原教程:https://forum.unity.com/threads/unity-5-3-simple-multiplayer-game-tutorial.390812/
大部分源于該翻譯教程,某些根據實踐有改動或筆記
完成的工程,可直接下載:https://download.csdn.net/download/lagoon_lala/10950454
目錄
1一個簡單的聯機示例
Unity網絡功能概覽(Networking Overview)
高級腳本API(High level scripting API)
與編輯器和引擎的結合
網絡服務
網絡傳輸以及實時傳輸層(real-time transport layer)
(2)網絡管理器(Network Manager)
(3)建立Player預制件(Prefab)
(4)注冊Player預制件
(5)讓Player動起來
(6)對Player進行在線測試
以Host的身份進行測試
以客戶端的身份進行測試
(7)讓Player的移動在線化。
(8)測試聯機功能
(9)標識不同的玩家
(10)射擊(單人)
(11)射擊(聯機)
(12)玩家血量(單機)
創建Bullet碰撞(Collisions)
玩家血量
血槽
(13)玩家血量(聯機)
(14)死亡與復位
(15)處理非玩家控制的對象
(16)摧毀Enemy
(17)出生與復位
(18)總結
1一個簡單的聯機示例
Unity的內嵌的多人聯機系統以及HLAPI(High Level API)
示例內容:這篇文檔會手把手地展示如何使用Unity內嵌的多人聯機系統和HLAPI搭建一個多人聯機項目。我們設計的每一步不僅泛用性強,而且包含許多關于多人聯機的重要概念。開發者能夠根據自己的需要擴展這些步驟以適應不同類型的游戲。當項目開發完畢后,它將能夠支持兩名玩家在兩個不同的項目實例上獨立地控制自己的角色,服務器會負責角色行為的控制和同步。玩家之間能夠互相射擊,也可以射擊其他敵人。當玩家被擊敗時,他控制的游戲角色會復位。
先閱讀一下我們的多人聯機開發手冊,特別是Networking Overview部分以及The High Level API以及它們的子頁,包括Network System Concepts。
Unity網絡功能概覽(Networking Overview)
使用聯網特性的開發者大致可以分為兩類:
- 使用Unity開發聯機游戲的開發者。這類開發者應當首先閱讀NetworkManager部分或是High Level API部分。
- 搭建網絡基礎部分以及開發高級聯機游戲的開發者。這類開發者應當首先閱讀NetworkTransport API部分。
高級腳本API(High level scripting API)
Unity的聯網系統有一組高級腳本API(HLAPI)。這些方法基本上能覆蓋絕大部分的多人游戲的共同需求。使用這些API,你可以忽略底層細節,專注于功能的開發。簡單來講,這些API能夠:
- 使用NetworkManager控制游戲的連接狀態。
- 管理”客戶端主機”游戲,這類游戲中的主機由一個玩家客戶端扮演。
- 使用一個多功能的serializer序列化數據。
- 發送以及接收網絡消息。
- 從客戶端向服務器端發送指令。
- 實現客戶端到服務器端的遠程過程調用(RPC)。
- 從客戶端向服務器端發送網絡事件。
?
與編輯器和引擎的結合
Unity的網絡系統嵌入到了它的編輯器和引擎中,這讓網絡游戲的開發變得可視化。它提供了:
- NetworkIdentity,用于需要聯網的組件。
- NetworkBehaviour,用于聯機腳本。
- 可配置的對象變化自動同步。
- 腳本變量自動同步。
- 在Unity Scene中放置網絡構件。
- Network components
網絡服務
Unity提供了網絡服務來為你的游戲開發提供便利,包括以下功能:
- 比賽匹配。
- 創建比賽以及通告比賽。
- 顯示可用的比賽以及加入。
- 中繼服務器(Relay Server)。
- 無服務器的聯網對局。
- 向比賽參與者發送消息。
網絡傳輸以及實時傳輸層(real-time transport layer)
Unity提供了實時傳輸層(real-time transport layer),提供了:
- 最優化的基于UDP的傳輸協議。
- 多通道設計,用于避免隊頭消息阻塞。
- 支持設置每個通道的服務質量(QoS)。
- 靈活的網絡拓撲結構,支持端到端以及客戶端-服務器結構。
?
開始之前,請先:
- 創建一個空的Unity3D項目。
- 將默認的scene保存為”Main”。
(2)網絡管理器(Network Manager)
這節課中,我們將創建一個新的Network Manager對象。這個Network Manager對象會控制這個多人聯機項目的狀態信息,包括游戲狀態管理,場景管理,比賽創建,并且支持訪問Debug信息。高級開發者還能夠擴展NetworkManager類來自定義組件的行為,不過這部分內容不會包含在這節課中。?
想要創建一個新的Network Manager對象,我們需要創建一個新的GameObject,并為其加上NetworkManager與NetworkManagerHUD組件(Component):?
- 創建一個空的Object;?
- 將其重命名為”Network Manager”;?
- 選中這個Object;?
- 添加組件(Add Component):Network > NetworkManager;?
- 添加組件(Add Component):Network > NetworkManagerHUD;
NetworkManager組件會管理游戲的聯網狀態。?
?
NetworkManagerHUD組件和NetworkManager協同工作,并提供了一個簡單的用戶接口來控制游戲在運行時的聯網狀態。?
?
在運行時,NetworkManagerHUD看起來會像是這樣:?
?
文檔手冊可以找到更多細節:https://docs.unity3d.com/Manual/UNetManager.html?_ga=2.40090082.1374918886.1508243698-263594280.1507883404
(3)建立Player預制件(Prefab)
在這個項目中,player預制件用于代表玩家們。?
默認情況下,NetworkManager會通過克隆player預制件并生成到游戲中來為每個連接進游戲的玩家實例化一個游戲對象。?
網絡生成(Network Spawning)以及在客戶端和服務器上同步游戲對象的細節會在后面的課程中介紹。?
這里,玩家的GameObject會是一個簡單的膠囊體,上面附著一個”臉”,用來告訴我們這個膠囊體的朝向。?
完成后的GameObject會是這樣:?
?
要創建這個GameObject,你需要:?
- 創建一個Capsule。?
- 將其重命名為”Player”。
為了指示出這個對象的“前方”,為它添加一個子立方體,并將顏色設置為黑色:?
- 選中Player。?
- 創建一個立方體,并將其設置為Player的子物體。?
- 將其重命名為”Visor”。(面甲)?
- 設置它的Scale為(0.95, 0.25, 0.5)。?
- 設置它的Position為 (0.0, 0.5, 0.24)。?
- 創建一個新的Material。?
- 將其重命名為”Black”。?
- 選中Black。?
- 將它的Albedo color改為黑色。?
- 將Visor的Material設置為Black。簡單的方法是直接把Material拖到Scene視圖的Visor上。
為了將Player標識為一個特殊的聯網的游戲對象,為Player添加一個NetworkIdentity組件:?
- 選中Player。?
- 添加組件(Add Component):Network > NetworkIdentity。
NetworkIdentity組件用來在網絡上識別這個物體,并讓網絡系統意識到它。?
- 將 Local Player Authority 設置為true。
?
將Player的NetworkIdentity設置為Local Player Authority會允許客戶端控制Player的移動。?
接下來由Player創建一個預制件:?
- 把Player從Hierarchy視圖拖到Project視圖來創建一個新的prefab資源。?
- 從場景中刪除Player。?
- 保存場景。
(4)注冊Player預制件
當Player預制件創建完畢后,我們需要對其進行注冊。Network Manager會用這個預制件來生成新的玩家控制的對象,并置入場景中。?
- 在Hierarchy視圖中選中之前創建的Network Manager。?
- 在Inspector視圖中打開Spawn Info標簽。?
- 把Player預制件拖進Player Prefab域中。
?
NetworkManager組件被用來控制聯機對象的生成,包括Player。在許多游戲中,玩家都會有一個歸自己控制的標志的對象。NetworkManager有一個專門的域,用來存放用于代表玩家的Player預制件。每個進入游戲的玩家客戶端都會得到一個新創建的游戲對象
(5)讓Player動起來
接下來我們會制作游戲的第一個功能特性:在場景中移動Player。為此,我們會編寫一個新的腳本,叫做“PlayerController”。?
首先編寫最簡單的代碼部分,這部分不會涉及到聯網功能,僅僅在單一玩家環境下工作。?
- 為Player預制件(prefab)創建一個新的腳本,命名為”PlayerController”。?
- 打開腳本編輯器。?
- 寫入如下代碼:
| using UnityEngine; ? ??? public class PlayerController : MonoBehaviour ??? { ??????? void Update() ??????? { ??????????? var y = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f; ??????????? var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f; ? ??????????? transform.Rotate(0, y, 0); ?? ?????????transform.Translate(0, 0, z);//平移 ??????? } ??? } |
這個簡單的腳本能實現移動人物與轉向的功能。默認情況下Input.GetAxis("Horizontal")與Input.GetAxis("Vertical")允許玩家通過WASD與方向鍵乃至觸摸面板來控制玩家。如果想要改變鍵位,請查詢Input Manager相關內容(Edit-Project Settings-Input)。?
接下來:?
- 保存腳本。?
- 回到Unity。?
- 保存場景。
(6)對Player進行在線測試
以Host的身份進行測試
現在,Player還只能夠在客戶端上移動,沒有聯網功能。?
想要進行聯網測試:?
- 進入Play模式。
在Play模式中,NetworkManagerHUD默認顯示如下:?
?
- 單擊LAN Host按鈕,這能夠讓你以主機的身份啟動游戲。
NetworkManager會用Player預制件創建一個新的Player,NetworkManagerHUD會改變顯示來表明服務器目前處于活動狀態。?
這種情況下,游戲以”Host”模式運行。服務器和客戶端處于同一個進程中。?
接下來:?
- 用WASD來控制Player。?
- 單擊Stop按鈕來斷開連接。?
- 退出Player模式。
以客戶端的身份進行測試
想要以客戶端的身份進行測試,我們需要兩個同時運行的游戲實例,其中一個扮演Host。一個可以從編輯器中打開,而另一個就必須首先Build項目之后才能打開。因此,如果我們想在客戶端上測試游戲,就必須Build這個項目。?
- File-Build Settings。?
- 加入場景Main。?
- 點擊Build and run。?
- 啟動時選擇一個較小的分辨率,保證能夠同時看到編輯器。
游戲啟動(后面稱這個實例為Instance)后,你應該能看到NetworkManagerHUD面板。?
- 點擊Host按鈕,這樣Instance會扮演Host。
這時你應該能看到一個Player。試著用WASD控制它。之后:?
- 返回Unity。?
- 進入Play模式。?
- 點擊LAN Client按鈕來扮演客戶端,并和Host建立連接。?
- 試著用WASD控制它。
你會發現兩個Player都在移動。這時:?
- 回到Instance。
你應該還會發現,在Editor中兩個Player的位置和Instance中不同。這是因為PlayerController腳本現在還沒有聯網功能。當前,兩個Player上都附著同樣的腳本。在兩個不同的實例中,這兩個腳本都在處理同樣的輸入信息。Host和Client彼此都能意識到對方的存在,NetworkManager也為它們分別創建了兩個不同的Player,但是Player對象沒有和Host進行交流,因此NetworkManager無法追蹤它的位置,簡單來說就是沒有同步。?
接下來:?
- 關掉Instance。?
- 回到Unity。?
- 退出Play模式。
(7)讓Player的移動在線化。
為了給Player的移動賦予在線特性,并保證每個玩家只能控制它們自己的Player,我們需要更新PlayerController腳本。我們需要給腳本做兩個大的改動:使用UnityEngine.Networking命名空間,以及讓PlayerController繼承自NetworkBehaviour,而不是MonoBehaviour。?
- 打開PlayerController腳本。?
- 添加UnityEngine.Networking命名空間。using UnityEngine.Networking;?
- 將MonoBehaviour改成NetworkBehaviour。public class PlayerController : NetworkBehaviour
接下來加入一段代碼,用于檢查是不是本地對象,這樣就能保證只有玩家只能控制對應的Player。
| ? if (!isLocalPlayer) ??? { ??????? return; ??? } |
下面是完整的腳本:
| using UnityEngine; ??? using UnityEngine.Networking; ? ??? public class PlayerController : NetworkBehaviour ??? { ??????? void Update() ??????? { ??????????? if (!isLocalPlayer) ??????????? { ??????????????? return; ??????????? } ? ??????????? var x = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f; ??????????? var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f; ? ??????????? transform.Rotate(0, x, 0); ??????????? transform.Translate(0, 0, z); ??????? } ??? } |
?
UnityEngine.Networking命名空間包含了編寫具有聯網功能的腳本所需的內容。?
NetworkBehaviour是一個基于Monobehaviour的特別的類。所有自定義的,使用聯網特性的腳本都繼承自它。?
注意到isLocalPlayer。LocalPlayer是NetworkBehaviour的一部分,所有繼承自NetworkBehaviour的腳本都能夠理解它的含義。想要理解LocalPlayer是什么,以及它是如何工作的的話,需要查閱關于HLAPI的文檔。?
在一個聯機項目中,無論是服務器還是客戶端,執行的代碼都來自于同一個腳本,在上文中就是PlayerController腳本。假設有一個服務器端和兩個客戶端,那么就會有6個需要處理的游戲對象。這是因為玩家有兩名,服務器端與兩個客戶端都會存在這兩個玩家控制的游戲對象,2×3=6。?
?
每個游戲對象都是從同一個預制件克隆得到的,因此它們擁有同樣的腳本文件。如果腳本是繼承自NetworkBehaviour的,那么它就能夠了解到哪個對象屬于哪個玩家。LocalPlayer就是對應的客戶端擁有的游戲對象。這個歸屬關系是由NetworkManager在玩家連接進游戲時建立的。當客戶端連接上服務器后,客戶端上創建的游戲對象就被標識為LocalPlayer。其他的游戲對象,無論在客戶端上還是服務器上,都不會是LocalPlayer。?
通過檢查isLocalPlayer,我們就可以判斷腳本是否要繼續執行。
這個判斷保證了只有LocalPlayer能夠執行移動對象的代碼。?
這里還有一個問題,如果我們現在進行測試,Player對象依然沒有實現同步。每個Player只會在本地進行移動,而不會實時更新到網絡上去。為了保證同步,我們需要為Player添加一個NetworkTransform組件。?
- 保存腳本。?
- 回到Unity。?
- 在Project視圖中選中Player預制件。?
- Add Component-Network > NetworkTransform。?
- 保存。
?
NetworkTransform會同步GameObject的移動與變化。?
總結一下本節的內容:?
- isLocalPlayer檢查保證了玩家只能控制自己的Player。?
- NetworkTransform實現了Player之間的同步。
(8)測試聯機功能
測試之前,首先要把之前Build得到的舊版游戲刪除,并重新Build一個新版本。之后的步驟和(6)中相同。如果你前面的步驟沒有出錯的話,此時兩個Player應當能夠獨立地移動,并且實現了同步。你可能會感覺到遠端的Player的移動不是很平滑,有一點卡頓的感覺。你需要記住一點:所有需要聯網的應用都回或多或少地收到網絡條件的限制,也就是客戶端和服務器間數據傳輸的速度。?
有一些方法可以優化網絡狀況與數據傳輸。比如,NetworkTransform有一個Network Send Rate設置,能夠確定NetworkTransform發送同步數據的頻率。這對玩家的游戲體驗會有非常大的影響。?
?
更重要的是,一個需要聯網的應用有許多方法可以解決不同步的問題,比如插值、外推法以及其他不同形式的平滑與預測技術。不過我們的課程中不會涉及到這些。?
最好能夠記住一些關鍵的概念。我們應當在同步數據的頻率和游戲表現(Performance)上取得一個平衡。同步數據的頻率過高或是過低都不合適。如果想要讓用戶有較好的游戲體驗,最好能夠為那些需要同步的游戲對象進行一些預測,讓它們看起來似乎在平滑移動。任何聯機游戲都不可能做到完美的同步,因為玩家所處的網絡環境有好有壞。但是游戲開發者應當付出一些努力,即使在比較差的網絡環境下,至少要讓玩家感覺游戲的同步狀態還不錯。接下來:?
- 關掉Instance。?
- 回到Unity。?
- 退出Play模式。
(9)標識不同的玩家
現在,每個玩家控制的Player都是一模一樣的,這讓玩家無法判斷哪個Player屬于它。為了標識不同的玩家,我們需要給Player上色。?
- 打開PlayerController。?
- 覆寫OnStartLocalPlayer方法來為Player上色。
| ??? public override void OnStartLocalPlayer() ??? { ??????? GetComponent<MeshRenderer>().material.color = Color.blue; ??? } |
此時,完整的代碼如下:
| using UnityEngine; ??? using UnityEngine.Networking; ? ??? public class PlayerController : NetworkBehaviour ??? { ??????? void Update() ??????? { ??????????? if (!isLocalPlayer) ??????????? { ??????????????? return; ??????????? } ? ??????????? var x = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f; ??????????? var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f; ? ?? ?????????transform.Rotate(0, x, 0); ??????????? transform.Translate(0, 0, z); ??????? } ? ??????? public override void OnStartLocalPlayer() ??????? { ??????????? GetComponent<MeshRenderer>().material.color = Color.blue; ??????? } ??? } |
?
這個方法只會在LocalPlayer上調用,因此每個玩家看他們自己的Player時會發現是藍色的。OnStartLocalPlayer方法用來放置一些只會作用于LocalPlayer的代碼,比如配置攝像機與輸入。?
NetworkBehaviour中還有許多有用的方法,最好去查查它的文檔。?
接下來:?
- 保存腳本。?
- 回到Unity。?
- 進行聯網測試(同8)。
?
你會發現自己控制的角色是藍色的。
(10)射擊(單人)
射擊是聯機游戲的一個經典游戲內容,玩家們能夠發射子彈,這些子彈在每個客戶端都能看到。本節會先介紹怎么在單機環境下發射子彈,聯機部分在下一節。?
- 創建一個新的Sphere Object。?
- 重命名為”Bullet”。?
- 選中Bullet。?
- 將其Scale改為(0.2, 0.2, 0.2)。?
- Add Component-Physics > Rigidbody。?
- 設置Rigidbody的Use Gravity為false。?
- 把它拖到Project視圖中,創建一個Bullet預制件。?
- 從場景中刪除Bullet。?
- 保存場景。
現在需要更新PlayerController腳本,讓它具有發射子彈的功能。為此,腳本需要持有Bullet的一個引用。?
- 打開PlayerController腳本。?
- 為Bullet添加一個public的域。
??? public GameObject bulletPrefab;
為子彈發射器添加一個Transform域。
??? public Transform bulletSpawn;
添加輸入處理邏輯:
??? if (Input.GetKeyDown(KeyCode.Space))
??? {
?????? ?Fire();
??? }
添加Fire()方法:
??? void Fire()
??? {
??????? // Create the Bullet from the Bullet Prefab
??????? var bullet = (GameObject)Instantiate (
??????????? bulletPrefab,
??????????? bulletSpawn.position,
??????????? bulletSpawn.rotation);
?
??????? // Add velocity to the bullet
??????? bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 6;
?
??????? // Destroy the bullet after 2 seconds
??????? Destroy(bullet, 2.0f);
??? }
最終的腳本如下:
??? using UnityEngine;
??? using UnityEngine.Networking;
?
??? public class PlayerController : NetworkBehaviour
??? {
??????? public GameObject bulletPrefab;
??????? public Transform bulletSpawn;
?
??????? void Update()
??????? {
??????????? if (!isLocalPlayer)
??????????? {
??????????????? return;
??????????? }
?
??????????? var x = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f;
??????????? var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f;
?
??????????? transform.Rotate(0, x, 0);
??????????? transform.Translate(0, 0, z);
?
??????????? if (Input.GetKeyDown(KeyCode.Space))
??????????? {
??????????????? Fire();
??????????? }
??????? }
?
?
??????? void Fire()
??????? {
??????????? // Create the Bullet from the Bullet Prefab
??????????? var bullet = (GameObject)Instantiate(
??????????????? bulletPrefab,
? ??????????????bulletSpawn.position,
??????????????? bulletSpawn.rotation);
?
??????????? // Add velocity to the bullet
??????????? bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 6;
?
??????????? // Destroy the bullet after 2 seconds
??????????? Destroy(bullet, 2.0f);
??????? }
?
??????? public override void OnStartLocalPlayer ()
??????? {
??????????? GetComponent<MeshRenderer>().material.color = Color.blue;
??????? }
??? }
保存腳本。
回到Unity。
接下來創建一把槍:?
- 把Player預制件拖進Scene中。?
- 選中Player。?
- 創建一個Cylinder作為它的子物體。?
- 將其重命名為”Gun”。?
- 選中Gun。?
- 移除它的Capsule Collider組件。?
- 設置Position為:(0.5, 0.0, 0.5)。?
- 設置Rotation為:(90.0, 0.0, 0.0)。?
- 設置Scale為:(0.25, 0.5, 0.25)。?
- 設置Material為Black。
完成圖:?
?
接下來創建子彈發射器:?
- 選中Player。?
- 創建一個空GameObject作為其子物體。?
- 將其重命名為Bullet Spawn。?
- 設置其Position為(0.5, 0.0, 1.0)。
創建完畢后,子彈發射器應當在槍口位置:?
?
- 選中Player。?
- 將改動保存到Player預制件中。(直接拖到Project視圖的Player上)。?
- 從場景中刪除Player。?
- 保存。
接下來要為PlayerController腳本設置Bullet與Bullet Spawn的引用。?
- 選中Player。?
- 在Inspector視圖中打開Player Controller標簽。?
- 設置Bullet Prefab。?
- 設置Bullet Spawn。?
- 保存。
接下來你可以進行單機測試和聯機測試。你會發現玩家只能看到自己發射的子彈。
總結:
發射需要-子彈預制件,發射位置組件,獲得輸入信號,調用發射函數(在對應位置實例化對象,在剛體組件中設置速度,銷毀)
(11)射擊(聯機)
這部分會為Bullet添加聯機特性。我們需要更新Bullet預制件和射擊代碼。?
前面的課程已經告訴我們,想要讓Bullet具有聯機特性,需要為其添加NetworkIdentity來標識它在網絡上的獨特性,添加NetworkTransform來同步它的位置和旋轉。除此之外,還要向Network Manager將其注冊為一個Spawnable Prefab。?
- 選中Bullet預制件。?
- add component: Network > NetworkIdentity。?
- add component: Network > NetworkTransform。?
- 在NetworkTransform中,將Network Send Rate設置為0。
Bullet在射出之后,方向、速度和旋轉角都不會發生變化,因此不需要發送更新信息。每個客戶端都能夠自己計算出子彈在某個時刻所處的位置。通過將Network Send Rate設置為0,子彈的位置信息將不會通過網絡同步,因此可以降低網絡負載。?
接下來:?
- 在Hierarchy視圖中選中NetworkManager。?
- 打開Spawn Info標簽。?
- 在Registered Spawnable Prefabs列表中,通過+按鈕添加一行。?
- 選中NetworkManager。?
- 把Bullet加入到Registered Spawnable Prefabs列表中。
?
現在,我們需要更新PlayerController腳本。從腳本編輯器中打開它。?
之前,當我們討論如何讓Player的移動具有聯機特性時,我們提到過HLAPI的結構。一個基礎概念是服務器和所有客戶端執行的都是同樣的腳本。想要區分開服務器和不同客戶端的行為,需要用isLocalPlayer來進行判定。?
另外一種控制方法是使用[Command]特性(Attribute)。[Command]用來指明某個方法是由客戶端調用,但是是在服務器上運行的。方法所需的參數都會和命令一起被傳遞到服務器端。命令只能從本地項目實例中發出。當創建一個Command時,Command對應的方法必須以Cmd開頭。?
- 為Fire方法添加[Command]特性,使其成為一個Command。?
- 將其名稱改為CmdFire。
??? [Command]
??? void CmdFire()
對應地,修改調用Fire方法的代碼。
??? CmdFire();
下一個需要知道的概念是Network Spawning(網絡生成?)。在HLAPI中,”Spawn”不僅僅包含”Instantiate”,它意味著在服務器和所有與其連接的客戶端上創建一個對象。這個對象會由spawning system(生成系統?)管理,當其在服務器上發生改變時,狀態變更信息會被發送到客戶端。當服務器上的該對象被摧毀時,客戶端上的該對象也會被摧毀。除此之外,網絡系統還會持有所有spawned GameObject(生成的對象?)的引用,如果一個新玩家加入,這些對象也會在新玩家的客戶端上生成。?
你需要在CmdFire方法中添加這么一行代碼:
??? NetworkServer.Spawn(bullet);
這是最終的腳本:
??? using UnityEngine;
??? using UnityEngine.Networking;
?
??? public class PlayerController : NetworkBehaviour
??? {
??????? public GameObject bulletPrefab;
??????? public Transform bulletSpawn;
?
??????? void Update()
??????? {
??????????? if (!isLocalPlayer)
??????????? {
??????????????? return;
??????????? }
?
??????????? var x = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f;
??????????? var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f;
?
??????????? transform.Rotate(0, x, 0);
??????????? transform.Translate(0, 0, z);
?
??????????? if (Input.GetKeyDown(KeyCode.Space))
??????????? {
??????????????? CmdFire();
??????????? }
??????? }
?
??????? // This [Command] code is called on the Client …
??????? // … but it is run on the Server!
??????? [Command]
??????? void CmdFire()
??????? {
??????????? // Create the Bullet from the Bullet Prefab
??????????? var bullet = (GameObject)Instantiate(
??????????????? bulletPrefab,
??????????????? bulletSpawn.position,
??????????????? bulletSpawn.rotation);
?
??????? ????// Add velocity to the bullet
??????????? bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 6;
?
??????????? // Spawn the bullet on the Clients
??????????? NetworkServer.Spawn(bullet);
?
??????????? // Destroy the bullet after 2 seconds
??????????? Destroy(bullet, 2.0f);
??????? }
?
??????? public override void OnStartLocalPlayer ()
??????? {
??????????? GetComponent<MeshRenderer>().material.color = Color.blue;
??????? }
??? }
保存并回到Unity。接下來你可以進行測試了。不出意外的話,子彈已經能夠正確顯示在各個窗口中了。不過,現在子彈只會在其他玩家身上彈開,不會產生任何影響。
(12)玩家血量(單機)
在網絡游戲中,同步玩家狀態信息是很重要的一個概念。下面我們會為Bullet添加傷害效果,被Bullet擊中會削減玩家的HP值。玩家的HP值就是一個需要在網絡上進行同步的數據。
創建Bullet碰撞(Collisions)
首先為Bullet創建碰撞處理邏輯。這里,我們僅僅讓子彈在撞到其他物體時摧毀自己。?
- 為Bullet預制件添加一個腳本,并改名為”Bullet”。?
- 打開腳本編輯器。?
- 補充邏輯(新版中方法簽名不太一樣):
??? using UnityEngine;
??? using System.Collections;
?
??? public class Bullet : MonoBehaviour {
?
??????? void OnCollisionEnter(Collision?collision)//參考的原教程這里沒有傳入參數,導致控制碰撞對象減去血量時報錯
??????? {
?????????? ?Destroy(gameObject);
??????? }
??? }
保存腳本。
回到Unity。
現在你可以進行一下測試。當子彈碰到其他玩家時,所有窗口中該子彈都會消失。
玩家血量
為了創建玩家的HP,我們需要一個新的腳本來追蹤我們的Player的當前HP。?
- 為Player預制件創建一個新的腳本,并取名為”Health”。?
- 打開腳本編輯器。?
- 創建一個常量來確定HP最大值。
??? public const int maxHealth = 100;
創建一個變量來維護當前血量,初始時為maxHealth。
??? public int currentHealth = maxHealth;
添加一個方法來削減HP:
??? public void TakeDamage(int amount)
??? {
??????? currentHealth -= amount;
??????? if (currentHealth <= 0)
??????? {
??????????? currentHealth = 0;
??????????? Debug.Log("Dead!");
????? ??}
??? }
接下來,我們需要修改Bullet腳本的OnCollisionEnter方法,添加以下代碼:
??? var hit = collision.gameObject;// collision為傳入參數
??? var health = hit.GetComponent<Health>();
??? if (health != null)
??? {
??????? health.TakeDamage(10);
??? }
血槽
為了創建血槽,我們需要創建一些簡單的UI組件。下面的方法并不是最佳的選擇,我們僅僅是希望能用最簡單的方式來解決這個問題。?
- 創建一個UI Image。
需要注意的是,這也會同時創建一個Canvas父對象和一個EventSystem對象。?
- 將Canvas改名為”Healthbar Canvas”。?
- 將Image改名為”Background”。?
- 選中Background。?
- 將Width設置為100。?
- 將Height設置為10。?
- 將它的Source Image設置為內置的InputFieldBackground。?
- 設置其顏色為Red。?
- 不要改動它的Anchor與Pivot。?
- 拷貝BackGround對象。?
- 將新的改名為”Foreground”,并將其設置為Background的子對象。?
- 選中Foreground。?
- 將其設置為綠色。?
- 打開Anchor Presets,并將其Pivot(按shift)與Position(按alt)設置為Middle Left。(這里存疑,我在操作中發現現實相反,設為靠右,顯示才正常)
?
這個HealthBar需要被加入到Player預制件中,并且和生命值與傷害系統綁定起來。?
首先,需要將Canvas從默認的Overlay Canvas改成一個World Space Canvas,然后再將其加入到Player預制件中。?
- 將Player預制件拖進Scene中。?
- 選中HealthBar。?
- 將Canvas的Render Mode改為World Space。?
- 讓HealthBar成為Player的子對象。
此時的結構大致是這樣:?
?
- 選中HealthBar。?
- 對RectTransform執行reset(右上角的小齒輪)。?
- 將RectTransform的Scale設置為(0.01, 0.01, 0.01)。?
- 將RectTransform的Position設置為(0.0, 1.5, 0.0)。?
- 選中Player。?
- 將改動保存進Player預制件中。?
- 保存。
為了將血槽綁定到生命值與傷害系統中,我們需要讓Health腳本獲取它的引用,并根據當前HP設置Foreground的寬度。?
- 打開Health腳本。?
- 添加UnityEngine.UI命名空間。
??? using UnityEngine.UI;
添加一個public的域來保存Healthbar的RectTransform的引用。
??? public RectTransform healthBar;
這里我們需要引用的是HealthBar的Foreground的RectTransform的引用。有了這個引用,我們只需要根據當前血量設置它的Width屬性就可以了。
??? healthBar.sizeDelta = new Vector2(
??????? currentHealth,
??????? healthBar.sizeDelta.y);
這里我們使用了Vector2來設置其width與height。?
完整的Health腳本如下:
??? using UnityEngine;
??? using UnityEngine.UI;
??? using System.Collections;
?
??? public class Health : MonoBehaviour {
?
??????? public const int maxHealth = 100;
??????? public int currentHealth = maxHealth;
??????? public RectTransform healthBar;
?
??????? public void TakeDamage(int amount)
??????? {
??????????? currentHealth -= amount;
??????????? if (currentHealth <= 0)
??????????? {
??????????????? currentHealth = 0;
??????????????? Debug.Log("Dead!");
?? ?????????}
?
??????????? healthBar.sizeDelta = new Vector2(currentHealth, healthBar.sizeDelta.y);
??????? }
??? }
保存腳本。
回到Unity。
在hirearchy視圖中選中Player。
將它的Health組件中的Health Bar屬性設置為Foreground。
將結果保存到Player預制件中。
在場景中刪除Player。
保存。
最后,我們需要讓HealthBar始終面朝主攝像機。?
- 選中HealthBar。?
- 為其添加一個新的腳本,起名為Billboard。?
- 打開腳本編輯器。?
- 在Update方法中添加邏輯,使得HealthBar始終面朝主攝像機。
??? transform.LookAt(Camera.main.transform);
刪除多余的代碼。
完整的Billboard腳本如下:
??? using UnityEngine;
??? using System.Collections;
?
??? public class Billboard : MonoBehaviour {
?
??????? void Update () {
??????????? transform.LookAt(Camera.main.transform);
??????? }
??? }
現在你可以進行測試了。沒有問題的話,你應該能夠通過射擊削減目標的HP了。?
現在,玩家HP的變化是在服務器和客戶端上獨立進行的。當一個玩家射擊另一個玩家,客戶端和服務器都在運行Bullet與Player的腳本。這里沒有進行任何同步。然而,子彈是由NetworkManager控制生成的。當檢測到碰撞時,所有客戶端上的子彈都會被摧毀。由于子彈在每個客戶端上都存在,因此子彈和玩家之間會有碰撞,玩家能夠受到來自子彈的傷害。但是,由于網絡狀態的不穩定性,可能在某個客戶端上子彈已經發生了碰撞,而在另一個客戶端上子彈還沒生成。由于子彈是同步的,而HP不是同步的,玩家的HP在不同的客戶端上可能會產生差異。
(13)玩家血量(聯機)
想要解決上一節留下來的問題,一個方式是讓HP的變化僅僅發生在服務器上,之后再讓服務器對所有客戶端上的玩家血量進行同步。這個概念被稱為服務器權限(Server Authority)。?
為了讓我們的生命值和傷害系統在服務器權限下工作,我們需要使用狀態同步(State Synchronization)和一個特殊的變量:SyncVars。需要網絡同步的變量,或者說SyncVars,需要加上[SyncVar]特性。?
- 打開Health腳本。?
- 添加UnityEngine.Networking命名空間。?
- 讓腳本繼承自NetworkBehaviour。?
- 為currentHealth加上[SyncVar]特性。
??? [SyncVar]
??? public int currentHealth = maxHealth;
為TakeDamage方法加上isServer判定,若false則直接返回。
??? if (!isServer)
??? {
??????? return;
??? }
最終的Health腳本如下:
??? using UnityEngine;
??? using UnityEngine.UI;
??? using UnityEngine.Networking;
??? using System.Collections;
?
??? public class Health : NetworkBehaviour {
?
??????? public const int maxHealth = 100;
?
??????? [SyncVar]
??????? public int currentHealth = maxHealth;
??????? public RectTransform healthBar;
?
??????? public void TakeDamage(int amount)
??????? {
??????????? if (!isServer)
??????????? {
??????????????? return;
??????????? }
?
??????????? currentHealth -= amount;
??????????? if (currentHealth <= 0)
??????????? {
??????????????? currentHealth = 0;
??????????????? Debug.Log("Dead!");
??????????? }
?
??????????? healthBar.sizeDelta = new Vector2(currentHealth, healthBar.sizeDelta.y);
??????? }
??? }
現在你可以進行測試了。這里建議你讓Instance作為Host,編輯器作為客戶端,并控制Instance的Player射擊編輯器的Player。你會發現,Instance上的顯示是正確的,而Client上血條沒有變化。但是,如果你在Inspector視圖中查看Player的當前血量屬性,你會發現它的確發生了變化:?
?
這是因為我們僅僅同步了HP值,而沒有同步Foreground的寬度。改變Foreground寬度的代碼在TakeDamage中,而這個方法由于isServer判定而只能在服務器上運行,因此才會出現服務器上能夠正常顯示,而客戶端上無法正確顯示的問題。?
現在我們需要對Foreground的寬度進行同步。這里我們需要使用另外一個狀態同步工具:SyncVar hook。SyncVar hook能夠將SyncVar連接到一個方法,當SyncVar發生變化,服務器和所有客戶端上的這個方法都會被調用。需要注意的是,這個方法必須有一個參數,類型和SyncVar相同。當方法被調用時,SyncVar的當前值會被傳到方法的參數中。?
下面演示它的使用方法:?
- 打開Health腳本。?
- 把改變Foreground寬度的代碼移到一個單獨的方法中。
??? void OnChangeHealth (int currentHealth)
??? {
??????? healthBar.sizeDelta = new Vector2(health, currentHealth.sizeDelta.y);
??? }
為currentHealth的[SyncVar]特性添加一個屬性。
??? [SyncVar(hook = "OnChangeHealth")]
最終的Health腳本如下:
??? using UnityEngine;
??? using UnityEngine.UI;
??? using UnityEngine.Networking;
??? using System.Collections;
?
??? public class Health : NetworkBehaviour {
?
??????? public const int maxHealth = 100;
?
??????? [SyncVar(hook = "OnChangeHealth")]
??????? public int currentHealth = maxHealth;
?
??????? public RectTransform healthBar;
?
??????? public void TakeDamage(int amount)
??????? {
??????????? if (!isServer)
??????????????? return;
?
??????????? currentHealth -= amount;
??????????? if (currentHealth <= 0)
??????????? {
??????????????? currentHealth = 0;
??????? ????????Debug.Log("Dead!");
??????????? }
??????? }
?
??????? void OnChangeHealth (int health)
??????? {
??????????? healthBar.sizeDelta = new Vector2(health, healthBar.sizeDelta.y);
??????? }
??? }
現在你可以進行測試了。此時客戶端和服務器上的血條應當都能正確變化了。
(14)死亡與復位
現在,即便玩家的HP歸零也不會發生任何事情。為了讓這個示例更加像一個游戲,我們讓玩家的Player在HP歸零時自動在出生點滿HP復活。這里會用到狀態同步的另一個工具——[ClientRpc]特性。?
ClientRpc指令可以由擁有NetworkIdentity的生成對象發出。這個方法由服務器調用,但在客戶端上執行。ClientRpc恰好是Command的反義詞。Command是由客戶端調用,但由服務器執行。?
為了讓一個方法稱為Rpc方法,我們需要使用[ClientRpc]特性,并在方法的名字的前面加上Rpc。現在,這個方法會在客戶端上運行。盡管它是在服務器上調用的。方法所需的參數會自動被發送到客戶端。?
為了添加一個復位功能,我們需要在Health腳本中創建一個新的Respawn方法,并在TakeDamage中進行判定,如果HP歸零則調用這個方法。?
- 打開Health腳本。?
- 創建一個新的方法,命名為RpcRespawn,并加上[ClientRpc]特性。
??? [ClientRpc]
??? void RpcRespawn()
??? {
??????? if (isLocalPlayer)
??????? {
?? ?????????// move back to zero location
??????????? transform.position = Vector3.zero;
??????? }
??? }
在TakeDamage中進行判定,如果HP歸零就讓其回滿,并調用RpcRespawn方法進行復位。
最終的Health腳本如下:
??? using UnityEngine;
??? using UnityEngine.UI;
??? using UnityEngine.Networking;
??? using System.Collections;
?
??? public class Health : NetworkBehaviour {
?
??????? public const int maxHealth = 100;
?
??????? [SyncVar(hook = "OnChangeHealth")]
??????? public int currentHealth = maxHealth;
?
??????? public RectTransform healthBar;
?
??????? public void TakeDamage(int amount)
??????? {
??????????? if (!isServer)
??????????????? return;
?
??????????? currentHealth -= amount;
??????????? if (currentHealth <= 0)
??????????? {
??????????????? currentHealth = maxHealth;
?
??????????????? // called on the Server, but invoked on the Clients
??????????????? RpcRespawn();
??????????? }
??????? }
?
??????? void OnChangeHealth (int currentHealth )
??????? {
??????????? healthBar.sizeDelta = new Vector2(currentHealth , healthBar.sizeDelta.y);
??????? }
?
??????? [ClientRpc]
??????? void RpcRespawn()
??????? {
??????????? if (isLocalPlayer)
??????????? {
??????????????? // move back to zero location
??????????????? transform.position = Vector3.zero;
??????????? }
??????? }
??? }
?
在我們的示例中,客戶端能夠操縱本地Player對象,這是因為Player在客戶端擁有本地權限(local authority)。如果服務器簡單地對Player進行復位,那么客戶端會蓋過服務器,因為Player的操作權限在客戶端手上。為了避免這種情況,服務器通過ClientRpc方法對客戶端發出指令,讓客戶端對Player進行復位。之后,由于NetworkTransform的作用,Player的位置信息會在網絡上同步。?
現在你可以進行測試了。現在HP歸零的Player會在出生點滿血復活。
(15)處理非玩家控制的對象
到目前為止,我們的示例一直關注于玩家控制的對象。然而,許多游戲中都存在一些非玩家控制的對象。這一節中,我們會專注于開發一些類似敵人的游戲對象。?
我們已經知道,玩家控制的Player對象是在客戶端連接上Host后生成的,它由玩家控制。與此相反,敵人對象全都是由服務器控制的。?
這一節中,我們會創建一個敵人生成器(Enemy Spawner),它能夠生成非玩家控制的敵人對象,這些對象可以被任意一個玩家攻擊與殺死。?
- 創建一個空的GameObject。?
- 重命名為”Enemy Spawner”。?
- 選中Enemy Spawner。?
- add component: Network > NetworkIdentity。?
- 在Inspector視圖的NetworkIdentity中,將Server Only設置為true。
將Server Only設置為true能夠防止Enemy Spawner被發送到客戶端。?
- 選中Enemy Spawner。?
- 創建一個新的腳本,并取名為EnemySpawner。?
- 打開腳本編輯器。?
- 用下面的代碼替換掉原來的代碼:
??? using UnityEngine;
??? using UnityEngine.Networking;
?
??? public class EnemySpawner : NetworkBehaviour {
?
??????? public GameObject enemyPrefab;
??????? public int numberOfEnemies;
?
??????? public override void OnStartServer()
??????? {
??????????? for (int i=0; i < numberOfEnemies; i++)
????? ??????{
??????????????? var spawnPosition = new Vector3(
??????????????????? Random.Range(-8.0f, 8.0f),
??????????????????? 0.0f,
??????????????????? Random.Range(-8.0f, 8.0f));
?
??????????????? var spawnRotation = Quaternion.Euler(
??????????????????? 0.0f,
??????????????????? Random.Range(0,180),
??????????????????? 0.0f);
?
??????????????? var enemy = (GameObject)Instantiate(enemyPrefab, spawnPosition, spawnRotation);
??????????????? NetworkServer.Spawn(enemy);
??????????? }
??????? }
??? }
關于上面這段代碼,有幾個注意點:?
- 需要添加UnityEngine.Networking命名空間。?
- 類需要繼承自NetworkBehaviour。?
- 類覆蓋了一個OnStartServer方法。?
- 當服務器啟動,它會創建一組擁有隨機的初始位置和朝向的敵人。之后,它們會通過NetworkServer.Spawn(enemy)生成。
OnStartServer方法和前面用來為本地Player上色的OnStartLocalPlayer方法很像。OnStartServer是在服務器開始監聽網絡時調用。NetworkBehaviour類中還有許多能夠被覆蓋的方法,詳情請查詢文檔。?
接下來保存腳本并返回Unity。?
現在Enemy Spawner已經創建完畢了,我們需要一個用于生成的敵人對象。為了盡可能地加快速度,我們會對Player預制件進行一些簡單的改動,讓他成為一個Enemy。事實上,Enemy和Player有許多共同之處,比如都需要NetworkIdentity和NetworkTransform,都需要生命值系統和血槽等。?
- 把Player預制件拖進Hierarchy視圖。?
- 將其改名為Enemy。?
- 再將Enemy拖回Project視圖以創建一個Enemy預制件。
這么做的目的是防止我們對Enemy的改動影響到Player。?
- 刪除Enemy的Gun子對象。
Unity會警告我們這是一個會破壞預制件的行為。?
- 點擊continue。?
- 選中Enemy。?
- 刪除Bullet Spawn子對象。?
- 在Inspector視圖中,移除PlayerController腳本組件。
現在,這個Enemy已經可以準備上路了。不過,它和Player看起來太相似了,我們再做一些修改,讓它變得和Player不同。?
- 對Enemy應用Black Material。?
- 設置Visor的Material為Default-Material(可以創建一個新的Material并應用在Visor上)。?
- 選中Enemy。?
- 創建一個子Cube對象,并重命名為”Mohawk”。?
- 將Position改為(0.0. 0.55, -0.15)。?
- 將Scale改為(0.2, 1.0, 1.0)。?
- 刪除Mohawk的BoxCollider組件。
最后,Enemy看起來會是這樣:?
?
- 將改動保存到Eneny預制件。?
- 從Scene中刪除Enemy。?
- 保存。
最后,我們需要注冊Enemy預制件,并將其引用賦給Enemy Spawner。?
- 在Hierarchy視圖中選中NetworkManager。?
- 打開Spawn Info標簽。?
- 在Registered Spawnable Prefabs列表中添加一行。?
- 添加Enemy預制件。?
- 選中Enemy Spawner。?
- 將Enemy預制件賦給Enemy Spawner的Enemy Prefab屬性。?
- 設置敵人數量為4。
?
- 保存。
現在你可以開始測試了。不出意外的話,你應當能夠看到幾個隨機出現的敵人,并且可以射擊它們。問題在于,即便把它們的HP打到0,它們不僅不會消失,而且HP會回到滿。這是因為復位功能在RpcRespawn方法中,而Enemy對象無法通過isLocalPlayer判定,因此不會復位。而回復HP的功能在TakeDamage方法中,這個方法是服務器控制的。
(16)摧毀Enemy
我們需要進行一些改動,讓Enemy在HP歸零時被摧毀。最簡單的實現方法是對Health腳本進行一些修改,讓它把Player和Enemy區分開。?
- 打開Health腳本。?
- 添加一個public的bool域destroyOnDeath。
??? public bool destroyOnDeath;
在TakeDamage方法中進行一下判定:
| if (destroyOnDeath) ??? { ??????????????? Destroy(gameObject); ??? } ??? else ??? { ????????????? // existing Respawn code ??? } |
最終的Health腳本如下:
??? using UnityEngine;
??? using UnityEngine.UI;
??? using UnityEngine.Networking;
??? using System.Collections;
?
??? public class Health : NetworkBehaviour {
?
??????? public const int maxHealth = 100;
?
??????? public bool destroyOnDeath;
?
??????? [SyncVar(hook = "OnChangeHealth")]
??????? public int currentHealth = maxHealth;
?
??????? public RectTransform healthBar;
?
??????? public void TakeDamage(int amount)
??????? {
??????????? if (!isServer)
??????????????? return;
?
??????????? currentHealth -= amount;
??????????? if (currentHealth <= 0)
??????????? {
??????????????? if (destroyOnDeath)
??????????????? {
??????????????????? Destroy(gameObject);
??????????????? }
??????????????? else
??????????????? {
??????????????????? currentHealth = maxHealth;
?
??????????????????? // called on the Server, will be invoked on the Clients
??????????????????? RpcRespawn();
??????????????? }
??????????? }
??????? }
?
??????? void OnChangeHealth (int currentHealth)
??????? {
??????????? healthBar.sizeDelta = new Vector2(currentHealth, healthBar.sizeDelta.y);
??????? }
?
??????? [ClientRpc]
??????? void RpcRespawn()
??????? {??????? if (isLocalPlayer)
??????????? {
??????????????? // Set the player’s position to origin
??????????????? transform.position = Vector3.zero;
??????????? }
??????? }
}
保存腳本并返回Unity。
選中Enemy預制件。
設置Destroy On Death屬性為true。
保存。
?
現在你可以進行測試了。射擊敵人應當能夠正確地削減其生命值,當HP歸零時敵人會被摧毀。Player的行為保持不變。
(17)出生與復位
目前,每個玩家的出生點和復活點都在原點。在游戲的一開始,除非我們移動一個Player,否則它們會重疊在一起。理想情況下,玩家們應當在不同的地方出生。NetworkStartPosition能夠幫助我們實現這個功能,它是Unity系統提供的一個專門用來處理出生點問題的組件。?
為了創建兩個不同的出生點,我們需要創建兩個新的GameObject,并為它們各自添加一個NetworkStartPosition組件。?
- 創建一個空的Object。?
- 將其重命名為”Spawn Position 1”。?
- 選中Spawn Position 1。?
- add component Network > NetworkStartPosition。?
- 將Position設置為(3, 0, 0)。?
- 拷貝一個Spawn Position 1。?
- 將新的改名為Spawn Position 2。?
- 選中Spawn Position 2。?
- 將Position設置為(-3, 0, 0)。?
- 選中Hierarchy視圖中的NetworkManager。?
- 打開Spawn Info標簽。?
- 將Player Spawn Method改為Round Robin。
因為Spawn Position擁有NetworkStartPosition組件,NetworkManager會自動找到它們。之后,NetworkManager會用它們的Transform信息來為新加入的客戶端分配一個出生點。?
前面提到了Player Spawn Method,這是一種確定出生點的方法,有兩種:Random and Round Robin。顧名思義,Random會從可用的NetworkSpawnPosition中隨機選擇一個,而Round Robin(輪詢算法)會在可用的出生點間進行循環。如果使用Random,那么多個玩家可能會分配到同一個出生點;如果使用Round Robin,那么除非玩家數大于出生點數,否則它們絕對不會出生在同一個位置。?
現在你可以進行測試了。你應當能看到兩個Player出生在不同的位置。?
現在只剩下最后一步了。現在玩家雖然能夠在不同的地方出生,但是他們被打爆之后復位的地方卻始終是在原點。我們需要建立一個簡單的系統,利用NetworkStartPosition組件來創建一個出生點數組,以此來改變復活點。這一步雖然不是必須的,但這能夠讓這個示例顯得更加完整。?
值得注意的是,有一個更簡單的方法可以存儲每名玩家的出生點位置,那就是在Start方法中記錄下由輪詢算法分配的出生點位置,并用這個作為玩家的復活點。?
現在我們需要創建一個數組,找到所有擁有NetworkStartPosition組件的GameObject,并將它們加入數組中,并以它們的Transform作為出生點。這和NetworkManager做的工作非常相似。不過這里我們就不需要實現輪詢算法了,直接用隨機算法就好。?
- 打開Health腳本。?
- 添加一個數組用于存儲出生點:
??? private NetworkStartPosition[] spawnPoints;
添加一個Start()方法。
在Start()方法中對是否是本地Player進行判斷。
添加尋找NetworkStartPosition組件的邏輯:
??? spawnPoints = FindObjectsOfType<NetworkStartPosition>();
注意這里使用了復數形式的版本:FindObjectsOfType。?
- 在RpcRespawn方法中,刪除原本用來重設Player位置的代碼,并用下面的代碼代替:
??? // Set the spawn point to origin as a default value
??? Vector3 spawnPoint = Vector3.zero;
?
??? // If there is a spawn point array and the array is not empty, pick a spawn point at random
??? if (spawnPoints != null && spawnPoints.Length > 0)
??? {
??????? spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)].transform.position;
??? }
?
??? // Set the player’s position to the chosen spawn point
??? transform.position = spawnPoint;
最終的Health腳本:
??? using UnityEngine;
??? using UnityEngine.UI;
??? using UnityEngine.Networking;
??? using System.Collections;
?
??? public class Health : NetworkBehaviour {
?
??????? public const int maxHealth = 100;
??????? public bool destroyOnDeath;
?
??????? [SyncVar(hook = "OnChangeHealth")]
??????? public int currentHealth = maxHealth;
?
??????? public RectTransform healthBar;
?
??????? private NetworkStartPosition[] spawnPoints;
?
??????? void Start ()
??????? {
??????????? if (isLocalPlayer)
??????????? {
??????????????? spawnPoints = FindObjectsOfType<NetworkStartPosition>();
??????????? }
??????? }
?
??????? public void TakeDamage(int amount)
??????? {
??????????? if (!isServer)
??????????????? return;
?
??????????? currentHealth -= amount;
??????????? if (currentHealth <= 0)
??????????? {
??????????????? if (destroyOnDeath)
??????????????? {
??????????????????? Destroy(gameObject);
??????????????? }
??????????????? else
??????????????? {
??????????????????? currentHealth = maxHealth;
?
??????????????????? // called on the Server, invoked on the Clients
??????????????????? RpcRespawn();
??????????????? }
??????????? }
??????? }
?
??????? void OnChangeHealth (int currentHealth )
??????? {
??????????? healthBar.sizeDelta = new Vector2(currentHealth , healthBar.sizeDelta.y);
??????? }
?
??????? [ClientRpc]
??????? void RpcRespawn()
??????? {
??????????? if (isLocalPlayer)
??????????? {
??????????????? // Set the spawn point to origin as a default value
??????????????? Vector3 spawnPoint = Vector3.zero;
?
??????????????? // If there is a spawn point array and the array is not empty, pick one at random
??????????????? if (spawnPoints != null && spawnPoints.Length > 0)
??????????????? {
??????????????????? spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)].transform.position;
??????????????? }
?
??????????????? // Set the player’s position to the chosen spawn point
??????????????? transform.position = spawnPoint;
??????????? }
??????? }
??? }
現在你可以進行測試了。玩家應當不會一直在原點復活了。
(18)總結
通過這個例子,你已經了解到了創建一個多人聯機游戲所需的基礎概念和組件。?
我們講解了HLAPI的概念與主要的使用方法。當我們使用HLAPI時,服務器和所有客戶端都在運行同樣的腳本中的同樣的代碼。我們還講述了如何讓客戶端和服務器的邏輯分流,通過isLocalPlayer、isServer等判定條件。?
我們講解了HLAPI中實現Rpc的兩種方式:Command與ClientRpc。Command是在客戶端調用而在服務器端執行的方法,ClientRpc是在服務器端調用,而在客戶端執行的方法。?
我們講解了SyncVar與SyncVar hook。為變量賦予[SyncVar]特性,當變量改變時SyncVar hooks方法會自動被調用。?
我們講解了如何通過NetworkIdentity與NetworkTransform實現各個客戶端上的Player的同步。?
我們講解了許多可用的網絡組件,包括NetworkManager、NetworkManagerHUD與NetworkStartPosition。除了這些之外,還有更多的組件可供使用。它們的功能各異,你可以查找文檔或是我們的其他課程來學習它們。?
我們希望這門課程能夠作為你在Unity中開發聯機游戲的一個好的起點。
?
總結
以上是生活随笔為你收集整理的Unity5.联机笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于U-Net的的图像分割代码详解及应用
- 下一篇: numpy的narray数组与txt文件