c++俄罗斯方块

赖卓成2024年3月19日
大约 13 分钟

02-项目创建

创建空的c++项目。创建两个类,定义好方法,生成代码:

Block.h

#pragma once
class Block {
public:
	// 构造函数
	Block();

	// 下降
	void drop();

	// 左右移动
	void moveLeftRight(int offset);

	// 旋转
	void retate();

	// 绘制 左边界、上边界
	void draw(int leftMargin,int topMargin);


};

Block.cpp

#include "Block.h"

Block::Block() {
}

void Block::drop() {
}

void Block::moveLeftRight(int offset) {
}

void Block::retate() {
}

void Block::draw(int leftMargin, int topMargin) {
}

Tetris.h

#pragma once
class Tetris {
public:
	//  构造函数 行数、列数、从哪里开始降落(距离左侧和顶部)、方块大小
	Tetris(int rows,int clos,int left,int top,int blockSize);

	// 初始化
	void init();

	// 开始游戏
	void play();
};


Tetris.cpp

#include "Tetris.h"

Tetris::Tetris(int rows, int clos, int left, int top, int blockSize) {
}

void Tetris::init() {
}

void Tetris::play() {
}

main.cpp

#include "Tetris.h"

int main() {
	// 在栈上创建一个名为game的Tetris对象,并调用构造函数初始化对象的状态。栈内存不需要手动释放
	// 参数需要根据背景图计算
	Tetris game(20,10,163,133,36);
	game.play();
	return 0;
}

编译运行:如果报错,关闭杀毒软件

image-20240319221354896

03-游戏主循环

写个死循环,到指定时间就刷新画面,定义私有变量记录时间,哨兵标记是否刷新(方便用户按键时不需要等待,直接刷新),声明延时、刷新画面、清行等方法:

#pragma once
class Tetris {
public:
	//  构造函数 行数、列数、从哪里开始降落(距离左侧和顶部)、方块大小
	Tetris(int rows,int clos,int left,int top,int blockSize);

	// 初始化
	void init();

	// 开始游戏
	void play();

private:
	// 接收用户输入
	void keyEvent();

	// 渲染游戏界面
	void updateWindows();

	// 返回距离上一次渲染的时间,第一次调用时返回0
	int getDelay();

	// 方块降落
	void drop();
	
	// 计算,清行
	void clearLine();


private:
	// 延迟参数,多久渲染一次画面
	int delay;

	// 控制是否渲染
	bool update;
};


#pragma once
class Tetris {
public:
	//  构造函数 行数、列数、从哪里开始降落(距离左侧和顶部)、方块大小
	Tetris(int rows,int clos,int left,int top,int blockSize);

	// 初始化
	void init();

	// 开始游戏
	void play();

private:
	// 接收用户输入
	void keyEvent();

	// 渲染游戏界面
	void updateWindows();

	// 返回距离上一次渲染的时间,第一次调用时返回0
	int getDelay();

	// 方块降落
	void drop();
	
	// 计算,清行
	void clearLine();


private:
	// 延迟参数,多久渲染一次画面
	int delay;

	// 控制是否渲染
	bool update;
};


04-创建方块

参考:点击跳转

一共有7中方块,每个大方块中有4个小方块,定义二位数组,表示这7种方块,二维数组用于记录小方块的值:

	// 定义7中方块类型
	int blocks[7][4] = {
		1,3,5,7, // I
		2,4,5,7, // Z 1型
		3,5,4,6, // Z 2型
		3,5,4,7, // T
		2,3,5,7, // L
		3,5,7,6, // J
		2,3,4,5, // 田
	};

在方块类中定义结构体,记录小方块:

	// 定义结构体,用于表示大方块中每个小方块具体在第几行第几列
	struct Point {
		int row;
		int col;
	};
private:
	// 方块类型
	int blockType;

	// 小方块
	Point smallBlocks[4];

随机生成一种方块:取随机数

#include <stdlib.h>
	// 随机生成一种
	this->blockType = rand() % 7 + 1;

在游戏初始化方法中置随机数种子:

#include <stdlib.h>
#include <time.h>


void Tetris::init() {
	this->delay = 30;
	// 置随机数种子
	srand(time(NULL));
}

随机生成后,计算大方块中,每个小方块的具体位置:

	// 计算小方块的具体位置,每个大方块有4个小方块
	for (size_t i = 0; i < 4; i++) {
		int value = blocks[blockType-1][i];
		// 经过观察,方块的具体位置row为值除以2,col为值对2取余
		 this->smallBlocks[i].row = value / 2;
		 this->smallBlocks[i].col = value % 2;
	}

完整代码:

Block.h:

#pragma once
class Block {

	// 定义结构体,用于表示大方块中每个小方块具体在第几行第几列
	struct Point {
		int row;
		int col;
	};
public:
	// 构造函数
	Block();

	// 下降
	void drop();

	// 左右移动
	void moveLeftRight(int offset);

	// 旋转
	void retate();

	// 绘制 左边界、上边界
	void draw(int leftMargin,int topMargin);

private:
	// 方块类型
	int blockType;

	// 小方块
	Point smallBlocks[4];

};


Block.cpp:

#include "Block.h"
#include <stdlib.h>

Block::Block() {
	// 定义7中方块类型
	int blocks[7][4] = {
		1,3,5,7, // I
		2,4,5,7, // Z 1型
		3,5,4,6, // Z 2型
		3,5,4,7, // T
		2,3,5,7, // L
		3,5,7,6, // J
		2,3,4,5, // 田
	};

	// 随机生成一种
	this->blockType = rand() % 7 + 1;

	// 计算小方块的具体位置,每个大方块有4个小方块
	for (size_t i = 0; i < 4; i++) {
		int value = blocks[blockType-1][i];
		// 经过观察,方块的具体位置row为值除以2,col为值对2取余
		 this->smallBlocks[i].row = value / 2;
		 this->smallBlocks[i].col = value % 2;
	}

}

void Block::drop() {
}

void Block::moveLeftRight(int offset) {
}

void Block::retate() {
}

void Block::draw(int leftMargin, int topMargin) {
}

Tetris.cpp:

#pragma once
class Tetris {
public:
	//  构造函数 行数、列数、从哪里开始降落(距离左侧和顶部)、方块大小
	Tetris(int rows,int clos,int left,int top,int blockSize);

	// 初始化
	void init();

	// 开始游戏
	void play();

private:
	// 接收用户输入
	void keyEvent();

	// 渲染游戏界面
	void updateWindows();

	// 返回距离上一次渲染的时间,第一次调用时返回0
	int getDelay();

	// 方块降落
	void drop();
	
	// 计算,清行
	void clearLine();


private:
	// 延迟参数,多久渲染一次画面
	int delay;

	// 控制是否渲染
	bool update;
};


05-创建方块的图像纹理

绘制图形,需要先安装easyx图形库。点击跳转

在Block.h中引入:

#include <graphics.h>

在Block类中添加图片指针变量用于指向具体的图片对象,定义静态图片指针数组存储固定不变的7中小方块。

private:
	// 方块类型
	int blockType;

	// 小方块
	Point smallBlocks[4];

	// 图片变量,因为游戏中的方块图片都是一样的,所以使用指针即可
	IMAGE *img;

	// 一共有7种大方块,我们用不同的颜色表示,所以定义一个静态图片指针数组用来存储,定义静态变量用于存储方块大小
	static IMAGE* images[7] ;
	static int size;

在Block类中初始化:

// 在类外部对静态数组images进行了定义和初始化
IMAGE* Block::images[7] = { NULL };
// 初始化方块大小
int Block::size = 36;

构造函数中添加判断,如果第一次为NULL,则加载图片进行切割:

	// 第一次判断图片是否加载到静态数组,未加载则加载
	if (images[0] == NULL) {
		// 临时变量用于加载图片,进行切割
		IMAGE imgTmp;
		loadimage(&imgTmp,"res/tiles.png");
		// 切割
		SetWorkingImage(&imgTmp);
		for (size_t i = 0; i < 7; i++) {
			images[i] = new IMAGE();
			// 图片对象,切割的x,y,宽度,高度
			getimage(images[i], i*size,0,size,size);
		}
		// 恢复工作区
		SetWorkingImage();

	}

上面已经切割好图片,并创建了图片对象,将对象地址记录在数组中了,所以在生成方块时,直接将方块对象中的图片指针指向图片数组对应的图片即可:

		 this->img = images[blockType-1];

完整代码:

Block.h:

#pragma once
#include <graphics.h>
class Block {

	// 定义结构体,用于表示大方块中每个小方块具体在第几行第几列
	struct Point {
		int row;
		int col;
	};
public:
	// 构造函数
	Block();

	// 下降
	void drop();

	// 左右移动
	void moveLeftRight(int offset);

	// 旋转
	void retate();

	// 绘制 左边界、上边界
	void draw(int leftMargin,int topMargin);

private:
	// 方块类型
	int blockType;

	// 小方块
	Point smallBlocks[4];

	// 图片变量,因为游戏中的方块图片都是一样的,所以使用指针即可
	IMAGE *img;

	// 一共有7种大方块,我们用不同的颜色表示,所以定义一个静态图片指针数组用来存储,定义静态变量用于存储方块大小
	static IMAGE* images[7] ;
	static int size;



};


Block.cpp:

#include "Block.h"
#include <stdlib.h>

// 在类外部对静态数组images进行了定义和初始化
IMAGE* Block::images[7] = { NULL };
// 初始化方块大小
int Block::size = 36;

Block::Block() {

	// 第一次判断图片是否加载到静态数组,未加载则加载
	if (images[0] == NULL) {
		// 临时变量用于加载图片,进行切割
		IMAGE imgTmp;
		loadimage(&imgTmp,"res/tiles.png");
		// 切割
		SetWorkingImage(&imgTmp);
		for (size_t i = 0; i < 7; i++) {
			images[i] = new IMAGE();
			// 图片对象,切割的x,y,宽度,高度
			getimage(images[i], i*size,0,size,size);
		}
		// 恢复工作区
		SetWorkingImage();

	}


	// 定义7中方块类型
	int blocks[7][4] = {
		1,3,5,7, // I
		2,4,5,7, // Z 1型
		3,5,4,6, // Z 2型
		3,5,4,7, // T
		2,3,5,7, // L
		3,5,7,6, // J
		2,3,4,5, // 田
	};

	// 随机生成一种
	this->blockType = rand() % 7 + 1;

	// 计算小方块的具体位置,每个大方块有4个小方块
	for (size_t i = 0; i < 4; i++) {
		int value = blocks[blockType-1][i];
		// 经过观察,方块的具体位置row为值除以2,col为值对2取余
		 this->smallBlocks[i].row = value / 2;
		 this->smallBlocks[i].col = value % 2;
	}
     this->img = images[blockType-1];

}

void Block::drop() {
}

void Block::moveLeftRight(int offset) {
}

void Block::retate() {
}

void Block::draw(int leftMargin, int topMargin) {
}

在加载图片时可能报红,右键项目-属性-高级:

image-20240320000927620

06-绘制俄罗斯方块

void Block::draw(int leftMargin, int topMargin) {

	for (size_t i = 0; i < 4; i++) {
		int x = this->smallBlocks[i].col * size + leftMargin;
		int y = this->smallBlocks[i].row * size + topMargin;
		putimage(x, y, this->img);
	}

}

07-存储游戏数据

补充Tetris成员变量:

private:
	// 延迟参数,多久渲染一次画面
	int delay;

	// 控制是否渲染
	bool update;

	// 游戏参数,多少行,多少列,左边边距,顶部边距,方块大小,在构造函数中进行赋值
	int rows;
	int cols;
	int leftMargin;
	int topMargin;
	int blockSize;

	// 背景图
	IMAGE bg;

	// 定义动态二维数组,记录游戏中的方块数据 0为空白,其他数字对应静态数组images中的小方块
	vector<vector<int>> data;
};

编写构造函数:

Tetris::Tetris(int rows, int clos, int left, int top, int blockSize) {
	// 初始化变量
	this->rows = rows;
	this->cols = clos;
	this->leftMargin = left;
	this->topMargin = top;
	this->blockSize = blockSize;

	// 初始化容器
	for (size_t i = 0; i < rows; i++) {
		// 创建第二维数组
		vector<int> dataRow;
		for (size_t j = 0; j < clos; j++) {
			dataRow.push_back(0);
		}
		// 第二维数组添加到第一维数组中
		data.push_back(dataRow);
	}
}

08-实现游戏场景

编写Tetris中的initi方法:创建游戏创建,加载背景图到内存中,初始化游戏数据,这里只加载背景图,不放到界面中

void Tetris::init() {
	this->delay = SPEED_NOMAR;
	// 置随机数种子
	srand(time(NULL));

	// 创建游戏窗口
	initgraph(938, 896);
	// 加载背景图片
	loadimage(&this->bg, "res/bg2.png");
	// 初始化游戏中的数据,全置0
	for (size_t i = 0; i < this->data.size(); i++) {
		for (size_t j = 0; j < this->data[i].size(); j++) {
			this->data[i][j] = 0;
		}
	}
}

在刷新界面时,把图片加进来:

void Tetris::updateWindows() {
	putimage(0, 0, &this->bg);
}

编写延迟方法:

int Tetris::getDelay() {
	// 静态变量,第一次为0,
	 static unsigned long long lastTime = 0;
	// 获取开机后,cpu时钟
	 unsigned long long currentTime =  GetTickCount();
	 if (lastTime == 0) {
		 lastTime = currentTime;
		 return 0;
	 } else {
		 int ret = currentTime - lastTime;
		 lastTime = currentTime;
		 return ret;
	 }

}

完整Tetris.cpp代码:

#include "Tetris.h"
#include <stdlib.h>
#include <time.h>

// 定义常量,游戏难度对应的刷新页面的时间
const  int SPEED_NOMAR = 500;
const  int SEEED_QUICK = 50;

Tetris::Tetris(int rows, int clos, int left, int top, int blockSize) {
	// 初始化变量
	this->rows = rows;
	this->cols = clos;
	this->leftMargin = left;
	this->topMargin = top;
	this->blockSize = blockSize;

	// 初始化容器
	for (size_t i = 0; i < rows; i++) {
		// 创建第二维数组
		vector<int> dataRow;
		for (size_t j = 0; j < clos; j++) {
			dataRow.push_back(0);
		}
		// 第二维数组添加到第一维数组中
		data.push_back(dataRow);
	}
}

void Tetris::init() {
	this->delay = SPEED_NOMAR;
	// 置随机数种子
	srand(time(NULL));

	// 创建游戏窗口
	initgraph(938, 896);
	// 加载背景图片
	loadimage(&this->bg, "res/bg2.png");
	// 初始化游戏中的数据,全置0
	for (size_t i = 0; i < this->data.size(); i++) {
		for (size_t j = 0; j < this->data[i].size(); j++) {
			this->data[i][j] = 0;
		}
	}


}

void Tetris::play() {
	// 调用初始化函数
	this->init();

	// 计时,
	int timer = 0;

	while (true) {
		// 接收用户输入
		this->keyEvent();

		// 计算是否到了自动渲染的时间
		timer += this->getDelay();
		if (timer >= this->delay) {
			timer = 0;
			// 方块降落
			this->drop();
			this->update = true;
		}

		if (this->update) {
			// 渲染游戏画面
			this->updateWindows();
			this->update = false;

			// 更新游戏数据,清行
			this->clearLine();
		}
	}
}

void Tetris::keyEvent() {
}

void Tetris::updateWindows() {
	putimage(0, 0, &this->bg);
}

int Tetris::getDelay() {
	// 静态变量,第一次为0,
	 static unsigned long long lastTime = 0;
	// 获取开机后,cpu时钟
	 unsigned long long currentTime =  GetTickCount();
	 if (lastTime == 0) {
		 lastTime = currentTime;
		 return 0;
	 } else {
		 int ret = currentTime - lastTime;
		 lastTime = currentTime;
		 return ret;
	 }

}

void Tetris::drop() {
}

void Tetris::clearLine() {
}

启动效果图:

image-20240320020957785

测试一下随机方块渲染:每次调用时创建Block对象并渲染

void Tetris::updateWindows() {
	// 测试方块
	putimage(0, 0, &this->bg);
	Block block;
	block.draw(this->leftMargin, this->topMargin);
}

效果图:

image-20240326003807911

09-实现预告方块、优化方块的渲染

上面只是生成了测试方块,接下来做预告方块和正在降落方块的渲染。

Tetris中定义静态变量,用于记录当前方块和下一个方块:

	// 当前方块和下一个方块
	Block *currentBlock;
	Block *nextBlock;

游戏开始时Tetris::play(),创建当前方块和下一个方块:

	// 游戏开始,创建当前方块和预告方块
	this->currentBlock = new Block;
	this->nextBlock = new Block;

计算好位置,修改刷新出口方法Tetris::updateWindows()

void Tetris::updateWindows() {
	// 测试方块
	putimage(0, 0, &this->bg);
	this->currentBlock->draw(this->leftMargin, this->topMargin);
	this->nextBlock->draw(this->leftMargin - 237, this->topMargin + 33);

	//Block block;
	//block.draw(this->leftMargin, this->topMargin);
}

主方法:

int main() {
	// 在栈上创建一个名为game的Tetris对象,并调用构造函数初始化对象的状态。栈内存不需要手动释放
	// 参数需要根据背景图计算
	Tetris game(20,10,400,100,36);
	game.play();
	return 0;
}

启动效果图:

image-20240326010222640

此外,当方块降落到底部时,应该固定。所以在刷新窗口页面渲染,方法很多,这里使用Tetris中之前定义的用于保存游戏数据的变量 vector<vector<int>> data;

渲染时需要用到方块图片,由于之前定义为私有静态指针数组,不方便获取,所以定义一个public方法用于获取:

	// 由于images数组是私有且静态的,不方便获取,所以定义一个静态方法来获取,需要用二级指针
	static IMAGE** getImages();
IMAGE** Block::getImages() {
	return images;
}

原静态变量长这样:

	static IMAGE* images[7] ;

这里为什么用二级指针?因为这个静态变量是指针数组,里面的每个值都是指向图片对象的指针,如果不用二级指针接收,直接返回一级指针,则是返回了数组首地址指向的指针(指针数组第0个元素的地址),我们的目的是返回整个数组,所以返回值是二级指针,相当于对数组变量取地址,使用时 IMAGE** images = Block::getImages();,images[this->data[i][j]-1]就是对指针数组+指针步长来获取一级指针,得到的是指向图片对象的指针。

在刷新窗口时循环渲染游戏数据:

void Tetris::updateWindows() {

	putimage(0, 0, &this->bg);

	// 用于获取当前渲染的是哪一种方块
	IMAGE** images = Block::getImages();

	// 优化屏幕闪烁
	BeginBatchDraw();

	for (int i = 0; i < this->rows; i++) {
		for (int j = 0; j < this->cols; j++) {
			// 如果方块中的数据为0,则跳过,无需渲染
			if (this->data[i][j] == 0) {
				continue;
			}
			// y:当前第几行*方块大小+距离顶部的距离,x:当前第几列*方块大小+距离左边界的距离
			int y = this->blockSize * i + this->topMargin;
			int x = this->blockSize * j + this->leftMargin;

			// 渲染方块,data[i][j]记录的是哪一种颜色的方块(1-7)
			putimage(x, y, images[this->data[i][j]-1]);

		}
	}

	this->currentBlock->draw(this->leftMargin, this->topMargin);
	this->nextBlock->draw(this->leftMargin - 237, this->topMargin + 33);

	// 优化屏幕闪烁
	EndBatchDraw();
	// 测试方块
	//Block block;
	//block.draw(this->leftMargin, this->topMargin);
}
Loading...