Unity中IMGUI实现井字棋小游戏
前言
这是中大计算机学院3D游戏编程课的一次作业,在这里分享一下设计思路。 主要代码上传到了gitee上,请按照后文的操作运行。 项目地址:https://gitee.com/cuizx19308024/unity-games/tree/master/hw1 成果视频:https://www.bilibili.com/video/BV13q4y1f7if?spm_id_from=333.851.dynamic.content.click
游戏说明
- 使用IMGUI完成井字棋小游戏。
- 使用ECS的基本理念,实现实体、系统和组件的分离。
- 完成PVP和PVAI两个模式,可以选择先后手。
项目组成和运行环境
由于文件过大,本项目仅上传了Assets文件夹。不过其中包含了TicTacToc.cs代码文件,以及一些组件(图片、背景等)放在了Pictures文件夹中。 请按以下步骤进入项目:
-
在Unity Hub中创建一个新的项目,找到项目的位置,之后将Assets文件夹替换。 -
打开项目,选择左边栏SampleScene中的Script项。 -
将下方Assets/Pictures中的三张图片依次拖到右边属性栏中对应的三个位置,如图所示:(如果仍然保存这Script的属性,则不需要拖动) -
编译,运行游戏。
设计与实现
整体思路和ECS
实体层需要设计基本的数据结构,如棋盘、玩家(可以用轮次代替)、游戏当前状态和游戏结果,此外还需要设计一些UI界面所需要的图片、背景等。 系统层需要实现游戏逻辑。设计了人机和双人两种模式,两种模式中人下棋子(检测棋子的位置)操作是完全相同的,因此可以抽象出人下棋和AI下棋两种方法。此外,还需要根据规则判断游戏结束,还需要设计初始化和修改参数的设置。 部件层需要展示交互界面,通过状态变量检测切换游戏界面(Unity的IMGUI是每一帧都要刷新一次的,因此需要利用这一机制根据变量改变自动切换)。
数据结构
数据结构如下:
private int[,] board = new int[3,3];
private int mode = 0; //0为人人,1为人机
private int initTurn = 0; //先手
private int turn = 0; //当前轮次,0-玩家1,1-玩家2
private int state = -1; //状态,1-进行中,0-结束,-1准备中
private int result = 0; //结果,0-平局,1-玩家1获胜,2-玩家2获胜
//背景、样式、文字、棋盘位置
public Texture2D Background;
public Texture2D X;
···
注意,由于本游戏设计简单,不需要存储玩家信息或复杂的棋盘信息,因此可以只用轮次和矩阵代替。
游戏逻辑
- 初始化
void init(){
turn = initTurn;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
board[i,j] = 0;
}
}
}
- 玩家放棋子,找到对应的数组元素改变,并注意判断游戏结束和更换轮次。
void PlayerPut(int i, int j){
board[i,j] = turn + 1; //放棋子
int ret = judgeGame(); //判断
if(ret != -1){ //状态切换
state = 0;
result = ret;
}
turn = 1-turn; //轮换
}
- AI放棋子策略
AI应当做局部最优操作。对每一个方格,如果自己能赢,优先自己赢;之后如果对方能赢,优先堵住;否则,随机下。这里每一格做两个假设,即先将board[i,j] 更改为1或2,得到测试结果,之后一定要将board[i,j] 改回来。 注意,应当先遍历方格判断自己赢,再遍历方格判断对方赢,否则得到的不是最优解。//先让自己赢(篇幅原因只展示局部代码)
for(int i = 0; i<3;i++){
for(int j=0;j<3;j++){
if(board[i,j]==0){
//假设下2,若赢,则下这。
board[i,j] = 2;
ret = judgeGame();
board[i,j] = 0;
if(ret == 2){
posX = i;
posY = j;
isLocated = 1;
break;
}
}
}
if(isLocated == 1){
break;
}
}
- 判断游戏结束
该函数应当返回不同的值来确定游戏结果,供逻辑层判断和处理。//行列连成,且不能均为空(部分代码)
for(int i=0;i<3;i++){
if(board[i,0]!=0 && board[i,0]==board[i,1] && board[i,0]==board[i,2]){
return board[i,0];
}
if(board[0,i]!=0 && board[0,i]==board[1,i] && board[0,i]==board[2,i]){
return board[0,i];
}
}
UI界面
- 辅助的函数为
showBoard() ,这个函数的设计思路也应用在PVP() 和PVAI() 中。主要是通过IMGUI的刷新机制,实时更新每个方格按钮上的图标来展示不同的棋盘。void showBoard(){
//遍历整个棋盘,每一帧都要生成
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
if(board[i,j] == 0){
GUI.Button(new Rect(board_left + i * board_size, board_top + j * board_size, board_size, board_size), None);
}
if(board[i,j] == 1){
GUI.Button(new Rect(board_left + i * board_size, board_top + j * board_size, board_size, board_size), X);
}
if(board[i,j] == 2){
GUI.Button(new Rect(board_left + i * board_size, board_top + j * board_size, board_size, board_size), O);
}
}
}
}
Start() 和OnGUI() 函数可以初始化UI界面和切换界面:private void OnGUI ()
{
GUI.Label(new Rect(0, 0, 1000, 800), Background);
GUI.Label(new Rect(260, 20, 200, 50), "井字棋小游戏", bigBlackStyle);
//根据不同状态切换到不同界面
if(state == -1){
prepare();
}
else if(state == 1){
if(mode == 0){
PVP();
}
else{
PVAI();
}
if (GUI.Button(new Rect(330, 400, 100, 50), "重新开始")){
state = -1;
}
}
else{//游戏结束
gameOver();
if (GUI.Button(new Rect(330, 400, 100, 50), "重新开始")){
state = -1;
}
}
}
prepare() 通过单选框来改变模式和先后手:GUI.Box(new Rect(280, 230, 200, 80), "模式:");
mode = GUI.Toolbar (new Rect (300, 260, 150, 30), mode , toolbarModeStrings);
PVP() 和PVAI() 中,人的操作都是检测到被点击的按钮后,直接使用PlayerPut() 函数进行处理,AI的操作都是直接调用AI() 函数处理后显示棋盘。gamever() 函数会显示出游戏结果。这样可以达到UI界面和其他两个层次之间的分离。
运行结果
请参考展示视频
|