使用Unity 3D制作一款黑白棋游戏
项目github地址:https://github.com/lababababidu/A-BoardGame-In-Unity-3D
github中只包含Assets文件,包含脚本,预制件模型。
游戏画面
下棋:
游戏结束:
MVC模式:
将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller),以实现关注点分离。
·**Model(模型) **- 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
·View(视图) - 视图代表模型包含的数据的可视化。
·Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
本黑白棋游戏采用MVC设计模式设计,以Unity 3D进行实现。
模型:
棋盘:
对于棋类游戏,一个通常的做法是将棋盘抽象成二维数组,以用于记录棋盘上的信息,由于黑白棋棋子种类简单,棋盘可被抽象成int类型的二维数组,数组中每个元素的值对应棋盘每个格子上的落子情况。
棋子:
黑白棋游戏中,棋子只分黑棋与白棋两种,故将棋子种类信息可用一个int类型变量表示,已落下棋子的信息存储在棋盘二维数组内即可。同时,黑白棋游戏需要在每次落下棋子时做一次判定,判断哪些棋子会被翻转,这个判断需要用到棋盘信息,棋子种类信息,以及落下棋子的位置,故我们还需两个int变量用于表示落下棋子的行列位置x和y。
玩家:
一局黑白棋的游戏只有两人参与,当前落子玩家可用一个int类型变量表示。
玩家操作:玩家所要进行的操作只有:在x行y列落下自己的棋子。
游戏逻辑:
- 游戏初始化
- 循环:
- 玩家落子
- 判断有哪些棋子翻转
- 执行翻转
- 若棋盘已满:
- 游戏结束,退出循环
- 循环结束
- 清理释放游戏资源
棋子翻转逻辑:
当一颗棋子落下时,在这个落下的棋子的八个方向上,如果在某个方向上有一个跟它相同颜色的棋子,且这个方向上直到那个跟它同颜色的棋子之外的其他格子都是其他颜色的棋子(不能有空格子),则这些被同颜色包夹的其他颜色棋子反面,变成与落下的棋子相同颜色。
综上,一共会用到:
- 一个int类型用来同时表示当前落子的玩家的落下棋子的种类
- 一个int类型用来表示落子的行数
- 一个int类型用来表示落子的列数
- 一个int类型二维数组记录棋盘上当前状态
将这些数据全部集成在一个GameLogic类中。
视图:
对于所制作的黑白棋游戏,我们希望显示出以下的内容:
- 棋盘
- 棋盘上的棋子
对此,我们需要在Unity 3D中创建出对应对象的模型。
棋盘样式如下:
黑白棋的所有棋子实际上都是长一样的,一面黑一面白,通过朝上的面来区分棋子的类型,棋子样式如下:
除此之外,还有一些可选内容可以显示,比如落子的动画,棋子翻转的动画等,我在这里实现了棋子反转的动画(项目中script中的Flip.cs)。
控制:
在黑白棋游戏中,玩家所能做的只有将自己的棋子下至棋盘某处。玩家使用鼠标,在鼠标选定棋盘的某个方格,单机左键,即可落下玩家相应的棋子。
各部分实现:
public int CurrentPlayer;
public int lastdropx=0,lastdropy=0;
public int[,] board = new int[10,10]
public bool isdrop = false;
private int winner = 0;
public List<(int, int)> toflip;
除开上文模型中所述的四个int类型变量外,为方便逻辑处理,还加入了几个其他的变量:
- 一个公共的bool类型值isdrop判断有无落子,有落子则进入翻转检测,初始化为0。
- 一个私有的int类型变量winner,在游戏最后结束时存放谁是赢家,初始化为0。
- 一个公共的(int, int)类型列表List<(int, int)> toflip,用于存储哪些棋子需要翻转。
初始化函数如下:
将棋盘变量board全部清零,然后清除场上所有的棋子复制体。
void Init() {
CurrentPlayer = 1;
winner = 0;
for(int i = 0; i < 10; i++)
for(int j = 0; j < 10; j++)
board[i, j] = 0;
GameObject[] objectsToDelete = GameObject.FindGameObjectsWithTag("clone");
foreach (GameObject obj in objectsToDelete)
{
Destroy(obj);
}
}
游戏结束判断逻辑如下:
遍历棋盘,看那边棋子数量多,多的是赢家,修改winner的值,如果一样,则平局winner的值为0。
bool isover(){
int P1_Score = 0;
for (int i = 0; i < board.GetLength(0); i++)
{
for (int j = 0; j < board.GetLength(1); j++)
{
if(board[i,j] == 0){
return false;
}
if(board[i,j] == 1){
P1_Score += 1;
}
}
}
if(P1_Score>50){
winner = 1;
}
else if(P1_Score<50){
winner = 2;
}
return true;
}
游戏循环主体:
当这个GameLogic类脚本被挂载在一个游戏对象后,Unity会不断地调用这个update函数,当有落子后,获取要翻转的棋子的列表,将其存入toflip变量中,便于其他脚本获取。然后修改当前玩家CurrentPlayer的值,将isdrop变量归零。
void Update()
{
if(isdrop == true){
List<(int, int)> t = GetFlippedDiscs(board,lastdropx,lastdropy,CurrentPlayer);
toflip = t;
CurrentPlayer = 3 - CurrentPlayer;
isdrop = false;
}
}
获取要翻转棋子列表函数:
public static List<(int, int)> GetFlippedDiscs(int[,] board,int x, int y, int player)
{
int opponent = (player == 1) ? 2 : 1;
List<(int, int)> flippedDiscs = new List<(int, int)>();
// 8 directions: {dx, dy}
int[][] directions = new int[][]
{
new int[] {0, 1}, // right
new int[] {1, 1}, // down-right
new int[] {1, 0}, // down
new int[] {1, -1}, // down-left
new int[] {0, -1}, // left
new int[] {-1, -1},// up-left
new int[] {-1, 0}, // up
new int[] {-1, 1} // up-right
};
// Check each direction
foreach (var direction in directions)
{
List<(int, int)> tempFlipped = new List<(int, int)>();
int dx = direction[0];
int dy = direction[1];
int nx = x + dx;
int ny = y + dy;
while (nx >= 0 && nx < board.GetLength(0) && ny >= 0 && ny < board.GetLength(1) && board[nx, ny] == opponent)
{
tempFlipped.Add((nx, ny));
nx += dx;
ny += dy;
}
if (nx >= 0 && nx < board.GetLength(0) && ny >= 0 && ny < board.GetLength(1) && board[nx, ny] == player)
{
flippedDiscs.AddRange(tempFlipped);
}
}
return flippedDiscs;
}
-
落子功能实现
玩家通过鼠标选择棋盘上的一个网格,之后点击左键,即可在网格上生成对应玩家的棋子。
获取鼠标选择的网格部分,通过摄像头发出的照相鼠标的一条射线,求出与棋盘的交点,判断该交点处于哪个网格。
生成棋子部分使用Instantiate函数,对一个样板棋子生成一个复制体。同时修改GameLogic类中board对应位置的值。复制体和board的值修改完后,将GameLogic类中的isdrop设置为true。
这部分功能实现较繁琐复杂,详情请参考github上NewBehaviourScript.cs脚本。 -
翻转功能实现
创建一个Flip类,其实现了翻转动画,以及棋子在克隆出来后的自动设置哪一面朝上,所有被克隆的棋子都会挂载这个类脚本。
这个类的成员变量如下,col和row表示了挂载这个脚本的棋子的横纵坐标,player表示这个棋子哪面朝上:
public int col ;
public int row ;
public int player;
private float duration = 0.15f; // 旋转的持续时间
private bool isRotating = false; // 标记是否正在旋转
这个类有一个Start函数,每当有棋子被克隆出来时,都会执行这个函数,即当前落子为玩家2时,将克隆体立即翻面:
void Start()
{
if(player == 2){
immediate_flip();
}
}
这个类的update函数逻辑如下:
当查询到GameLogic中的toflip List中有自己的col,row坐标,自己执行翻面动画,将GameLogic的board中的对应项调整,翻转完毕后删除toflip List中的对应项。
void Update()
{
GameObject boardobj = GameObject.Find("Board");
if (boardobj != null)
{
GameLogic logic = boardobj.GetComponent<GameLogic>();
for (int i = 0; i < logic.toflip.Count; i++)
{
if(logic.toflip[i].Item1+1==col&&logic.toflip[i].Item2+1==row){
logic.toflip.Remove(logic.toflip[i]);
player = 3 - player;
logic.board[col-1,row-1] = player;
if(!isRotating){
StartCoroutine(RotateOverTime(180f, duration));
}
break;
}
}
}
}
void OnGUI() {
float screenWidth = Screen.width;
float screenHeight = Screen.height;
float windowWidth = 200;
float windowHeight = 200;
if(!isover()){
Debug.Log("not over");
GUI.Box(new Rect((screenWidth - windowWidth) / 2, 25, 200, 25), "now player: "+CurrentPlayer.ToString());
}
else{
Debug.Log("over");
if (winner != 0)
GUI.Box(new Rect(
(screenWidth - windowWidth) / 2,
(screenHeight - windowHeight) / 2,
windowWidth,
windowHeight
), "\n\n\n\n\nCongratulations!\n Player "+winner+" has won.");
else
GUI.Box(new Rect(
(screenWidth - windowWidth) / 2,
(screenHeight - windowHeight) / 2,
windowWidth,
windowHeight
), "\n\n\n\n\n\nThis is a draw!");
if (GUI.Button(new Rect((screenWidth - 100) / 2, 270, 100, 30), "Restart")) Init();
}
}
总结
得益于数据和游戏逻辑分离,在编写游戏主逻辑时思路会更清晰。分离关注点也让开发者在开发的过程中更清楚自己要干什么,在设计逻辑时专心设计逻辑,在搭建场景物体模型时转型负责模型。
同时也加强了游戏的可测试性,由于各个组件独立,测试和调试变得更加容易。当我想单独测试落下棋子功能是否正常时,我只需要测试对应的函数。
总的来说,MVC模式提供了一种清晰的架构,帮助开发团队更好地组织代码,提高代码的可读性和可理解性。