小游戏和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();
获取鼠标位置
以获取到的鼠标点击位置为中心, 上下左右各自扩展 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。 效果:
对应代码:
#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();
每个网格内,如果鼠标有点击则执行绘制
根据前一步绘制的网格, 可以确定每个小格子的坐标范围。 根据鼠标点击获取的位置,遍历每个格子, 如果是在当前格子内部, 那么执行绘制。 简单起见, 为了区分绘制内容,只要点击了当前小格子就执行绘制.
先将刚刚回执的网格的代码做重构, 根据起始点 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);
}
}
效果:
完整代码:
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 个格子的范围内,也是落子无效。
#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);
效果如下:
完整代码是
#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. 最终结果
运行效果:
代码:
#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 的时候想逻辑, 需要对界面代码的编写比较熟悉才会不卡壳, 为了避免卡壳, 用熟悉的控制台去写, 是很方便的。文章来源:https://www.toymoban.com/news/detail-826668.html
这个基于 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模板网!