[Unity-3d] 联网五子棋~~

联机版五子棋


这次的作业花了我好长好长的时间…其实实现联机版的小游戏说难不难,然而我也只能说…没那么简单吧…OwO 话不多说,来看一看这次联机版五子棋的实现吧。
[Unity-3d] 联网五子棋~~_第1张图片
左边为host,右边为client

1. 创建联网环境

1)NetworkManager 网络控制器设置
第一步是在项目中创建 NetworkManager 对象:从菜单 Game Object -> Create Empty 添加一个新的空游戏对象,重命名为“NetworkManager”。然后添加组件 Network -> NetworkManager,该组件管理游戏的网络状态。
[Unity-3d] 联网五子棋~~_第2张图片
2)设置玩家对象预制
下一步是设置代表游戏中玩家对象的 Unity Prefab。默认情况下,NetworkManager 通过克隆玩家预制来为 Client 实例化玩家对象。在五子棋游戏中,玩家对象是一个空对象,因为玩家只需要通过点击鼠标下棋,并不需要操作游戏对象。将空对象重命名为Player,添加组件 Network -> NetworkIdentity 到对象,该组件用于标识服务器和客户端之间的对象。

接下来,将 NetworkIdentity 上的 “Local Player Authority” 复选框设置为 true。这将允许客户端(本地玩家)拥有一定的权限,控制游戏对象。

将Player拖放到资源窗口中制作预制件。创建一个名为 “Player” 的预制件。从场景中删除 Player对象,因为我们只需要预制。

3)注册玩家预制
一旦玩家预制被创建,它就必须在网络系统上注册。
在层次视图中找到 NetworkManager 对象并选择它在 NetworkManager 组件面板打开 “Spawn Info” 折叠找到 “Player Prefab” 插槽(可以拖入对象的属性)将 PlayerCube 预制件拖入 “Player Prefab” 插槽。

2.实现游戏五子棋

1) 新建棋盘
从菜单 3D Object -> Plane 创建一个平面,添加material,调整位置和角度(竖立以便下棋),作为棋盘。在Plane下新建4个Sphere对象LeftTop/RightTop/LeftButton/RightButton,分别放置在棋盘的四个网格顶点,这是为了在之后方便计算棋子的落子点位置。
[Unity-3d] 联网五子棋~~_第3张图片
2) 创建五子棋脚本chess.cs
因为实际上五子棋的实现比较简单,因此这一次没有使用架构,只用了一个脚本实现,以下说一说各部分函数的作用。

首先是变量说明,主要是分为两部分,一部分是关于位置描述的变量,另一部分是关于游戏状态记录的变量。

    //-----------Part 1 对象&位置相关------------------------ 

    //四个锚点位置,用于计算棋子落点
    public GameObject LeftTop;
    public GameObject RightTop;
    public GameObject LeftBottom;
    public GameObject RightBottom;

    //主摄像机
    public Camera cam;

    //锚点在屏幕上的映射位置
    public Vector3 LTPos;
    public Vector3 RTPos;
    public Vector3 LBPos;
    public Vector3 RBPos;

    Vector3 PointPos;           //当前点选的位置
    float gridWidth = 1;        //棋盘网格宽度
    float gridHeight = 1;       //棋盘网格高度
    float minGridDis;           //网格宽和高中较小的一个


    //-----------Part 2 游戏状态相关------------------------ 

    // 记录棋子状态
    public SyncListInt cs = new SyncListInt();

    // 同步记录游戏状态
    [SyncVar]
    public int winner = 0;             //获胜方,1为黑子,-1为白子
    [SyncVar]
    public bool isPlaying = true;      //是否处于对弈状态

    enum turn { black, white };
    [SyncVar]
    turn chessTurn;             //落子顺序(需要同步)

    [SyncVar]
    public bool isRestart;      //重新开始信号(需要同步)

    Vector2[,] chessPos;        //存储棋盘上所有可以落子的位置
    int[,] chessState;          //存储棋盘位置上的落子状态

    public Texture2D white;     //白棋子贴图
    public Texture2D black;     //黑棋子贴图

其中值得注意的是[SyncVar]标签以及SyncListInt类:

同步变量
同步变量是NetworkBehaviour脚本中的成员变量,他们会从服务器同步到客户端上。当一个物体被派生出来之后,或者一个新的玩家中途加入游戏后,他会接收到他的视野内所有物体的同步变量。成员变量通过[SyncVar]标签被配置成同步变量。
同步列表(SyncLists
同步列表类似于同步变量,但是他们是一些值的列表而不是单个值。同步列表和同步变量都包含在初始的状态更新里。同步列表不需要[SyncVar]属性标识,他们是特殊的类。

更多学习请看这里。
另外需要提醒的是,为了实现联网,需要使用命名空间“Using UnityEngine.Networking”,以及脚本中的MonoBehaviour需要更改为NetworkBehaviour。

游戏启动 void start()
获取四个锚点对象,初始化同步列表及其他同步变量。
另外,需要利用四个锚点对象的位置,计算棋盘中15*15个落子点的位置。

    void Start()
    {
        for (int i = 0; i < 225; i++)
            cs.Add(0);

        LeftTop = GameObject.Find("Main Camera/Plane/LeftTop");
        RightTop = GameObject.Find("Main Camera/Plane/RightTop");
        LeftBottom = GameObject.Find("Main Camera/Plane/LeftBottom");
        RightBottom = GameObject.Find("Main Camera/Plane/RightBottom");
        cam = GameObject.Find("Main Camera").GetComponent();

        chessPos = new Vector2[15, 15];     // 记录落子点位置
        chessState = new int[15, 15];       // 记录下子信息
        chessTurn = turn.black;             // 黑棋先手

        //计算锚点位置
        LTPos = cam.WorldToScreenPoint(LeftTop.transform.position);
        RTPos = cam.WorldToScreenPoint(RightTop.transform.position);
        LBPos = cam.WorldToScreenPoint(LeftBottom.transform.position);
        RBPos = cam.WorldToScreenPoint(RightBottom.transform.position);

        //计算网格宽度
        gridWidth = (RTPos.x - LTPos.x) / 14;
        gridHeight = (LTPos.y - LBPos.y) / 14;
        minGridDis = gridWidth < gridHeight ? gridWidth : gridHeight;

        //计算落子点位置
        for (int i = 0; i < 15; i++)
            for (int j = 0; j < 15; j++)
                chessPos[i, j] = new Vector2(LBPos.x + gridWidth * i, LBPos.y + gridHeight * j);
    }

计算两点间距离
用于计算鼠标点击的位置以及最近的落子点位置的距离。

    //计算平面距离函数
    float Dis(Vector3 mPos, Vector2 gridPos)
    {
        return Mathf.Sqrt(Mathf.Pow(mPos.x - gridPos.x, 2) + Mathf.Pow(mPos.y - gridPos.y, 2));
    }

更新 void Update()
要注意辨别当前下棋的一方,这里使用了Network.isServer来区分是否服务端,与当前回合(黑子or白子)结合判断。另外还需要注意同步变量的更新。这一部分的函数需要细心地去实现,其他具体部分见代码。

void Update()
    {
        if (!isLocalPlayer) return;

        // 非server端-执白子,轮到黑子下棋时不可下棋
        if (chessTurn == turn.black && !isServer)
            return;

        // server端-执黑子,轮到白子下棋时不可下棋
        if (chessTurn == turn.white && isServer)
           return;

        // 更新同步变量
        for (int i = 0; i < 15; i++)
            for (int j = 0; j < 15; j++)
                cs[i * 15 + j] = chessState[i, j];

        //检测鼠标输入并确定落子状态
        if (isPlaying && Input.GetMouseButtonDown(0))
        {
            PointPos = Input.mousePosition;
            for (int i = 0; i < 15; i++)
            {
                for (int j = 0; j < 15; j++)
                {
                    //找到最接近鼠标点击位置的落子点,如果空则落子
                    if (Dis(PointPos, chessPos[i, j]) < minGridDis / 2 && chessState[i, j] == 0)
                    {
                        //根据下棋顺序确定落子颜色
                        chessState[i, j] = chessTurn == turn.black ? 1 : -1;
                        //落子成功,更换下棋顺序
                        chessTurn = chessTurn == turn.black ? turn.white : turn.black;
                    }
                }
            }

            //调用判断函数,确定是否有获胜方
            int re = result();
            if (re == 1)
            {
                winner = 1;
                isPlaying = false;
            }
            else if (re == -1)
            {
                winner = -1;
                isPlaying = false;
            }
        }

        if (isRestart)
            restart();
    }

游戏界面
游戏界面的更新使用了GUI用户交互控件,相信在前面的学习中已经很熟悉了。同样这部分函数也很重要…
这里需要利用同步列表cs来更新图形界面(黑白棋子贴图)。如果cs对应下标的数据为1则贴图黑子,若为-1则贴图白子,若为0则不贴图。
另外还有一个按钮,当一方获胜时弹出该按钮,显示获胜方。点击按钮后游戏重新开始。

void OnGUI()
    {
        // 绘制棋子
        for (int i = 0; i < 15; i++)
        {
            for (int j = 0; j < 15; j++)
            {
                if (cs[i * 15 + j] == 1)
                {
                    GUI.DrawTexture(new Rect(chessPos[i, j].x - gridWidth / 2, Screen.height - chessPos[i, j].y - gridHeight / 2, gridWidth, gridHeight), black);
                }
                if (cs[i * 15 + j] == -1)
                {
                    GUI.DrawTexture(new Rect(chessPos[i, j].x - gridWidth / 2, Screen.height - chessPos[i, j].y - gridHeight / 2, gridWidth, gridHeight), white);
                }
            }
        }

        //根据获胜状态,弹出相应的胜利图片
        if (winner == 1)
        {
            if (GUI.Button(new Rect(Screen.width * 0.45f, Screen.height * 0.45f, Screen.width * 0.1f, Screen.height * 0.1f), "黑子胜利!"))
                isRestart = true;
        }
        else if (winner == -1)
        {
            if (GUI.Button(new Rect(Screen.width * 0.45f, Screen.height * 0.45f, Screen.width * 0.1f, Screen.height * 0.1f), "白子胜利!"))
                isRestart = true;
        }
        else
            isRestart = false;
    }

重新开始
重置变量即可。

    // 游戏重置
    void restart()
    {
        for (int i = 0; i < 15; i++)
            for (int j = 0; j < 15; j++)
            {
                chessState[i, j] = 0;
                cs[i * 15 + j] = 0;
            }

        isPlaying = true;
        chessTurn = turn.black;
        winner = 0;
    }

判断是否有获胜方
只需要判断横、竖、右斜、左斜四个方向即可。注意不要出现数组越界。

    //检测是够获胜的函数,不含黑棋禁手检测
    int result()
    {
        for (int i = 0; i < 15; i++)
            for (int j = 0; j < 15; j++)
            {
                int sum = 0;
                if (j < 11)
                {
                    //横向 →
                    sum = chessState[i, j] + chessState[i, j + 1] + chessState[i, j + 2] + chessState[i, j + 3] + chessState[i, j + 4];
                    if (sum == 5)  return 1;    // 黑子胜
                    if (sum == -5) return -1;   // 白子胜
                }
                if (i < 11)
                {
                    //纵向 ↓
                    sum = chessState[i, j] + chessState[i + 1, j] + chessState[i + 2, j] + chessState[i + 3, j] + chessState[i + 4, j];
                    if (sum == 5) return 1;
                    if (sum == -5) return -1;
                }
                if (i < 11 && j < 11)
                {
                    // 右斜线 ↘
                    sum = chessState[i, j] + chessState[i + 1, j + 1] + chessState[i + 2, j + 2] + chessState[i + 3, j + 3] + chessState[i + 4, j + 4];
                    if (sum == 5) return 1;
                    if (sum == -5) return -1;
                }
                if (i >= 4 && j < 11)
                {
                    // 左斜线 ↗
                    sum = chessState[i, j] + chessState[i - 1, j + 1] + chessState[i - 2, j + 2] + chessState[i - 3, j + 3] + chessState[i - 4, j + 4];
                    if (sum == 5) return 1;
                    if (sum == -5) return -1;
                }
            }
        return 0; // 胜负未分
    }

以上就是全部代码了,编写完后,将chess.cs脚本挂在Player上,这样联网启动之后就会创建克隆对象,可以愉快下棋了。点击File -> Build & Run,在弹出窗口点击Play,就可以通过exe文件运行五子棋了。同时运行两个exe,分别以host和client的身份下棋。
[Unity-3d] 联网五子棋~~_第4张图片

总结

其实这个五子棋的代码量和难度都不是很大,但是在实现的过程中遇到了很多关于同步变量的问题。由于Unity5以后才使用了新的网络模块Unet,因此网上并没有很多博客可供参考,很多东西都要靠自己摸索。由于时间关系,这次的作业还存在着一些不完善的地方,比如没有限定游戏人数为2人(就是说第3人加入游戏后还可以以白子一方的身份跟第2人竞争下棋)如果感兴趣的话可以继续完善一下,另外如果有什么改进的意见,欢迎提出。

你可能感兴趣的:([Unity-3d] 联网五子棋~~)