c++俄罗斯方块
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;
}
编译运行:如果报错,关闭杀毒软件
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) {
}
在加载图片时可能报红,右键项目-属性-高级:
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() {
}
启动效果图:
测试一下随机方块渲染:每次调用时创建Block
对象并渲染
void Tetris::updateWindows() {
// 测试方块
putimage(0, 0, &this->bg);
Block block;
block.draw(this->leftMargin, this->topMargin);
}
效果图:
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;
}
启动效果图:
此外,当方块降落到底部时,应该固定。所以在刷新窗口页面渲染,方法很多,这里使用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);
}