天天看點

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

UNet開發多人聯機射擊遊戲

引言: Networking作為Unity官方的用于開發多人線上遊戲的網絡子產品,開發者可以不用自己搭建網絡子產品的底層,通過使用Unity提供的一些相關元件,可以輕松實作簡單的多人線上遊戲。本片部落格為泰課線上賈老師的《Unity多人網絡系統講解》的學習筆記,連結位址在文末。

開發版本: Unity 2017.2

文章目錄

  • UNet開發多人聯機射擊遊戲
    • 1. 網絡管理器
    • 2. 建立Player預制體
    • 3. 注冊Player
    • 4. 控制玩家移動
    • 5. 初始化LocalPlayer顔色
    • 6. 添加射擊功能
    • 7. 顯示玩家生命值
    • 8. 處理死亡
    • 9. 添加敵人
    • 10. 修改出生位置

1. 網絡管理器

建立空對象,添加Network Manager和Network Manager HUD元件,如下圖所示:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

2. 建立Player預制體

玩家可以分為LocalPlayer和RemotePlayer:

LocalPlayer指本地玩家控制的對象

RemotePlayer指多人遊戲中其他玩家控制的對象

為提供的坦克Player添加Network Identity元件,勾選Local Player Authority,表示該對象由本地玩家控制,而不是伺服器。并将該對象制作為預制體。

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲
Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲
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删除,如下所示:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

運作遊戲,點選左上角的LAN Host按鈕,将其作為伺服器,又作為用戶端使用,如下所示:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

然後,Network Manager會自動在原點生成一個LocalPlayer,左上角表示用戶端連接配接的IP為本地IP,端口号為7777

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

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按鈕,效果如下所示:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

我們發現如下問題:

  • 無論在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)表示網絡資料同步的頻率,如果同步頻率太頻繁會導緻網絡延遲等問題,而頻率太低又會影響使用者的體驗。

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

5. 初始化LocalPlayer顔色

為PlayerController腳本添加如下方法

//用于本地玩家初始化
public override void OnStartLocalPlayer()
{
    MeshRenderer[] renderers = gameObject.GetComponentsInChildren<MeshRenderer>();
    foreach (var render in renderers)
    {
        render.material.color = Color.blue;
    }
}
           
Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

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);
	}
}
           

在坦克炮口位置建立一個空物體,作為子彈生成的位置

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

将子彈預制體和BulletSpawnPos對象指派到PlayerController上,如下所示:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

此時,打包測試,會發現一方發射子彈,另一方不會同步,如下所示:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

解決該問題,需要先将子彈在Network Manager中注冊為可生成預制體,如下:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

然後将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等。以及維護所有用戶端連接配接。

打包測試效果如下:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

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模式,如下:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

需要将BloodNum圖檔的錨點設定在左側,然後将其指派給Health中的bloodNum,如下:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

為了讓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);
    }
}
           

打包測試,血條可以正常同步,如下所示:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

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腳本。

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲
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中。運作後如下:

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

10. 修改出生位置

建立空的預制體,添加Network Start Position元件

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

将Network Manager中的Player Spawn Method修改為Round Robin,表示按生成點順序一個一個生成

Unity Networking開發多人聯機射擊遊戲UNet開發多人聯機射擊遊戲

修改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文檔翻譯

繼續閱讀