unity实现井字棋
一、简介
井字棋是一个很古老很简单的游戏,两名玩家在一个3X3的网格上画上自己的图标,每回合玩家只能选择一个格子,率先将三个自己图标连成一条直线的玩家获胜(如果在九个格子都被填充后仍没有获胜者,则判为平局。
本游戏用unity的IMGUI实现。
二、实现效果
三、具体实现
1. 基本数据
private int [,] board = new int [3,3];//棋盘,0、1、2分别代表空、玩家1占据、玩家2占据
private int turn = 0;//0表示当前为玩家回合,1表示人机回合(用于人机模式)
private int square_size = Screen.width / 10;//一个格子的尺寸
private int menu_width = Screen.width / 5, menu_height = Screen.width / 10;//主菜单每一个按键的宽度和高度
private int mode = 0;//根据不同mode显示不同场景
/*
0 主菜单
1 玩家VS玩家
2 玩家VS人机
*/
private GUIStyle bigStyle, blackStyle;//自定义字体格式
public Texture2D empty, icon1, icon2;//不同玩家的图标(圈圈和勾勾)
public float timer;//定时器,用于模拟人机的延迟
public float default_timer = 0.5f;//默认定时器,用于给timer赋值
2. 初始化
初始化的内容主要放在 Start 函数中,这个函数在开始运行时执行。
- 初始化定时器和自定义字体
timer = default_timer;
//大字体初始化
bigStyle = new GUIStyle();
bigStyle.normal.textColor = Color.white;
bigStyle.normal.background = null;
bigStyle.fontSize = 50;
//black
blackStyle = new GUIStyle();
blackStyle.normal.textColor = Color.black;
blackStyle.normal.background = null;
blackStyle.fontSize = 50;
3. 实现OnGUI函数
OnGUI函数是使用IMGUI编程的关键函数,它在运行时的每帧执行一次,所以我们要让这个函数帮我们画出每一帧的内容。
void OnGUI() {
switch(mode) {
case 0:
mainMenu();
break;
case 1:
playerVsPlayer();
break;
case 2:
playerVSComputer();
break;
}
}
根据我们在基本数据中对mode(模式)的定义,OnGUI可以在不同mode下画出相应的界面,分别是主菜单、玩家VS玩家、玩家VS人机。
主菜单的组成比较简单,接下来主要介绍后两种模式的实现。
4. 玩家VS玩家
在这个游戏中有两种move,一种是玩家move,另一种是人机move(用于人机模式),它们分别定义了玩家和人机的行为(即选择哪一个格子进行填充)。在玩家VS玩家模式中显然只有玩家move,所以playerVsPlayer()仅仅做了一件事情——调用playerMove()。
前面我们介绍过turn这个变量,它表示“轮到谁落子了”,比如0代表等待玩家1落子,1代表等待玩家2落子,通过这个变量的值的切换,可以只用一个函数playerMove()同时模拟两个玩家的move。
- playerMove的主要逻辑
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
switch(board[i,j]) {//board[i,j]有三种状态,空、被玩家1占据、被玩家2占据
case 0://空格子
if (GUI.Button(new Rect(Screen.width / 2 + (i - 1.5f) * square_size, Screen.height / 2 + (j - 1.5f)* square_size, square_size, square_size), empty)) {
//如果当前玩家选择了这个格子,则为此格子赋上代表玩家的值,在下一帧时这个格子上会显示相应的图标
board[i,j] = turn + 1;
turn = 1 - turn;//turn的切换代表着从下一帧开始轮到另一个玩家落子了
}
break;
case 1://画上玩家1的图标
GUI.Button(new Rect(Screen.width / 2 + (i - 1.5f) * square_size, Screen.height / 2 + (j - 1.5f) * square_size, square_size, square_size), icon1);
break;
case 2://画上玩家2的图标
GUI.Button(new Rect(Screen.width / 2 + (i - 1.5f) * square_size, Screen.height / 2 + (j - 1.5f) * square_size, square_size, square_size), icon2);
break;
}
}
}
checkState();
这段代码很好懂,除了最后调用的checkState函数,这是判定游戏状态的关键函数。
在游戏进行过程中,程序需要判定什么时候哪个玩家赢了,什么时候两个玩家都无法再落子了(即平局),甚至人机的走法也要根据下一步可能的状态来决定(这部分后边讲)。这些逻辑都通过checkState来实现。
所以,checkState主要做的事情是
- 判断当前玩家是否胜利
- 判断是否为平局
- 得出结果时(一方玩家胜利或平局)在界面上显示
判断当前玩家是否胜利可以通过检测每一行、每一列,以及对角线是否有三个相同图标连在一起,如果存在,则判定该图标对应的玩家胜利。
判断是否平局更简单了,如果当前所有格子都被填充了,且没有玩家胜利,则为平局。
5. 玩家VS人机
这个部分与上一个部分实现大同小异,主要的修改有两部分:
- turn不同时调用不同函数
turn为0调用playerMove,turn为1时调用machineMove - machineMove的实现
人机操作与玩家操作的不同在于人机需要通过算法来选择一个合适的格子进行填充。
这里运用了一个简单的算法,可以分为三轮:
- 第一轮,遍历每一个空格子,尝试填充该格并调用checkStateWithoutOutput(与checkState的判断逻辑相同,去除了在屏幕上显示结果的部分)来判断是否能赢,如果能赢,选择该格子。第一轮没有选到格子则进入第二轮。
- 第二轮,遍历每一个空格子,尝试将该格子填充为代表玩家的值,然后调用checkStateWithoutOutput来判断玩家是否能赢,如果玩家能赢,那么为了不让玩家赢,人机选择该格子进行填充。第二轮没有选到格子则进入第三轮。
- 第三轮,人机从所有空格子里随机选择一个格子进行填充。选择这样的算法是有考虑的。其一,使用深度优先搜索找到最优解比较麻烦且耗时;其二,如果总是下最优解,则变化数会比较少,随机的下法更具趣味性。