大概是一周前,在知乎上偶然看见一位前辈提供的意见,他认为在一个立志于进入游戏行业发展的程序员在大学期间应该多动手,亲自去完成一些项目,在遇到坑,填补坑的过程中快速的提升技术水平。想来我虽然已经入职游戏策划职业将近一年了。但是内心还是无时无刻不想自己成为一个“有技术的游戏策划”,因此将自己定位成一个萌萌的新人。开始一步一个脚印的学习之路。
之前有朋友在我的文章下面评论我,质疑我写的文章并非原创,诚然,之前关于cocos2d等其他游戏引擎的“研究”都是皮毛,很少静下心来尝试制作一款完整的项目。因此这次对于我来说,也是一个很好的机会吧。写这个系列文章,一来可以帮助自己梳理知识。二来可以帮助后来者,大家一起学习交流,三来可以让有经验的前辈帮助我指正。
我会将代码提交到我的GitHub,并将网址贴在此处。
前辈的建议有以下步骤:
而我在第一步上就有点走偏了,实际上我有自己的考虑,我想开发一款“五子连珠”的游戏,同样是二维网格类消除游戏,我认为其更能考验算法设计能力和编程基本功,因为五子棋的最大难点在于AI。而AI在这个游戏开发中又不是我们重点关注的目标。因此我做了这个决定。
五子连珠游戏,顾名思义就是当五个相同颜色的棋子在八个方向上数量大于等于5时即可消除得分。棋盘是一个9×9的网格。每回合玩家可以移动一个棋子,玩家移动之后会在棋盘上随机产生三个新的棋子(消除则不产生)。当所有网格中均有棋子时,游戏结束。棋子的种类共有五种颜色。
整个游戏在算法上有以下需要考虑的地方
在实际的游戏开发中,实际上在进行寻路算法过程中也需要进行能否移动的检测,因此移动检测是多余的。随机位置产生棋子的算法相对简单,只需要用到随机数;棋子能否消除的算法与一般的三消游戏类似,也可以参考五子棋胜利检测的算法,我这里使用的是辐射法。寻路算法最为复杂,采用的是四方向的A星算法。
游戏的流程图如下图所示:
//此处省略,之后补充
我的思路是首先创建三个脚本分别是:
首先看GridScr.cs这个脚本的代码;
using UnityEngine;
using System.Collections;
///
/// 挂在Grid游戏对象上,用来记录信息
///
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;
///
/// 设置颜色
///
///
public void SetChessmanColor(int type)
{
ChessmanColor = type;
}
///
/// 获取颜色
///
///
public int GetChessmanColor()
{
return ChessmanColor;
}
///
/// 设置Grid对象,伪构造函数
///
///
///
///
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 ().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 ChessmanTransform;
public GameObject GridParent;
public int NewChessmanNum = 3;
//棋子相关的
//public GameObject ChessmanObj;
public GameObject[] ChessmanObjGrop;
private int GridTotal; //总的棋子数(网格数)
private int NowChessmanNum = 0; //目前棋子数
private List pathObj = new List();
[HideInInspector]
public GridScr Selected01;
[HideInInspector]
public GridScr Selected02;
[HideInInspector]
public AStarFindPath asr;
Route_pt[] result = null;
private int[] ramNum;
void Awake()
{
_gridcontroller = this;
}
///
/// 新建棋盘
///
public void NewGridCreate()
{
GridTransform = new GameObject[MaxRowNum , MaxColNum];
GridScrTransform = new GridScr[MaxRowNum, MaxColNum];
//ChessmanTransform = new List();
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();
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();
//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();
Scr.SetChessmanColor(ram + 1);
//ChessmanScr ChessmanScr = obj.GetComponent();
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();
//获取棋子颜色
scr.SetChessmanColor(ramNum[i] + 1);
//将网格设置为非空
//ChessmanTransform[weizhi] = obj;
}
//移动棋子
public void ChessmanMoveTo()
{
Debug.Log(Selected01.Gridx + "," + Selected01.Gridy);
Debug.Log(Selected02.Gridx + "," + Selected02.Gridy);
}
}
在这个脚本文件中,
运行之后的游戏是这样的:
在控制台中会出现如下图的提示信息,在极少数情况下会出现“重复1”这样的提示信息,证明我们的逻辑没有错;
对了,这个黑白棋盘网格是我自己画得,棋子是随便找的,这么看起来还挺好看的。不过下一步还是会进行优化的。
游戏中的Prefab如下图,实际上我后来发现Chessman也可以用一个游戏对象,通过改变sprite来改变显示。我会在下一篇文章中做优化。
当然,还有游戏中的Hierarchy,我是这样设置的,将GameManager脚本直接放在空物体GameManager上,将GridController放在GridParent上。而GridController脚本中相关变量的赋值如下图所示:
本章总结。二维网格类三消游戏,逻辑层面实际上就是对于二维数组的操作。因此将游戏对象储存在相应的二维数组中就尤为重要,可以说是这个游戏的一个根基。只有这个根基没有问题,才能使之后的算法层面的东西实现起来没有偏差。明天我会继续进行寻路算法及消除检测方面的开发。
这篇文章写出来,希望可以和和我一样的小白交流,也更希望有热心大神批评指正(跪求人来损我,真的,太需要鞭策了!!!)
最后,祝大家中秋节快乐。