C++/SDL实现打砖块游戏(2)——Game类(编写中)

Game类是对一局游戏的抽象。它保存着游戏的所有数据,拥有一些游戏逻辑层面的方法。如:开始新游戏、重置滑板、道具生效等。

class Game
{
public:
	Game(void);
	~Game(void);
	

	// 分数、命数
	int score;
	int life;
	bool paused;
	int level;
	// 板
	Paddle paddle;
	// 球
	list<Ball*> balls;
	//Ball *balls[3];
	// 砖块
	//Block **blocks;
	list<Block*> blocks;
	// 道具
	list<Item*> items;
	// 动画
	list<Animation*> animations;

	// 新游戏
	void new_game();
	// 读取关卡
	void load_stage(int level);
	// 装载材质
	void set_textures(SDL_Texture *textures[]);
	// 有关道具的函数
	void improve_ball();
	void triple_ball();
	// 重置滑板
	void reset();

	bool is_clear();
};

成员变量

score、life、level 分别是分数、生命数、关卡号。

paused是暂停标志,如果是true,window类会停止更新游戏状态。

paddle是滑板对象。

成员函数

new_game()用于开始一局新的游戏。要做的操作有:清空砖块、小球列表,重置滑板位置、长度、生命数等信息。

improve_ball()是用来实现小球升级道具的效果的函数,会将小球升级为高级版本(如果有的话)。P.S.我在考虑是否要把所有用于实现道具效果的函数加个命名前缀

  我没有找到什么好的办法来确定小球当前的类型,最后使用了RTTI一个一个试......=。= ,因为我觉得如果给小球类加个变量表示它是什么类型,好像还不如干脆不集成这些类,直接用一个类的成员变量来区别小球呢......如果有更好的办法,请不吝赐教。

void Game::improve_ball()
{
	Ball *temp = nullptr;
	for(list<Ball*>::iterator ball = balls.begin(); ball!=balls.end(); ++ball)
	{
		if(*ball)
		{
			// 这里把派生类当成基类了
			cout << typeid(*ball).name();
			if(dynamic_cast<NormalBall*>(*ball) != nullptr)
			{
				temp = new GravityBall();
				temp->set_geometry((*ball)->get_geometry());
				temp->speed = (*ball)->speed;
				temp->set_texture((*ball)->textures);
				temp->launch();
				delete (*ball);
				*ball = temp;
				// TODO: 这里以后可以用拷贝构造函数重写
			}
			else if(dynamic_cast<GravityBall*>(*ball) != nullptr)
			{
				temp = new SuperBall();
				temp->set_geometry((*ball)->get_geometry());
				temp->speed = (*ball)->speed;
				temp->set_texture((*ball)->textures);
				temp->launch();
				delete (*ball);
				*ball = temp;
			}
		}
	}
}

 

C++/SDL实现打砖块游戏(1)

这是我的面向对象程序设计实习题目,发上来与各位分享。

git地址:https://github.com/DEAGS3000/BlockBreaker-with-SDL2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

之所以选择SDL,是因为它有自动控制帧率的功能。之前我使用WIN32 API编程的时候,仅仅是刷新文字就会出现难以消除的闪动状况。为了避免再次踩坑,这次选择了SDL。

首先,我们需要一个Window类来封装一下(姑且算是)SDL的功能。

有如下几个目标:

1. 在构造函数中初始化SDL及其模块,在析构函数中卸载这些内容。

2. 包含读取游戏要用到的素材的操作。

 

class Window
{
public:

	SDL_Window *ptr;
	SDL_Renderer *renderer;
	SDL_Texture *textures[30];
	SDL_Texture *background;
	Game *game;
	TTF_Font *font;
	bool game_started;

    Window(void);
	~Window(void);
	// 初始化
	void init();
	// 重绘
	void refresh();
	// 更新
	void update();
	// 处理碰撞
	void handle_collision();
	// 读取图像
	SDL_Texture* load_image(string path);
	SDL_Texture* render_text(string message);
	bool quit;
	bool mouse_on_button;
	SDL_Rect option_rect;
};

我对于这个类的定位是:处理所有图像和物理层面上的功能。

成员变量

ptr当然就是SDL创建的窗口的指针。实际上ptr这个变量名也就是会在析构的时候用一下而已。

renderer是渲染器。我个人的理解是,这玩意儿类似于WIN32的hdc,需要通过它才能将数据画到屏幕上。如果你不知道这个东西,可以先看看SDL的教程。

textures[30]这个数组存储了游戏需要的所有贴图。之所以这样做,是因为这个游戏很小,需要用到的贴图不多。我的思路是:既然两个相同的图样部件持有的都是其贴图的指针而非一个副本(当然得这样做),那么贴图可以由Window统一管理,具体的类只要知道自己具体取用其中的哪一个就行。这个设计通过宏定义来实现,把图像部件的名称对应到整数索引上。

#define BACKGROUND				0		// 背景
#define STICK					1		// 板子
#define NORMAL_BALL				2		// 普通球
#define GRAVITY_BALL			3		// 重力球
#define SUPER_BALL				4		// 穿透球
#define NORMAL_BLOCK			5		// 普通砖块
#define HARD_BLOCK				6		// 硬砖块
#define ITEM_BLOCK				7		// 物品砖块
#define STEEL_BLOCK				8		// 钢砖块
#define HALF_BLOCK				9		// 半钢砖块
#define BOMB_BLOCK				10		// 爆炸砖块
#define ITEM_SHORT				12		// 板变短
#define ITEM_SPEED_UP			13		// 板速度上升
#define ITEM_SPEED_DOWN			14		// 板速度下降
#define ITEM_LONG_PADDLE		15		// 板变长
#define ITEM_ADD_LIFE			16		// 加命
#define ITEM_TRIPLE_BALL		17		// 小球分裂
#define ITEM_IMPROVE_BALL		18		// 小球升级
#define HARD_BLOCK_CRACKED      19      // 碎裂硬块
#define ANIMATION				20      // 爆炸动画
#define PAUSE					21		// 暂停
#define TITLE					22		// 标题
#define NEW_GAME				23		// 新游戏
#define CHOOSE_STAGE			24		// 选关
#define NEW_GAME_2				25		// 高亮的按钮

background是背景贴图。放在这里没什么特别的意义,就是开发过程中的遗留而已。完全可以放到textures里。

game是游戏类Game的一个实例的指针。Game和Window两个类是聚合关系,代表着“一个窗口中进行着一局游戏”。

font是一个字体,用于渲染游戏中的文字。他的读取和释放也在构造函数完成。

game_started是一个bool变量,用来标志当前窗口是处于游戏状态还是标题菜单。由于我的主菜单是最后加上去的,所以看着有些牵强。只能暂且这样做了。

成员函数

Window的构造函数中包含SDL及其相关模块的初始化操作,析构函数中包含卸载这些模块的操作。

Window::Window(void)
{
	game_started = false;
	// 载入SDL
	if (SDL_Init(SDL_INIT_VIDEO) != 0)
	{
		cout << "SDL_Init Error: " << SDL_GetError() << endl;
		exit(1);
	}
	IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG);
	TTF_Init();
	// 创建窗口
	ptr = SDL_CreateWindow("test", 100, 100, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
	// 创建渲染器,其中SDL_RENDERER_PRESENTVSYNC可以自动控制帧率。
	renderer = SDL_CreateRenderer(ptr, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
	// 创建游戏
	game = new Game;
	game->new_game();
	// 清空材质列表
	for (int i = 0; i < 30; i++)
		textures[i] = nullptr;
	// 初始化字体
	font = nullptr;
	font = TTF_OpenFont("res/SourceSansPro-Regular.ttf", 20);
	quit = false;
	// 这个用来实现鼠标放到新游戏按钮上使其变色的功能
	mouse_on_button = false;
}

init函数是读取游戏所需素材的函数。其实这个函数也可以合并到构造函数。与之相应的,

void Window::init()
{
	textures[BACKGROUND] = IMG_LoadTexture(renderer, "res/bg2.jpg");

	// 球
	textures[NORMAL_BALL] = load_image("res/normal_ball.png");
	textures[GRAVITY_BALL] = load_image("res/gravity_ball.png");
	textures[SUPER_BALL] = load_image("res/super_ball.png");
	// 板
	textures[STICK] = IMG_LoadTexture(renderer, "res/stick.bmp");

	// 砖块
	textures[NORMAL_BLOCK] = load_image("res/normal_block.png");
	textures[HARD_BLOCK] = load_image("res/hard_block.png");
	textures[HARD_BLOCK_CRACKED] = load_image("res/hard_block_cracked.png");
	textures[STEEL_BLOCK] = load_image("res/steel_block.png");
	textures[ITEM_BLOCK] = load_image("res/item_block.png");

	// 物品
	textures[ITEM_LONG_PADDLE] = load_image("res/item_long.jpg");
	textures[ITEM_IMPROVE_BALL] = load_image("res/item_improve_ball.jpg");
	textures[ITEM_TRIPLE_BALL] = load_image("res/item_triple_ball.png");
	textures[ITEM_ADD_LIFE] = load_image("res/item_add_life.png");

	// 动画
	textures[ANIMATION] = load_image("res/animation2.png");
	// 暂停
	textures[PAUSE] = load_image("res/pause.png");
	// 标题
	textures[TITLE] = load_image("res/title.jpg");
	textures[NEW_GAME] = load_image("res/new_game.png");
	textures[NEW_GAME_2] = load_image("res/new_game2.png");
	textures[CHOOSE_STAGE] = load_image("res/choose_stage.png");

	this->background = textures[BACKGROUND];

	// 为游戏中的对象设置材质
	game->set_textures(textures);
}

refresh这个函数比较鸡肋,实际上只是调用了一下SDL_RenderPresent而已。在优化结构时完全可以去掉。

void Window::refresh()
{
	SDL_RenderPresent(renderer);
}

update函数比较重要,它会要求game将其下辖的所有图像部件的状态更新,期间调用handle_collision函数进行碰撞检测和处理,最后将更新后的图像数据画到屏幕上。

下面的函数定义包含了简单的标题菜单和游戏结束、进入下一关对话框,这部分可以先无视。

// 更新图像
void Window::update()
{
	if (game_started)
	{
		//SDL_Event event;
		if (!game->paused)
		{
			// 清空图像
			SDL_RenderClear(renderer);

			// 如果过关
			if (game->is_clear())
			{
				MessageBox(NULL, "请点击OK进入下一关...", "恭喜过关!", MB_OK);
				game->level++;
				game->new_game();
				game->set_textures(textures);
				return;
			}

			// 如果没有小球,且有生命,重置
			if (game->balls.size() == 0)
			{
				if (game->life == 0)
				{
					int choice = 0;
					choice = MessageBox(NULL, "再试一次?\n点是重试\n点否退出", "Game Over", MB_YESNO);
					// 判断用户的选择
					switch (choice)
					{
						// 用户选择是
					case 6:
						game->level = 1;
						game->new_game();
						game->set_textures(textures);
						return;
						break;
					case 7:
						//event.type = SDL_QUIT;
						// 事件会被复制一份到事件队列,这里可以不管
						// 收不到事件
						quit = true;
						//SDL_PushEvent(&event);
						return;
						break;
					case 2:
						break;
					}
				}
				else
				{
					game->reset();
					game->balls.front()->set_texture(textures);
				}

			}
			// 更新素材
			//game->set_textures(textures);



			// 背景
			SDL_RenderCopy(renderer, background, NULL, NULL);

			// 更新板状态
			game->paddle.update();
			SDL_RenderCopy(renderer, game->paddle.get_texture(), NULL, &game->paddle.get_geometry());

			// 更新所有球状态
			for (list<Ball*>::iterator it = game->balls.begin(); it != game->balls.end(); ++it)
			{
				if (*it)
				{
					// 如果小球未发射,跟着板走
					if (!(*it)->is_launched())
						(*it)->set_pos(game->paddle.get_pos().x + game->paddle.get_geometry().w / 2 - (*it)->get_geometry().w / 2, game->paddle.get_pos().y - (*it)->get_geometry().h);
					(*it)->update();
				}
				SDL_RenderCopy(renderer, (*it)->get_texture(), NULL, &(*it)->get_geometry());
			}

			// 更新所有物品状态
			for (list<Item*>::iterator item = game->items.begin(); item != game->items.end(); ++item)
			{
				if (*item)
				{
					(*item)->update();
				}
				SDL_RenderCopy(renderer, (*item)->get_texture(), NULL, &(*item)->get_geometry());
			}

			// 处理碰撞
			handle_collision();


			// 所有砖块更新
			for (list<Block*>::iterator block = game->blocks.begin(); block != game->blocks.end(); ++block)
			{
				if (*block)
				{
					SDL_RenderCopy(renderer, (*block)->get_texture(), NULL, &(*block)->get_geometry());
				}
			}

			// 获取帧数,更新动画信息
			int frame = int(((SDL_GetTicks() / 10) % 30));
			for (list<Animation*>::iterator anim = game->animations.begin(); anim != game->animations.end(); )
			{
				if (*anim)
				{
					(*anim)->update2();
					SDL_RenderCopy(renderer, (*anim)->texture, &(*anim)->source_rect, &(*anim)->target_rect);
				}
				// 删除播放完的
				if ((*anim)->frame == 27)
					anim = game->animations.erase(anim);
				else
					++anim;
			}

			// 渲染生命数和关卡号
			SDL_Rect text_rect = { 880, 700, 130, 25 };
			char temp_str[50];
			sprintf(temp_str, "LEVEL: %d LIFE: %d", game->level, game->life);
			SDL_RenderCopy(renderer, render_text(temp_str), NULL, &text_rect);
		}
		else
		{
			SDL_Rect temp = { SCREEN_WIDTH / 2 - 50, SCREEN_HEIGHT / 2 - 50, 100, 100 };
			SDL_RenderCopy(renderer, textures[PAUSE], NULL, &temp);
		}
	}
	// 如果没有开始游戏
	else
	{
		SDL_RenderClear(renderer);
		SDL_RenderCopy(renderer, textures[TITLE], NULL, NULL);
		// SDL_Rect option_rect;
		if(!mouse_on_button)
		{
			SDL_QueryTexture(textures[NEW_GAME], NULL, NULL, &option_rect.w, &option_rect.h);
			option_rect.x = SCREEN_WIDTH/2 - option_rect.w/2;
			option_rect.y = 450;
			SDL_RenderCopy(renderer, textures[NEW_GAME], NULL, &option_rect);
		}
		else
		{
			SDL_QueryTexture(textures[NEW_GAME_2], NULL, NULL, &option_rect.w, &option_rect.h);
			option_rect.x = SCREEN_WIDTH / 2 - option_rect.w / 2;
			option_rect.y = 450;
			SDL_RenderCopy(renderer, textures[NEW_GAME_2], NULL, &option_rect);
		}
		
		/*SDL_QueryTexture(textures[CHOOSE_STAGE], NULL, NULL, &option_rect.w, &option_rect.h);
		option_rect.x = SCREEN_WIDTH / 2 - option_rect.w/2;
		option_rect.y = 520;
		SDL_RenderCopy(renderer, textures[CHOOSE_STAGE], NULL, &option_rect);*/
		SDL_RenderPresent(renderer);
	}
}

handle_collision函数进行碰撞事件的检测和处理。

尽管游戏支持多个小球、多个砖块、多个掉落物品,但碰撞检测是线性的。也就是说,要不断进行遍历操作。对每个小球,判断它和每个砖块是否发生了碰撞,诸如此类。

这里写得有点繁琐,因为在遍历过程中,可能要删除list的某些元素。所以对iterator的++操作不是每次循环都执行的。

void Window::handle_collision()
{
	for (list<Ball*>::iterator ball = game->balls.begin(); ball != game->balls.end(); )
	{
		// 墙壁
		if ((*ball)->speed.x > 0 && (*ball)->get_geometry().x >= SCREEN_WIDTH - (*ball)->get_geometry().w)
			(*ball)->speed.x *= -1;
		if ((*ball)->speed.x < 0 && (*ball)->get_geometry().x <= 0)
			(*ball)->speed.x *= -1;
		if ((*ball)->speed.y > 0 && (*ball)->get_geometry().y >= SCREEN_HEIGHT - (*ball)->get_geometry().h)
		{
			// 坠毁
			delete (*ball);
			ball = game->balls.erase(ball);
			continue;
		}

		if ((*ball)->speed.y < 0 && (*ball)->get_geometry().y <= 0)
			(*ball)->speed.y *= -1;
		if ((*ball)->speed.y > 0 && (*ball)->get_geometry().y >= game->paddle.get_geometry().y - (*ball)->get_geometry().h && (*ball)->get_geometry().x + (*ball)->get_geometry().w / 2 > game->paddle.get_geometry().x && (*ball)->get_geometry().x + (*ball)->get_geometry().w / 2 < game->paddle.get_geometry().x + game->paddle.get_geometry().w)
		{
			(*ball)->speed.y *= -1;
			// 板的速度影响小球的速度
			(*ball)->speed.x += game->paddle.speed.x / 3;
			/*// 小球水平速度最高正负12
			if(abs((*ball)->speed.x)>12)
			(*ball)->speed.x = (*ball)->speed.x / abs((*ball)->speed.x) *12;*/
			// 打在板的不同位置会对小球水平速度造成不同的影响
			/*(*ball)->speed.x += ((*ball)->get_pos().x-game->stick.get_pos().x - game->stick.get_geometry().w / 2)
			/ (game->stick.get_geometry().w/2) * 12;*/

			(*ball)->speed.x = (double)((*ball)->get_pos().x + (*ball)->get_geometry().w / 2 - game->paddle.get_pos().x - game->paddle.get_geometry().w / 2)
				/ (double)(game->paddle.get_geometry().w / 2) * 8;
			// 小球水平速度最高正负12
			/*if(abs((*ball)->speed.x)>12)
			(*ball)->speed.x = (*ball)->speed.x / abs((*ball)->speed.x) *12;*/
		}
		// 砖块
		bool hit = false;
		for (list<Block*>::iterator block = game->blocks.begin(); block != game->blocks.end();)
		{

			hit = false;
			if (*block)
			{
				// 测试
				if ((*block)->get_geometry().x == 287 && (*block)->get_geometry().y == 370 && (*ball)->get_top_edge() <= 370 && (*ball)->speed.y < 0)
				{
					int a;
					a = 10;
				}
				// 首先判断小球是否与砖块发生了接触
				// 碰撞砖块上下边界
				// 如果小球有像素位于砖块左右边界之内
				// 关于角度,先把等于也放在碰撞上下边界中
				double algle_with_ball = (*block)->angle_with_ball((*ball)->get_geometry());
				double right_top_angle = (*block)->right_top_angle();
				double left_top_angle = (*block)->left_top_angle();
				if (((*ball)->get_h_mid() >= (*block)->get_left_edge() && (*ball)->get_h_mid() <= (*block)->get_right_edge())
					&& (((*block)->angle_with_ball((*ball)->get_geometry()) <= (*block)->right_top_angle()
						&& (*block)->angle_with_ball((*ball)->get_geometry()) >= (*block)->left_top_angle())
						|| ((*block)->angle_with_ball((*ball)->get_geometry()) <= (*block)->left_bottom_angle()
							&& (*block)->angle_with_ball((*ball)->get_geometry()) >= (*block)->right_bottom_angle())))
				{
					// 碰撞砖块下界
					if ((*ball)->get_top_edge() < (*block)->get_bottom_edge() && (*ball)->get_bottom_edge() > (*block)->get_bottom_edge())
					{
						(*ball)->hit(BOTTOM);
						(*block)->hit_by((*ball), &game->items);
					}

					// 碰撞砖块上界
					else if ((*ball)->get_bottom_edge() > (*block)->get_top_edge() && (*ball)->get_top_edge() < (*block)->get_top_edge())
					{
						(*ball)->hit(TOP);
						(*block)->hit_by((*ball), &game->items);
					}
				}
				// 碰撞砖块左右边界
				// 如果小球有像素位于砖块上下边界之内
				else if ((*ball)->get_v_mid() >= (*block)->get_top_edge() && (*ball)->get_v_mid() <= (*block)->get_bottom_edge())
				{

					// 碰撞砖块左界
					int ball_right = (*ball)->get_right_edge();
					int block_left = (*block)->get_left_edge();
					int ball_left = (*ball)->get_left_edge();

					if ((*ball)->get_right_edge() > (*block)->get_left_edge() && (*ball)->get_left_edge() < (*block)->get_left_edge())
					{
						(*ball)->hit(LEFT);
						(*block)->hit_by((*ball), &game->items);
					}
					// 碰撞砖块右界
					else if ((*ball)->get_left_edge() < (*block)->get_right_edge() && (*ball)->get_right_edge() > (*block)->get_right_edge())
					{
						(*ball)->hit(RIGHT);
						(*block)->hit_by((*ball), &game->items);
					}
				}
				// 删除生命值小于等于0 的砖块
				// 如果砖块被破坏,创建一个动画
				if ((*block)->health <= 0)
				{
					// 在砖块的位置创建一个动画
					game->animations.push_back(new Animation(textures[ANIMATION],
						(*block)->get_h_mid() - 35,
						(*block)->get_v_mid() - 17));
					delete (*block);
					*block = nullptr;
					block = game->blocks.erase(block);
					hit = true;
				}
				if (!hit)
					block++;
			}
		}
		++ball;
	}
	// 道具
	for (list<Item*>::iterator item = game->items.begin(); item != game->items.end();)
	{
		// 如果道具被吃到
		if ((*item)->speed.y > 0 &&
			(*item)->get_geometry().y >= game->paddle.get_geometry().y - (*item)->get_geometry().h &&
			(*item)->get_geometry().x + (*item)->get_geometry().w / 2 > game->paddle.get_geometry().x &&
			(*item)->get_geometry().x + (*item)->get_geometry().w / 2 < game->paddle.get_right_edge() &&
			(*item)->get_top_edge() <= game->paddle.get_bottom_edge())
		{
			// 判断类型,发动效果
			switch ((*item)->type)
			{
			case ITEM_LONG_PADDLE:
				game->paddle.size_up();
				(*item)->gotten = true;
				break;
			case ITEM_IMPROVE_BALL:
				game->improve_ball();
				(*item)->gotten = true;
				break;
			case ITEM_TRIPLE_BALL:
				game->triple_ball();
				(*item)->gotten = true;
				break;
			case ITEM_ADD_LIFE:
				game->life++;
				(*item)->gotten = true;
				break;
			}
		}
		// 如果没吃到
		else if ((*item)->get_bottom_edge() == SCREEN_HEIGHT)
			(*item)->gotten = true;
		// 删除生命值小于等于0 的砖块
		if ((*item)->gotten)
		{
			delete (*item);
			*item = nullptr;
			item = game->items.erase(item);
			continue;
		}
		++item;
	}
}

 

strcpy和strcpy_s

我在做Windows程序设计课程实验的时候,参照书上的例子使用了strcpy,结果VS报错说这个函数不安全,应该使用strcpy_s。

两者有什么区别呢?其实是因为strcpy无法判断他的操作对象是否有足够的缓冲区,若缓冲区不够的话,就会发生溢出。

strcpy_s避免了这个问题。它会显式地向你报告缓冲区不足的错误,避免丈二和尚摸不着头脑。