小游戏和GUI编程(6) | 基于 SFML 的井字棋

这篇具有很好参考价值的文章主要介绍了小游戏和GUI编程(6) | 基于 SFML 的井字棋。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

小游戏和GUI编程(6) | 基于 SFML 的井字棋

0. 简介

使用 SFML 实现井字棋(tic-tac-toe), 规划如下:

  • 了解规则, 使用命令行实现(已经实现了)
  • 使用 SFML,提供极简的交互(预计 1 小时)
  • 制作 SVG 图像, 美化界面(预计 1 小时)

1. 基于命令行的实现

实现了两个用户 X 和 O 的交互下棋, 判断了输赢、 平局:

  • 有胜负: 每个用户落下棋子后, 检查整个棋盘中的能获胜的 8 个线段上三个点,如果都等于当前落子的值(X或O)那么赢了
  • 没胜负: 如果没有判断出有人赢了, 并且扫描棋盘网格出现了空格, 那么继续下棋; 没有扫描到空格, 说明没法落子了,是平局。
#include <stdio.h>
#include <string.h>

char board[3][3];

void show_board()
{
    for (int i = 0; i < 3; i++)
    {
        if (i > 0) printf("-----\n");
        for (int j = 0; j < 3; j++)
        {
            if (j > 0) printf("|");
            printf("%c", board[i][j]);
        }
        printf("\n");
    }
}

char user = 'X';

enum State {
    PLAYING = 0,
    WIN = 1,
    DRAW = 2
};
State state = PLAYING;
bool played = false;

bool user_play()
{
    printf("[user=%c] please input position ([1-3] [1-3])): ", user);
    int x;
    int y;
    scanf("%d %d", &x, &y);
    if (x < 1 || x > 3 || y < 1 || y > 3)
    {
        printf("invalid position\n");
        return false;
    }
    if (board[x-1][y-1] != ' ')
    {
        printf("invalid position\n");
        return false;
    }
    board[x-1][y-1] = user;
    return true;
}

void update_user()
{
    if (!played) return;
    if (user == 'X')
    {
        user = 'O';
    }
    else
    {
        user = 'X';
    }
}

void judge()
{
    int data[8][6] = {
        {0, 0, 0, 1, 0, 2},
        {1, 0, 1, 1, 1, 2},
        {2, 0, 2, 1, 2, 2},
        {0, 0, 1, 0, 2, 0},
        {0, 1, 1, 1, 2, 1},
        {0, 2, 1, 2, 2, 2},
        {0, 0, 1, 1, 2, 2},
        {0, 2, 1, 1, 2, 0}
    };

    for (int i = 0; i < 8; i++)
    {
        int x0 = data[i][0];
        int y0 = data[i][1];
        int x1 = data[i][2];
        int y1 = data[i][3];
        int x2 = data[i][4];
        int y2 = data[i][5];
        if (board[x0][y0] == user && board[x1][y1] == user && board[x2][y2] == user)
        {
            state = WIN;
            return;
        }
    }

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            if (board[i][j] == ' ')
            {
                return;
            }
        }
    }

    state = DRAW;
}

int main()
{
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            board[i][j] = ' ';
        }
    }

    while (true)
    {
        show_board();
        judge();
        if (state != PLAYING) break;
        update_user();
        played = user_play();
    }

    if (state == WIN)
    {
        printf("Game end, user %c win\n", user);
    }
    else if (state == DRAW)
    {
        printf("Game end, draw\n");
    }
    return 0;
}

2. 基于 SFML 的极简实现

所谓极简是说, 先不考虑美观性, 实现鼠标交互落子即可。

规划:

  • 2.1 绘制窗口
  • 2.2 鼠标点击后绘制单个棋子
  • 2.3 绘制棋盘网格
  • 2.4 轮流绘制棋子
  • 2.5 显示局面信息

2.1 绘制窗口

#include <SFML/Graphics.hpp>

int main()
{
    constexpr int win_width = 500;
    constexpr int win_height = 500;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);

    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }
        }

        window.clear();

        // draw everything here...

        window.display();
    }

    return 0;
}

2.2 鼠标点击后绘制单个棋子

拆解为两部分: 获取到鼠标点击的位置; 根据位置绘制棋子。

根据位置绘制棋子
先直接绘制棋子 ‘X’. 可以使用 sf::RectangleShape 创建线段对象, 它具有宽度, 旋转它45° 和-45° 可以得到交叉效果, 比较麻烦的地方在于, 需要复杂的计算才能确保交叉点是中心点。

另一个思路是使用 sf::Vertext, 通过在两个 vertex 之间用 sf::Lines 类型执行渲染, 省去了计算, 缺点是线段的宽度很窄。 不过够用了。

window.clear(sf::Color::White);

// draw everything here...
// A    B
// +----+
// |    |
// +----+
// C    D
sf::Vector2f A(100, 100);
sf::Vector2f D(150, 150);

// draw line AD
sf::Vertex vertex[2];
vertex[0].position = A;
vertex[0].color  = sf::Color::Blue;
vertex[1].position = D;
vertex[1].color = sf::Color::Blue;
window.draw(vertex, 2, sf::Lines);

// draw line BC
sf::Vector2f B(150, 100);
sf::Vector2f C(100, 150);
vertex[0].position = B;
vertex[0].color  = sf::Color::Blue;
vertex[1].position = C;
vertex[1].color = sf::Color::Blue;
window.draw(vertex, 2, sf::Lines);

window.display();

小游戏和GUI编程(6) | 基于 SFML 的井字棋,小游戏和GUI编程,SFML,tic-tac-toe,井字棋,小游戏,EasyX,c++

获取鼠标位置

以获取到的鼠标点击位置为中心, 上下左右各自扩展 50 个像素, 得到的 ABCD 区域里面, 绘制 ‘X’.

获取鼠标点击位置, 是在 sfml 教程的 window - keyboard, mouse event 里:

    sf::Vector2i localPosition(-1, -1);
    while (window.pollEvent(event))
    {
        if (event.type == sf::Event::Closed) { window.close(); }

        if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
        {
            // get the local mouse position (relative to a window)
            localPosition = sf::Mouse::getPosition(window);
        }
    }

为了避免屏幕闪烁, 如果当前帧没有获取到新的鼠标位置, 那么 A,B,C,D 四个点的坐标不变,仍然执行渲染和绘制; 如果鼠标点击了, 才更新 A,B,C,D。 效果:

小游戏和GUI编程(6) | 基于 SFML 的井字棋,小游戏和GUI编程,SFML,tic-tac-toe,井字棋,小游戏,EasyX,c++

对应代码:

#include <SFML/Graphics.hpp>
#include <iostream>

int main()
{
    constexpr int win_width = 500;
    constexpr int win_height = 500;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);

    sf::Vector2f A, B, C, D;
    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear();

        constexpr int grid_len = 50;
        
        if (localPosition != sf::Vector2i(-1, -1))
        {
            // print the local position to console
            std::cout << "localPosition: " << localPosition.x << ", " << localPosition.y << std::endl;
            A = sf::Vector2f(localPosition.x - grid_len, localPosition.y - grid_len);
            D = sf::Vector2f(localPosition.x + grid_len, localPosition.y + grid_len);
            B = sf::Vector2f(localPosition.x + grid_len, localPosition.y - grid_len);
            C = sf::Vector2f(localPosition.x - grid_len, localPosition.y + grid_len);
        }
    
        // draw everything here...
        // A    B
        // +----+
        // |    |
        // +----+
        // C    D

        // draw line AD
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Yellow;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Yellow;
        window.draw(vertex, 2, sf::Lines);

        // draw line BC
        vertex[0].position = B;
        vertex[0].color  = sf::Color::Yellow;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Yellow;
        window.draw(vertex, 2, sf::Lines);

        window.display();
    }

    return 0;
}

2.3 绘制棋盘网格

拆解为 2 个部分: 绘制 3x3 的网格; 对于每个网格,如果鼠标点击了它,就执行绘制。

绘制3x3网格

横向 2 条线, 纵向 2 条线。 绘制它们后就得到了网格。

window.clear(sf::Color::White);

sf::Color grid_color(74, 74, 74);

// draw a 3x3 grid lines
sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
horizon_line1.setPosition(100, 100 + grid_len);
horizon_line1.setFillColor(grid_color);
window.draw(horizon_line1);

sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
horizon_line2.setPosition(100, 100 + 2 * grid_len);
horizon_line2.setFillColor(grid_color);
window.draw(horizon_line2);

sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
vertical_line1.setPosition(100 + grid_len, 100);
vertical_line1.setFillColor(grid_color);
window.draw(vertical_line1);

sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
vertical_line2.setPosition(100 + 2 * grid_len, 100);
vertical_line2.setFillColor(grid_color);
window.draw(vertical_line2);

window.display();

小游戏和GUI编程(6) | 基于 SFML 的井字棋,小游戏和GUI编程,SFML,tic-tac-toe,井字棋,小游戏,EasyX,c++

每个网格内,如果鼠标有点击则执行绘制

根据前一步绘制的网格, 可以确定每个小格子的坐标范围。 根据鼠标点击获取的位置,遍历每个格子, 如果是在当前格子内部, 那么执行绘制。 简单起见, 为了区分绘制内容,只要点击了当前小格子就执行绘制.

先将刚刚回执的网格的代码做重构, 根据起始点 p00(100, 100) 和网格宽度 grid_len = 50 进行绘制, 而不是硬编码每个线段的起点。

根据网格点的起始位置, 可以定义 9 个格子的A,B,C,D坐标取值。 为了避免绘制时的闪烁,定义 class Grid, 由每个网格自己决定是否绘制, 也就是提供 void draw(sf::RenderWindow& window) 函数, 如果鼠标左键点击时落在格子范围内, 那么 shouldRender 变量更新为 true, 执行有效的渲染:

// main 函数
for (int i = 0; i < 9; i++)
{
    // if the mouse is inside the grid, draw a cross
    if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
        localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y)
    {
        printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
        grid[i].shouldRender = true;
    }
    grid[i].draw(window);
}

// Grid::draw() 函数
void draw(sf::RenderWindow& window)
{
    if (shouldRender)
    {
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);

        vertex[0].position = B;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);
    }
}

效果:
小游戏和GUI编程(6) | 基于 SFML 的井字棋,小游戏和GUI编程,SFML,tic-tac-toe,井字棋,小游戏,EasyX,c++

完整代码:

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(74, 74, 74);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(sf::Color::White);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        for (int i = 0; i < 9; i++)
        {
            // if the mouse is inside the grid, draw a cross
            if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y)
            {
                printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                grid[i].shouldRender = true;
            }
            grid[i].draw(window);
        }
        
        window.display();
    }

    return 0;
}

2.4 轮流绘制棋子

拆解为如下部分: 绘制单个圆形⭕️, 交替绘制 ❌ 和 ⭕️.

绘制单个圆形

sf::CircleShape circle(grid_len / 3);
circle.setFillColor(bg_color);
circle.setOutlineThickness(4);
circle.setOutlineColor(sf::Color::Red);
circle.setPosition(200, 200);
window.draw(circle);

交替绘制 ❌ 和 ⭕️

每个网格被点击后, 要根据现有情况来执行绘制: 如果之前没有绘制过(没有落子过), 则绘制当前用户的棋子; 如果已经绘制过, 那么不能绘制, 说明当前用户落子无效, 要换一个地方落子。 如果鼠标点击的位置不在 9 个格子的范围内,也是落子无效。

小游戏和GUI编程(6) | 基于 SFML 的井字棋,小游戏和GUI编程,SFML,tic-tac-toe,井字棋,小游戏,EasyX,c++

#include <SFML/Graphics.hpp>
#include <iostream>
#include <array>

char user = 'X';

enum class DrawShape
{
    NONE = 0,
    CROSS = 1, 
    CIRCLE = 2
};

class Grid
{
public:
    Grid() = default;
    Grid(sf::Vector2f start_pos, int a_grid_len)
    {
        update(start_pos, a_grid_len);
    }

    void update(sf::Vector2f start_pos, int a_grid_len)
    {
        grid_len = a_grid_len;
        A = start_pos;
        B = sf::Vector2f(start_pos.x + grid_len, start_pos.y);
        C = sf::Vector2f(start_pos.x, start_pos.y + grid_len);
        D = sf::Vector2f(start_pos.x + grid_len, start_pos.y + grid_len);
    }

    void draw(sf::RenderWindow& window)
    {
        if (drawShape == DrawShape::CROSS)
        {
            drawCross(window);
        }
        else if (drawShape == DrawShape::CIRCLE)
        {
            drawCircle(window);
        }
    }

    void drawCross(sf::RenderWindow& window)
    {
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);

        vertex[0].position = B;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);
    }

    void drawCircle(sf::RenderWindow& window)
    {
        // draw a circle
        float radius = grid_len / 3;
        sf::CircleShape circle(radius);
        int thickness = 4;
        circle.setOutlineThickness(thickness);
        circle.setOutlineColor(sf::Color::Red);

        // M is middle point of A and D
        sf::Vector2f M((A.x + D.x) / 2, (A.y + D.y) / 2);
        sf::Vector2f circle_position(M.x - radius + thickness, M.y - radius + thickness);
        circle.setPosition(circle_position);

        window.draw(circle);
    }

    sf::Vector2f A, B, C, D;
    DrawShape drawShape{};
    int grid_len;
};

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(74, 74, 74);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    bool played = false;

    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(sf::Color::White);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        for (int i = 0; i < 9; i++)
        {
            // if the mouse is inside the grid, draw a cross
            if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y &&
                grid[i].drawShape == DrawShape::NONE)
            {
                printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                if (user == 'X')
                {
                    grid[i].drawShape = DrawShape::CROSS;
                }
                else if (user == 'O')
                {
                    grid[i].drawShape = DrawShape::CIRCLE;
                }
                played = true;
            }
            grid[i].draw(window);
        }

        if (played)
        {
            user = (user == 'X') ? 'O' : 'X';
            played = false;
        }
        
        window.display();
    }

    return 0;
}

2.5 显示局面信息

显示这些信息:

  • 轮到哪个用户落子, X 还是 O?
  • 落子后判断输赢
  • 如果平局, 显示平局
  • 允许中途或结束时,重新来一局

显示轮到谁落子

// draw a text to show the current user
sf::Text text;
text.setFont(font);
text.setString("Current user: " + std::string(1, user));
text.setCharacterSize(24);
text.setFillColor(sf::Color::Black);
text.setPosition(10, 10);
window.draw(text);

落子后判断输赢

在基于控制台的实现中, 使用的是 char board[3][3] 记录棋子, 相比于 enum class DrawShape 要直观的多。 因此我们抛弃 enum class DrawShape, 在 Grid 类中使用 char 类型的数据来存储当前网格里的棋子情况:

class Grid
{
public:
    ...
    void draw(sf::RenderWindow& window)
    {
        if (data == 'X')
        {
            drawCross(window);
        }
        else if (data == 'O')
        {
            drawCircle(window);
        }
    }
private:
    char data = ' ';
};

输赢局面的判断: 如果是有效落子, 那么在 8 条线段上分别判断。 如果判断出来赢了, 则更新 state; 如果没有有效落子, 检查是否存在能落子的地方 (’ '), 如果没有能落子的地方, 说明是平局(Draw):

if (played)
{
    int data[8][6] = {
        {0, 0, 0, 1, 0, 2},
        {1, 0, 1, 1, 1, 2},
        {2, 0, 2, 1, 2, 2},
        {0, 0, 1, 0, 2, 0},
        {0, 1, 1, 1, 2, 1},
        {0, 2, 1, 2, 2, 2},
        {0, 0, 1, 1, 2, 2},
        {0, 2, 1, 1, 2, 0}
    };   

    for (int i = 0; i < 8; i++)
    {
        int x0 = data[i][0];
        int y0 = data[i][1];
        int x1 = data[i][2];
        int y1 = data[i][3];
        int x2 = data[i][4];
        int y2 = data[i][5];
        int idx0 = x0 * 3 + y0;
        int idx1 = x1 * 3 + y1;
        int idx2 = x2 * 3 + y2;
        if (grid[idx0].data == user && grid[idx1].data == user && grid[idx2].data == user)
        {
            if (user == 'X')
            {
                state = PlayState::X_WIN;
            }
            else
            {
                state = PlayState::O_WIN;
            }
            break;
        }
    }

    user = (user == 'X') ? 'O' : 'X'; 
}

if (!played)
{
    state = PlayState::DRAW;
    for (int i = 0; i < 9; i++)
    {
        if (grid[i].data == ' ')
        {
            state = PlayState::PLAYING;
            break;
        }
    }
}

顺带, 在界面上通过绘制文字的方式, 显示当前轮到谁下子、 谁赢了、 是否平局信息, 以及随时可以点击 “restart” 重来一局:

        sf::Vector2f boxPos(130, 300);
        sf::Vector2f boxSize(100, 24);

        sf::RectangleShape box(boxSize);
        box.setFillColor(sf::Color::Red);
        box.setPosition(boxPos);
        window.draw(box);

        // A B
        // C D

        sf::Vector2f boxA(boxPos);
        sf::Vector2f boxB(boxPos.x + boxSize.x, boxPos.y);
        sf::Vector2f boxC(boxPos.x, boxPos.y + boxSize.y);
        sf::Vector2f boxD(boxPos.x + boxSize.x, boxPos.y + boxSize.y);

        if (localPosition.x >= boxA.x && localPosition.y >= boxA.y &&
            localPosition.x <= boxD.x && localPosition.y <= boxD.y)
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].data = ' ';
            }
            user = 'X';
            state = PlayState::PLAYING;
        }

        // draw a button with text "restart"
        sf::Text restart;
        restart.setFont(font);
        restart.setString("Restart");
        restart.setCharacterSize(24);
        restart.setFillColor(sf::Color::White);
        restart.setPosition(boxPos);
        window.draw(restart);

效果如下:

小游戏和GUI编程(6) | 基于 SFML 的井字棋,小游戏和GUI编程,SFML,tic-tac-toe,井字棋,小游戏,EasyX,c++

完整代码是

#include <SFML/Graphics.hpp>
#include <iostream>
#include <array>

enum class PlayState
{
    PLAYING = 0,
    X_WIN = 1,
    O_WIN = 2,
    DRAW = 3
};

class Grid
{
public:
    Grid() = default;
    Grid(sf::Vector2f start_pos, int a_grid_len)
    {
        update(start_pos, a_grid_len);
    }

    void update(sf::Vector2f start_pos, int a_grid_len)
    {
        grid_len = a_grid_len;
        A = start_pos;
        B = sf::Vector2f(start_pos.x + grid_len, start_pos.y);
        C = sf::Vector2f(start_pos.x, start_pos.y + grid_len);
        D = sf::Vector2f(start_pos.x + grid_len, start_pos.y + grid_len);
    }

    void draw(sf::RenderWindow& window)
    {
        if (data == 'X')
        {
            drawCross(window);
        }
        else if (data == 'O')
        {
            drawCircle(window);
        }
    }

    void drawCross(sf::RenderWindow& window)
    {
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);

        vertex[0].position = B;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);
    }

    void drawCircle(sf::RenderWindow& window)
    {
        // draw a circle
        float radius = grid_len / 3;
        sf::CircleShape circle(radius);
        int thickness = 4;
        circle.setOutlineThickness(thickness);
        circle.setOutlineColor(sf::Color::Red);

        // M is middle point of A and D
        sf::Vector2f M((A.x + D.x) / 2, (A.y + D.y) / 2);
        sf::Vector2f circle_position(M.x - radius + thickness, M.y - radius + thickness);
        circle.setPosition(circle_position);

        window.draw(circle);
    }

    sf::Vector2f A, B, C, D;
    char data = ' ';
    int grid_len;
};

char user = 'X';

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(74, 74, 74);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    sf::Font font;
    const std::string asset_dir = "../Resources";
    if (!font.loadFromFile(asset_dir + "/Arial.ttf"))
    {
        std::cerr << "failed to load font\n";
        return 1;
    }
    
    PlayState state = PlayState::PLAYING;
    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(sf::Color::White);

        // draw a text to show the current user
        sf::Text text;
        text.setFont(font);

        std::string state_str;
        if (state == PlayState::DRAW)
        {
            state_str = "Draw";
        }
        else if (state == PlayState::X_WIN)
        {
            state_str = "User X wins!";
        }
        else if (state == PlayState::O_WIN)
        {
            state_str = "User O wins!";
        }
        else
        {
            state_str = "Current user: " + std::string(1, user);
        }

        printf("state: %s\n", state_str.c_str());

        text.setString(state_str);
        text.setCharacterSize(24);
        text.setFillColor(sf::Color::Black);
        text.setPosition(10, 10);
        window.draw(text);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        bool played = false;
        if (state == PlayState::PLAYING)
        {
            for (int i = 0; i < 9; i++)
            {
                // if the mouse is inside the grid, draw a cross
                if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                    localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y &&
                    grid[i].data == ' ')
                {
                    printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                    grid[i].data = user;
                    played = true;
                }
                grid[i].draw(window);

                if (played)
                {
                    break;
                }
            }

            if (played)
            {
                int data[8][6] = {
                    {0, 0, 0, 1, 0, 2},
                    {1, 0, 1, 1, 1, 2},
                    {2, 0, 2, 1, 2, 2},
                    {0, 0, 1, 0, 2, 0},
                    {0, 1, 1, 1, 2, 1},
                    {0, 2, 1, 2, 2, 2},
                    {0, 0, 1, 1, 2, 2},
                    {0, 2, 1, 1, 2, 0}
                };   

                for (int i = 0; i < 8; i++)
                {
                    int x0 = data[i][0];
                    int y0 = data[i][1];
                    int x1 = data[i][2];
                    int y1 = data[i][3];
                    int x2 = data[i][4];
                    int y2 = data[i][5];
                    int idx0 = x0 * 3 + y0;
                    int idx1 = x1 * 3 + y1;
                    int idx2 = x2 * 3 + y2;
                    if (grid[idx0].data == user && grid[idx1].data == user && grid[idx2].data == user)
                    {
                        if (user == 'X')
                        {
                            state = PlayState::X_WIN;
                        }
                        else
                        {
                            state = PlayState::O_WIN;
                        }
                        break;
                    }
                }

                user = (user == 'X') ? 'O' : 'X'; 
            }

            if (!played)
            {
                state = PlayState::DRAW;
                for (int i = 0; i < 9; i++)
                {
                    if (grid[i].data == ' ')
                    {
                        state = PlayState::PLAYING;
                        break;
                    }
                }
            }
        }
        else
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].draw(window);
            }
        }

        sf::Vector2f boxPos(130, 300);
        sf::Vector2f boxSize(100, 24);

        sf::RectangleShape box(boxSize);
        box.setFillColor(sf::Color::Red);
        box.setPosition(boxPos);
        window.draw(box);

        // A B
        // C D

        sf::Vector2f boxA(boxPos);
        sf::Vector2f boxB(boxPos.x + boxSize.x, boxPos.y);
        sf::Vector2f boxC(boxPos.x, boxPos.y + boxSize.y);
        sf::Vector2f boxD(boxPos.x + boxSize.x, boxPos.y + boxSize.y);

        if (localPosition.x >= boxA.x && localPosition.y >= boxA.y &&
            localPosition.x <= boxD.x && localPosition.y <= boxD.y)
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].data = ' ';
            }
            user = 'X';
            state = PlayState::PLAYING;
        }

        // draw a button with text "restart"
        sf::Text restart;
        restart.setFont(font);
        restart.setString("Restart");
        restart.setCharacterSize(24);
        restart.setFillColor(sf::Color::White);
        restart.setPosition(boxPos);
        window.draw(restart);

        window.display();
    }

    return 0;
}

int main()
{
    draw_grid_and_response_mouse();
    return 0;
}

3. 制作 SVG 图像, 美化界面

任务分解为: 制作 ‘X’ 和 ‘O’ 棋子的 svg 图像, 使用 SFML 导入 SVG 图像并替代先前棋子的绘制。

使用在线工具(1, 2), 结合 inkscape 和 VSCode svg 插件, 得到 X 和 O 的 svg 图像:

ttt-cross.svg:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   width="300"
   height="300"
   version="1.1"
   id="svg1"
   sodipodi:docname="ttt-cross.svg"
   inkscape:version="1.3.2 (091e20e, 2023-11-25)"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs1" />
  <sodipodi:namedview
     id="namedview1"
     pagecolor="#ffffff"
     bordercolor="#000000"
     borderopacity="0.25"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0.0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     inkscape:zoom="1.616"
     inkscape:cx="247.83416"
     inkscape:cy="162.74752"
     inkscape:window-width="1408"
     inkscape:window-height="953"
     inkscape:window-x="0"
     inkscape:window-y="25"
     inkscape:window-maximized="0"
     inkscape:current-layer="svg1" />
  <!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
  <g
     class="layer"
     id="g1"
     transform="translate(25.371288,-13.61386)">
    <title
       id="title1">Layer 1</title>
    <rect
       fill="#747474"
       height="27"
       id="svg_5"
       stroke="#747474"
       transform="rotate(45,229.99949,92.78901)"
       width="300"
       x="50"
       y="200" />
    <rect
       fill="#747474"
       height="27"
       id="svg_7"
       stroke="#747474"
       transform="rotate(135,175,175.58344)"
       width="300"
       x="50"
       y="200" />
  </g>
</svg>

ttt-circle.svg:

<svg id="svgelem" height="140" width="140" xmlns="http://www.w3.org/2000/svg">
    <circle cx="70" cy="70" r="50" stroke="#eee8cf" stroke-width="10" fill="none" />
</svg>

SFML 不支持 svg 的导入, 因此转换为 png 后再使用:

cairosvg ttt-cross.svg -o ttt-cross.png 
cairosvg ttt-circle.svg -o ttt-circle.png 

4. 最终结果

运行效果:
小游戏和GUI编程(6) | 基于 SFML 的井字棋,小游戏和GUI编程,SFML,tic-tac-toe,井字棋,小游戏,EasyX,c++

代码:

#include <SFML/Graphics.hpp>
#include <iostream>
#include <array>

enum class PlayState
{
    PLAYING = 0,
    X_WIN = 1,
    O_WIN = 2,
    DRAW = 3
};

class Grid
{
public:
    Grid() = default;
    Grid(sf::Vector2f start_pos, int a_grid_len)
    {
        update(start_pos, a_grid_len);
    }

    void update(sf::Vector2f start_pos, int a_grid_len)
    {
        grid_len = a_grid_len;
        A = start_pos;
        B = sf::Vector2f(start_pos.x + grid_len, start_pos.y);
        C = sf::Vector2f(start_pos.x, start_pos.y + grid_len);
        D = sf::Vector2f(start_pos.x + grid_len, start_pos.y + grid_len);
    }

    void draw(sf::RenderWindow& window)
    {
        if (data == 'X')
        {
            drawCross(window);
        }
        else if (data == 'O')
        {
            drawCircle(window);
        }
    }

    void drawCross(sf::RenderWindow& window)
    {
        if (0)
        {
            sf::Vertex vertex[2];
            vertex[0].position = A;
            vertex[0].color  = sf::Color::Blue;
            vertex[1].position = D;
            vertex[1].color = sf::Color::Blue;
            window.draw(vertex, 2, sf::Lines);

            vertex[0].position = B;
            vertex[0].color  = sf::Color::Blue;
            vertex[1].position = C;
            vertex[1].color = sf::Color::Blue;
            window.draw(vertex, 2, sf::Lines);
        }
        else
        {
            // load a svg file to create a texture
            sf::Texture texture;
            if (!texture.loadFromFile("../ttt-cross.png"))
            {
                std::cerr << "failed to load cross image\n";
                return;
            }
            // draw a sprite in box region A, D
            sf::Sprite sprite(texture);
            sprite.setPosition(A);
            sprite.setScale(0.2, 0.2);
            window.draw(sprite);
        }
    }

    void drawCircle(sf::RenderWindow& window)
    {
        if (0)
        {
            // draw a circle
            float radius = grid_len / 3;
            sf::CircleShape circle(radius);
            int thickness = 4;
            circle.setOutlineThickness(thickness);
            circle.setOutlineColor(sf::Color::Red);

            // M is middle point of A and D
            sf::Vector2f M((A.x + D.x) / 2, (A.y + D.y) / 2);
            sf::Vector2f circle_position(M.x - radius + thickness, M.y - radius + thickness);
            circle.setPosition(circle_position);

            window.draw(circle);
        }
        else
        {
            // load a svg file to create a texture
            sf::Texture texture;
            if (!texture.loadFromFile("../ttt-circle.png"))
            {
                std::cerr << "failed to load circle image\n";
                return;
            }
            // draw a sprite in box region A, D
            sf::Sprite sprite(texture);
            sprite.setPosition(A);
            sprite.setScale(0.4, 0.4);
            window.draw(sprite);   
        }
    }

    sf::Vector2f A, B, C, D;
    char data = ' ';
    int grid_len;
};

char user = 'X';

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(64, 148, 135);
    sf::Color bg_color(78, 177, 163);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    sf::Font font;
    const std::string asset_dir = "../Resources";
    if (!font.loadFromFile(asset_dir + "/Arial.ttf"))
    {
        std::cerr << "failed to load font\n";
        return 1;
    }
    
    PlayState state = PlayState::PLAYING;
    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(bg_color);

        // draw a text to show the current user
        sf::Text text;
        text.setFont(font);

        std::string state_str;
        if (state == PlayState::DRAW)
        {
            state_str = "Draw";
        }
        else if (state == PlayState::X_WIN)
        {
            state_str = "User X wins!";
        }
        else if (state == PlayState::O_WIN)
        {
            state_str = "User O wins!";
        }
        else
        {
            state_str = "Current user: " + std::string(1, user);
        }

        printf("state: %s\n", state_str.c_str());

        text.setString(state_str);
        text.setCharacterSize(24);
        text.setFillColor(sf::Color::Black);
        text.setPosition(10, 10);
        window.draw(text);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        bool played = false;
        if (state == PlayState::PLAYING)
        {
            for (int i = 0; i < 9; i++)
            {
                // if the mouse is inside the grid, draw a cross
                if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                    localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y &&
                    grid[i].data == ' ')
                {
                    printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                    grid[i].data = user;
                    played = true;
                }
                grid[i].draw(window);

                if (played)
                {
                    break;
                }
            }

            if (played)
            {
                int data[8][6] = {
                    {0, 0, 0, 1, 0, 2},
                    {1, 0, 1, 1, 1, 2},
                    {2, 0, 2, 1, 2, 2},
                    {0, 0, 1, 0, 2, 0},
                    {0, 1, 1, 1, 2, 1},
                    {0, 2, 1, 2, 2, 2},
                    {0, 0, 1, 1, 2, 2},
                    {0, 2, 1, 1, 2, 0}
                };   

                for (int i = 0; i < 8; i++)
                {
                    int x0 = data[i][0];
                    int y0 = data[i][1];
                    int x1 = data[i][2];
                    int y1 = data[i][3];
                    int x2 = data[i][4];
                    int y2 = data[i][5];
                    int idx0 = x0 * 3 + y0;
                    int idx1 = x1 * 3 + y1;
                    int idx2 = x2 * 3 + y2;
                    if (grid[idx0].data == user && grid[idx1].data == user && grid[idx2].data == user)
                    {
                        if (user == 'X')
                        {
                            state = PlayState::X_WIN;
                        }
                        else
                        {
                            state = PlayState::O_WIN;
                        }
                        break;
                    }
                }

                user = (user == 'X') ? 'O' : 'X'; 
            }

            if (!played)
            {
                state = PlayState::DRAW;
                for (int i = 0; i < 9; i++)
                {
                    if (grid[i].data == ' ')
                    {
                        state = PlayState::PLAYING;
                        break;
                    }
                }
            }
        }
        else
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].draw(window);
            }
        }

        sf::Vector2f boxPos(130, 300);
        sf::Vector2f boxSize(80, 30);

        // A B
        // C D

        sf::Vector2f boxA(boxPos);
        sf::Vector2f boxB(boxPos.x + boxSize.x, boxPos.y);
        sf::Vector2f boxC(boxPos.x, boxPos.y + boxSize.y);
        sf::Vector2f boxD(boxPos.x + boxSize.x, boxPos.y + boxSize.y);

        if (localPosition.x >= boxA.x && localPosition.y >= boxA.y &&
            localPosition.x <= boxD.x && localPosition.y <= boxD.y)
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].data = ' ';
            }
            user = 'X';
            state = PlayState::PLAYING;
        }

        // draw a button with text "restart"
        sf::Text restart;
        restart.setFont(font);
        restart.setString("Restart");
        restart.setCharacterSize(24);
        restart.setFillColor(sf::Color(35, 44, 44));
        restart.setPosition(boxPos);
        restart.setOutlineColor(sf::Color(35, 44, 44));
        window.draw(restart);

        window.display();
    }

    return 0;
}

int main()
{
    draw_grid_and_response_mouse();
    return 0;
}

总结

花了 4 个半小时, 在先前写好了控制台版本 tic-tac-toe 的基础上, 使用 SFML 做了简陋的界面, 让游戏先运行起来能够正常玩; 然后制作了 SVG 图像, 仿照谷歌搜索结果里的在线版本的界面, 稍微美化了一下显示效果。

在使用 SFML 制作界面的过程中, 没有一上来就苛求制作好看的 ‘X’ 形状, 因为它后期可以重新调整; 本来引入了 DrawShape 枚举类型, 不过后来发现有点冗余, 在 class Grid 中用 char data 更方便。

对于绘制的最终结果, 不是一蹴而就的, 而是分别写了小型函数来验证, ‘X’ 可以单独绘制正确, 并且利用 class Grid 类的 draw() 方法, 根据落子的情况做了绘制(没有落子则不绘制)。

对于输赢局面的判别, 是先前控制台版本代码里有的, 直接拿来用了。 先写控制台版本看来确实可以加速迭代, 套着一套 GUI 的时候想逻辑, 需要对界面代码的编写比较熟悉才会不卡壳, 为了避免卡壳, 用熟悉的控制台去写, 是很方便的。

这个基于 SFML 的 tic-tac-toe 可以进一步扩展, 例如增加音效, 在获胜的时候用动态效果把三个棋子连接起来, 增加人机对战模式, 并利用 alpha-beta 剪枝算法进行搜索优化。文章来源地址https://www.toymoban.com/news/detail-826668.html

References

  • https://en.sfml-dev.org/forums/index.php?topic=21620.0
  • https://www.sfml-dev.org/tutorials/2.6/window-inputs.php
  • https://www.sfml-dev.org/tutorials/2.6/graphics-shape.php
  • https://github.com/skiff/TicTacToe
  • https://github.com/juchem/tic-tac-toe
  • https://svgedit.netlify.app/editor/index.html
  • https://www.nhooo.com/note/qa09md.html

到了这里,关于小游戏和GUI编程(6) | 基于 SFML 的井字棋的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 输入代码即可玩的小游戏,python简单编程小游戏

    大家好,本文将围绕python编写的入门简单小游戏有哪些展开说明,python编写的入门简单小游戏教程是一个很多人都想弄明白的事情,想搞清楚python编写的入门简单小游戏复制需要先了解以下几个事情。 大家好,小编来为大家解答以下问题,一些简单好玩的python编程游戏,py

    2024年02月21日
    浏览(46)
  • python小游戏代码200行左右,python编程小游戏代码

    大家好,本文将围绕python小游戏代码200行左右展开说明,小游戏程序代码python是一个很多人都想弄明白的事情,想搞清楚python编程小游戏代码需要先了解以下几个事情。 python编程小游戏代码 1、Python猜拳小游戏代码: 2、import random #导入随机模块 3、 4、num = 1 5、yin_num = 0 6、

    2024年02月04日
    浏览(44)
  • python简单小游戏代码教程,python编程小游戏简单的

    大家好,小编来为大家解答以下问题,一些简单好玩的python编程游戏,python编写的入门简单小游戏,今天让我们一起来看看吧! 哈喽铁子们 表弟最近在学Python,总是跟我抱怨很枯燥无味,其实,他有没有认真想过,可能是自己学习姿势不对? 比方说,可以通过打游戏来学编

    2024年04月23日
    浏览(39)
  • 用核桃编程编出来一款游戏,核桃编程小游戏制作

    大家好,本文将围绕核桃编程python简单小游戏代码大全展开说明,核桃编程python简单小游戏代码是一个很多人都想弄明白的事情,想搞清楚用核桃编程编出来一款游戏需要先了解以下几个事情。 for是一个扮腊循环,语法:厅乱滑陪凯 for i in iterable: 语句 注意:冒号后有

    2024年02月11日
    浏览(37)
  • 【Java编程指南】猜数字小游戏

    目录 一、前言 二、Random类 1.导包 2.创建对象 3.生成随机数 4.更改范围 5.演示 三、猜数字 1.需求 2.生成随机数范围 3.不限次数 4.规定次数 学习概述 :Java编程指南第6天、第7天我们学习了分支、循环结构,今天我们利用前面学过的知识点,加上一个 Random 类,动手完成一个 猜

    2023年04月08日
    浏览(44)
  • python编程游戏代码大全,python简单的小游戏代码

    大家好,本文将围绕python编程一个最简单游戏代码展开说明,20行python代码的入门级小游戏是一个很多人都想弄明白的事情,想搞清楚python游戏编程入门游戏代码需要先了解以下几个事情。 目标 :创建一个命令行游戏,游戏者可以在石头、剪刀和布之间进行选择,与计算机

    2024年02月11日
    浏览(62)
  • python编写小游戏的代码,python游戏编程代码大全

    大家好,小编来为大家解答以下问题,python游戏编程入门游戏代码,python编写小游戏的代码,现在让我们一起来看看吧! 大家好,本文将围绕python小游戏编程100例运行成功截图展开说明,python小游戏编程100例1000行源码是一个很多人都想弄明白的事情,想搞清楚python编写的入

    2024年02月19日
    浏览(66)
  • 学生学python编程---实现贪吃蛇小游戏+源码

    前言 这几年人工智能技术大发展,Python因此几乎成了第一位的语言。实际上,多年来,它不仅在软件工程师中得到广泛使用,也是各行业通用的专家语言,就是说,不管孩子以后做什么,都可能用得着。准备针对我自己上小学的孩子,每周抽出些时间,通过学习他们感兴趣的

    2023年04月09日
    浏览(41)
  • 小游戏实战丨基于PyGame的贪吃蛇小游戏

    本期内容:基于pygame的贪吃蛇小游戏 下载地址:https://download.csdn.net/download/m0_68111267/88700188 实验环境 python3.11及以上 pycharm pygame 安装pygame的命令: pygame是一个开源的Python模块,专门用于编写电子游戏。它使用简单、功能强大,因此非常适合初学者入门。pygame的主要特点包括图

    2024年02月03日
    浏览(57)
  • 小游戏实战丨基于Tkinter的五子棋小游戏

    本期内容:基于tkinter的五子棋小游戏 下载地址:https://download.csdn.net/download/m0_68111267/88700190 实验环境 python3.11及以上 pycharm tkinter Tkinter是Python的一个标准的图形用户界面(GUI)库,它是基于Tk工具包的。Tkinter库提供了GUI应用程序开发所需的各种组件和方法,方便开发者快速创建

    2024年01月22日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包