简单的介绍
本人本科是上海师范大学教育类的专业,由于对于计算机领域的兴趣考研考到了本校的计算机专业。对于跨考计算机的心路历程也许会专门写一篇文章。之所以会尝试自学unity独立开发自己的游戏,部分原因是因为自己对于计算机的领域的兴趣,也有自己喜欢自由自在的创造,还有部分原因是一位多年好友对开发游戏的执着。期间遇到的困难和压力也是非常具有挑战性,我们通过观看各种unity大佬的视频,搜集项目源码进行学习。实现了一个又一个功能,还是挺有成就感。我会挑选几个让我眼前一亮地代码和思路进行记录分享,也是对于自己小结。
我之后所提到的思路和代码并不适合unity初学者,尽管我也只是个初学者。因为我确实没有做游戏软件有个宏观的设计,之前也没接触过任何类似的项目。我属于是想到哪写到哪的那种情况,因此我需要这篇文档来对我自己之前的工作有个回顾。
如果你能给予我任何意见,我一定会认真听取,我也会尽我所能完善这篇文章。
前言
该项目是一款unity2D回合制战略卡牌游戏,简单来说就是每名玩家操控一枚棋子移动,并且利用各个棋子的技能以及卡牌与其他玩家战斗。使用的是Photon网络框架进行联机的设置。
登录界面
实现的功能
玩家输入自己的昵称,然后选择加入的房间。实现起来难度不大。
部分代码
using Photon.Pun;//联网
using UnityEngine.UI;//操作UI,好像暂时用不到
using Photon.Realtime;//使用RoomOptions类设置房间相关信息时需要用用到
using TMPro;//操作TextMeshPro文本需要用到
public class NetworkLaunch : MonoBehaviourPunCallbacks{}
//unity中的脚本默认继承MonoBehaviour,这里继承PunCallbacks是因为我们需要重写Photon中的回调函数
private void Start()
{
PhotonNetwork.ConnectUsingSettings();//连接到服务器
}
public override void OnConnectedToMaster()//回调函数,设置UI可见性
{
base.OnConnectedToMaster();
nameUI.SetActive(true);
}
public void OnClickPlayBtn()//输入完昵称,按下按钮调用
{
nameUI.SetActive(false);
PhotonNetwork.NickName = playerName.GetComponent<TMP_InputField>().text;
loginUI.SetActive(true);
}
public void OnClickJoinBtn()//输入完房间名,按下按钮调用
{
if (roomName.GetComponent<TMP_InputField>().text.Length < 2)
{
return;
}
RoomOptions roomoptions = new RoomOptions();
roomoptions.MaxPlayers = 8;
PhotonNetwork.JoinOrCreateRoom(roomName.GetComponent<TMP_InputField>().text, roomoptions, TypedLobby.Default);
}
public override void OnJoinedRoom()//回调函数,玩家加入房间时调用
{
base.OnJoinedRoom();
PhotonNetwork.LoadLevel(1);
}
房间等待界面
房主视角:
其他成员视角:
其他成员准备后
实现的功能
左上角显示房间名,每个玩家上方显示自己的昵称。房主只有开始游戏按钮,其他成员只有准备按钮,其他玩家点击准备,所有房间中的玩家均显示该玩家准备状态,再次点击准备按钮,准备会取消显示。后进入的玩家按顺序入座。当所有玩家均准备时,房主点击开始游戏才会生效,所有玩家进入下一个游戏场景。
这个房间的设计简直是Photon网络框架实现网络同步的入门考试!下面我将详细说明。
部分代码
我会按照我当时完成功能的先后顺序来进行描述,可能会缺少一定的逻辑
生成房间成员到指定位置
PhotonNetwork.Instantiate("Roomer", Roomerloc[i], Quaternion.identity, 0);
这里使用的是Photon的网络生成预制件,需要将预制件存储指定的Resource文件夹中,这样Photon会根据第一参数的字符串找到指定预制件。第二个参数是世界坐标的位置(区别于Canvas中的坐标),后面几个参数没具体了解,好像默认的就行。
使用Photon自带的网络生成的好处是同步方便,一旦生成所有玩家都能看到这个游戏对象。但也有一定的缺点,canvas中利用Grid layout Group可以方便地对生成在其中地游戏对象进行排列,但是Photon网络生成的游戏对象只能在世界坐标下,如何使得Roomer排列整齐成为了第一个困难。
public List<Vector3> Roomerloc = new List<Vector3>();
public void InitRoomloc()
{
Vector3 v3 = new Vector3(-6, 2, 0);
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 4; j++)
{
Roomerloc.Add(v3);
v3 += new Vector3(4, 0, 0);
}
v3 = new Vector3(-6, -2, 0);
}
}
解决这个问题其实不难,我只需要利用简单的循环计算出适合Roomer放置的位置,存储在Roomerloc当中。接下来需要解决新的问题,如何将Roomer生成到我们想要的位置,这里我想到了一个比较容易地方法:设置一个bool型的数组is_sit来标记哪些位置已经被占用了,后加入房间的玩家只需要寻找一个没有被占座的座位坐下来即可。
这个问题说实话困扰了我几天的时间,原来“欠下的还是要换的”。之前使用网络生成轻而易举地解决了生成同步的问题,但是这个入座问题还是回到了同步问题上去。最初的尝试的结果都是is_sit中数据未能同步,导致几名Roomer坐到同一个位置的情况。
如何实现同步
RPC方式
gameobject.GetComponent<PhotonView>().RPC("UpdateRoomInfo", RpcTarget.MasterClient,传递参数);
需要传递参数的脚本需要挂载Photonview脚本,使用RPC(Remote Procedure Call)进行数据传输,第一参数是函数名称,这些函数之前需要[PunRPC]进行标记,RPC才能找到他们。第二参数是发送数据的对象,RpcTarget.All是房间中所有对象(包括自己),RpcTarget.MasterClient只发送给主机。第三个参数传递参数可以是基本数据类型(int,int[]等)但不能是Gameobject对象。传递参数与调用的函数中的形参需要对应。
SetCustomProperty方式
//设置玩家的属性
ExitGames.Client.Photon.Hashtable table = new ExitGames.Client.Photon.Hashtable();
table.Add("IsReady", false);
PhotonNetwork.LocalPlayer.SetCustomProperties(table);
//获取玩家的属性
object isready;
if (player.CustomProperties.TryGetValue("IsReady", out isready)){}
//使用isready需要强制转换
我们可以来设置玩家属性,Photon自动会帮我们同步到其他主机之上(只不过速度真的很慢),根据一些经验,一个客户端只能设置自己的property,设置其他玩家的property是不会同步的。
知道了同步的工具还不够,因为一个新玩家加入到房间当中并不是将自己数据告诉其他人,而是请求其他人把数据(is_sit)同步给自己。所以在游戏对象的Start方法中,用RPC向主机请求is_sit数据,主机向所有人发送is_sit数据。对于is_sit的修改只需要用RPC修改主机中的数据即可。简而言之主机来确保is_sit的数据一致。
初始进入房间的同步
现在玩家生成到指定的位置,刚加入的玩家确实能看到前面有几个和他自己一样的“Roomer”预制件,他们是谁,他们的准备状态如何?这些又该如何显示呢?
//显示名字
if (photonView.IsMine)
{
SetRoomerName(PhotonNetwork.NickName);
}
else
{
SetRoomerName(photonView.Owner.NickName);
}
//显示准备状态
if (photonView.Owner.IsMasterClient)
{
roomhostimage.SetActive(true);
}
object isready;
if (photonView.Owner.CustomProperties.TryGetValue("IsReady", out isready))
{
SetReadyImage((bool)isready);
}
这是写在每个Roomer在唤醒时所调用的代码,虽然“生成“在一个终端只有一行代码,但是他的生成在所有终端都会调用,也就是说所有终端都会调用每个Roomer生成代码。显示名字的这几行if/else言简意赅但表达的意思很多。如果一个客户机生成自己创建的Roomer,就会执行if中的代码,获取全局变量PhotonView中的NickName,因为这个变量就是当前操作的玩家。如果一个客户机生成其他的Roomer就会执行else中的代码。其中photonview是每个roomer各自的,也就是获取创建Roomer中的玩家的NickName。准备状态的显示也是同理,当然前提需要我们在点击准备时,时刻修改property玩家的准备信息。
游戏主界面
这是我们游戏中最为核心的部分,也会是篇幅最长的一个部分。我将游戏中的内容大致分为四个板块:
1,初始化
2,玩家移动
3,卡牌制作
4,玩家对战
可以简单看一下这个游戏设计得界面,类似于飞行棋,玩家在移动阶段时可以在棋盘上自由移动,之后可以按照一定规则对棋盘上的其他玩家使用牌或者释放技能
初始化
初始化工作我在写代码之初并没有仔细考虑过,但是随着功能的增加,我发现必须要用初始化来做一些游戏进行必要的准备工作,尤其在联机的情况下。并且这些初始化工作也是必须按照一定的先后顺序执行,我将逐一介绍初始化我做了哪些准备工作。
Gamer的生成
//Networkmanager中的start中生成
gamer = PhotonNetwork.Instantiate("Gamer", Vector3.zero, Quaternion.identity, 0);
//Gamer中的Start中执行
if (photonView.IsMine)
{
ownid = PhotonNetwork.LocalPlayer.ActorNumber;
}
else
{
ownid = photonView.Owner.ActorNumber;
}
Networkmanager网络管理每个玩家都拥有有且仅有一个,用于管理客户端通信之间的代码。Gamer是网络生成的“玩家”,是在世界坐标系下的一个游戏对象。我知道photon为每个房间中的玩家创建了Player类的实例化对象 ,而我最终生成的“棋子”是在canvas当中。为了能使得他们Player和棋子产生联系,我考虑使用Gamer这个中间量。通过用Gamer中的变量指向操作的棋子,来建立操作者和他们操作的棋子之间的联系。
你如果能看到这里,关于ownid想必应该不陌生,我看大佬用这个变量记录玩家的唯一标识符,也就是可以通过Gamer.ownid简单判断是否属于某个Player。我也就依葫芦画瓢,也确实对于后面的代码非常有帮助。