iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 1
0

過年時就應該要花時間來了解連線遊戲怎麼進行(long-overdue),但中間一些身體的狀況一直無法展開,這一晃就到了鐵人賽的賽事時間。這幾天幾經思量後,決定還是利用這次的挑戰,好好的花一些時間了解如何做連線遊戲,並確實的在一個月的時間裡完成一個小規模的連線遊戲,抓緊今年所剩不多的時間,取得一些遊戲開發上的收穫。

現下暫無完整的製作藍圖,故整個系列會以製作連線遊戲時記錄過程、碰到的問題為主,這也是不參與任何一主題的好處,可以隨性一點。反正沒有特別的規劃,就直接進入製作。

連線框架背景

既然要做連線遊戲,又不再額外考量Unity以外的環境,那就從現有的連線框架中選取一個方向來進行。

決定用這些時間進行連線遊戲的製作後,最先開始要克服的就是連線技術的使用。目前的Unity處在一個很難選擇要用什麼連線框架的階段。前些年Unity主打的UNET,官方於2018年就決定要捨棄掉,從這篇FAQ裡可以了解更多。而一直位為大宗的Photon,就這次想要試驗的Dedicated Server來說,也不是很好的選擇。Unity目前著手的NetCore,建構在DOTS上,問題實在一大堆,在這樣短時間的情況下,不要碰是最明智的。而Improbable一樣是建構在DOTS的,雖然我相當的看好這個服務,但建構在Preview DOTS的版本,一樣會有一些開發時意想不到的問題,從長遠的角度來看,是值得慢慢地轉移到這樣的概念、平台上進行連線遊戲的開發。就這個時間點,還是趨於保守的選擇了Mirror,一個延續UNET而產生的專案。

從範例中著手學習

目前多數的Unity第三方套件都有Package的版本,可以直接用Package Manager進行。但Mirror然是提供最基本的

有GitHub還是從這裡取得比較方便。

在完全不知道怎麼展開的情況下,所幸有不少的範例可供參考。而裡面的Basic目錄,讓人不猶豫的就會從這個範例開始了解。

Mirror Example Folder

執行後用Unity Editor當做Server,另外開二個Client進行測試,畫面如下

Two Cliens with Server

就這個連線Demo也看到了它的潛力,撇開有可能製作動作類型的遊戲會碰到的位移問題,用它來做卡牌類型或是桌遊類型的遊戲應該會很直覺才對。而程式碼透露了不少該注意的部份。

public class Player : NetworkBehaviour
{
    [SyncVar]
    int playerNo;

    [SyncVar(hook = nameof(OnPlayerDataChanged))]
    public int playerData;
    void OnPlayerDataChanged(int oldPlayerData, int newPlayerData) {}

    public override void OnStartServer() {}
    [ServerCallback]
    void UpdateData() {}

    public override void OnStartClient() {}
    public override void OnStartLocalPlayer() {}
}

首先是Player要繼承NetworkBehaviour,而裡面有著被定義好的virtual method,而從名稱上來看,Server和Client都是一起的,也就是行為的部份寫在同一個地方。

  • OnStartClient用在所有的Client端
  • OnStartLocalPlayer只用在自己的這份Client端

利用SyncVar搭配hook也就是callback的概,每次只要這個值被改變,就會呼叫callback執行其它行為。

一個好的框架就是看了基礎的Demo後依樣畫葫蘆就可以得到另人滿意的結果,所以先不進行其它範例的了解,直接切入到遊戲的製作,待被卡關後再回來看看其它的範例有沒有需要注意的點。

用方塊代表玩家的製作

創建新的場景後加入NetworkManager和其相關的元件

  • NetworkManagerHud
  • NetworkManager
  • TelepathyTransport

若是產生的Player Prefab忘了加上必要的元件

  • NetworkIdentity

則在拖入到NetworkManager時會有一個防呆的提示

Need NetworkIdentity

執行後果如預期般的有個代表玩家的方塊被產生出來。接下來要處理簡易移動的機制,在Tanks目錄了可以看到相關的程式碼。

void Update()
{
    // movement for local player
    if (!isLocalPlayer)
        return;
    float vertical = Input.GetAxis("Vertical");
    Vector3 forward = transform.TransformDirection(Vector3.forward);
    agent.velocity = forward * Mathf.Max(vertical, 0) * agent.speed;
    animator.SetBool("Moving", agent.velocity != Vector3.zero);  
}

看到這段程式碼就至了解,需要一個判定同樣在Client端的玩家是否是自己的方式。可以追溯到NetworkBehaviour這層

/// <summary>
/// This returns true if this object is the one that represents the player on the local machine.
/// <para>In multiplayer games, there are multiple instances of the Player object. The client needs to know which one is for "themselves" so that only that player processes input and potentially has a camera attached. The IsLocalPlayer function will return true only for the player instance that belongs to the player on the local machine, so it can be used to filter out input for non-local players.</para>
/// </summary>
public bool isLocalPlayer => netIdentity.isLocalPlayer;

似乎又解決一個問題了。但有個沒有注意到的地方,造成了Client和Server端不同步的問題

Clien and Server Not Sync

一個小小的NetworkTransform,如果沒有加上來就會造成不同步。但在執行時仍會看到Server端有點頓,之後要再找找看是什麼原因造成的。

Add NetworkTransform Component

初步的移動沒有問題後當然就要來解決射擊了彈的部份了。

額外產生了Projectile後進行發射,一樣要繼承NetworkBehaviour的行為

[Server]
void DestroySelf()
{
    NetworkServer.Destroy(gameObject);
}

public override void OnStartServer()
{
    // Destory after certain time
    // Try to use UniRx to see if it fits or not
    Observable.Timer(System.TimeSpan.FromMilliseconds(2000))
        .Subscribe(_ =>
        {
            DestroySelf();
        })
        .AddTo(_compositeDisposable);
}

而最複雜的部份則是回到TankGameManager,因為是繼承MonoBehaviour,所以裡面要處理連線的事務要用NetworkManager,利用其singleton的物件查詢連線狀態決定如何處理

void FindLocalTank()
{
    //Check to see if the player is loaded in yet
    if (ClientScene.localPlayer == null)
        return;

    LocalPlayer = ClientScene.localPlayer.GetComponent<Tank>();
}

void ShowReadyMenu()
{
    if (NetworkManager.singleton.mode == NetworkManagerMode.ServerOnly)
        return;

    if (LocalPlayer.isReady)
        return;

    StartPanel.SetActive(true);
}

void Update()
{
    if (NetworkManager.singleton.isNetworkActive)
    {
        GameReadyCheck();
        GameOverCheck();

        if (LocalPlayer == null)
        {
            FindLocalTank();
        }
        else
        {
            ShowReadyMenu();
            UpdateStats();
        }
    }
    else
    {
        //Cleanup state once network goes offline
        IsGameReady = false;
        LocalPlayer = null;
        players.Clear();
    }
}

這樣的寫法看起怪怪地,只要連線中本地端的玩家也設定好後,就會不停的執行ShowReadyMenu(),看起來很沒有效率。應該要有某個對應的事件?而此處的FindLocalTankClientScene.localPlayer是關鍵,在每個Client裡當下的Scene中,一定會有一個localPlayer。一直到這裡以前還覺得此框架相當的直覺,但看到這段程式碼後,會覺得要寫連線遊戲,還是要順著框架裡的某些寫法才行。之後若是有時間要用UniRx進行改寫,但現在還是以可以用Mirror進行開發為優先。

從Tank範例這裡已經看到了很夠用的連線處理,接下來就要將從Mirror範例理解到的連線Demo套用到製作的遊戲上。先將專案調整後上到Git Repo,明天再開始來進行連線的行為撰寫和研究Mirror其它的範例。若是順利可以掌握Mirror的用法,看能到什麼樣的階段,才能決定接下來遊戲大概的走向。


下一篇
加入扣血功能
系列文
用Unity製作連線遊戲30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言