ixRic
2020-11-26 21:53:31
可能更好的阅读体验
太大了传不了就把偶数帧删了。
最初想要制作一个小游戏的 OIer 或许都会这么想。事实上,以常规 OI 编译器(例如,Windows 下的 MinGW)是无法完成所谓的图形化界面的,因此出现了很多的 C++ GUI(图形用户界面)开发框架,例如大家熟知的 Qt。然而他们较高的门槛让一个普通的 OIer 没有时间和精力去接触,因此一些简单的 C / C++ 图形库 应运而生 进入了我们的视野,EGE(Easy Graphics Engine)就是其中之一。
另一个为人熟知的简单图形库是 EasyX,我选用 EGE 的原因主要是其可以在众多 IDE 下使用(包括一切使用 MinGW 或 MSVC 编译器的 IDE),而 EasyX 仅支持 VC,虽然对于大部分大学生 IDE无关紧要,但对于 OIer 来说,能用熟悉的 DevC++。如果还要问有什么区别的话:
那么 EGE 到底能干什么呢?
并且它远没有 Qt 这么多复杂的概念,所有操作都由简单的函数实现。适合大家平时写着玩。
在上文提到的两个链接中都能找到十分详细的安装教程,本文就只介绍下自己用的 Code::Blocks 20.03 的安装方法(DevC++ 类似,可参照下面的方法):
进入官网 https://xege.org/ 可以看到:
点击下方的按钮后选择第一项即可得到:
解压后进入有这五个文件(夹):
通常它在你的 IDE 文件夹中,例如 C:\Program Files\CodeBlocks\MinGW
或者 C:\Program Files (x86)\Dev-Cpp\MinGW64
。
据说由于自动安装包没有开发好, 我们需要手动把头文件和链接库之类的东西复制 MinGW 中:
把 ege20.08_all\EGE20.08\include
中 所有东西 复制到 MinGW\x86_64-w64-mingw32\include
中。
把 \ege20.08_all\EGE20.08\lib\codeblocks20.03
里面的文件复制到 MinGW\x86_64-w64-mingw32\lib
中。
打开 Code::Blocks,进入 Settings - Compiler
,点击 Linker settings
选项卡。
在右侧的 Other linker options
框中输入 -lgraphics64 -luuid -lmsimg32 -lgdi32 -limm32 -lole32 -loleaut32 -lwinmm -lgdiplus
,点击右下角的 OK。
如果你是 Code::Blocks 20.03 版本,建议在 Compiler settings
选项卡里勾选图示三个编译选项,因为不勾选编译得到的 exe 文件无法双击运行只能在 CB 中点击运行:
好了,现在你已经拥有了 EGE。
和正常写代码完全一样,在 Code::Blocks 中新建一个控制台的 Project 输入以下代码:
#include <graphics.h>
int main() {
initgraph(500, 600); // 初始化画布, 宽 500 像素, 高 600 像素
circle(250, 300, 200); // 在 (250, 300) 处画一个半径为 200 的圆
getch(); // 等待任意按键
closegraph(); // 关闭画布
return 0;
}
然后编译运行:
我将按照自己完成代码的顺序介绍编写一个小游戏的步骤。
我们可以通过 Apktool 解析一个 apk 文件得到我们所需要的图片素材,当然如果你足够强可以自己画(注意背景得是透明的而不是白色)。这里既然是模仿就直接白嫖了 。
我放了一份在网盘上:https://pan.baidu.com/s/1OYjVkfWGUXIaUhOasgo2sg,提取码 f18k
。当然也可以从 github 上面下,但是需要自己配置运行。
随便找一个手机应用商店,下载一个 Flappy Bird 的安卓安装包,为了方便我也把我找的放在这里:https://pan.baidu.com/s/1rxOhuYxoQVsG03m4N2DZEw,提取码: nqay
。
可以看到 apktool 中只有一个 bat 文件和一个 jar 文件(要用到 java,电脑上没安装的就自己处理了 都是玩 MC 的还会没有 java 么 ):
在 cmd 中输入 ...\apktool
进入工作目录,其中 ...
是上图两个文件的位置,出现下图说明安装好了:
然后输入 apktool d -f 输入文件位置 -o 输出文件夹
,例如:
出现上图就已经解析好了!然后进入输出文件夹,是这个样子的:
翻一翻就能找到我们想要的素材了:
用某软件打开 atlas.png
可以看到背景是透明的:
实际上在 atlas.txt
中有各个图像在 atlas.png
中的位置和大小等数据,但是用起来比较麻烦(事实上后面会涉及到图片的旋转),因此我用肝把这玩意裁剪了一下。为什么是用肝,因为你要记录好每张图片大小,并且要尽量从边缘裁,否则你的碰撞检测就更难判断了。我也把本次要用到的图片都传了上去:https://pan.baidu.com/s/1jrZLZ8HrhhjW0a1AaN8ISw,提取码 6dqu
。
PIMAGE
,其实际上是一个指针指向该图像。因此定义一个图像要使用 PIMAGE img = newimage()
,此时 img
指向了一个 getimage
从路径获得图像,在后文会详细说明;由于程序中大量设计调用图片,我们可以先把图片名与数字 id 用宏定义联系起来:
#define BACKGROUND_DAY 0
#define BACKGROUND_NIGHT 1
#define BIRD_UP 2
#define BIRD_HORIZONTAL 3
#define BIRD_DOWN 4
#define BOARD 5
#define BUTTON_LIGHT 6
#define BUTTON_DARK 7
#define GROUND 8
#define INSTRUCTION 9
#define SILVER_MEDAL 10
#define GOLD_MEDAL 11
#define PIPE 12
#define TITLE 13
#define GAME_OVER 14 /* 将图片名称与标号对应, 代
码中涉及一些运算和标号有
关, 不可随意更改名称与标
号的对应关系 */
然后就是图片路径与图片大小,注意与上面的 id 相对应:
const int IMAGE_NUMBER = 15; /* 图片数量 */
const char *IMAGE_PATH[IMAGE_NUMBER + 5] = { /* 图片文件位置 */
"resource/background1.png", // 0 白天背景
"resource/background2.png", // 1 夜晚背景
"resource/bird1.png", // 2 鸟 (翅膀向上)
"resource/bird2.png", // 3 鸟 (翅膀水平)
"resource/bird3.png", // 4 鸟 (翅膀向下)
"resource/board.png", // 5 记分板
"resource/button1.png", // 6 按钮 (未激活)
"resource/button2.png", // 7 按钮 (激活)
"resource/ground.png", // 8 地面
"resource/instruction.png", // 9 开始提示
"resource/medal1.png", // 10 银奖章
"resource/medal2.png", // 11 金奖章
"resource/pipe.png", // 12 管道 (朝上)
"resource/title.png", // 13 标题 (Flappy Bird)
"resource/gameOver.png", // 14 Game Over
};
const int IMAGE_SIZE[IMAGE_NUMBER][2] = { /* 图片的宽和高 */
{ 285, 510 },
{ 285, 510 },
{ 35, 25 },
{ 35, 25 },
{ 35, 25 },
{ 228, 116 },
{ 116, 69 },
{ 116, 69 },
{ 285, 110 },
{ 113, 100 },
{ 45, 45 },
{ 45, 45 },
{ 54, 321 },
{ 185, 50 },
{ 194, 44 }
};
大部分常量都是在编写过程中再去定义的,毕竟刚开始写也不能完全想好要用到哪些,我先定义了下面这些:
const int WINDOW_WIDTH = 285, WINDOW_HEIGHT = 510; /* 窗口大小 */
const int FPS = 60; /* 帧率, 游戏时间单位为帧,
长度单位为像素 */
const float GA = 0.42; /* 重力加速度 (每帧增加的速
度) */
注意窗口大小(其实是画布大小)应该跟背景一样,帧率大家都知道。
事实上最终代码的常量大约占用的 100 行。
Flappy Bird 的精灵(Sprite,指二维动画中的图形对象)很简单,最基本的只有两个:鸟和管道。我使用了一个结构体储存鸟的相关信息:
struct bird { /* 鸟结构体 */
int posX, posY; /* 图像左上角位置 */
int shape, /* 形态 (2 / 3 / 4 <=> 翅膀
向上 / 水平 / 向下) */
cnt; /* 帧计数, 当 speed < 0 且
cnt % TIME_PER_WINGING =
0 时切换形态; 当 speed =
0 时为形态 3; 当 speed <
0 时为形态 4 */
float speedX, speedY; /* 以 x, y 正方向为正, 图像
角度与速度相关, 由于开场
从左边进入, 所以需要 x 方
向的速度 */
} player;
对于管道,用循环队列即可:
int pipeHead, pipeTail; /* 使用循环队列储存管道, 不
能使用 STL, 左闭右开 */
std::pair<int, int> pipeOnScreen[MAX_PIPE_NUMBER
+ 5]; /* 储存所有下管道的左上角 */
还有游戏中用到的图像,实际上初始化完了过后不再改变,但我们视为变量:
PIMAGE image[IMAGE_NUMBER + 5]; /* 储存所有图像 */
当然还有当前分数变量:
int score; /* 得分 */
变量定义中涉及到的常量:
const int MAX_PIPE_NUMBER = 8; /* 屏幕上最多出现多少管道 */
注意应该在 main
前声明所有函数,在 main
后进行实现,否则在使用的时候还没声明就很尴尬。
根据鸟当前的状态画出鸟下一帧的状态,返回是否发生与地面或管道的碰撞。一些细节问题:
inline int drawNextBird(bird &p) {
int w = IMAGE_SIZE[p.shape][0], h = IMAGE_SIZE[p.shape][1];
p.cnt = (p.cnt + 1) % TIME_PER_WINGING;
if (p.speedY < 0 && p.cnt == 0)
p.shape = (p.shape - 1) % 3 + 2; // 切换翅膀形态
if (p.speedY < MAX_DOWN_SPEED) // 限定最大下降速度
p.speedY += GA;
float angle = speedToAngle(p.speedY); // 根据速度计算图像要偏转的角度
p.posY += (int)p.speedY;
p.posX += (int)p.speedX;
if (p.posX >= WINDOW_WIDTH / 2 - IMAGE_SIZE[BIRD_UP][0] / 2)
// 开始一段时间横向运动
p.posX = WINDOW_WIDTH / 2 - IMAGE_SIZE[BIRD_UP][0] / 2 - 1,
p.speedX = 0;
if (p.posY < 0) p.posY = 0; // 不能向上出界, 但碰到顶部不算失败
w /= 2, h /= 2;
putimage_rotate(NULL, image[p.shape], p.posX + w, p.posY + h, 0.5, 0.5,
angle, 1); // 旋转一定角度输出
// xyprintf(100, 10, "111");
return birdCrash(player); // 碰撞判断
}
其中用到了 EGE 库的
putimage_rotate
函数,用于绘制旋转后的图像,其 8 个参数分别为:
- 目标图像指针,若为
NULL
,则是窗口。- 绘制图像指针。
- 绘制图像左上角
x 坐标。- 绘制图像左上角
y 坐标。- 旋转中心在绘制图像坐标系的
x 坐标。- 旋转中心在绘制图像坐标系的
y 坐标。- 旋转角度(弧度),逆时针为正。
- 是否允许透明通道。
注意参数 5 和 6,值为
[0, 1] ,因为是与原图像宽/ 高的比值。 另外,透明通道可以直接理解为图像的透明像素,如果不允许透明通道,透明像素会被绘制成白色,允许则不绘制透明像素。
inline float speedToAngle(const float &speed) {
if (speed < SMOOTH_ROTATE_SPEED) return PI / 6;
// 上升时和刚开始下降时稳定为向上 30 度
return -((speed - SMOOTH_ROTATE_SPEED) /
(DOWN_SPEED - SMOOTH_ROTATE_SPEED) * PI / 2) + PI / 6;
// 下落时计算速度占竖直朝下速度的比例, 转化为角度, 由于刚开始下降时没有旋转,
// 要减掉那部分速度才能平滑旋转
}
写到这里会定义常量 SMOOTH_ROTATE_SPEED
以及 DOWN_SPEED
:
const float DOWN_SPEED = 8.2; /* 鸟头竖直朝下时的速度, 用
于转换速度和飞行角度 */
const float SMOOTH_ROTATE_SPEED = 6; /* 为了旋转不太剧烈设置了这
个参数, 使得上升时和下降
开始时不按照速度旋转图片 */
事实上这个函数比较复杂:
inline int birdCrash(bird cur) {
int x = cur.posX, y = cur.posY;
int w = IMAGE_SIZE[BIRD_UP][0],
h = IMAGE_SIZE[BIRD_UP][1];
int lft = x - CRASH_SIZE, rgt = x + w + CRASH_SIZE,
up = y - CRASH_SIZE, dwn = y + h + CRASH_SIZE;
// 实际碰撞体积比图片大小小
int groundHeight = IMAGE_SIZE[GROUND][1];
static bool added = false;
if (cur.speedX) added = false; // 游戏开始时的初始化
if (y + h >= WINDOW_HEIGHT - groundHeight) // 落地
return 2;
// xyprintf(10, 80, "%d", nextPipeToMeet);
if ((nextPipeToMeet >= pipeHead && nextPipeToMeet < pipeTail) ||
(nextPipeToMeet < pipeTail && pipeTail <= pipeHead) ||
(nextPipeToMeet >= pipeHead && pipeHead >= pipeTail)) {
// 当前有管道 (考虑循环队列)
int pipeX = pipeOnScreen[nextPipeToMeet].first,
pipeY = pipeOnScreen[nextPipeToMeet].second;
if (x + IMAGE_SIZE[BIRD_UP][0] / 2 >= pipeX && !added)
// 第一次通过当前管道
++score, added = true; // 加分
if (lft > pipeX + IMAGE_SIZE[PIPE][0]) { // 完全通过当前管道
nextPipeToMeet = circleNext(nextPipeToMeet);
added = false;
}
// xyprintf(30, 10, "%d %d", y, pipeY);
return rgt >= pipeX && lft <= pipeX + IMAGE_SIZE[PIPE][0] &&
(!(up > pipeY - PIPE_GAP_VERTICAL && dwn < pipeY));
}
return 0;
}
写到这里会发现需要一个 nextPipeToMeet
变量记录鸟前面第一个变量。又由于鸟不是一个标准的矩形,我们不能直接判断,我为了方便就设置一个 CRASH_SIZE
常量进行模糊处理:
const int CRASH_SIZE = -2; /* 考虑到管道和鸟都不是矩形,
实际碰撞体积比图片大小小 */
另外,circleNext
函数会返回循环加一的值,比较简单不特意说明。
取出管道队列中的每一个元素,画下一帧即可。需要随机判断是否增加一个管道。
void drawNextPipe(const int &speed) {
int d = random(2 * PIPE_GAP_HORIZONTAL_RANGE) - PIPE_GAP_HORIZONTAL_RANGE +
PIPE_GAP_HORIZONTAL; // 随机最后一个管道和新加管道之间的间隔
for (int i = pipeHead; i != pipeTail; i = circleNext(i)) {
std::pair<int, int> &p = pipeOnScreen[i];
int x = p.first, y = p.second;
putimage_withalpha(NULL, image[PIPE], x, y); // 输出下半部分
putimage_rotate(NULL, image[PIPE], x + IMAGE_SIZE[PIPE][0] / 2,
y - PIPE_GAP_VERTICAL, 0.5, 0, PI, 1); // 输出上半部分
p.first += (int)speed; // 移动
if (i == pipeHead) {
if (p.first + IMAGE_SIZE[PIPE][0] < 0) // 最左边的管道出界了
pipeHead = circleNext(pipeHead);
}
if (circleNext(i) == pipeTail) {
if (p.first + d <= WINDOW_WIDTH) { // 可以新加一个管道
pipeOnScreen[pipeTail] = { WINDOW_WIDTH, getPipeHeight() };
// 确保从窗口最右边进入
pipeTail = circleNext(pipeTail);
}
}
}
}
其中用到了
putimage_withalpha
函数,用于绘制带透明通道的图像,四个参数分别为:
- 目标图像指针,若为
NULL
,则是窗口。- 绘制图像指针。
- 绘制图像左上角
x 坐标。- 绘制图像左上角
y 坐标。
新增的常量如下:
//const int PIPE_GAP_VERTICAL = 90; /* 上下管道间隙 (变态) */
const int PIPE_GAP_VERTICAL = 105; /* 上下管道间隙 (正常) */
//const int PIPE_GAP_VERTICAL = 150; /* 上下管道间隙 (几乎无敌) */
const int PIPE_GAP_HORIZONTAL = 260; /* 左右管道间隙基准值 */
const int PIPE_GAP_HORIZONTAL_RANGE = 100; /* 左右管道间隙随机范围 */
另外 getPipeHeight()
用于随机一个管道的高度,比较简单不过多说明。
setcaption("Flappy Bird");
// 设置窗口标题
setinitmode(INIT_WITHLOGO | INIT_NOFORCEEXIT, 100, 100);
// 显示开场 LOGO | 关闭窗口时不强制结束程序
// INIT_NOFORCEEXIT 意味着有长时间循环时必须判断 is_run()
// 否则会出现用户无法关闭窗口的情况
initgraph(WINDOW_WIDTH, WINDOW_HEIGHT);
// 初始化画布
randomize();
// 随机函数初始化
for (int i = 0; i < IMAGE_NUMBER; i++) {
image[i] = newimage();
getimage(image[i], IMAGE_PATH[i]);
}
// 初始化图片
setcaption
函数,用于设置绘图主窗口的标题。
setinitmode
函数,用于初始化窗口的参数,这里用到的 3 个参数分别为:
- 窗口属性。
- 窗口左上角在显示器的位置
x 坐标。- 窗口左上角在显示器的位置
y 坐标。第一个参数通常使用多个常量或起来表达,我设置了显示开场 logo 和关闭窗口时不强制退出程序。
关于不强制退出程序:如果不设置,那么用户点击界面右上角的叉时,窗口会立即关闭且程序会立即结束;如果设置了,窗口不会关闭程序也不会结束,但是所有的
is_run
函数会返回false
,当然如果窗口还在,is_run
函数会返回true
,因此我们需要时刻使用is_run
函数检测窗口是否需要关闭,如果is_run
函数返回了false
,我们需要用closegraph
函数手动帮助用户关闭窗口。这点非常重要。 因此我们不能用 EGE 库的delay_ms
函数(延迟给定的毫秒数),因为这样会造成无法关闭窗口的情况,于是我将delay_ms
函数重新进行了宏定义:#define delay_ms(t) \ [](const int &msTime) { \ int fpsTime = msTime * FPS / 1000; \ while (fpsTime-- && is_run()) \ delay_fps(FPS); \ } (t) // 将 delay_ms 转化为 delay_fps (因为要判断 is_run 所以不能直接 delay_ms) // 用 lambda 可以避免变量重名
后两个参数在我写文章时 EGE 20.08 版本似乎有一些 bug(
可以创建过后用
movewindow
函数改。
initgraph
函数:初始化绘图环境,两个参数分别是窗口的宽和高。
randomize
函数:相当于srand(time(NULL))
之类的,EGE 库中有自带的随机函数,据说随机度比rand
函数高。
getimage
函数,用于从路径中获取图片,两个参数分别为存入位置和路径。
putimage(0, 0, image[BACKGROUND_DAY]);
putimage(0, WINDOW_HEIGHT - IMAGE_SIZE[GROUND][1], image[GROUND]);
// 打印背景
for (int i = 0; i <= TITLE_APPEAR_TIME && is_run(); i++) {
reprintBackground();
reprintGround(); // 注意要重印背景否则下面设置的透明度就是假的
putimage_alphatransparent(NULL, image[TITLE],
WINDOW_WIDTH / 2 - IMAGE_SIZE[TITLE][0] / 2,
WINDOW_HEIGHT / 2 - IMAGE_SIZE[TITLE][1] / 2 -
IMAGE_SIZE[TITLE][1] / 2 - 100,
0, i * 0xFF / TITLE_APPEAR_TIME);
delay_fps(FPS);
}
delay_ms(1000);
putimage_withalpha(NULL, image[INSTRUCTION],
WINDOW_WIDTH / 2 - IMAGE_SIZE[INSTRUCTION][0] / 2,
WINDOW_HEIGHT / 2 - IMAGE_SIZE[INSTRUCTION][1] / 2);
// 打印提示和标题
flushmouse();
// 清空鼠标消息缓冲区
mouse_msg msg = { 0 };
bool isBegin = false;
while (!isBegin && is_run()) {
msg = getmouse();
if (msg.is_left() && msg.is_down())
isBegin = true;
}
// 等待开始
gameBegin: // 游戏开始
flushmouse();
background = BACKGROUND_DAY;
int maxScore = 0;
std::ofstream outMaxScore(MAX_SCORE_PATH,
std::ios::in); // 输出流 (没有则新建, 必须加
// ios::in 否则会清空文件)
std::ifstream inMaxScore(MAX_SCORE_PATH); // 输入流
if (inMaxScore.peek() != EOF) // 有记录则读取
inMaxScore >> maxScore;
// 读取历史最大分数
player.shape = 3;
player.posX = 0;
player.posY = (WINDOW_HEIGHT - IMAGE_SIZE[GROUND][1]) / 2 -
IMAGE_SIZE[player.shape][1] / 2;
player.cnt = 0;
player.speedX = INITIAL_SPEEDX;
player.speedY = CLICK_SPEED;
// 初始化玩家的鸟
pipeHead = pipeTail = nextPipeToMeet = 1;
pipeOnScreen[pipeTail] = { WINDOW_WIDTH, getPipeHeight() };
pipeTail = circleNext(pipeTail);
// 初始化第一个管道
setfont(SCORE_HEIGHT, SCORE_WIDTH, SCORE_FONT, 0, 0,
SCORE_WEIGHT, false, false, false);
setcolor(SCORE_COLOR);
// 设置分数的字体及颜色
这里涉及到一个渐入的标题,用循环加上 putimage_alphatransparent
函数实现。
putimage_alphatransparent
函数:按指定透明度输出一个带透明通道的图片,参数:
- 透明混合的背景图片,若为
NULL
则为窗口。- 透明绘制的图片。
- 绘制位置
x 坐标。- 绘制位置
y 坐标。- 变为透明的像素颜色。
- 透明度,若为
0x0
则完全透明,若为0xFF
则为完全不透明。
注意每次都需要重印背景,否则不断叠加,透明度会很快变成完全不透明。
reprintBackground
和 reprintGround
是重印背景和重印地面的函数,reprintGround
比较简单,reprintBackground
涉及到一个变背景的细节,即每得 10 分变换一次白天黑夜:
inline void reprintBackground() {
static int lastBackground = background, cnt = 0; // cnt 对应 change 后的时间
if (lastBackground != background && !cnt)
cnt = CHANGE_BACKGROUND_TIME;
putimage(0, 0, image[lastBackground]);
// 注意先输出 lastbackground 以达到清屏的目的
if (cnt) { // 再混合新背景
putimage_alphablend(NULL, image[background], 0, 0,
(CHANGE_BACKGROUND_TIME - cnt) * 0xFF /
CHANGE_BACKGROUND_TIME); // 根据 cnt 计算透明度
if (!(--cnt)) lastBackground = background;
}
}
delay_fps(FPS)
:等待帧率为FPS
时的一帧所需时间,用这个函数可以很方便地控制动画。
delay_ms(t)
:等待t 毫秒,注意这不是 EGE 库的函数,而是重新宏定义后的。
flushmouse
:清空鼠标消息的缓冲区,和常见的键盘消息类似,防止用户在之前的操作被后面的getmouse
获取到。
mouse_msg
:这是一个鼠标消息类,可以类比于键盘消息的char
。
getmouse
:获取鼠标消息,类比于键盘消息的getch()
。
mouse_msg::is_left()
:消息是否右鼠标左键产生。
mouse_msg::is_down()
:是否有鼠标按键按下。
setfont
:设置文字输出时的字体,参数分别时:
- 字高。
- 字宽,若为
0
,则自适应。- 字体名称,一个字符串。
- 字符串书写角度。
- 字符书写角度。
- 笔画粗细。
- 是否下划线
- 是否斜体。
- 是否删除线。
setcolor
:设置文本输出颜色。
主函数中涉及的常量比较多,这里都不再展示了,可以参看最后的完整代码。
gameBegin:
标签用于重新开始。
int lastScore = 0;
int shineCount = -1;
// 都是防止动画永动
score = 0;
bool brokenRecord = false;
while (is_run()) {
reprintBackground();
if (drawNextBird(player)) // 鸟运动并判断是否碰撞
break; // 碰撞则游戏结束
if (player.speedX == 0) // 不横向运动再开始输出管道
drawNextPipe(PIPE_SPEED); // 管道运动
reprintGround(); // 确保地面显示在最前
if (score == maxScore + 1) {
if (shineCount == -1) { // 破纪录动画只播放一次
brokenRecord = true;
shineCount = SHINING_TIME;
setfont(SHINING_SCORE_HEIGHT, SHINING_SCORE_WIDTH,
SHINING_SCORE_FONT, 0, 0,
SHINING_SCORE_WEIGHT, false, false, false);
// 换字体
}
maxScore = score;
outMaxScore.seekp(std::ios::beg);
outMaxScore << maxScore;
// 实时更新文件中的最大分数
}
if (shineCount > 0) {
if (shineCount % (SHINING_TIME / SHINING_TIMES) == 0) {
int c = shineCount / (SHINING_TIME / SHINING_TIMES);
if (c & 1) setcolor(SCORE_COLOR); // 恢复颜色
else setcolor(SHINING_SCORE_COLOR); // 变色
}
--shineCount;
if (!shineCount) {
setfont(SCORE_HEIGHT, SCORE_WIDTH, SCORE_FONT, 0, 0,
SCORE_WEIGHT, false, false, false);
setcolor(SCORE_COLOR); // 变回来
}
}
char scoreString[20];
sprintf(scoreString, "%d", score);
int len = strlen(scoreString);
if (shineCount > 0) // 闪烁时字体大小不同位置也要变
ege_drawtext(scoreString,
WINDOW_WIDTH / 2 - len * SHINING_SCORE_WIDTH / 2,
SCORE_Y - SHINING_SCORE_HEIGHT / 2);
else
ege_drawtext(scoreString, WINDOW_WIDTH / 2 - len * SCORE_WIDTH / 2,
SCORE_Y - SCORE_HEIGHT / 2);
// 打印分数
if (score != lastScore && score % 10 == 0) { // lastScore 防止永动
background = (background + 1) % 2,
lastScore = score; // 切换背景
}
bool isClick = false;
bool isPause = false;
while (mousemsg()) {
msg = getmouse();
if (msg.is_left() && msg.is_down()) // 判断是否左键单击
isClick = true;
if (msg.is_right() && msg.is_down()) // 判断是否右键单击
isPause = true;
}
while (isPause && is_run()) {
while (mousemsg()) {
msg = getmouse();
if (msg.is_right() && msg.is_down())
isPause = false;
if (msg.is_left() && msg.is_down())
isPause = false, isClick = true;
}
delay_fps(FPS);
}
// 右键单击, 暂停; 左键或右键单击解除暂停
if (isClick)
player.speedY = CLICK_SPEED;
// 左键单击, 速度改变
delay_fps(FPS); // 延迟
}
// 游戏主体
ege_drawtext
:在屏幕上指定位置绘制字符串,三个参数分别为输出字符串和输出位置左上角的x, y 坐标。
mousemsg()
:相当于键盘事件的kbhit()
函数,判断有没有产生鼠标事件,必须用while
处理,因为鼠标时间会短时间内大量产生,用if
会处理不完。
flushmouse();
gameOver:
reprintBackground();
reprintGround();
putimage_withalpha(NULL, image[GAME_OVER],
WINDOW_WIDTH / 2 - IMAGE_SIZE[GAME_OVER][0] / 2,
WINDOW_HEIGHT / 2 - IMAGE_SIZE[GAME_OVER][1] / 2 - 160);
// 重印背景
putimage_alphatransparent(NULL, image[BOARD],
WINDOW_WIDTH / 2 - IMAGE_SIZE[BOARD][0] / 2,
WINDOW_HEIGHT / 2 - IMAGE_SIZE[BOARD][0] / 2,
EGERGB(255, 255, 255), // 设置白色为透明色
0xFF);
// 重印计分板
char scoreString[20];
sprintf(scoreString, "%d", score);
int len = strlen(scoreString);
setfont(END_SCORE_HEIGHT, END_SCORE_WIDTH, END_SCORE_FONT, 0, 0,
END_SCORE_WEIGHT, false, false, false);
setcolor(brokenRecord ? SHINING_SCORE_COLOR : SCORE_COLOR);
// 打破纪录换颜色
ege_drawtext(scoreString, END_SCORE_X - len * END_SCORE_WIDTH, // 右对齐
END_SCORE_Y - END_SCORE_HEIGHT / 2);
// 打印得分
sprintf(scoreString, "%d", maxScore);
len = strlen(scoreString);
ege_drawtext(scoreString, END_SCORE_X - len * END_SCORE_WIDTH, // 右对齐
END_MAX_SCORE_Y - END_SCORE_HEIGHT / 2);
// 打印最佳得分
int medal = score >= 30 ? GOLD_MEDAL : SILVER_MEDAL;
if (score >= 10)
putimage_withalpha(NULL, image[medal], MEDAL_X, MEDAL_Y);
// 打印奖章
if (is_run()) {
putimage_withalpha(NULL, image[BUTTON_LIGHT], BUTTON_X, BUTTON_Y);
int x, y;
mousepos(&x, &y);
if (x >= BUTTON_X && x <= BUTTON_X + IMAGE_SIZE[BUTTON_LIGHT][0] &&
y >= BUTTON_Y && y <= BUTTON_Y + IMAGE_SIZE[BUTTON_LIGHT][1]) {
putimage_withalpha(NULL, image[BUTTON_DARK], BUTTON_X, BUTTON_Y);
// 按钮变暗产生互动感
bool isClick = false;
while (mousemsg()) {
msg = getmouse();
if (msg.is_left() && msg.is_down()) // 判断是否左键单击
isClick = true;
}
if (isClick)
goto gameBegin; // 重新开始
}
delay_fps(FPS);
goto gameOver;
// 由于按钮的阴影重复打印会变黑, 这里用循环的话要重印很多图片
// 所以直接用了 goto
}
for (int i = 0; i < IMAGE_NUMBER; i++)
delimage(image[i]);
// 销毁图像, 释放内存
return 0; // 结束程序
delimage
:释放图像所占有的内存。 这里需要提醒:一定不要在循环中大量使用getimage
,因为这样会不断申请新空间而没有销毁原来的空间!除非每次用完了都delimage
。
(Tab 是 8 个空格所以没有对齐)https://paste.ubuntu.com/p/jR7dbZ75Ck/
在链接选项(Dev C++ 的编译选项)中加入 -mwindows
即可。
提示一下(虽然我没写)控制台窗口可以用于输出程序运行日志便于查错,用正常的 printf
或者 std::cout
即可。例如著名的 Teeworlds。
Code::Blocks 可以通过资源文件快速设置应用图标:
左上角 File - New - Empty file
,然后点“是”加入当前工程,保存为 xxx.rc
。
然后 Debug
和 Release
版本都选上,点 OK。
在里面输入:MAINICON ICON "FlappyBird.ico"
,其中 FlappyBird.ico
是你的图标,存在 main.cpp
同级文件夹里。再编译一次,图标就按上去了。
之前用 Apktool 解析后的文件中其实也有 png
格式的图标,可以自己寻找一下,网上一搜可以搜到很多在线 png
转 ico
的网站,转换一下就行辣。
https://github.com/ixRic/Flappy-Bird