Homework_1
Tic Tac Toe
游戏内容: 井字棋
技术限制: 仅允许使用 IMGUI 构建 UI
本次作业是利用 Unity 制作 Tic Tac Toe 游戏。这个技术限制是不让我们使用 Unity 自带的视图操作工具进行制作,而是利用 code 来达到目的。将本次作业分为两个模块:
- 界面模块
- 逻辑模块
我们先将基础界面创建好,再进行逻辑代码的实现。
实现过程
基础界面
井字棋游戏的主要界面是它的井字区域。我们利用九个按钮来实现这个井字区域。添加按钮并居中
我们使用OnGUI()来实现控件的添加。添加按钮控件使用如下函数:1
2
3
4
5
6
7
8
9/*Prototype:
GUI.Button(Rect position, string text);
*/
int bHeight = 100;
int bWidth = 100;
float height = Screen.height * 0.5f - 150;
float width = Screen.width * 0.5f - 150;
GUI.Button(new Rect(width, height, bWidth, bHeight), "");其中,Screen.height 和 Screen.width 是当前窗口的高和宽,利用这两个参数,我们设置按钮的位置局中。要注意到的是,参数 position 给定的是按钮左上角的位置,进行简单的数学分析,就可以得到按钮中心点的位置设置方法。
基础页面
单个按钮添加完毕,接下来就完成井字区域和Reset按钮的添加,以及界面欢迎词和游戏结束提示词的添加。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//UI Style parameters
GUIStyle tStyle = new GUIStyle {
fontSize = 50,
fontStyle = FontStyle.Bold
};
GUIStyle mStyle = new GUIStyle {
fontSize = 25,
fontStyle = FontStyle.Bold
};
mStyle.normal.textColor = Color.red;
GUI.Button(new Rect(width + bWidth / 2, height + 3.5f * bHeight, bWidth * 2, bHeight / 2), "Reset");
GUI.Label(new Rect(width + 50, height - 75, 100, 100), msg, msgStyle);
GUI.Label(new Rect(width + 20, height - 150, 100, 100), "Tic Tac Toe", tStyle);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
GUI.Button(new Rect(width + i * bWidth, height + j * bHeight, bWidth, bHeight), "")
}
}GUI添加控件的构造函数的参数有一个 GUIStyle 类型的参数,可以用来设置控件的样式。推荐使用变量来进行样式设置(如上代码),这样可以使得代码维护更为方便。
逻辑实现
基础界面实现完毕,接下来我们来完成逻辑代码的实现。辅助参数设计
1
2
3
4public enum Player { player0, player1, player2};
private bool playing = true;
private bool turn = true;
private Player[,] symbol = new Player [3,3];枚举变量帮助判断无人获胜,先手获胜,后手获胜;playing判断游戏是否结束;turn判断轮到哪一位玩家;symbol用来判断哪位玩家占据井字区域的那一块。
symbol 填充
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
if (symbol[i, j] == Player.player1) {
GUI.Button(new Rect(width + i * bWidth, height + j * bHeight, bWidth, bHeight), "X");
} else if (symbol[i, j] == Player.player2) {
GUI.Button(new Rect(width + i * bWidth, height + j * bHeight, bWidth, bHeight), "O");
} else {
if (GUI.Button(new Rect(width + i * bWidth, height + j * bHeight, bWidth, bHeight), "")) {
if (playing) {
symbol[i, j] = turn ? Player.player1 : Player.player2;
turn = !turn;
}
}
}
}
}每次刷新页面,可以分出三种情况:
- 当前区域被一号玩家占据,更新为 X
- 当前区域被二号玩家占据,更新为 O
- 当前区域未被占据,且被点击,则更新符号,交换回合
获胜判断
采用枚举法进行获胜的判断,如果无人获胜,返回0号玩家。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26private Player Check() {
//Row check
for (int i = 0; i < 3; ++i) {
if (symbol[i, 0] != Player.player0 &&
symbol[i, 0] == symbol[i, 1] &&
symbol[i, 1] == symbol[i, 2]) {
return symbol[i, 0];
}
}
//Column check
for (int j = 0; j < 3; ++j) {
if (symbol[0, j] != Player.player0 &&
symbol[0, j] == symbol[1, j] &&
symbol[1, j] == symbol[2, j]) {
return symbol[0, j];
}
}
//Cross line check
if (symbol[1, 1] != Player.player0) {
if (symbol[1, 1] == symbol[0, 0] && symbol[1, 1] == symbol[2, 2] ||
symbol[1, 1] == symbol[0, 2] && symbol[1, 1] == symbol[2, 0]) {
return symbol[1, 1];
}
}
return Player.player0;
}有人获胜时,显示获胜信息(写在OnGUI()函数内)。
1
2
3
4
5
6
7//Check if someone wins
if (winner != Player.player0) {
msg = (winner == Player.player1 ? "Player1(X) Wins!" : "Player2(O) Wins!");
GUI.Label(new Rect(width + 50, height - 75, 100, 100), msg, mStyle);
playing = !playing;
GUI.enabled = false;
}界面重置
将所有辅助变量重置为初始值。1
2
3
4
5
6
7
8
9
10// Reset the screen
private void Reset() {
playing = true;
turn = true;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
symbol[i,j] = Player.player0;
}
}
}点击Reset按钮时,同样进行重置(写在OnGUI()函数内)。
1
2
3
4
5//Reset button
if (GUI.Button(new Rect(width + bWidth / 2, height + 3.5f * bHeight, bWidth * 2, bHeight / 2), "Reset")) {
Reset();
return;
}
进阶游戏
欢迎页面与页面跳转
在Unity项目中,新建一个场景,当作初始页面。然后为其添加一个 C# script 文件,为其创建跳转按钮。界面设计,在此略过。首先,在 unity 中,场景跳转依靠 Application.LoadLevel() 函数。
1
2
3/*有两个重载函数*/
Application.LoadLevel(int screenId);
Application.LoadLevel(string screenName);为了让这个函数能够找到我们写的场景,我们需要先做这样一件事:File->Build Setting->将所有 Screen 拖入其中。然后我们就可以完成起始页面的逻辑代码实现了,如下:
1
2
3
4
5
6
7
8
9
10
11
12float height = Screen.height * 0.5f;
float width = Screen.width * 0.5f;
int bHeight = 100;
int bWidth = 150;
if (GUI.Button(new Rect(width - bWidth / 2 - 100, height - bHeight / 2, bWidth, bHeight), "One Player Mode")) {
Application.LoadLevel("OnePlayerMode");
}
if (GUI.Button(new Rect(width - bWidth / 2 + 100, height - bHeight / 2, bWidth, bHeight), "Two Player Mode")) {
Application.LoadLevel("TwoPlayersMode");
}单人模式
这里,我们需要实现一个 “AI” 来和玩家进行游戏。看起来工作很复杂,但可以分解为两个部分,游戏回合的执行过程和 “AI” 的动作执行
游戏回合的执行过程
执行过程并不复杂,更改部分就是不同回合的执行动作过程。在 Two Players Mode 的基础代码下,对回合进行判断。在 “AI” turn 的情况下,分解动作执行过程:
1
2
3
4
5
6
7
8
9if (能获胜) {
获胜
} else {
if (对手下一步获胜) {
堵住对手
} else {
下一步棋子
}
}封装函数之后,大致回合代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
if (symbol[i, j] == Player.player1) {
//Change to X
} else if (symbol[i, j] == Player.player2) {
//Change to O
} else {
if (playing) {
if (turn) {
//Player turn
//Listening for click event
turn = false;
} else {
GUI.Button(new Rect(width + i * bWidth, height + j * bHeight, bWidth, bHeight), "");
a_x = a_y = -1;
CanWin();
if (a_x == -1 && a_y == -1) {
Block();
}
if (a_x == -1 && a_y == -1) {
RandomStep();
}
if (a_x != -1 && a_y != -1 && symbol[a_x, a_y] == Player.player0) {
symbol[a_x, a_y] = Player.player2;
}
turn = true;
}
}
}
}
}“AI” 动作执行
可以获胜,我们需要对三个情况进行遍历:对角线,行和列。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80private void CanWin() {
//cross line to win
if (symbol[1, 1] == Player.player2) {
if (symbol[0, 0] == Player.player2 &&
symbol[2, 2] == Player.player0) {
a_x = 2;
a_y = 2;
return;
}
if (symbol[2, 2] == Player.player2 &&
symbol[0, 0] == Player.player0) {
a_x = 0;
a_y = 0;
return;
}
if (symbol[2, 0] == Player.player2 &&
symbol[0, 2] == Player.player0) {
a_x = 0;
a_y = 2;
return;
}
if (symbol[0, 2] == Player.player2 &&
symbol[2, 0] == Player.player0) {
a_x = 2;
a_y = 0;
return;
}
}
for (int i = 0; i < 3; ++i) {
int row = i;
int col = i;
//row to win
if (symbol[row, 0] == Player.player2) {
if (symbol[row, 1] == Player.player2 &&
symbol[row, 2] == Player.player0) {
a_x = row;
a_y = 2;
return;
}
if (symbol[row, 2] == Player.player2 &&
symbol[row, 1] == Player.player0) {
a_x = row;
a_y = 1;
return;
}
}
if (symbol[row, 1] == Player.player2) {
if (symbol[row, 2] == Player.player2 &&
symbol[row, 0] == Player.player0) {
a_x = row;
a_y = 0;
return;
}
}
//column to win
if (symbol[0, col] == Player.player2) {
if (symbol[1, col] == Player.player2 &&
symbol[2, col] == Player.player0) {
a_x = 2;
a_y = col;
return;
}
if (symbol[2, col] == Player.player2 &&
symbol[1, col] == Player.player0) {
a_x = 1;
a_y = col;
return;
}
}
if (symbol[1, col] == Player.player2) {
if (symbol[2, col] == Player.player2 &&
symbol[0, col] == Player.player0) {
a_x = 0;
a_y = col;
return;
}
}
}
}没错,对于封堵对手,其实就是上面函数的翻版,将判断条件改成对手就行了。
1
2
3
4
5
6
7
8
9
10
11
12private void Block() {
//cross line to win
//...
for (int i = 0; i < 3; ++i) {
int row = i;
int col = i;
//row to win
//...
//column to win
//...
}
}下一步棋。除却上面两个函数的算法外,这是另一个决定这个 “AI” 智能程度的函数。技术较菜加时间因素,只能实现随机在空白区域下一步棋。在这个函数实现过程中,容易出现访问越界,选取到已填充区块和暴力穷举程序崩溃等问题,请善用Debug.Log()进行辅助。这里采用每次遍历井字区域并存储空白区域坐标,再在坐标内进行随机选取,避免越界,暴力循环的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private void RandomStep() {
List<int> row = new List<int>();
List<int> col = new List<int>();
int count = 0;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
if (symbol[i,j] == Player.player0) {
row.Add(i);
col.Add(j);
count++;
}
}
}
if (count != 0) {
System.Random ran = new System.Random();
int index = ran.Next(0, count);
a_x = row[index];
a_y = col[index];
} else {
a_x = a_y = -1;
}
}
效果图:
最后附上 github 项目链接:TicTacToe