1. 程式人生 > >Unity3d學習之路-初識GameSparks多人遊戲外掛

Unity3d學習之路-初識GameSparks多人遊戲外掛

初識GameSparks多人遊戲外掛

簡介

本文跟隨Building a Turn-Based Multiplayer Game with GameSparks and Unity對GameSparks進行學習,做一個聯網五子棋的遊戲,教程中在GameSparks中的js雲程式碼我沒有修改,Unity上程式碼有所修改

GameSparks介紹

GameSparks是一個雲服務,可以提供使用者認證,自定義匹配,回合制或多人遊戲,玩家和遊戲資料儲存等功能。GameSparks可以與客戶端通訊,可以在通訊攔截點(比如收到請求但在處理前,傳送訊息前等)執行雲程式碼,完成自定義的功能。在unity中使用GameSparks,直接在官網下載,然後匯入專案中即可

建立遊戲

官方教程建立一個遊戲比較友好,講得很清楚,這次作業做一個聯網五子棋遊戲,所以就只使用GameSparks提供的Events、Multiplayer、Cloud Code服務。

圖片

雲服務配置

Authentication

使用者驗證不用修改,直接使用GameSparks提供的驗證方法,使用使用者名稱和密碼登入。

Matches

Matches類似於一個遊戲房間,它可以設定遊戲開始的最低人數和最多人數,在Thresholds中可以自定義玩家匹配,還可以使用自定義的指令碼對匹配過程進行控制等。通過Configuration/Multiplayer可以進入該頁面。

圖片

Challenge

Challenge類似於一個遊戲,當Matches匹配成功達到進入遊戲的最低人數的時候,就會觸發Challenge開始。在整個遊戲中,客戶端傳遞的訊息,會在Challenge的有關函式中進行處理,通過Configuration/Multiplayer可以進入該頁面。

圖片

MatchFoundMessage

Configurator/CloudCode選擇UserMessages/MatchFoundMessage,編寫在匹配成功時候將要執行的程式碼。這次遊戲是判斷客戶端的玩家ID是不是第一個先匹配的玩家ID,然後將它作為挑戰者(類似於房主),然後建立一個新的Challenge,將其他玩家新增到Challenge中。程式碼見:部落格傳送門

ChallengeIssuedMessage

Configurator/CloudCode選擇UserMessages/ChallengeIssuedMessage,編寫Challenge建立成功後將Challenge的詳細資訊傳送到客戶端。程式碼見:部落格傳送門

Event

Event是客戶端進行某個操作後會觸發雲端的實現,可以定義傳入的引數。這次遊戲建立一個Move事件,當客戶端的玩家落子的時候會觸發這個事件。引數是X,Y,代表落子的位置。

圖片

遊戲邏輯雲程式碼

  • Board

    Configurator/CloudCode選擇Modules,新建一個Modules叫Board。使用一個一維陣列儲存了棋盤上的資訊(棋子型別或空),可以初始化棋盤,得到棋盤的對應位置的資訊,修改棋盤對應位置的資訊,檢測落子是否有效以及檢測遊戲是否結束。程式碼見:部落格傳送門

  • Move

    Configurator/CloudCode選擇ChallengeEvents/Move,在裡面實現當玩家落子之後的程式碼,玩家落子後,傳入傳送落子位置的訊息到伺服器上,然後伺服器獲取玩家的資訊使用剛才新建的Board,判斷落子是否有效,切換下一個玩家,檢測遊戲是否結束。程式碼見:部落格傳送門

Unity實現

登入/註冊介面

首先匯入GameSparks的unity包,然後找到GameSparksSettings將網頁上的Api Key和API Secret填入。

圖片
圖片

建立一個空物件,命名為GameSparksManager,然後將GameSparksUnity指令碼作為元件,Settings選擇GameSparksSettings。

圖片

建立UI,在Panel上有使用者名稱輸入框,密碼輸入框,註冊按鈕,登入按鈕。將LoginPanel指令碼掛載在Panel上。指令碼使用GameSparks的Api,在按鈕點選的時候向服務端傳送訊息,GameSparks會自動檢測使用者是否重名,密碼是否正確等,並且將訊息返回客戶端。在登入、註冊成功成功則轉到主介面。

public class LoginPanel : MonoBehaviour
{
    public InputField userNameInput;           //使用者名稱輸入框
    public InputField passwordInput;           //密碼輸入框
    public Button loginButton;                 //登入按鈕
    public Button registerButton;              //註冊按鈕
    public Text errorMessageText;              //錯誤訊息文字

    void Awake()
    {
        loginButton.onClick.AddListener(Login);
        registerButton.onClick.AddListener(Register);
    }

    private void Login()
    {
        BlockInput();
        //傳送登入使用者的請求
        AuthenticationRequest request = new AuthenticationRequest();
        request.SetUserName(userNameInput.text);
        request.SetPassword(passwordInput.text);
        request.Send(OnLoginSuccess, OnLoginError);
    }

    private void OnLoginSuccess(AuthenticationResponse response)
    {
        //切換到遊戲開始介面
        LoadingManager.Instance.LoadNextScene();
    }

    private void OnLoginError(AuthenticationResponse response)
    {
        UnblockInput();
        //將錯誤資訊顯示出來
        errorMessageText.text = response.Errors.JSON.ToString();
    }

    private void Register()
    {
        BlockInput();
        //傳送註冊使用者的請求
        RegistrationRequest request = new RegistrationRequest();
        request.SetUserName(userNameInput.text);
        request.SetDisplayName(userNameInput.text);
        request.SetPassword(passwordInput.text);
        request.Send(OnRegistrationSuccess, OnRegistrationError);
    }

    private void OnRegistrationSuccess(RegistrationResponse response)
    {
        //註冊成功則登入
        Login();
    }

    private void OnRegistrationError(RegistrationResponse response)
    {
        UnblockInput();
        errorMessageText.text = response.Errors.JSON.ToString();
    }
    //禁用輸入
    private void BlockInput()
    {
        userNameInput.interactable = false;
        passwordInput.interactable = false;
        loginButton.interactable = false;
        registerButton.interactable = false;
    }
    //可以使用輸入
    private void UnblockInput()
    {
        userNameInput.interactable = true;
        passwordInput.interactable = true;
        loginButton.interactable = true;
        registerButton.interactable = true;
    }
}

圖片

載入場景函式,通過場景管理得到當前場景的索引,可以知道前或後一個場景的索引。將當前場景命名為Login,建立新的場景分別命名為MainMenu和Game。並且按順序加入Bulid場景中.將LoadingManager作為元件掛載在GameSparksManager上。

public class LoadingManager : Singleton<LoadingManager>
{
    public void LoadNextScene()
    {
        //得到當前場景的索引
        int activeSceneIndex = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(activeSceneIndex + 1);
    }

    public void LoadPreviousScene()
    {
        int activeSceneIndex = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(activeSceneIndex - 1);
    }
}

圖片

主選單介面

在Panel上建立一個play按鈕,當點選的時候,向伺服器傳送請求匹配玩家的訊息,然後等待匹配,匹配成功之後將跳轉到遊戲場景。將MainMenuPanel指令碼掛載到該Panel上,將play按鈕放到playButton處。

public class MainMenuPanel : MonoBehaviour
{
    public Button playButton;

    void Awake()
    {
        playButton.onClick.AddListener(Play);
        MatchNotFoundMessage.Listener += OnMatchNotFound;
        //監聽挑戰開始事件
        ChallengeStartedMessage.Listener += OnChallengeStarted;
    }

    //建立挑戰成功,跳轉到遊戲介面
    private void OnChallengeStarted(ChallengeStartedMessage message)
    {
        LoadingManager.Instance.LoadNextScene();
    }

    private void Play()
    {
        BlockInput();
        //傳送匹配玩家的請求
        MatchmakingRequest request = new MatchmakingRequest();
        request.SetMatchShortCode("DefaultMatch");
        request.SetSkill(0);
        request.Send(OnMatchmakingSuccess, OnMatchmakingError);
    }

    private void OnMatchmakingSuccess(MatchmakingResponse response) { }

    private void OnMatchmakingError(MatchmakingResponse response)
    {
        UnblockInput();
    }

    private void OnMatchNotFound(MatchNotFoundMessage message)
    {
        UnblockInput();
    }
    //保證只匹配一次
    private void BlockInput()
    {
        playButton.interactable = false;
    }

    private void UnblockInput()
    {
        playButton.interactable = true;
    }
}

圖片

建立一個ChallengeManager指令碼,用於管理該遊戲中玩家的資訊以及挑戰的狀態改變。ChallengeStartedMessage在挑戰建立的時候觸發,ChallengeTurnTakenMessage在回合改變的時候觸發,ChallengeWonMessage在玩家獲勝後觸發,ChallengeLostMessage在玩家失敗後觸發。在挑戰建立的時候獲取兩邊玩家的ID以及使用者名稱,挑戰ID,拿到儲存在雲端的棋盤資訊。在回合改變的時候,切換當前玩家的使用者名稱,拿到最新的棋盤資訊。將指令碼掛載在一個空物件上。

public class ChallengeManager : Singleton<ChallengeManager>
{
    public UnityEvent ChallengeStarted;    //可註冊的事件,當挑戰開始
    public UnityEvent ChallengeTurnTaken;  //可註冊的事件,切換回合
    public UnityEvent ChallengeWon;        //可註冊的事件,勝利
    public UnityEvent ChallengeLost;       //可註冊的事件,失敗

    private string challengeID;         //挑戰的ID,遊戲ID
    public bool IsChallengeStart;       //挑戰開始
    public string CurrentPlayerName;    //當前玩家名字
    public string HeartsPlayerName;     //心形棋子的玩家名字
    public string HeartsPlayerId;       //心形棋子的玩家ID
    public string SkullsPlayerName;     //骷髏棋子的玩家名字
    public string SkullsPlayerId;       //骷髏棋子的玩家ID
    public PieceType[] Fields;          //整個棋盤的資料

    void Start()
    {
        //註冊監聽方法
        ChallengeStartedMessage.Listener += OnChallengeStarted;
        ChallengeTurnTakenMessage.Listener += OnChallengeTurnTaken;
        ChallengeWonMessage.Listener += OnChallengeWon;
        ChallengeLostMessage.Listener += OnChallengeLost;
    } 

    private void OnChallengeStarted(ChallengeStartedMessage message)
    {
        challengeID = message.Challenge.ChallengeId;
        HeartsPlayerName = message.Challenge.Challenger.Name;
        HeartsPlayerId = message.Challenge.Challenger.Id;
        SkullsPlayerName = message.Challenge.Challenged.First().Name;
        SkullsPlayerId = message.Challenge.Challenged.First().Id;
        CurrentPlayerName = message.Challenge.NextPlayer == HeartsPlayerId ? HeartsPlayerName : SkullsPlayerName;
        IsChallengeStart = true;
        //將資料庫中的棋盤資料拿到
        Fields = message.Challenge.ScriptData.GetIntList("fields").Cast<PieceType>().ToArray();
        ChallengeStarted.Invoke();
    }

    private void OnChallengeTurnTaken(ChallengeTurnTakenMessage message)
    {
        //切換當前玩家名字
        CurrentPlayerName = message.Challenge.NextPlayer == HeartsPlayerId ? HeartsPlayerName : SkullsPlayerName;
        //將資料庫中的棋盤資料拿到
        Fields = message.Challenge.ScriptData.GetIntList("fields").Cast<PieceType>().ToArray();
        ChallengeTurnTaken.Invoke();
    }

    private void OnChallengeWon(ChallengeWonMessage message)
    {
        IsChallengeStart = false;
        ChallengeWon.Invoke();
    }

    private void OnChallengeLost(ChallengeLostMessage message)
    {
        IsChallengeStart = false;
        ChallengeLost.Invoke();
    }

    public void Move(int x, int y)
    {
        //傳送落子的位置資訊
        LogChallengeEventRequest request = new LogChallengeEventRequest();
        request.SetChallengeInstanceId(challengeID);
        request.SetEventKey("Move");
        request.SetEventAttribute("X", x);
        request.SetEventAttribute("Y", y);
        request.Send(OnMoveSuccess, OnMoveError);
    }

    private void OnMoveSuccess(LogChallengeEventResponse response)
    {
        print(response.JSONString);
    }

    private void OnMoveError(LogChallengeEventResponse response)
    {
        print(response.Errors.JSON.ToString());
    }
}

遊戲介面

棋子中每一個格子作為獨立的預製體,建立一個空物體命名為Field,新增AnimatorBox Collider 2D元件,新增一個空物件作為其子物件,子物件新增Sprite Renderer元件,選擇所需的Sprite。建立新的Animator Controller,通過改變子物件的Sprite Renderer中的Sprite從而實現滑鼠觸碰時候的加粗效果以及點選之後的棋子落上去的效果。詳情見:部落格地址。將Field指令碼掛載到Field上,儲存為預製體。

public class Field : MonoBehaviour
{
    private Animator animator;        //棋子的動畫,棋子的切換是由狀態機控制的
    private int x;
    private int y;

    void Awake()
    {
        animator = GetComponent<Animator>();
        //監聽回合切換事件
        ChallengeManager.Instance.ChallengeTurnTaken.AddListener(OnChallengeTurnTaken);
    }

    public void Initialize(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    void OnMouseDown()
    {
        //傳送落子位置
        ChallengeManager.Instance.Move(x, y);
    }

    void OnMouseEnter()
    {
        animator.SetBool("IsHovered", true);
    }

    void OnMouseExit()
    {
        animator.SetBool("IsHovered", false);
    }

    private void OnChallengeTurnTaken()
    {
        //玩家落子後,獲取落子位置的型別
        PieceType pieceType = ChallengeManager.Instance.Fields[x + y * ChessBoard.boardSize];
        //改變圖形
        if (pieceType == PieceType.Heart)
        {
            animator.SetBool("IsHeart", true);
        }
        else if (pieceType == PieceType.Skull)
        {
            animator.SetBool("IsSkull", true);
        }
    }
}

建立一個Board的空物體,用於載入棋盤,棋盤由一個15X15個格子組成,在初始化的時候算出他們的位置放在場景中。這裡將預製體Field拖到fieldPrefab位置上。將ChessBoard指令碼作為Board的元件

public class ChessBoard : MonoBehaviour
{
    public const int boardSize = 15;         //棋盤的大小
    public Field fieldPrefab;                //棋盤的棋子預製體
    public float fieldSpacing = 0.25f;       //棋盤棋子之間的間隔

    void Awake()
    {
        for (int x = 0; x < boardSize; x++)
        {
            for (int y = 0; y < boardSize; y++)
            {
                //算出每個棋子的位置
                float offset = -fieldSpacing * (boardSize - 1) / 2.0f;
                Vector3 position = new Vector3(x * fieldSpacing + offset, y * fieldSpacing + offset, 0.0f);
                Field field = Instantiate(fieldPrefab, position, Quaternion.identity, this.transform);
                field.Initialize(x, y);
            }
        }
    }
}

圖片

製作一個UI用於顯示當前是哪個玩家的回合,以及變換回合後對應玩家的棋子將會放大的效果。

public class HeadPanel : MonoBehaviour
{
    public PieceType PlayerType;          //玩家型別
    public Text text;
    public Image HeadImage;               //玩家代表的棋子圖片
    private string PlayerName;             //玩家名字

    void Awake()
    {
        //根據棋子型別獲取使用者名稱
        PlayerName = (PlayerType == PieceType.Heart) ? ChallengeManager.Instance.HeartsPlayerName : ChallengeManager.Instance.SkullsPlayerName;
    }

    void Update()
    {
        //顯示是哪一個使用者的回合
        if(PlayerName == ChallengeManager.Instance.CurrentPlayerName)
        {
            HeadImage.rectTransform.localScale = new Vector3(1, 1, 1);
            text.text = PlayerName + " Turn";
        }
        else
        {
            HeadImage.rectTransform.localScale = new Vector3(0.7f, 0.7f, 0.7f);
        }

    }
}

圖片

圖片

製作一個UI,顯示失敗或者成功的介面,並新增返回按鈕

public class WinLossPanel : MonoBehaviour
{
    public RectTransform content;
    public Text winText;
    public Text lossText;
    public Button backButton;      //返回按鈕

    void Awake()
    {
        ChallengeManager.Instance.ChallengeWon.AddListener(OnChallengeWon);
        ChallengeManager.Instance.ChallengeLost.AddListener(OnChallengeLost);
        backButton.onClick.AddListener(OnBackButtonClick);
        //隱藏結束介面
        Hide();
    }

    private void Show()
    {
        content.gameObject.SetActive(true);
    }

    private void Hide()
    {
        content.gameObject.SetActive(false);
    }

    private void OnChallengeWon()
    {
        winText.enabled = true;
        lossText.enabled = false;
        Show();
    }

    private void OnChallengeLost()
    {
        winText.enabled = false;
        lossText.enabled = true;
        Show();
    }

    private void OnBackButtonClick()
    {
        //返回上一個場景
        LoadingManager.Instance.LoadPreviousScene();
    }
}

圖片

圖片

小結

此次遊戲製作遇到一個問題在MainMenuPanel.cs使用

ChallengeManager.Instance.ChallengeStarted.AddListener(OnChallengeStarted);

註冊OnChallengeStarted函式沒有用,在挑戰開始的時候也不會觸發,但是在Field.cs中使用

ChallengeManager.Instance.ChallengeTurnTaken.AddListener(OnChallengeTurnTaken);

在回合切換的時候OnChallengeTurnTaken會被呼叫。所以最後只能使用

ChallengeStartedMessage.Listener += OnChallengeStarted;

註冊函式。但是不同指令碼在不同場景的ChallengeStartedMessage好像是不一樣的,因為在單例類ChallengeManager中使用ChallengeStartedMessage.Listener 註冊函式可以觸發,但是在場景切換後MainMenuPanel使用上述方法註冊OnChallengeStarted方法並不會被觸發,所以我讓它在場景切換後不登出之前的監聽解決這個問題

遊戲視訊