文章目录
回顾
上一个博客里我们只是简单地显示了一个窗口,这次我们把主要的游戏逻辑给它加进去。这一部分里我们要做的任务有:
- 控制帧率:即每秒渲染多少帧;
- 用户交互:处理用户的鼠标点击事件;
- 完成相关棋子的渲染。
完整代码已经放上github了,在这里
用户交互
先把整段代码放出来
// connect_four_1.h
#ifndef CONNECT_FOUR_1_H
#define CONNECT_FOUR_1_H
#include <SDL.h>
#include <SDL_ttf.h>
#include <SDL_image.h>
#include <cstdio>
#include <vector>
#include <string>
using namespace std;
SDL_Window *gWindow = nullptr;
SDL_Renderer *gRenderer = nullptr;
constexpr int GRID_SIZE = 50;
constexpr int SPACE = 0;
constexpr int RED_PLAYER = 1;
constexpr int BLACK_PLAYER = 2;
class ConnectFour {
public:
ConnectFour() = default;
ConnectFour(int nbWGrids, int nbHGrids) :
_nbWGrids(nbWGrids),
_nbHGrids(nbHGrids),
_nbGrids(nbWGrids * nbHGrids),
_contents(nbWGrids * nbHGrids, SPACE)
{
_winHeight = GRID_SIZE * nbHGrids;
_winWidth = GRID_SIZE * nbWGrids;
}
void start() {
pre_run();
run();
}
private:
int _nbHGrids = 0;
int _nbWGrids = 0;
int _nbGrids = 0;
int _winHeight = 0;
int _winWidth = 0;
bool _running = false;
int _player = RED_PLAYER;
int _mousePos = -1;
int _lastPos = -1;
vector<int> _contents;
SDL_Texture *_grayCircle = nullptr;
SDL_Texture *_redCircle = nullptr;
SDL_Texture *_blackCircle = nullptr;
SDL_Surface *_icon = nullptr;
void pre_run() {
gWindow = SDL_CreateWindow("Connect Four", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, _winWidth, _winHeight, SDL_WINDOW_SHOWN);
gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED);
_grayCircle = loadFromFile("images\\gray.png");
_redCircle = loadFromFile("images\\red.png");
_blackCircle = loadFromFile("images\\blue.png");
_icon = IMG_Load("images\\icon.png");
SDL_SetWindowIcon(gWindow, _icon);
}
SDL_Texture *loadFromFile(string path) {
SDL_Texture *texture = nullptr;
SDL_Surface *surface = IMG_Load(path.c_str());
if (surface) {
texture = SDL_CreateTextureFromSurface(gRenderer, surface);
SDL_FreeSurface(surface);
}
else {
printf("Failed to load `%s`! IMG Error: %s\n", path.c_str(), IMG_GetError());
}
return texture;
}
void run() {
_running = true;
Uint32 tic, elapsed;
while (_running) {
tic = SDL_GetTicks();
handleEvent();
clearScreen();
renderScreen();
SDL_RenderPresent(gRenderer);
elapsed = SDL_GetTicks() - tic;
if (elapsed < 30)
SDL_Delay(30 - elapsed);
}
}
void handleEvent() {
SDL_Event e;
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT)
_running = false;
handleMouseEvent(e);
}
}
void handleMouseEvent(SDL_Event &e) {
_mousePos = -1;
if (e.type == SDL_MOUSEMOTION || e.type == SDL_MOUSEBUTTONUP) {
int x, y;
SDL_GetMouseState(&x, &y);
if (x < 0 || y < 0 || x >= _winWidth || y >= _winHeight)
return;
int gx = x / GRID_SIZE, gy = y / GRID_SIZE;
_mousePos = gx + gy * _nbWGrids;
if (e.type == SDL_MOUSEBUTTONUP) {
if (_contents[_mousePos] == SPACE) {
_contents[_mousePos] = _player;
_lastPos = _mousePos;
switchPlayer();
}
}
}
}
void switchPlayer() {
_player = _player == BLACK_PLAYER ? RED_PLAYER : BLACK_PLAYER;
}
void clearScreen() {
SDL_SetRenderDrawColor(gRenderer, 0xff, 0xff, 0xff, 0xff);
SDL_RenderClear(gRenderer);
}
void renderScreen() {
renderPieces();
renderPiecePreview();
}
void renderPieces() {
for (int i = 0; i < _nbGrids; i++) {
int gx = i % _nbWGrids, gy = i / _nbWGrids;
SDL_Rect rect = { gx * GRID_SIZE, gy * GRID_SIZE, GRID_SIZE, GRID_SIZE };
if (_contents[i] == SPACE) {
SDL_RenderCopy(gRenderer, _grayCircle, nullptr, &rect);
}
else {
SDL_Texture *target = _contents[i] == BLACK_PLAYER ? _blackCircle : _redCircle;
SDL_RenderCopy(gRenderer, target, nullptr, &rect);
}
}
}
void renderPiecePreview() {
if (_mousePos != -1 && _contents[_mousePos] == SPACE) {
SDL_Texture *target = _player == BLACK_PLAYER ? _blackCircle : _redCircle;
int gx = _mousePos % _nbWGrids, gy = _mousePos / _nbWGrids;
SDL_Rect rect = { gx * GRID_SIZE, gy * GRID_SIZE, GRID_SIZE, GRID_SIZE };
SDL_SetRenderDrawColor(gRenderer, 0xff, 0xff, 0xff, 0xff);
SDL_RenderFillRect(gRenderer, &rect);
int s = 5;
rect = { gx * GRID_SIZE + s, gy * GRID_SIZE + s, GRID_SIZE - 2 * s, GRID_SIZE - 2 * s };
SDL_RenderCopy(gRenderer, target, nullptr, &rect);
}
}
};
我们首先需要一个SDL_Renderer
,就是用来把图片画到窗口的东西。同样我们用一个全局变量来保存它。另外,四子棋每个棋子位置都有三种状态:空的(SPACE)、红棋(RED_PLAYER)和黑棋(BLACK_PLAYER),这里分别对应3个常量。
我们还需要一个变量来保存整个棋局的状态。虽然棋局是一个二维的矩阵,但我们也可以把它表示成为一维的数组,到时候再把相应的x和y计算出来就好了,这里我们采用一维数组的方式,用成员_contents
来保存。
分别介绍一下新引入的成员变量:
bool _running = false; // 是否继续执行游戏的主循环
int _player = RED_PLAYER; // 记录当前下棋的玩家是哪一方,红方或者黑方
int _mousePos = -1; // 记录当前鼠标在哪一个格子里
int _lastPos = -1; // 记录上一次玩家落子的格子位置
vector<int> _contents; // 记录整个棋局的状况,哪些是黑的,哪些是红的,哪些是空白的
SDL_Texture *_grayCircle = nullptr; // 存储空白棋子的图片
SDL_Texture *_redCircle = nullptr; // 存储红旗的图片
SDL_Texture *_blackCircle = nullptr; // 存储黑骑的图片
SDL_Surface *_icon = nullptr; // 存储这个游戏程序的图标图片,一般显示在窗口左上角
在pre_run()
函数里,我们先把需要的renderer以及图片都加载进来(加载图片的辅助函数loadFromFile
自己看上面的代码了):
void pre_run() {
gWindow = SDL_CreateWindow("Connect Four", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, _winWidth, _winHeight, SDL_WINDOW_SHOWN);
gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED);
_grayCircle = loadFromFile("images\\gray.png");
_redCircle = loadFromFile("images\\red.png");
_blackCircle = loadFromFile("images\\blue.png");
_icon = IMG_Load("images\\icon.png");
SDL_SetWindowIcon(gWindow, _icon);
}
即如下4张图片
接着看游戏的主循环:
void run() {
_running = true;
Uint32 tic, elapsed;
while (_running) {
tic = SDL_GetTicks();
handleEvent(); // 处理用户交互事件,如鼠标点击
clearScreen(); // 先清屏
renderScreen(); // 然后把棋子之类的画上去
SDL_RenderPresent(gRenderer); // 刷新屏幕
elapsed = SDL_GetTicks() - tic; // 看一下一次循环用时多少
if (elapsed < 30)
SDL_Delay(30 - elapsed); // 如果太快了,可以让CPU休息一下
}
}
游戏主循环里分别做了如下几件事:
- 处理用户交互事件,如鼠标点击;
- 清屏,清除上一帧的东西;
- 把棋子之类的画上去;
- 刷新屏幕;
- 决定是否要等待一些时间,一般帧率fps = 30就可以了,这里我们把每次循环都控制在30ms,帧率也就大概在1000 / 30 = 33左右。
处理交互事件
void handleEvent() {
SDL_Event e;
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT)
_running = false;
handleMouseEvent(e);
}
}
主要三种事件:
- 用户点击了右上角的退出按钮;
- 用户移动了鼠标;
- 用户点击了空白棋子的未知。
第一个事件我们独立处理,后面两种统一在鼠标事件中处理handleMouseEvent()
。鼠标事件的处理简单说一下:
- 获取鼠标事件的位置x和y,只处理棋局范围内的鼠标事件
- 计算出鼠标所在的格子;
- 如果是点击事件,我们在BUTTONUP的时候还要把棋子放上去,并且切换玩家
switchPlayer()
;
紧接着,我们要把游戏画面渲染出来renderScreen()
,也要做两件事情:
renderPieces()
: 把棋子画上去,有三种棋子:SPACE、RED_PLAYER和BLACK_PLAYER;renderPiecePreview
: 当鼠标在空白棋子位置时,我们预先显示该玩家的棋子。这个也很简单,因为前面鼠标事件的时候我们记录了当前鼠标所在的格子_mousePos
,如果这个格子是空的,我们就把当前玩家的棋子预先显示在这个格子上。为了有一种动画的效果,代码理preview的时候我把棋子缩小了一点。