[从零开始unity3D]“五子连珠”游戏实现,网格数组的创建(1)
2016-09-16 00:11
489 查看
大概是一周前,在知乎上偶然看见一位前辈提供的意见,他认为在一个立志于进入游戏行业发展的程序员在大学期间应该多动手,亲自去完成一些项目,在遇到坑,填补坑的过程中快速的提升技术水平。想来我虽然已经入职游戏策划职业将近一年了。但是内心还是无时无刻不想自己成为一个“有技术的游戏策划”,因此将自己定位成一个萌萌的新人。开始一步一个脚印的学习之路。
之前有朋友在我的文章下面评论我,质疑我写的文章并非原创,诚然,之前关于cocos2d等其他游戏引擎的“研究”都是皮毛,很少静下心来尝试制作一款完整的项目。因此这次对于我来说,也是一个很好的机会吧。写这个系列文章,一来可以帮助自己梳理知识。二来可以帮助后来者,大家一起学习交流,三来可以让有经验的前辈帮助我指正。
我会将代码提交到我的GitHub,并将网址贴在此处。
前辈的建议有以下步骤:
开发一款五子棋游戏,AI可以在网上查找;
开发五子棋的局域网对战功能,并且分数可以保存在数据库MySQL等;
开发一款ARP游戏,实现单机部分;
ARPG游戏网络化,开发服务器端;
山寨市面上比较热门的一款卡牌游戏;
而我在第一步上就有点走偏了,实际上我有自己的考虑,我想开发一款“五子连珠”的游戏,同样是二维网格类消除游戏,我认为其更能考验算法设计能力和编程基本功,因为五子棋的最大难点在于AI。而AI在这个游戏开发中又不是我们重点关注的目标。因此我做了这个决定。
五子连珠游戏,顾名思义就是当五个相同颜色的棋子在八个方向上数量大于等于5时即可消除得分。棋盘是一个9×9的网格。每回合玩家可以移动一个棋子,玩家移动之后会在棋盘上随机产生三个新的棋子(消除则不产生)。当所有网格中均有棋子时,游戏结束。棋子的种类共有五种颜色。
整个游戏在算法上有以下需要考虑的地方
随机位置产生棋子的算法;
棋子能否移动的检测;
棋子能否消除的检测;
棋子的寻路算法;
在实际的游戏开发中,实际上在进行寻路算法过程中也需要进行能否移动的检测,因此移动检测是多余的。随机位置产生棋子的算法相对简单,只需要用到随机数;棋子能否消除的算法与一般的三消游戏类似,也可以参考五子棋胜利检测的算法,我这里使用的是辐射法。寻路算法最为复杂,采用的是四方向的A星算法。
游戏的流程图如下图所示:
//此处省略,之后补充
我的思路是首先创建三个脚本分别是:
GameManager.cs 游戏管理器,用于管理游戏开始、储存、读取和游戏结束等;
GridController.cs 网格控制器,用于管理网格,游戏中大部分逻辑运算在这个脚本中;
GridScr.cs 网格脚本,挂在网格游戏对象身上,用来记录网格的信息;
首先看GridScr.cs这个脚本的代码;
在这个方法中首先是记录了网格的横纵坐标以及编号和棋子颜色(这个很重要)等,之后是G、H和F这三个值是用来寻路的,之后定义了一些方法,包括设置网格的、设置和获取网格的颜色的等等。最后是一个设置网格精灵(sprite)的方法,这个方法主要是为了实现游戏中“棋盘是国际象棋效果”这一需求(只是我自己给自己设置的一个需求罢了)。
之后是GameManager.cs脚本,这个脚本我觉得现在还没有贴出来的必要,因为这个脚本现在可以说是一个空壳。
这个脚本目前的功能是实现游戏的新建、储存载入和结束的,而具体的方法操作还没有实现,因此目前没什么好说的。
最后是这篇文章的重头戏,GridController.cs网格控制器脚本。先看代码:
在这个脚本文件中,
首先是脚本的实例化,没什么特别的。
之后便是新建网格,在此之前我将两个二维数组(分别用来记录网格游戏对象和网格身上挂着的脚本,将脚本拿出来存入二维数组可以提高运行效率)之后便是循环创建9×9个网格对象;在这个脚本中,我直接将创建预览网格、创建预览棋子和随机位置创建棋子写在此处,目的是不讲其他方法设置成公有。这里我再之后会进一步完善。
棋盘网格对象的实例化包括几部分,首先是实例化对象、设置父级对象和坐标等;之后是设置脚本,将信息记录在脚本中;然后将其记录在二维网格中;最后是实现国际象棋棋盘效果;
所谓预览棋子,是指预告下回合将出现的三个棋子的颜色。这里分两个方法操作,首先是创建网格,与创建棋盘网格类似,要注意这里创建的网格是没有Box Collider 2D的,目的是为了不能相应鼠标点击事件。之后就是创建三个棋子。分别在三个网格中。
随机位置创建棋子这个方法,首先应该进行一个判断,就是网格中还有足够的位置放置棋子。然后随机一个位置放置预览网格中的棋子,如果随机的位置有棋子了,那么就重新进行随机位置;最后是要将新创建的棋子个数加到当前总个数上。
移动棋子需要先进行寻路,不在这片文章内容中。但是我这里写的是输出两个位置点的坐标。方便测试我当前的逻辑是否正确。正确的话才能进行下一步,也就是寻路算法,以及沿着路径行走的方法。
此外,在创建预览棋子之前,需要先清空一下旧的预览棋子。
运行之后的游戏是这样的:
在控制台中会出现如下图的提示信息,在极少数情况下会出现“重复1”这样的提示信息,证明我们的逻辑没有错;
对了,这个黑白棋盘网格是我自己画得,棋子是随便找的,这么看起来还挺好看的。不过下一步还是会进行优化的。
游戏中的Prefab如下图,实际上我后来发现Chessman也可以用一个游戏对象,通过改变sprite来改变显示。我会在下一篇文章中做优化。
当然,还有游戏中的Hierarchy,我是这样设置的,将GameManager脚本直接放在空物体GameManager上,将GridController放在GridParent上。而GridController脚本中相关变量的赋值如下图所示:
本章总结。二维网格类三消游戏,逻辑层面实际上就是对于二维数组的操作。因此将游戏对象储存在相应的二维数组中就尤为重要,可以说是这个游戏的一个根基。只有这个根基没有问题,才能使之后的算法层面的东西实现起来没有偏差。明天我会继续进行寻路算法及消除检测方面的开发。
这篇文章写出来,希望可以和和我一样的小白交流,也更希望有热心大神批评指正(跪求人来损我,真的,太需要鞭策了!!!)
最后,祝大家中秋节快乐。
之前有朋友在我的文章下面评论我,质疑我写的文章并非原创,诚然,之前关于cocos2d等其他游戏引擎的“研究”都是皮毛,很少静下心来尝试制作一款完整的项目。因此这次对于我来说,也是一个很好的机会吧。写这个系列文章,一来可以帮助自己梳理知识。二来可以帮助后来者,大家一起学习交流,三来可以让有经验的前辈帮助我指正。
我会将代码提交到我的GitHub,并将网址贴在此处。
前辈的建议有以下步骤:
开发一款五子棋游戏,AI可以在网上查找;
开发五子棋的局域网对战功能,并且分数可以保存在数据库MySQL等;
开发一款ARP游戏,实现单机部分;
ARPG游戏网络化,开发服务器端;
山寨市面上比较热门的一款卡牌游戏;
而我在第一步上就有点走偏了,实际上我有自己的考虑,我想开发一款“五子连珠”的游戏,同样是二维网格类消除游戏,我认为其更能考验算法设计能力和编程基本功,因为五子棋的最大难点在于AI。而AI在这个游戏开发中又不是我们重点关注的目标。因此我做了这个决定。
五子连珠游戏,顾名思义就是当五个相同颜色的棋子在八个方向上数量大于等于5时即可消除得分。棋盘是一个9×9的网格。每回合玩家可以移动一个棋子,玩家移动之后会在棋盘上随机产生三个新的棋子(消除则不产生)。当所有网格中均有棋子时,游戏结束。棋子的种类共有五种颜色。
整个游戏在算法上有以下需要考虑的地方
随机位置产生棋子的算法;
棋子能否移动的检测;
棋子能否消除的检测;
棋子的寻路算法;
在实际的游戏开发中,实际上在进行寻路算法过程中也需要进行能否移动的检测,因此移动检测是多余的。随机位置产生棋子的算法相对简单,只需要用到随机数;棋子能否消除的算法与一般的三消游戏类似,也可以参考五子棋胜利检测的算法,我这里使用的是辐射法。寻路算法最为复杂,采用的是四方向的A星算法。
游戏的流程图如下图所示:
//此处省略,之后补充
我的思路是首先创建三个脚本分别是:
GameManager.cs 游戏管理器,用于管理游戏开始、储存、读取和游戏结束等;
GridController.cs 网格控制器,用于管理网格,游戏中大部分逻辑运算在这个脚本中;
GridScr.cs 网格脚本,挂在网格游戏对象身上,用来记录网格的信息;
首先看GridScr.cs这个脚本的代码;
using UnityEngine; using System.Collections; /// <summary> /// 挂在Grid游戏对象上,用来记录信息 /// </summary> public class GridScr : MonoBehaviour { //网格信息 public int Gridx; public int Gridy; public int BianHao; //用来寻路 public int gCost; public int hCost; public int fCost { get { return gCost + hCost; } } public GridScr parent; //记录颜色 public int ChessmanColor; //0代表空 //记录坐标 public Vector3 pos; /// <summary> /// 设置颜色 /// </summary> /// <param name="type"></param> public void SetChessmanColor(int type) { ChessmanColor = type; } /// <summary> /// 获取颜色 /// </summary> /// <returns></returns> public int GetChessmanColor() { return ChessmanColor; } /// <summary> /// 设置Grid对象,伪构造函数 /// </summary> /// <param name="x"></param> /// <param name="y"></param> /// <param name="type"></param> public void SetGridObj(int x,int y,int type) { Gridx = x; Gridy = y; pos.x = Gridx; pos.y = Gridy; ChessmanColor = type; BianHao = Gridx + Gridy * GridController._gridcontroller.MaxColNum; this.gameObject.name = "Gird" + BianHao; } //处理鼠标点击时间 void OnMouseDown() { if (this.ChessmanColor > 0) { GridController._gridcontroller.Selected01 = this; } if (this.ChessmanColor == 0 && GridController._gridcontroller.Selected01 != null) { GridController._gridcontroller.Selected02 = this; GridController._gridcontroller.ChessmanMoveTo(); } } //设置图片对象 public void SetGridSprite(int spr) { Sprite sprite = GridController._gridcontroller.GridSpr[spr]; this.GetComponent<SpriteRenderer> ().sprite = sprite; } }
在这个方法中首先是记录了网格的横纵坐标以及编号和棋子颜色(这个很重要)等,之后是G、H和F这三个值是用来寻路的,之后定义了一些方法,包括设置网格的、设置和获取网格的颜色的等等。最后是一个设置网格精灵(sprite)的方法,这个方法主要是为了实现游戏中“棋盘是国际象棋效果”这一需求(只是我自己给自己设置的一个需求罢了)。
之后是GameManager.cs脚本,这个脚本我觉得现在还没有贴出来的必要,因为这个脚本现在可以说是一个空壳。
using System.Collections; using System.Collections.Generic; public class GameManager : MonoBehaviour { public static GameManager _gameManager; void Awake(){ _gameManager = this; } // Use this for initialization void Start () { NewGame(); } // Update is called once per frame void Update () { } //开始新游戏 public void NewGame() { //新建棋盘 GridController._gridcontroller.NewGridCreate(); } //储存游戏 public void SaveGame() { } //载入游戏 public void LoadGame() { } //结束游戏 public void GameOver() { Debug.Log("游戏结束"); } }
这个脚本目前的功能是实现游戏的新建、储存载入和结束的,而具体的方法操作还没有实现,因此目前没什么好说的。
最后是这篇文章的重头戏,GridController.cs网格控制器脚本。先看代码:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class GridController : MonoBehaviour { public static GridController _gridcontroller; //预览 public GameObject[] NextGridTransform; public GameObject[] NextChessmanTransform; public GameObject NextGridObj; public GameObject NextParent; //最大行列数 public int MaxRowNum = 9; public int MaxColNum = 9; //网格相关的 public GameObject GridObj; public Sprite[] GridSpr; [HideInInspector] public GridScr GridScr; public GameObject[,] GridTransform; public GridScr[,] GridScrTransform; //public List<GameObject> ChessmanTransform; public GameObject GridParent; public int NewChessmanNum = 3; //棋子相关的 //public GameObject ChessmanObj; public GameObject[] ChessmanObjGrop; private int GridTotal; //总的棋子数(网格数) private int NowChessmanNum = 0; //目前棋子数 private List<GameObject> pathObj = new List<GameObject>(); [HideInInspector] public GridScr Selected01; [HideInInspector] public GridScr Selected02; [HideInInspector] public AStarFindPath asr; Route_pt[] result = null; private int[] ramNum; void Awake() { _gridcontroller = this; } /// <summary> /// 新建棋盘 /// </summary> public void NewGridCreate() { GridTransform = new GameObject[MaxRowNum , MaxColNum]; GridScrTransform = new GridScr[MaxRowNum, MaxColNum]; //ChessmanTransform = new List<GameObject>(); GridTotal = MaxRowNum * MaxColNum; for (int i = 0; i < MaxRowNum; i++) { for (int j = 0; j < MaxColNum; j++) { NewCellCreate (i, j); } } //预览网格创建 NextGridGreate(); //预览棋子创建 NextChessmanCreate(); //随机位置创建棋子 DropChessman(); } //新建棋盘网格 void NewCellCreate(int row, int col) { //实例化网格 GameObject obj = Instantiate(GridObj); obj.transform.parent = GridParent.transform; obj.transform.localPosition = new Vector2(row, col); //设置脚本 GridScr Scr = obj.GetComponent<GridScr>(); Scr.SetGridObj(row, col, 0); //添加到二维网格 GridTransform[row, col] = obj; GridScrTransform[row, col] = Scr; //设置国际象棋棋盘效果 Scr.SetGridSprite((row + col) % 2); } //预览网格创建 void NextGridGreate() { NextGridTransform = new GameObject[NewChessmanNum]; NextChessmanTransform = new GameObject[NewChessmanNum]; for (int i = 0; i < NewChessmanNum; i++) { //实例化预览网格 GameObject obj = Instantiate(NextGridObj); obj.transform.parent = NextParent.transform; obj.transform.localPosition = new Vector2(i, 0); NextGridTransform[i] = obj; //GridScr Scr = obj.GetComponent<GridScr>(); //Scr.SetGridSprite(i % 2); //Scr.SetGridObj(i, 0, 0); } } //预览棋子的创建 void NextChessmanCreate() { ramNum = new int[NewChessmanNum]; for (int i = 0; i < NewChessmanNum; i++) { //删除当前已有的 if (NextChessmanTransform[i] != null) { Destroy(NextChessmanTransform[i]); } //随机对象 int ram = Random.Range(0, ChessmanObjGrop.Length - 1); ramNum[i] = ram; //ChessmanObj = ChessmanObjGrop[ram]; //实例化棋子 GameObject obj = Instantiate(ChessmanObjGrop[ram]); obj.transform.parent = NextGridTransform[i].transform; obj.transform.localPosition = new Vector2(0, 0); GridScr Scr = NextGridTransform[i].GetComponent<GridScr>(); Scr.SetChessmanColor(ram + 1); //ChessmanScr ChessmanScr = obj.GetComponent<ChessmanScr>(); NextChessmanTransform[i] = obj; } } //随机位置放置棋子 public void DropChessman() { //有足够位置 if (NowChessmanNum < GridTotal - NewChessmanNum) { for (int i = 0; i < NewChessmanNum; i++) //取决于游戏难度(每次产生数) { int weizhi = Random.Range(0, GridTotal); if (GridScrTransform[ weizhi % MaxRowNum , weizhi / MaxColNum ].ChessmanColor == 0) //网格中没有棋子 { //创建新棋子 NewChessmanCreate(i, weizhi); //创建新预览棋子 NextChessmanCreate(); Debug.Log("不重复" + i); } else { i -= 1; //不清楚这里会不会出错 Debug.Log("重复的" + i); } } NowChessmanNum += NewChessmanNum; } else { //无法创建时,游戏结束 GameManager._gameManager.GameOver(); } } //创建新棋子 void NewChessmanCreate(int i, int weizhi) { //创建新棋子根据预览棋子,因此需要获取预览棋子的对象 //实例化网格,网格位置 GameObject obj = Instantiate(ChessmanObjGrop[ramNum[i]]); obj.transform.parent = GridTransform[ weizhi % MaxRowNum , weizhi / MaxColNum ].transform; obj.transform.localPosition = new Vector2(0, 0); //脚本设置 GridScr scr = GridScrTransform[weizhi % MaxRowNum, weizhi / MaxColNum]; //ChessmanScr Che = chessman.GetComponent<ChessmanScr>(); //获取棋子颜色 scr.SetChessmanColor(ramNum[i] + 1); //将网格设置为非空 //ChessmanTransform[weizhi] = obj; } //移动棋子 public void ChessmanMoveTo() { Debug.Log(Selected01.Gridx + "," + Selected01.Gridy); Debug.Log(Selected02.Gridx + "," + Selected02.Gridy); } }
在这个脚本文件中,
首先是脚本的实例化,没什么特别的。
之后便是新建网格,在此之前我将两个二维数组(分别用来记录网格游戏对象和网格身上挂着的脚本,将脚本拿出来存入二维数组可以提高运行效率)之后便是循环创建9×9个网格对象;在这个脚本中,我直接将创建预览网格、创建预览棋子和随机位置创建棋子写在此处,目的是不讲其他方法设置成公有。这里我再之后会进一步完善。
棋盘网格对象的实例化包括几部分,首先是实例化对象、设置父级对象和坐标等;之后是设置脚本,将信息记录在脚本中;然后将其记录在二维网格中;最后是实现国际象棋棋盘效果;
所谓预览棋子,是指预告下回合将出现的三个棋子的颜色。这里分两个方法操作,首先是创建网格,与创建棋盘网格类似,要注意这里创建的网格是没有Box Collider 2D的,目的是为了不能相应鼠标点击事件。之后就是创建三个棋子。分别在三个网格中。
随机位置创建棋子这个方法,首先应该进行一个判断,就是网格中还有足够的位置放置棋子。然后随机一个位置放置预览网格中的棋子,如果随机的位置有棋子了,那么就重新进行随机位置;最后是要将新创建的棋子个数加到当前总个数上。
移动棋子需要先进行寻路,不在这片文章内容中。但是我这里写的是输出两个位置点的坐标。方便测试我当前的逻辑是否正确。正确的话才能进行下一步,也就是寻路算法,以及沿着路径行走的方法。
此外,在创建预览棋子之前,需要先清空一下旧的预览棋子。
运行之后的游戏是这样的:
在控制台中会出现如下图的提示信息,在极少数情况下会出现“重复1”这样的提示信息,证明我们的逻辑没有错;
对了,这个黑白棋盘网格是我自己画得,棋子是随便找的,这么看起来还挺好看的。不过下一步还是会进行优化的。
游戏中的Prefab如下图,实际上我后来发现Chessman也可以用一个游戏对象,通过改变sprite来改变显示。我会在下一篇文章中做优化。
当然,还有游戏中的Hierarchy,我是这样设置的,将GameManager脚本直接放在空物体GameManager上,将GridController放在GridParent上。而GridController脚本中相关变量的赋值如下图所示:
本章总结。二维网格类三消游戏,逻辑层面实际上就是对于二维数组的操作。因此将游戏对象储存在相应的二维数组中就尤为重要,可以说是这个游戏的一个根基。只有这个根基没有问题,才能使之后的算法层面的东西实现起来没有偏差。明天我会继续进行寻路算法及消除检测方面的开发。
这篇文章写出来,希望可以和和我一样的小白交流,也更希望有热心大神批评指正(跪求人来损我,真的,太需要鞭策了!!!)
最后,祝大家中秋节快乐。
相关文章推荐
- [Unity3D]手机3D游戏开发:如何实现最高分的存储与显示(三)----GUI Style与数组的使用
- 实现AMF3与Java之间数组的传递(动态创建数组)...
- 在Unity3D的网络游戏中实现资源动态加载
- Unity3D 游戏引擎之实现平面多点触摸
- [Unity3D]手机3D游戏开发:如何实现最高分的存储与显示(四)----使用PlayerPrefs存储数据到本地
- 用Delphi简单实现创建控件数组
- [Unity3D]手机3D游戏开发:如何实现最高分的存储与显示(八)----用TouchScreenKeyboard弹出键盘
- Unity3D+免费工具开发2D游戏之四:创建计分界面
- JS 实现 创建类、继承、方法添加、对象克隆、数组封装操作
- [Unity3D]手机3D游戏开发:如何实现最高分的存储与显示(六)----实现文字闪烁效果
- Unity3D 游戏引擎之实现平面多点触摸(二)
- 在Unity3D的网络游戏中实现资源动态加载
- Android 游戏编程之从零开始-----2.1创建第一个Andoroid项目+2.2剖析Android Project 结构
- 游戏公司面试题:Java如何实现二维逆时针旋转数组
- Unity3D实现动态加载游戏资源(转)
- asp.net通过js实现Cookie创建以及清除Cookie数组的代码
- Unity3D 游戏引擎之脚本实现模型的平移与旋转(六)
- 在Unity3D的网络游戏中实现资源动态加载
- [Unity3D]手机3D游戏开发:如何实现最高分的存储与显示(一)----初始化显示的高分榜
- [Unity3D]手机3D游戏开发:如何实现最高分的存储与显示(七)----使用Game ID避免数据重复输入