# Unity多人連線教學
## 前言
翻譯至 https://unity3d.com/learn/tutorials/topics/multiplayer-networking
2016.09.05
## 事前準備
製作連線遊戲前,一定要有的兩個Script。
### Network Manager
![Imgur](https://i.imgur.com/a2dyzrf.png)
連線機制管理器,遊戲中一定要有,功能很多後述。
* 在Unity中創一個空遊戲物件來掛載它。
### Network Manager HUD
![Imgur](https://i.imgur.com/YEfO74B.png)
輔助Network Manager,顯示建立伺服器或是加入伺服器的UI,方便開發時偵錯用。
* 同樣和 Network Manager 掛載在同一個物件下。
遊戲中的UI長這樣:
![Imgur](https://i.imgur.com/X4TXuFv.png)
## 建立和註冊玩家
### 建立玩家
* 準備要當成玩家的Prefab。
* 掛載 Network Identity 。
* 讓Network Manager 辨識並決定如何處置被掛載的物件。
* 有掛載此Component的物件才可被 Networking System 控制或產生。
* ![Imgur](https://i.imgur.com/NqmOnWs.png)
* 勾選 Local Player Authority ,標記此物件是可被Client端操控的物件。
### 註冊玩家
![Imgur](https://i.imgur.com/Z4UuhYj.png)
* 到 Network Manager
* 把剛剛準備好的玩家Prefab,放到Spawn Info的 Player Prefab 欄位內。
## 撰寫角色控制腳本
1. 新增Mono Behaviour,命名為 Player Controller ,並撰寫以下程式碼:
```
using UnityEngine;
public class PlayerController : MonoBehaviour {
void Update()
{
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);
}
}
```
2. 掛載到玩家Prefab上,讓玩家物件可以被移動。
* Get Axis方法中,Horizontal和 Vertical預設是WASD鍵控制,要自訂按鍵可至 Input Manager 修改。
## 測試連線
1. 打包一個獨立執行檔,並執行它。
2. 如何當伺服器:
1. 確定是否有啟用HUD。
2. 點擊 LAN Host 。
3. HUD會轉變成如下圖,代表成功啟動:
4. ![Imgur](https://i.imgur.com/gAZRt1L.png)
5. 莊家會產生一個可以控制的角色,也可以看到其他連入的角色。
3. 如何連線到伺服器:
1. 確定是否有啟用HUD。
2. 點擊 LAN Client 。
3. 會看到畫面會出現自己的角色物件和伺服器端的角色。
4. 現在在伺服器已開,並一客戶端加入的情況下,移動角色。會發現兩隻角色會移動!
* 這是因為角色的移動程式腳本沒有在程式碼內做出差異化,後述如何解決。
## 角色移動差異化
在上一章節,移動角色不是很完全,會造成全部的角色都移動到。現在要講解怎麼解決它。
1. 首先打開 Player Controller 腳本。
2. 引用 UnityEngine.Networking 命名空間。
3. 把原本的基底類別 Mono Behaviour 改為 Network Behaviour 。
4. 在Update 方法(或是類似的 Late Update等不斷更新的方法)內新增判斷式:
1. Networking 命名空間包含有關多人連線的類別和相關型別。
2. Networking Behaviour 繼承至 Mono Behaviour 。被掛載的遊戲物件需要使用到多人連線的屬性或成員時,就需要使用。
IsLocalPlayer 為Networking Behaviour 內的成員。
```
if (!isLocalPlayer)
{
return;
}
```
5. 現在再測試伺服器,發現只有伺服器的角色會移動。但在客戶端控制移動,伺服器端的畫面中客戶端角色卻不會移動!
6. 上述的問題需要在角色Prefab上,掛載 Network Transform 。
1. ![Imgur](https://i.imgur.com/WJGOl6e.png)
2. 此Component是用來同步角色的Transform。
3. 調整 Network Send Rate (Second) 回傳位置給伺服器同步的頻率。
1. 注意,適當的拿捏頻率是重要的地方。同步頻率太快會影響到遊戲效能,太慢會影響到遊戲體驗。
2. 可藉由內插和外插來彌補同步移動時不順的問題。
7. 這時再測試,會解決畫面移動同步的問題。
## 本地角色詳解
![Imgur](https://i.imgur.com/E1HxHZ5.png)
* 在Network Behaviour 中,有很多用來使用的屬性,IsLocalPlayer 是其中之一。
* 在多人連線的專案中,伺服器和客戶端們都是在同時間,使用同樣的腳本和遊戲物件。當有一伺服器和兩個客戶端時,這表示會有六個角色遊戲物件同時在執行。
* 這是因為各個執行中的遊戲,各有兩個角色遊戲物件。伺服器本身不產生角色物件,但會有兩個客戶端的角色實體。客戶端互相會產生對方的角色實體。
* 所有的角色物件皆從同樣的Prefab和使用同樣的Player Controller腳本。掛載從 Network Behaviour 繼承的腳本的遊戲物件,會自動被識別並納入產生(Spawn)程序中。
* Local Player 代表此遊戲物件是本地端所有。在連接伺服器並產生角色時,會被 Network Manager 標示所有權。所以客戶端連線後,自己可控制的角色會被標記為 Local Player。
* 除了自己的角色外,即遠端物件不會被標記為 Local Player。
## 本地角色特殊化
1. 打開 Player Controller 腳本。
2. 加入以下方法:
* OnStartLocalPlayer 方法只會被本地角色呼叫,方便用來設定本地角色連線後的一些初始設定。
```
public override void OnStartLocalPlayer()
{
GetComponent().material.color = Color.blue;
}
```
3. 測試後會發現,自己的角色會變成藍色的,其他角色依然是白色。
## 設定攻擊手段
1. 準備子彈的Prefab。
2. 在Player Controller 腳本內設置可以發射子彈的機制。
```
public GameObject bulletPrefab;
public Transform bulletSpawn;
//發射子彈觸發
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
void Fire()
{
// 產生實體
var bullet = (GameObject)Instantiate (
bulletPrefab,
bulletSpawn.position,
bulletSpawn.rotation);
// 賦予力量給子彈
bullet.GetComponent<RigidBody>().velocity = bullet.transform.forward * 6;
// 在兩秒後移除子彈
Destroy(bullet, 2.0f);
}
```
4. 調整角色的Prefab
1. 把子彈Prefab指定給Player Controller 腳本的 bullet Prefab 欄位。
2. 新增子彈產生點 遊戲物件給角色Prefab,並指定給Player Controller 腳本的 bullet Spawn 欄位。
5. 連線測試,會發現自己在發射子彈的時候,其他客戶端沒有同時呈現出來,在下一個章節會解釋如何修正。
## 子彈發射同步
在此章節會解釋如何在多人連線下發射子彈,並對腳本和Prefab做修改。
1. 在子彈Prefab掛載Network Transform 。
1. ![Imgur](https://i.imgur.com/p6uTqwa.png)
2. 設定 Network Send Rate (Second) 為零。
* 因為子彈在發射後不會改變速度和方向,所以不需要送出同步更新。每個客戶端皆依賴子彈目標的位置。
* 可以減輕遊戲的效能負擔。
2. 在Network Manager中:
1. ![Imgur](https://i.imgur.com/tFHI2xx.png)
2. 在 Spawn Info 中的 Registered Spawnable Preafbs 把子彈Prefab加進去。
3. 修改 Player Controller 腳本。
4. 把Fire方法改名為 CmdFire ,並加上屬性 Command 。
1. Command屬性標示此方法,從客戶端送出並由Server端發送給其他客戶端。
2. 只能從本地玩家中送出。
3. 使用屬性時,方法名稱必須都帶有Cmd前綴字。
5. 在CmdFire中新增述句:
```
NetworkServer.Spawn(bullet);
```
1. 此述句功能類似Game Object的 Instantiate ,要求伺服器端和所有有連線的客戶端產生子彈物件。
2. 被產生的物件是被產生系統管理,不管修改或刪除都會全部同步。
6. 測試結果:角色發射子彈時每個客戶端都會有同步,但是擊中後沒有任何效果。
## 設定角色血量
在以下的步驟,會製作子彈打到角色後,會扣除角色的血量。而每個角色的血量會同步到整個連線。
### 子彈碰撞
1. 建立 Mono Behaviour 腳本,命名為 Bullet
2. 修改程式碼為如下:
```
using UnityEngine;
using System.Collections;
public class Bullet : MonoBehaviour {
void OnCollisionEnter()
{
Destroy(gameObject);
}
}
```
4. Bullet 腳本掛載到 Bullet Prefab。
5. 測試,現在子彈碰到角色會自動消滅。且因為子彈 Prefab 是被 Network Manager 控管,所以所有連線的客戶端都會被同步到。
### 角色血量
1. 建立 Mono Behaviour 腳本,命名為 Health
2. 建立一個常數,代表血量的最大值
```
public const int maxHealth = 100;
```
3. 建立一個變數,代表目前角色的血量,初始預設為最大值
```
public int currentHealth = maxHealth;
```
4. 建立一個方法,實作扣血的機制
```
public void TakeDamage(int amount)
{
currentHealth -= amount;
if (currentHealth <= 0)
{
currentHealth = 0;
Debug.Log("Dead!");
}
}
```
5. 儲存腳本。
6. 修改 Bullet 腳本。
7. 修改 OnCollisionEnter 方法,讓它可以帶入參數。
```
void OnCollisionEnter(Collision collision)
```
8. 新增述句,讓子彈碰到角色時可以被扣血。
```
var hit = collision.gameObject;
var health = hit.GetComponent<Health>();
if (health != null)
{
health.TakeDamage(10);
}
```
9. 儲存腳本。但是現在只有數據,接下來要製作顯示UI。
### 血量UI
1. 在場景中新增 UIImage。
2. 新增後,Unity 會自動幫你產生 Canvas 和 EventSystem 物件。
3. Canvas 更名為 Healthbar Canvas 。
4. Image 更名為 Background ,並設定:
1. 設定 RectTransform Component 的寬為 100。
2. 同上,高為 10。
3. 設定 Image Component 的圖像來源為內建的 InputFieldBackground。
4. 設定 Image Component 的顏色為紅色。
5. 複製 Background 物件,命名為 Foreground ,並設定為 Background 的子物件。
6. 設定 Foreground 物件:
1. 設定 Image Component 的顏色為綠色。
2. 打開 RectTransform Component 的錨點設定( Anchor Presets Window )視窗,設定中心點和位置分別為中間和靠左。
3. ![Imgur](https://i.imgur.com/fWHpJ42.png)
7. 把 Healthbar Canvas 的 Render Mode 轉成 World Space 。
1. 此舉是將UI化為3D物件,讓UI可以跟著角色跑。
8. 將 Healthbar Canvas (含底下的子物件) 作為角色Prefab的子物件。
* ![Imgur](https://i.imgur.com/FBMmFd0.png)
10. 設定 Healthbar Canvas 的 RectTransform Component :
1. 比例設定為 (0.01, 0.01, 0.01)。
2. 位置設定為 (0.0, 1.5, 0.0)。
### 血量UI綁定腳本
1. 修改 Heath 腳本。
2. 使用UI命名空間,且新增一變數用來控制UI。
```
using UnityEngine.UI;
public RectTransform healthBar;
```
3. 新增述句,此機制是用來控制UI的血量條的長度。
```
healthBar.sizeDelta = new Vector2(currentHealth, healthBar.sizeDelta.y);
```
4. 設定角色Prefab,把剛剛做好的UI指定給 healthBar 。
5. 最後,我們需要讓血量條永遠面對攝影機。
6. 選擇 Healthbar Canvas 物件。
7. 新增腳本,命名為 Billboard 。
8. 在腳本內的 Update 方法新增以下此述句:
```
transform.LookAt(Camera.main.transform);
```
9. 移除腳本內沒用到的程式碼。
10. 測試,在客戶端發現子彈打到角色會扣血,但是沒有在所有已連線的客戶端中同步。
## 角色血量同步
有種情況是,變動須從伺服器廣播通知所有客戶端更新,此方式稱之為 Server Authority (伺服器權力)。
要讓血量藉由伺服器權力來進行同步,需要設定變數為 SyncVars ,這稱之為 State Synchronization (狀態同步)。
1. 修改 Health 腳本,引用 Networking 命名空間和改為 NetworkBehaviour。
```
using UnityEngine.Networking;
public class Health : NetworkBehaviour
```
2. 附加屬性 SyncVars 給目前血量變數。
```
[SyncVar]
public int currentHealth = maxHealth;
```
3. 設定判斷在 TakeDamage 方法,讓血量只能在伺服器端控制。
```
if (!isServer)
{
return;
}
```
4. 測試後發現,因為UI還沒設置同步,無法很明確的測試血量是否都同步。必須在編輯器底下才能看到數值的變化。
5. 現在我們要把UI也一併做同步。現在使用 SyncVar hook 來掛勾同步變數和方法,當變數被同步時,也會同時呼叫被掛勾的方法。
6. 把上一章修改UI的述句,移到以下的方法內:
* 值得注意的是,方法的參數型別必須和掛鉤的同步變數一致,才能成功地把值傳入到方法內。
```
void OnChangeHealth (int currentHealth)
{
healthBar.sizeDelta = new Vector2(health, currentHealth.sizeDelta.y);
}
```
7. 增加屬性在方法宣告前方,告知要掛勾哪個變數
```
[SyncVar(hook = "OnChangeHealth")]
```
9. 現在測試,互相射擊對方會發現血量UI都同步了!
## 死亡和重生
現在,當角色血量到零時只會顯示Log,而不會有其他效果。現在我們希望能在血量到零時,會回到出生點。
所以現在介紹另外一項同步的工具,屬性 [ClientRpc] 。ClientRpc 可以在伺服器端,被任何掛有 NetworkIdentity 的產生物件發送。 Command 是從客戶端發送,由伺服器執行,而 ClientRpc 剛好相反,是由伺服器發送,由客戶端執行。
1. 在 Health 腳本新增方法 Respawn ,讓伺服器端,TakeDamage 方法內判斷玩家血量等於零時,去呼叫此方法。
2. 就像使用 Command 一樣,使用 ClientRpc 的方法名稱也需要加上Rpc前綴詞。
```
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// move back to zero location
transform.position = Vector3.zero;
}
}
```
3. 現在回到 TakeDamage 方法做修改,設定血量回到預設值。
```
currentHealth = maxHealth;
```
4. 用呼叫 RpcReSpawn 方法來取代原先的 Log
```
// called on the Server, but invoked on the Clients
RpcRespawn();
```
5. 之前的示範都是客戶端控制角色,因為客戶端有 LocalAuthority 。所以伺服器端只有在角色血量歸零時,直接移回出生點的話,客戶端可以輕易的覆蓋伺服器端的結果。
6. 要避免這種情況,伺服器端藉由 ClientRpc 來控制所有連線的客戶端角色血量歸零時回到原點。因為角色掛載 Network Transform ,而所有的客戶端都會同步位置。
7. 測試,現在當角色血量歸零時,所有的客戶端該角色都會同步自動回到出生點。
## 處理非角色物件
在先前的章節,我們都在處理玩家。但是在遊戲中可能會有其他由伺服器控制的物件。在這個範例內專注於處理敵人這種會變化的伺服器物件,而不注意場景物件。
1. 製作敵人的出生點,新增空遊戲物件,命名為 Enemy Spawner 。
2. 掛載 NetworkIdentity Component ,並將 Server Only 打勾,表示這是伺服器才可觸發的物件。
3. 新增 MonoBehaviour 腳本,掛載在 Enemy Spawner 上。
4. 修改腳本程式碼為:
```
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);
}
}
}
```
5. 上方的程式碼裡面做了四件事:
1. 使用 Networking 命名空間。
2. 改用 NetworkBehaviour。
3. 改寫 OnStartServer 方法。
4. 當伺服器端開始執行時,會產生數個敵人在敵人出生點上。
5. OnStartServer 相當類似 OnStartLocalPlayer ,但在這情況下,前者是在伺服器端開始監控連線時被觸發。
6. 準備敵人的角色物件,我們從原本的玩家角色物件為基底重製。
7. 移除一些沒在敵人身上用到的物件,像是子彈發射腳本和角色控制腳本等等。
8. 到 Netowrk Manager ,把敵人的角色Prefab加到 Registered Spawnable Prefabs 內。
9. 回到 Enemy Spawner ,把敵人的角色Prefab指定給 enemyPrefab 欄位。
10. 設定產生的敵人數量為4。
* ![Imgur](https://i.imgur.com/rdwx9TK.png)
12. 儲存場景和專案。
13. 因為敵人扣血的機制和UI是要和玩家一樣的,所以就不修改腳本。
14. 測試,射擊敵人會扣血,但是當血量歸零時會跳回滿血狀態。因為敵人是伺服器物件,不會進到 RpcRespawn 方法內。
## 讓敵人倒
現在我們需要讓敵人被射擊,血量到零時消滅。要達成此效果,首先要在角色和敵人的述句做出歧異。
1. 修改 Heath 腳本,新增此變數。
```
public bool destroyOnDeath;
```
2. 在 TakeDamage 方法內,在血量歸零的述句內新增判斷:
```
if (destroyOnDeath)
{
Destroy(gameObject);
}
else
{
// existing Respawn code
}
```
3. 存檔,到敵人的角色Prefab內會發現多出了 destroyOnDeath 欄位。
4. 把 destroyOnDeath 欄位打勾,並儲存Prefab。
* ![Imgur](https://i.imgur.com/Bd8RxFz.png)
6. 測試,現在攻擊敵人血量歸零時會消滅,但是消滅後敵人出生點沒有再次產生敵人出來,下一章會解決此問題。
## 生成和再生成
#### 初始化生成點
現在玩家角色的生成點是從原點( Vector3.zero )開始。當有多個玩家連線,會重疊在出生點上,正常的情況是角色會在不同的地方生成。 不過連線系統提供了 NetworkStartPosition Component 來處理產生位置。
1. 首先產生空遊戲物件,命名為 Spawn Position 1 。
2. 掛載 NetworkStartPosition Component。
3. 設定位置。
4. 複製 Spawn Position 1 物件,改名為 Spawn Position 2 。
5. 改變 Spawn Position 2 的位置。
6. 到 Network Manager ,設定 Player Spawn 為 Round Robin 。
1. Network Manager 會自動場景上掛有 NetworkStartPosition 的物件,並取用其位置產生玩家。
2. Player Spawn 有兩種方法:Random 和 Round Robin 。
1. Random 作用為會在可用 NetworkStartPosition 的物件隨機挑一個出來使用。
2. Round Robin 作用為優先使用沒用過的 NetworkStartPosition 的物件,直到全部都輪流到了,才會開始再使用。
#### 設定生成點
在此教學最後一步,利用 NetworkStartPosition component 的陣列來製作一個簡單的玩家隨機生成系統。雖然跟網路連線比較無關,但會讓此範例更完整。
1. 開啟 Health 腳本,新增放置出生點的陣列。
```
private NetworkStartPosition[] spawnPoints;
```
2. 新增 Start 方法。
```
void Start ()
{
}
```
3. 判斷此物件是不是本地玩家
```
if (isLocalPlayer)
{
}
```
4. 找出所有掛載 NetworkStatrtPosition 的物件並加到陣列內
```
spawnPoints = FindObjectsOfType<NetworkStartPosition>();
```
5. 在RpcRespawn 方法內移除此述句
```
// Set the player’s position to origin
transform.position = Vector3.zero;
```
6. 替換為
```
// 宣告並設定區域變數為原點
Vector3 spawnPoint = Vector3.zero;
// 假如陣列有東西,就隨便拿一個出來用
if (spawnPoints != null && spawnPoints.Length > 0)
{
spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)].transform.position;
}
// 把取出來的出生點位置指定給角色物件
transform.position = spawnPoint;
```
7.儲存並測試,現在遊戲一連線上,角色會在隨機的位置上生成。