Unity独立开发游戏之路

简单的介绍

        本人本科是上海师范大学教育类的专业,由于对于计算机领域的兴趣考研考到了本校的计算机专业。对于跨考计算机的心路历程也许会专门写一篇文章。之所以会尝试自学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。我也就依葫芦画瓢,也确实对于后面的代码非常有帮助。

猜你喜欢

转载自blog.csdn.net/bbbbbbooom/article/details/126618940