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避免了这个问题。它会显式地向你报告缓冲区不足的错误,避免丈二和尚摸不着头脑。