初识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,添加
Animator
和Box 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方法并不会被触发,所以我让它在场景切换后不注销之前的监听解决这个问题