初识GameSparks多人游戏插件

  • 初识GameSparks多人游戏插件

    • 简介
    • GameSparks介绍
    • 创建游戏
    • 云服务配置
      • Authentication
      • Matches
      • Challenge
      • MatchFoundMessage
      • ChallengeIssuedMessage
      • Event
    • 游戏逻辑云代码
    • Unity实现
      • 登录/注册界面
      • 主菜单界面
      • 游戏界面
    • 小结

简介

本文跟随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,游戏IDpublic bool IsChallengeStart;       //挑战开始public string CurrentPlayerName;    //当前玩家名字public string HeartsPlayerName;     //心形棋子的玩家名字public string HeartsPlayerId;       //心形棋子的玩家IDpublic string SkullsPlayerName;     //骷髅棋子的玩家名字public string SkullsPlayerId;       //骷髅棋子的玩家IDpublic 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方法并不会被触发,所以我让它在场景切换后不注销之前的监听解决这个问题

游戏视频

Unity3d学习之路-初识GameSparks多人游戏插件相关推荐

  1. Unity3D学习之路——AI小坦克

    Unity3D学习之路--AI小坦克 作业要求: 坦克对战游戏 AI 设计 从商店下载游戏:"Kawaii" Tank 或 其他坦克模型,构建 AI 对战坦克.具体要求 使用&qu ...

  2. Unity3d学习之路-牧师与魔鬼

    Unity3d学习之路-牧师与魔鬼 游戏基本介绍 游戏规则: Priests and Devils is a puzzle game in which you will help the Priest ...

  3. Python学习之路—初识Python

    如果让你从数百种的编程语言中选择一个入门语言? 你会选择哪一个?是应用率最高.长期霸占排行榜的常青藤 Java? 是易于上手,难以精通的 C? 还是在游戏和工具领域仍占主流地位的 C++? 亦或是占据 ...

  4. Unity3D学习之路

    1.准备C#的开发环境 VS2015, Unity3D 5.5.1 2.准备通信协议 protobuf 3.3.0 具体请参考:Protobuf 3.3 使用总结 3.引入日志系统 :C#日志系统 L ...

  5. LUA学习之路--初识LUA

    LUA在葡萄牙语中是"月亮"的意思.1993年由巴西的Ponifical Catholic University开发.该语言是由一个来自计算机图形技术组织的团队开发,并作为自由软件 ...

  6. Unity3d学习之路-简单AR游戏

    简单AR游戏 简单AR游戏 游戏规则 游戏实现 游戏场景的搭建 游戏逻辑的实现 游戏脚本挂载 游戏打包到安卓平台 实现效果 小结 游戏规则 识别指定图片,显示玩家和防御塔,点击按键对玩家进行上下左右移 ...

  7. Unity3D学习之路Homework4—— 飞碟射击游戏

    简单打飞碟小游戏 游戏规则与要求 规则 鼠标点击飞碟,即可获得分数,不同飞碟分数不一样,飞碟的初始位置与飞行速度随机,随着分数增加,游戏难度增加.初始时每个玩家都有6条生命,漏打飞碟扣除一条生命,直到 ...

  8. python na不显示 占位_Python学习之路—Python基础(一)

    前言 在Python学习之路-初识Python一文中,简单的介绍了Python的基本信息,搭建了Python的基础运行环境,这篇文档将主要展现一些Python的入门必备内容,如有错误或遗漏的地方,欢迎 ...

  9. 前端Vue学习之路(一)-初识Vue

    Vue学习之路 (一) 1.引言 2.更换npm国内镜像源 3.用npm下载Vue 4.Vue全家桶 5.使用命令创建项目 5.推荐插件 6.推荐网站 7.学习扩展 1.引言 先安装node.js环境 ...

  10. Redis——学习之路三(初识redis config配置)

    我们先看看config 默认情况下系统是怎么配置的.在命令行中输入 config get *(如图) 默认情况下有61配置信息,每一个命令占两行,第一行为配置名称信息,第二行为配置的具体信息. 我们就 ...

最新文章

  1. Linux创建指定用户特定指定目录权限
  2. 如何设计一款大学生找实习的APP?
  3. jsp后台批量导入excel表格数据到mysql中_PHP批量导入excell表格到mysql数据库
  4. C#.NET 消息机制
  5. 第六届蓝桥杯java b组第一题
  6. Java 12网络编程
  7. 冗余网络构建方案对比:VRRP协议、多网卡绑定及WN202冗余链路网卡
  8. 冯诺依曼计算机硬件系统,冯·诺依曼型计算机硬件组成
  9. spyder数据分析
  10. Linux网卡灯橙色,Ubuntu下网卡灯不亮,是网卡物理损坏了?
  11. 88是python语言的整数类型_少儿Python编程_第三讲:常量变量和数据类型
  12. 【Python】阶乘实例,输入1个正整数,计算阶乘
  13. ipconfig ipconfig /all
  14. OpenStack七年之痒
  15. k8s Pod探针(健康检查和服务可用性检查)
  16. 13 微积分——级数
  17. html下拉菜单会影响下一行,CSS下拉菜单导致后续内容无法撑满页面
  18. 个税改革3个月减税约千亿 7000多万人工薪不再缴税
  19. android更新后名称不变,Android 10这个新名字虽然无聊 但改名却是正确之举
  20. 苹果公布9月9日发布会邀请函 iWatch可能亮相

热门文章

  1. 美团外卖订单系统演进
  2. Ubuntu16.04 php5安装失败解决方案
  3. 泰勒·斯威夫特(Taylor Swift)最好听歌曲专辑,喜欢的可以下载保存
  4. 概率论与数理统计---陈希孺---书籍链接下载
  5. linux xp双系统引导修复工具,双系统xp和linux xp重装后修复linux引导光盘修复方法...
  6. 常见端口入侵方法剖析
  7. 以编程方式使用 Microsoft Office Visio 2003 ActiveX 控件
  8. 软件测试中的测试进度安排怎么写,测试开发之测试计划
  9. ddk开发 c语言,使用DDK提供的build进行编译驱动一点总结
  10. 本文作者YY硕,来自大疆工程师《机器人工程师学习计划》