温州大学 数据科学与大数据技术 21 届 第二学期 程序设计课程设计 期末大作业
这是一个使用 C++ 构建的推箱子游戏,游戏内容如下:
# 项目介绍
# 项目背景
最开始我选择制作的游戏是俄罗斯方块,不过感觉俄罗斯方块所涉及到的方面还是太少了,并且难以添加一些新的功能与玩法,于是便决定换个游戏制作。

在试玩了网上的一些游戏后,我决定制作一款游戏 —— 涂鸦跳跃 Doodle Jump ,决定尝试面向对象编程,将游戏中的不同元素都分为各个对象进行编写。代码行数接近一千行,代码的可读性仍有待提高。
# 技术支持
所使用的 IDE 是由 royqh1979 所编译的 Dev C++
编译器版本是 GCC 10.3.0
程序代码部分使用了
#include "fstream" //C++ 文件处理库(用于游戏存档和得分记录) | |
#include "map" //C++ STL 利用红黑树存储键值对(得分记录) | |
#include "thread" //C++ STL 多线程支持(主要用于获取键盘以及鼠标操作) | |
#include "iostream" //C++ 标准输入输出库(仅在调试中使用) | |
#include "Logo.h" // 图形库使用的是虞老师的 LogoC |
编译时加入了 -lwinmm 指令以实现播放声音的支持(调用 mciSendString 函数)
# 项目结构
image:包含游戏的图片Land:地板的图片System:各个界面的图片以及按钮的图片Theme:包含四个主题不同的背景和小人Welcome:开始界面相关图片
sounds:包含游戏中的声音DoodleJump.h: 头文件包含函数的声明DoodleJump.cpp: 对子文件的调用Land.cpp: 包含Land类,与地块有关Player.cpp: 包含Player类,与小人有关SaveData.cpp: 包含SaveData类,存取存档Scene.cpp: 包含Scene类,场景有关Welcome.cpp: 包含Welcome类,欢迎界面
# 系统设计
# 四个宏定义
#define _Doodle_Jump_Width_ 500 // 游戏窗口宽度 | |
#define _Doodle_Jump_Height_ 800 // 游戏窗口高度 | |
#define _land_num_ 10 // 游戏中所包含的最大地块数量 | |
#define _Gravity_ 0.15 // 重力加速度 |
# 四种枚举类型
程序使用了较多的枚举类型以提高代码可读性:
enum InterfaceType { // 用于描述程序当前所在的界面 | |
I_welcome, I_rule1, I_rule2, I_rank, I_option, I_change_name, I_gamerun, I_gamepause, I_gameretry, I_gameover | |
}; | |
enum DJTheme { // 游戏当前所用的主题名称 | |
Classic, Jungle, Underwater, Winter | |
}; | |
enum PlayerStatus { // 玩家所操控的小人所处的状态 | |
p_left, p_right, fly_left1, fly_right1, fly_left2, fly_right2, rocket_left, rocket_right | |
}; | |
enum LandType { //LandType:地块的类型 | |
normal, fragile, broken, broken_over, mvland, landfly, landrocket, mvspring1, landspring1, mvspring2, landspring2 | |
}; |
# Welcome 类
IMAGE * (*);// 将图片存入该指针所指向的地址(按钮,各子界面图) | |
InterfaceType interfacetype;// 当前界面类型 | |
DJtheme themetype;// 当前主题类型 | |
string path, theme, name;// 路径;主题,玩家姓名 | |
thread p_mouseget;// 线程指针 | |
bool bgm, sound_effect;// 背景音乐,音效开关 | |
multimap<int, string, sortcmp> scorerank_map;// 记录排名的函数 | |
Welcome();// 构造函数,负责变量的初始化 | |
void rank_read();// 读取排名 | |
void rank_save();// 写入排名 | |
void change_name();// 改动玩家姓名 | |
void welcome_mouseget();// 捕获开始界面的操作,通常在多线程调用 | |
void main_interface();// 开始界面循环函数 | |
void draw_welcome();// 欢迎界面的绘制 | |
void show_rule();// 规则界面 | |
void show_rank(InterfaceType pre);// 排行榜界面 | |
void show_option(InterfaceType pre);// 设置界面 | |
void show_pauselabel();// 游戏暂停界面 | |
void show_gameover();// 游戏结束界面 | |
void change_theme();// 更改主题函数,调用各类变换主题子函数 | |
void draw_jump();// 开始界面跳动小人 | |
void pauseget_mouse();// 捕获暂停界面的操作,多线程中调用 | |
void game_run();// 游戏运行循环函数 |
# SaveData 类
void game_save();// 游戏存档 | |
bool game_load();// 游戏继续 |
# Scene 类
IMAGE * im_bk, im_basicline;// 游戏场景背景图 | |
int direct;// 记录接收的按键 | |
Scene();// 构造函数,负责初始化 | |
void change_theme(string theme, string path);// 更改游戏场景背景图 | |
void draw();// 背景绘制 | |
void PlayBGM(string MusicPath);// 播放背景音乐 | |
void PlayMusic(string MusicPath);// 播放音效 | |
void show();// 调用三个 draw 函数 | |
void updateWithoutInput();// 与输入无关的更新 | |
void updateWithKeyInput();// 与键入有关的更新 |
# Land 类
IMAGE * (*);// 将图片存入该指针所指向的地址(地面图片) | |
float land_width, land_height, land_vy;// 地板宽高,地板速度 | |
int score;// 当前成绩 | |
int broken_y;// 记录破碎地板的 y 坐标 | |
struct LandState {// 单个地板的状况 | |
float middle_x;// 地板中心 x 坐标 | |
float top_y;// 地板上部 y 坐标(上部是为了判断碰撞方便) | |
float vx;// 移动地板移动速度 | |
LandType landType;// 地板类型 | |
IMAGE *im_land;// 地板图片,赋图片地址 | |
} land[_land_num_];// 共生成_land_num_个地板 | |
Land(string path);// 构造函数初始化 | |
void retry_clean();// 重新运行游戏时的初始化 | |
void Land_type(int i);// 随机生成地板 | |
void draw();// 绘制地板 | |
void show_topbar();// 绘制顶栏 | |
void updateLandY();// 更新地板 |
# Player 类
IMAGE * (*);// 将图片存入该指针所指向的地址(玩家图片) | |
PlayerStatus playerStatus;// 小人当前状态 | |
float x_middle, y_bottom;// 小人中心 x 坐标,底部 y 坐标(方便碰撞判断) | |
float vx, vy;// 小人水平竖直方向速度 | |
float width, height;// 小人的宽高 | |
float rebound_vy;// 小人反弹的速度 | |
bool isPlayer_yMax, isPlayer_died;// 小人是否到达超过半高,超过半高则小人不动,地面动;判断小人是否死亡 | |
Player(string path, string theme);// 构造函数初始化 | |
void retry_clean();// 重新运行游戏时的初始化 | |
void change_theme(string theme, string path);// 更改主题 (小人) | |
void draw();// 绘制小人 | |
void moveLeft();// 小人左移显示切换 | |
void moveRight();// 小人右移显示切换 | |
void autoJump();// 回到初始小人 | |
void autoJump_fly();// 小人竹蜻蜓显示切换 | |
void autoJump_rocket();// 小人火箭显示切换 | |
void JudgeisPlayer_yMax();// 判断小人是否达到半高 | |
void isOnLand();// 小人与地面碰撞判断 | |
void updateYcoordinate();// 小人的运动更新 Y 轴坐标 | |
void JudgeisPlayer_died();// 判断玩家是否死亡 |
# 关键技术介绍
# LogoC 图片的存储
class IMAGE{ | |
public: | |
IMAGE(const char * s); | |
IMAGE(const IMAGE &); | |
~IMAGE(); | |
int getwidth(); | |
int getheight(); | |
Image * hImage; | |
private: | |
int width; | |
int height; | |
}; |
由于 LogoC 中 IMAGE 类的构造函数仅包含按照路径构造,为了方便修改,以及避免在 class 声明时就将 IMAGE 类初始化导致异常 ( IMAGE 类必须在好像在调用 setup 函数之后才能够使用),我都采用了 IMAGE * 指针类型来储存 IMAGE 对象的地址。
路径的写法我采用了 C++ 的 string 类型,方便修改,在最后使用 c_str 方法转化为 C 语言风格的字符数组字符串当作路径。
# 声音的播放与终止
void Scene::PlayBGM(string MusicPath) { | |
string op = "open " + MusicPath + " alias BGM"; | |
mciSendString(op.c_str(), NULL, 0, NULL); | |
mciSendString("play BGM repeat", NULL, 0, NULL); | |
} | |
void Scene::PlayMusic(string MusicPath) { | |
string op = "open " + MusicPath + " alias soundeffect"; | |
mciSendString("close soundeffect", NULL, 0, NULL); | |
mciSendString(op.c_str(), NULL, 0, NULL); | |
mciSendString("play soundeffect", NULL, 0, NULL); | |
} |
声音的播放我采用的是 MCI ( Media Control Interface ,媒体控制接口) 发送命令的方式实现的。
在 GCC 等非 MSVC 编译器中要加入 -lwinmm 指令使得编译器识别该函数。
# 键盘操作的获取
void Scene::updateWithKeyInput() { | |
if (keymsg()) { | |
kmsg = getkey(); | |
if (kmsg.flag == KEY_DOWN) { | |
if (kmsg.key == 'A' || kmsg.key == 'a' || kmsg.key == 37) { | |
direct = -1; | |
} else if (kmsg.key == 'D' || kmsg.key == 'd' || kmsg.key == 39) { | |
direct = 1; | |
} else if (kmsg.key == 27) { | |
welcome.interfacetype = I_gamepause; | |
} | |
} | |
if (kmsg.flag == KEY_UP) { | |
if ((kmsg.key == 'A' || kmsg.key == 'a' || kmsg.key == 37) || (kmsg.key == 'D' || kmsg.key == 'd' || kmsg.key == 39)) { | |
direct = 0; | |
} | |
} | |
} | |
if (direct == -1) { | |
player.moveLeft(); | |
} else if (direct == 1) { | |
player.moveRight(); | |
} | |
} |
由于键盘的长按有判定时间,大概在几百毫秒左右,但是显示在操作中就会导致小人先动一下然后再持续移动。
为了解决这个问题,我们可以把小人的左移右移设为一个状态,按下左移或者右移时记录下这个状态,然后在松开这些键的时候把状态归零。然后根据当前所处的状态进行操作的更新。
# 实现简陋的输入框功能

while (interfacetype == I_change_name) { | |
bool is_change = false; | |
if (keymsg()) { | |
kmsg = getkey(); | |
if (kmsg.flag == KEY_DOWN) { | |
if (isalnum(kmsg.key)) { | |
if (in.length() <= 10) { | |
in += kmsg.key; | |
is_change = true; | |
} | |
} else if (kmsg.key == 8) { | |
if (in.length() > 0) { | |
in.pop_back(); | |
is_change = true; | |
} | |
} else if (kmsg.key == 13 || kmsg.key == 27) { | |
interfacetype = I_option; | |
} | |
} | |
} |
通过接收输入输出存入字符串,再用 showtext 函数显示出来,虽然简陋,也不支持中文的输入,不过也算是实现了这个功能。
# 游戏的存档与读档
游戏存档为了防止被修改导致游戏出现异常,使用了二进制来存取数据,对于固定大小的类型直接按所占字节进行存取即可,对于非定长类型例如字符串,可以先存储字符串的长度,再存入字符数据即可,这样读取时就知道需要读取多少字节的数据,实现字符串的二进制读写。
存档部分代码:
void SaveData::game_save() { | |
ofstream savedata; | |
size_t StringLength; | |
savedata.open("savedata.dat", ios::out | ios::binary); | |
//Welcome 部分 | |
savedata.write((char*)&welcome.themetype, sizeof(DJTheme)); | |
savedata.write((char*)&welcome.bgm, sizeof(bool)); | |
savedata.write((char*)&welcome.sound_effect, sizeof(bool)); | |
StringLength = welcome.name.size(); | |
savedata.write((char*)&StringLength, sizeof(size_t)); | |
savedata.write(welcome.name.c_str(), welcome.name.size()); | |
//Land 部分 | |
savedata.write((char*)&land.land_width, sizeof(float)); | |
savedata.write((char*)&land.land_height, sizeof(float)); | |
savedata.write((char*)&land.land_vy, sizeof(float)); | |
savedata.write((char*)&land.score, sizeof(int)); | |
savedata.write((char*)&land.broken_y, sizeof(int)); | |
for (int i = 0; i < _land_num_; i++) | |
savedata.write((char*)&land.land[i], sizeof(Land::LandState)); | |
//Player 部分 | |
savedata.write((char*)&player.playerStatus, sizeof(PlayerStatus)); | |
savedata.write((char*)&player.x_middle, sizeof(float)); | |
savedata.write((char*)&player.y_bottom, sizeof(float)); | |
savedata.write((char*)&player.vx, sizeof(float)); | |
savedata.write((char*)&player.vy, sizeof(float)); | |
savedata.write((char*)&player.width, sizeof(float)); | |
savedata.write((char*)&player.height, sizeof(float)); | |
savedata.write((char*)&player.rebound_vy, sizeof(float)); | |
savedata.write((char*)&player.isPlayer_yMax, sizeof(bool)); | |
savedata.write((char*)&player.isPlayer_died, sizeof(bool)); | |
savedata.close(); | |
} |
读档部分代码:
bool SaveData::game_load() { | |
ifstream savedata; | |
size_t StringLength; | |
savedata.open("savedata.dat", ios::in | ios::binary); | |
if (savedata.peek() == EOF) { | |
savedata.close(); | |
return false; | |
} | |
//Welcome 部分 | |
savedata.read((char*)&welcome.themetype, sizeof(DJTheme)); | |
savedata.read((char*)&welcome.bgm, sizeof(bool)); | |
savedata.read((char*)&welcome.sound_effect, sizeof(bool)); | |
savedata.read((char*)&StringLength, sizeof(size_t)); | |
char* buffer = new char[StringLength + 1]; | |
savedata.read(buffer, StringLength); | |
buffer[StringLength] = '\0'; | |
welcome.name = buffer; | |
delete []buffer; | |
//Land 部分 | |
savedata.read((char*)&land.land_width, sizeof(float)); | |
savedata.read((char*)&land.land_height, sizeof(float)); | |
savedata.read((char*)&land.land_vy, sizeof(float)); | |
savedata.read((char*)&land.score, sizeof(int)); | |
savedata.read((char*)&land.broken_y, sizeof(int)); | |
for (int i = 0; i < _land_num_; i++) { | |
savedata.read((char*)&land.land[i], sizeof(Land::LandState)); | |
if (land.land[i].landType == landrocket) { | |
land.land[i].im_land = land.im_landrocket; | |
} else if (land.land[i].landType == landfly) { | |
land.land[i].im_land = land.im_landfly; | |
} else if (land.land[i].landType == mvspring1) { | |
land.land[i].im_land = land.im_mvspring1; | |
} else if (land.land[i].landType == mvland) { | |
land.land[i].im_land = land.im_move; | |
} else if (land.land[i].landType == fragile) { | |
land.land[i].im_land = land.im_break; | |
} else if (land.land[i].landType == landspring1) { | |
land.land[i].im_land = land.im_landspring1; | |
} else if (land.land[i].landType == normal) { | |
land.land[i].im_land = land.im_normal; | |
} | |
} | |
//Player 部分 | |
savedata.read((char*)&player.playerStatus, sizeof(PlayerStatus)); | |
savedata.read((char*)&player.x_middle, sizeof(float)); | |
savedata.read((char*)&player.y_bottom, sizeof(float)); | |
savedata.read((char*)&player.vx, sizeof(float)); | |
savedata.read((char*)&player.vy, sizeof(float)); | |
savedata.read((char*)&player.width, sizeof(float)); | |
savedata.read((char*)&player.height, sizeof(float)); | |
savedata.read((char*)&player.rebound_vy, sizeof(float)); | |
savedata.read((char*)&player.isPlayer_yMax, sizeof(bool)); | |
savedata.read((char*)&player.isPlayer_died, sizeof(bool)); | |
savedata.close(); | |
welcome.change_theme(); | |
return true; | |
} |
# 地板的随机生成
void Land::Land_type(int i, int lnor, int lfr, int mv, int lspr, int lfly, int mvspr, int lroc) { | |
int rd = rand() % 100; | |
if (rd < lnor) { | |
land[i].landType = normal; | |
land[i].im_land = im_normal; | |
} else if (rd < lfr) { | |
land[i].landType = fragile; | |
land[i].im_land = im_break; | |
} else if (rd < mv) { | |
land[i].landType = mvland; | |
land[i].im_land = im_move; | |
} else if (rd < lspr) { | |
land[i].landType = landspring1; | |
land[i].im_land = im_landspring1; | |
} else if (rd < lfly) { | |
land[i].landType = landfly; | |
land[i].im_land = im_landfly; | |
} else if (rd < mvspr) { | |
land[i].landType = mvspring1; | |
land[i].im_land = im_mvspring1; | |
} else if (rd < lroc) { | |
land[i].landType = landrocket; | |
land[i].im_land = im_landrocket; | |
} | |
land[i].middle_x = rand() % (_Doodle_Jump_Width_ - (int)land_width - _Doodle_Jump_Width_ / 2); | |
} |
先使用 srand((unsigned)time(NULL)); 按时间初始化随机数种子,再通过 rand() 函数生成随机数,按照不同概率生成地板类型。可以将概率写入函数参数表中,这样可以更加方便地按照游戏的进行更改不同的概率,提高游戏难度。
# 小人与地板的碰撞判断
碰撞部分较为愚蠢的采用了手写碰撞距离的判断,不过该游戏所需碰撞的分类并不多,手写距离只是限制了地板以及小人高度的变化。通过多分支结构来判断不同地板的情况。
void Player::isOnLand() { | |
for (int i = 0; i < _land_num_; i++) { | |
if ((abs(y_bottom - land.land[i].top_y) <= vy) && (vy > 0)) { | |
if (land.land[i].landType == mvspring1 || land.land[i].landType == landspring1) { | |
if ((x_middle + 61 <= land.land[i].middle_x) || ((x_middle + 25) >= land.land[i].middle_x + land.land_width)) { // 大约估算图像中人物脚的距离,玩家没有落在地面上 | |
return; | |
} else if ((x_middle + 61 >= land.land[i].middle_x + 30) && ((x_middle + 25) <= land.land[i].middle_x + 60)) { // 大约估算图像中人物脚的距离,玩家踩在弹簧上 | |
if (welcome.sound_effect)scene.PlayMusic("sounds/spring.wav"); | |
if (land.land[i].landType == mvspring1) | |
land.land[i].landType = mvspring2; | |
else { | |
land.land[i].landType = landspring2; | |
} | |
y_bottom = land.land[i].top_y; | |
vy = -(2.5) * rebound_vy; | |
} else { | |
if (welcome.sound_effect)scene.PlayMusic("sounds/jump.wav"); | |
vy = -rebound_vy; | |
} | |
autoJump(); | |
} else if (land.land[i].landType == landfly) { | |
if ((x_middle + 61 <= land.land[i].middle_x) || ((x_middle + 25) >= land.land[i].middle_x + land.land_width)) { // 大约估算图像中人物脚的距离,玩家没有落在地面上 | |
return; | |
} else if ((x_middle + 61 >= land.land[i].middle_x + 30) && ((x_middle + 25) <= land.land[i].middle_x + 60)) { // 大约估算图像中人物脚的距离,玩家踩在竹蜻蜓上 | |
if (welcome.sound_effect)scene.PlayMusic("sounds/fly.wav"); | |
y_bottom = land.land[i].top_y; | |
vy = -(4.2) * rebound_vy; | |
autoJump_fly(); | |
} else { | |
if (welcome.sound_effect)scene.PlayMusic("sounds/jump.wav"); | |
vy = -rebound_vy; | |
autoJump(); | |
} | |
} else if (land.land[i].landType == landrocket) { | |
if ((x_middle + 61 <= land.land[i].middle_x) || ((x_middle + 25) >= land.land[i].middle_x + land.land_width)) { // 大约估算图像中人物脚的距离,玩家没有落在地面上 | |
return; | |
} else if ((x_middle + 61 >= land.land[i].middle_x + 30) && ((x_middle + 25) <= land.land[i].middle_x + 60)) { // 大约估算图像中人物脚的距离,玩家踩在竹蜻蜓上 | |
if (welcome.sound_effect)scene.PlayMusic("sounds/rocket.wav"); | |
y_bottom = land.land[i].top_y; | |
vy = -(5.8) * rebound_vy; | |
autoJump_rocket(); | |
} else { | |
if (welcome.sound_effect)scene.PlayMusic("sounds/jump.wav"); | |
vy = -rebound_vy; | |
autoJump(); | |
} | |
} else if (land.land[i].landType != broken) { // 其他类型地面判断 | |
if ((x_middle + 61 >= land.land[i].middle_x) && ((x_middle + 25) <= land.land[i].middle_x + land.land_width && land.land[i].landType != broken_over)) { // 大约估算图像中人物脚的距离 | |
if (land.land[i].landType != fragile) { | |
if (welcome.sound_effect)scene.PlayMusic("sounds/jump.wav"); | |
} else { | |
if (welcome.sound_effect)scene.PlayMusic("sounds/break.wav"); | |
land.land[i].landType = broken; | |
land.broken_y = land.land[i].top_y; | |
} | |
y_bottom = land.land[i].top_y; | |
vy = - rebound_vy; | |
autoJump(); | |
} | |
} | |
} | |
} | |
JudgeisPlayer_died(); | |
} |
# 软件说明书
# 开始界面


开始:开始游戏并清空存档
继续:读取存档以开始游戏,若无存档会报错。


规则:按规则按钮会显示规则。按下一页及返回按钮可以退出规则界面。

排行榜:显示排行榜,按返回按键或者点击界面的其他地方可以退出排行榜界面。

设置:点击音乐音效旁的开关可以开关音乐音效;点击主题旁边的四个小人可以更改主题;点击 “点我改名” 可以改名。点设置界面的其他地方可以退出该界面

# 游戏界面

游戏界面:按 A/D/←/→ 来控制小人的运动



在碰到这三个地板上的时物件时会高速运动一段路程
碰到该地板时,地板会破碎并反弹一次
该地板是会左右移动的
游戏目标就是尽可能得到最高的分数
掉出游戏界面即角色死亡

暂停:暂停按键在右上角,也可以通过 ESC 按键来呼出暂停界面
设置按键可以调出设置;菜单按键可以回到开始界面,同时也会把当前游戏存档;重玩按键可以重开;按下返回按键退出暂停状态。

死亡后可以按重玩按钮重新游戏;按排行榜看排行榜;点击菜单回到开始界面
# 总结
# 遇到的问题及解决方案
# IMAGE 对象的创建
出于对性能以及代码可读性的考虑,我决定使用 IMAGE 对象来存储图片。但是我在创建 IMAGE 对象时经常遇到这种错误:

最后经过单独把 IMAGE 类抽出来排查发现 IMAGE 类的创建必须在运行 setup() 函数之后。
初步估计 setup() 函数中应该包含了 LogoC 图形库初始化相关代码。
# 输入框功能如何实现
由于 LogoC 中没有输入框相关控件的支持,我只能通过 getkey() 函数来实现键盘的输入和输出,再使用 showtext() 函数显示出来。
# 小人运动卡顿问题
游戏中在操作小人运动时会出现小人先动一下然后再持续移动。
这是因为由于键盘的长按有判定时间,大概在几百毫秒左右。为了解决这个问题,我们可以把小人的左移右移设为一个状态,按下左移或者右移时记录下这个状态,然后在松开这些键的时候把状态归零。然后根据当前所处的状态进行操作的更新,就能实现无延迟的小人移动。
# 鼠标运动卡顿问题
在游戏运行过程中,经常会出现鼠标点击却要过一段时间才有反应的情况,是因为等待接受的 mouse 信号过多导致迟滞。解决方案就是通过多线程将获取鼠标的代码独立在一个线程中运行。
# 尚未解决的问题
# 输入框功能仍旧不够完善
所实现的输入框功能过于简陋,仅能实现英文单词和数字的输入,无法实现例如中文的输入,导致玩家名称仅能是英文名,暂无良好的解决方法。
# MCI 对 wav 格式音乐支持问题
在背景音乐播放完一遍时,我需要重新播放一遍背景音乐,一般来说,我们可以在 MCI 命令中加一段 "repeat" 即可,但是在实际操作中,倘若播放的音乐格式是 wav 格式,不仅不能实现重复,音乐也会播放失败,我只能将需要循环播放的背景音乐转为 mp3 格式,经测试能够正常循环播放,但是还是对于 MCI 对 wav 格式的支持感到不解。
# 字体在不同电脑上显示不同
LogoC 中的 textfont() 函数支持的字体选择仅能通过字体在计算机中的序号来选择,导致了在不同电脑中的字体显示不一样。解决方法也是有的,因为我所需要的仅仅只是数字和字母,所以我可以通过把这些字体以位图的方式呈现,再在显示时截取相应片段即可。
但是工作量有亿点点大,最终决定还是放弃了。
# 代码行数过多,可读性较差
项目所有文件的代码总行数超过了 1000 行 (10+171+466+90+86+111+232=1166),在 UI 设计中的行数过于庞大 (超过半数代码),在实现基础的游戏功能时仅用了 200 多行的代码,在项目逐渐庞大之后,不经常写注释的弊端便暴露出来,导致后半段写的代码可能包含一些重复的代码,降低了可读性,也使得代码看上去没有那么美观。
# CPU 占用过高
写出来的程序所占用的资源过多,在我自己的电脑(2015 款的 MacBook Air)会有极为严重的卡顿现象,即使使用多线程也无法改善。而且在学校机房的电脑上,运行游戏时也会有接近 20% 的 CPU 占用,暂无良好的解决方案。
# 参考资料
- 《LogoC 使用参考》 虞铭财
- 菜鸟教程