3-6 项目-图形版井字棋(上)

3-6 项目-图形版井字棋(上)

⏱ 本节预计用时:30分钟

本节意义:可以进一步体会model-view架构,初步理解状态机的思想,这些都是必用的。更别提你是在结合前两节学到的知识,给图形版井字棋搭骨架,这难道不是一件很爽的事情吗?

本节目标:理清这节只改 view 和 main、model 一行不动;能画出主菜单和空棋盘;用 Screen 这个枚举当"状态变量"控制界面切换;理解"一个界面对应一个状态,改状态就切界面"。

1 只改view、不改model

第二章(2-6)我们已经把井字棋拆成了 5 个文件:

c 复制代码
code/6-4井字棋-多文件/
├── game.h     ← Model 头文件(类型定义 + 声明)
├── game.c     ← Model 实现(数据 + 规则)
├── view.h     ← View 头文件
├── view.c     ← View 实现(画界面)
└── main.c     ← 主控(安排流程)

这一节我们就照着 2-6 的样子,把界面换成图形。上一讲我们说过,井字棋从命令行版改成图形版,底层的逻辑完全没变,变的只是"界面"。 model 层里的board、currentPlayer、initBoard、checkWin 这些"数据 + 规则"不变,游戏规则不会因为换个界面就坏掉,所以model层的代码一个字母都不用动,需要重写的只有 view 和 main。

文件 这一节动不动 动了啥
game.h 不动 ------(直接复制 2-6 的)
game.c 不动 ------(直接复制 2-6 的)
view.h 小改 引入 raylib、加一个"当前界面"状态、加几个"判断点了哪里"的函数
view.c 大改 原来是一堆 printf,现在换成 raylib 的绘图
main.c 大改 原来"读数字选菜单",现在"读鼠标点按钮"

直接上 view.h :

c 复制代码
#ifndef VIEW_H
#define VIEW_H

#include "raylib.h"   
#include "game.h"     

/* 当前显示哪个界面(图形版的"状态",和 2-5 里靠数字切换是一个意思) */
typedef enum { SCREEN_MENU, SCREEN_GAME, SCREEN_RESULT } Screen;

/* 画主菜单(标题 + 开始/退出按钮) */
void showMainMenu(void);

/* 画游戏界面里的"返回主菜单"按钮 */
void showGameMenu(void);

/* 画棋盘和上面已经下的棋子 */
void showBoard(void);

/* 画"当前轮到谁"这行提示 */
void showPlayerInfo(void);

/* 画结算界面(谁赢了 / 平局,加一个返回按钮) */
void showResult(CellType winner);

/* ------ 下面几个函数把"鼠标点了哪里"翻译成"点中了什么",给 main 用来做决定 ------ */

int startBtnClicked(Vector2 mouse);                  /* 点中了"开始游戏"按钮 */
int exitBtnClicked(Vector2 mouse);                   /* 点中了"退出游戏"按钮 */
int backBtnClicked(Vector2 mouse);                   /* 点中了"返回主菜单"按钮 */
int cellClicked(Vector2 mouse, int row, int col);    /* 点中了棋盘第 row 行第 col 列 */

#endif

后面的工作就是在view.c里把这些函数补全,然后在main.c里组织界面按想要的逻辑显示。

2 第一步:加入主菜单

因为程序一启动是从主菜单启动,所以先画它,之后在这个基础上往上加其它的功能就行。

先设计界面,粗略确定每个矩形、每行字的位置,位置画出来不满意在这个基础上再改。

和他相关的函数有showMainMenu(显示主菜单),startBtnClicked(开始游戏按键被按下),backBtnClicked(返回游戏按键被按下)。

现在井字棋项目里的提示都是英文,下一章会说中文怎么加入。

view.c :

c 复制代码
#include "view.h"
#include "raylib.h"

/* 按钮位置(x, y, 宽, 高) */
Rectangle startBtn = { 250, 250, 300, 80 };   /* 主菜单的"开始游戏" */
Rectangle exitBtn  = { 250, 360, 300, 80 };   /* 主菜单的"退出游戏" */

/* 显示主菜单 */
void showMainMenu(void)
{
    DrawText("Tic-Tac-Toe", 240, 110, 50, DARKGRAY);
    DrawRectangleRec(startBtn, GRAY);   /* 画"开始游戏"按钮 */
    DrawRectangleRec(exitBtn, GRAY);    /* 画"退出游戏"按钮 */
    DrawText("START", 355, 272, 30, WHITE);
    DrawText("EXIT",  360, 382, 30, WHITE);
}

int startBtnClicked(Vector2 mouse) { 
    return CheckCollisionPointRec(mouse, startBtn); 
}

int exitBtnClicked(Vector2 mouse) { 
    return CheckCollisionPointRec(mouse, exitBtn); 
}

main.c :

c 复制代码
#include "raylib.h"
#include "view.h"

int main(void)
{
    InitWindow(800, 600, "Tic-Tac-Toe");
    SetTargetFPS(60);

    Screen screen = SCREEN_MENU;   /* 一开机,先显示主菜单 */

    while (!WindowShouldClose())
    {
    	// 1. 更新阶段:读取输入、更新游戏逻辑
        /* 每一帧,先把"输入"读进来 */
        Vector2 mouse = GetMousePosition();
        int clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);

        /* 处理这次点击(现在只有菜单这一层) */
        if (screen == SCREEN_MENU)
        {
        	//没有处理startBtnClicked相关逻辑,因为还没写游戏菜单,点Start键没东西
        	
            if (clicked && exitBtnClicked(mouse)) {
                break;              /* 点退出:关程序 */
            }
        }
        
        // 2. 渲染阶段:只负责画东西
        /* 画这一帧 */
        BeginDrawing();
        ClearBackground(RAYWHITE);
        if (screen == SCREEN_MENU) {
            showMainMenu();
        }
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

让ai帮你编译。命令大概是这样:

c 复制代码
gcc main.c game.c view.c -o "井字棋图形版.exe" -I ../raylib/include -L ../raylib/lib -lraylib -lopengl32 -lgdi32 -lwinmm

-I的参数写include的路径,-L的参数写lib的路径。

运行:

此时点Start键没东西(因为还没写游戏菜单),除了点右上角错号之外,点EXIT键也能关闭窗口。

3 第二步:空棋盘 + 返回按钮

游戏界面比主菜单复杂,所以我们照 2-5 的样子,把它拆成三步(对应着二、三、四),每步只加一部分东西。

我们先把棋盘的绘图和返回主菜单的按钮加进去。

和主菜单一样,先粗略确定棋盘和返回按钮的位置和大小。

先对 view.c 做改动:

我们不用把整张棋盘每个点的位置都记录下来,用"格子边长 + 棋盘左上角坐标"来定位就可以,这样能少记很多数据。

c 复制代码
/* 棋盘:格子边长 + 棋盘左上角坐标 */
#define CELL_SIZE 120
#define BOARD_X   220
#define BOARD_Y   140

开始画棋盘,这里还是用宏定义表示两个格子间的缝宽。

c 复制代码
#define LINE_SIZE 6   /* 格线的粗细(格子之间的缝) */
c 复制代码
/* 画棋盘:先铺深灰色大矩形当底,再盖 9 个浅灰色格子矩形,露出来的深灰就是格线 */
void showBoard(void)
{
    int row, col;

    /* 1. 深灰色大矩形:覆盖整个棋盘区域 */
    DrawRectangle(BOARD_X, BOARD_Y, 3 * CELL_SIZE, 3 * CELL_SIZE, DARKGRAY);

    /* 2. 9 个浅灰色格子:每个比一格小一圈(四周各让出 LINE_SIZE / 2),盖在深灰底上 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            DrawRectangle(BOARD_X + col * CELL_SIZE + LINE_SIZE / 2,
                          BOARD_Y + row * CELL_SIZE + LINE_SIZE / 2,
                          CELL_SIZE - LINE_SIZE,
                          CELL_SIZE - LINE_SIZE,
                          LIGHTGRAY);
        }
    }
}

加入返回主菜单按钮的部分:

c 复制代码
Rectangle backBtn = { 300, 515, 200, 60 };   /* 游戏界面里的"返回主菜单" */

/* 画游戏界面里的"返回主菜单"按钮 */
void showGameMenu(void)
{
    DrawRectangleRec(backBtn, LIGHTGRAY);
    DrawText("MENU", 370, 530, 25, DARKGRAY);
}

int backBtnClicked(Vector2 mouse) { return CheckCollisionPointRec(mouse, backBtn); }

完整的 view.c :

c 复制代码
#include "view.h"
#include "raylib.h"

/* ---------- 界面的尺寸和位置(都写在这里,以后改界面就改这里) ---------- */

#define CELL_SIZE 120   /* 棋盘每个格子的边长 */
#define BOARD_X   220   /* 棋盘左上角的横坐标 */
#define BOARD_Y   140   /* 棋盘左上角的纵坐标 */
#define LINE_SIZE 6     /* 格线的粗细(格子之间的缝) */

/* 三个按钮的位置(x, y, 宽, 高) */
Rectangle startBtn = { 250, 250, 300, 80 };   /* 主菜单的"开始游戏" */
Rectangle exitBtn  = { 250, 360, 300, 80 };   /* 主菜单的"退出游戏" */
Rectangle backBtn  = { 300, 515, 200, 60 };   /* 游戏界面里的"返回主菜单" */

/* 显示主菜单 */
void showMainMenu(void)
{
    DrawText("Tic-Tac-Toe", 240, 110, 50, DARKGRAY);
    DrawRectangleRec(startBtn, GRAY);   /* 画"开始游戏"按钮 */
    DrawRectangleRec(exitBtn, GRAY);    /* 画"退出游戏"按钮 */
    DrawText("START", 355, 272, 30, WHITE);
    DrawText("EXIT",  360, 382, 30, WHITE);
}

/* 画游戏界面里的"返回主菜单"按钮 */
void showGameMenu(void)
{
    DrawRectangleRec(backBtn, LIGHTGRAY);
    DrawText("MENU", 370, 530, 25, DARKGRAY);
}

/* 画棋盘:先铺深灰色大矩形当底,再盖 9 个浅灰色格子矩形,露出来的深灰就是格线 */
void showBoard(void)
{
    int row, col;

    /* 1. 深灰色大矩形:覆盖整个棋盘区域 */
    DrawRectangle(BOARD_X, BOARD_Y, 3 * CELL_SIZE, 3 * CELL_SIZE, DARKGRAY);

    /* 2. 9 个浅灰色格子:每个比一格小一圈(四周各让出 LINE_SIZE / 2),盖在深灰底上 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            DrawRectangle(BOARD_X + col * CELL_SIZE + LINE_SIZE / 2,
                          BOARD_Y + row * CELL_SIZE + LINE_SIZE / 2,
                          CELL_SIZE - LINE_SIZE,
                          CELL_SIZE - LINE_SIZE,
                          LIGHTGRAY);
        }
    }
}

/* ---------- 判断鼠标点了哪里(把鼠标坐标翻译成"点中了什么") ---------- */

int startBtnClicked(Vector2 mouse) { return CheckCollisionPointRec(mouse, startBtn); }
int exitBtnClicked(Vector2 mouse)  { return CheckCollisionPointRec(mouse, exitBtn); }
int backBtnClicked(Vector2 mouse)  { return CheckCollisionPointRec(mouse, backBtn); }

下面开始改 main.c 。

只需要在更新阶段和渲染阶段,把 screen == SCREEN_GAME 的情况加上就行了,于是能写出这样的代码:

c 复制代码
#include "raylib.h"
#include "view.h"

int main(void)
{
    InitWindow(800, 600, "Tic-Tac-Toe");
    SetTargetFPS(60);

    Screen screen = SCREEN_MENU;   /* 一开机,先显示主菜单 */

    while (!WindowShouldClose())
    {
        // 1. 更新阶段:读取输入、更新游戏逻辑
        Vector2 mouse = GetMousePosition();
        int clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);

        if (screen == SCREEN_MENU)
        {
            if (clicked && startBtnClicked(mouse)) {
                screen = SCREEN_GAME;   /* 点开始:进入游戏界面 */
            }
            else if (clicked && exitBtnClicked(mouse)) {
                break;                  /* 点退出:关程序 */
            }
        }
        else if (screen == SCREEN_GAME)
        {
            if (clicked && backBtnClicked(mouse)) {
                screen = SCREEN_MENU;   /* 点返回:回主菜单 */
            }
            /* 这一小步先不写下棋逻辑:点棋盘上的格子,没反应 */
        }

        // 2. 渲染阶段:只负责画东西
        BeginDrawing();
        ClearBackground(RAYWHITE);

        if (screen == SCREEN_MENU) {
            showMainMenu();
        }
        else {   /* SCREEN_GAME:空棋盘 + 返回按钮 */
            showBoard();
            showGameMenu();
        }

        EndDrawing();
    }

    CloseWindow();
    return 0;
}

编译运行:

点 START 进入游戏界面,看到空棋盘和下方的 MENU 按钮。

点 MENU 回到主菜单,点 EXIT 退出。

回过头去看代码,你会注意到一个特别有意思的事情,界面一直在刷新,如果状态screen == SCREEN_MENU的时候就加载主菜单界面,如果状态screen == SCREEN_GAME的时候就加载游戏界面,在主菜单界面的时候(screen = SCREEN_MENU时)你可以点按钮,令screen = SCREEN_GAME,这样就切到游戏界面了,反之亦如是。

一个界面对应一个状态变量,状态变量改变则界面改变。要增加一个新的界面,只需要再增加一个状态变量,在更新阶段下加入别的状态变量怎么变成它,它怎么变成别人的逻辑,在渲染阶段下加入如果当前的状态变量是我,就渲染新界面的逻辑,就行了。

我们管这种编程设计的思想叫状态机。

ok,本节我们写好了井字棋图形化项目的骨架,了解了状态机的思想,知道了怎么做才能点一下切换界面。下一讲,我们会进入这个项目真正的核心部分,你,准备好了吗?

相关推荐
从小白到软工2 小时前
3-5 画矩形和按钮
c
书生执笔画浮沉5 小时前
Linux 内核模块为什么不能自己解决兼容性问题?
linux·c·kernel·update·灵感
书生执笔画浮沉4 天前
分享一个开源小工具CAdapt
linux·c·gnu·glibc·kernel·升级·灵感
Sylvia-girl5 天前
堆排序算法
算法·排序算法·c
liulilittle5 天前
Linux 下 select 测试函数
linux·服务器·网络·数据库·c++·select·c
从小白到软工5 天前
2-6 将井字棋拆分成多个文件
c
C++ 老炮儿的技术栈6 天前
static的作用
c语言·c++·人工智能·单片机·c
C++ 老炮儿的技术栈6 天前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
C++ 老炮儿的技术栈7 天前
C++ const 的3个核心作用
服务器·c语言·c++·算法·c