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;
}
}
}
}
这是我的面向对象程序设计实习题目,发上来与各位分享。
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;
}
}
我在做Windows程序设计课程实验的时候,参照书上的例子使用了strcpy,结果VS报错说这个函数不安全,应该使用strcpy_s。
两者有什么区别呢?其实是因为strcpy无法判断他的操作对象是否有足够的缓冲区,若缓冲区不够的话,就会发生溢出。
strcpy_s避免了这个问题。它会显式地向你报告缓冲区不足的错误,避免丈二和尚摸不着头脑。