文章目录

回顾

上一个博客里我们只是简单地显示了一个窗口,这次我们把主要的游戏逻辑给它加进去。这一部分里我们要做的任务有:

  1. 控制帧率:即每秒渲染多少帧;
  2. 用户交互:处理用户的鼠标点击事件;
  3. 完成相关棋子的渲染。

完整代码已经放上github了,在这里
Connect Four四子棋c++程序 - 用户交互(1)-LMLPHP

用户交互

先把整段代码放出来

// 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张图片
Connect Four四子棋c++程序 - 用户交互(1)-LMLPHP
接着看游戏的主循环:

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休息一下
	}
}

游戏主循环里分别做了如下几件事:

  1. 处理用户交互事件,如鼠标点击;
  2. 清屏,清除上一帧的东西;
  3. 把棋子之类的画上去;
  4. 刷新屏幕;
  5. 决定是否要等待一些时间,一般帧率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);
	}
}

主要三种事件:

  1. 用户点击了右上角的退出按钮;
  2. 用户移动了鼠标;
  3. 用户点击了空白棋子的未知。

第一个事件我们独立处理,后面两种统一在鼠标事件中处理handleMouseEvent()。鼠标事件的处理简单说一下:

  1. 获取鼠标事件的位置x和y,只处理棋局范围内的鼠标事件
  2. 计算出鼠标所在的格子;
  3. 如果是点击事件,我们在BUTTONUP的时候还要把棋子放上去,并且切换玩家switchPlayer()

紧接着,我们要把游戏画面渲染出来renderScreen(),也要做两件事情:

  1. renderPieces(): 把棋子画上去,有三种棋子:SPACE、RED_PLAYER和BLACK_PLAYER;
  2. renderPiecePreview: 当鼠标在空白棋子位置时,我们预先显示该玩家的棋子。这个也很简单,因为前面鼠标事件的时候我们记录了当前鼠标所在的格子_mousePos,如果这个格子是空的,我们就把当前玩家的棋子预先显示在这个格子上。为了有一种动画的效果,代码理preview的时候我把棋子缩小了一点。
06-23 08:45