分享20级同学大一上学期用C语言(及少量C++)实现的小球进框游戏。由于同学们刚学了三个月的编程,实现还不够完善,工程代码、图片音乐素材可以从百度网盘下载:
链接:https://pan.baidu.com/s/1gcjDdn8HY373TcUSyrdC7Q
提取码:7q33
该程序功能的主体设计参考了网页小游戏“炮弹进框”(Cannon Strike),通过C语言编程进行实现。其中应用了EasyX插件作为辅助工具,力求最大程度地还原游戏功能及特色(如小球的下落及碰撞效果),以提升玩家的游戏体验,增强游戏的趣味性。同时,我们也对其中部分内容进行了创新和优化,使各界面尽可能简洁美观,并增加了多个交互界面(如选关,暂停,存档),为玩家提供良好的视觉效果,和更人性化的操作要求。玩家可通过点击鼠标实现基本的游戏操作。
游戏按照由易到难的顺序设计关卡,为了增加游戏的可玩性,我们每一关都导入了新的元素,以此逐步激发玩家的游戏兴趣和挑战激情。玩家可根据提示点击对应文字进入关卡,游戏中通过点击鼠标左键进行发射小球,使小球在空中碰撞或者躲避挡板和障碍物后落入框中,框中小球数量达到需要的数量即可过关。游戏过程中可随时暂停、存档、退出、重新开始,也可以选择进入任意已解锁的关卡。
2.1 定义结构体和变量
在代码开头,我们定义了关于小球参数的结构体和游戏中所要用到的全局变量,并进行了必要的初始化。
2.2 初始化函数
初始化函数为startup(),是为游戏的进行做一些提前准备。具体体现为:
用initgraph()函数开辟了画面,并将背景设为白色。
导入了游戏中所需要的图片及背景
2.3 游戏主界面函数
游戏主界面函数为Startmenu(),该函数功能为显示游戏主界面并通过代码:
if (MouseHit()) // 如果有鼠标消息
{
m = GetMouseMsg(); // 获得鼠标消息
}
等待用户点击鼠标进行操作,在接收到鼠标信息后通过if语句进行判断,并执行对应指令。在此界面中用户可以看见游戏名称及游戏说明,并能够通过点击对应文字来进行“开始新游戏”、“读档”、“存档”、“退出游戏”等操作。
2.4 关卡选择界面函数
关卡选择函数为Maininterface(),该函数通过Access[]数组中存储的信息来判断对应关卡是否已经解锁,通过绘制图像向用户直观显示所有关卡的状态(绿色表示该关卡可进入,灰色表示该关卡不可进入),并等待用户点击鼠标进行选关操作,玩家可以在解锁的关卡中自由选择进行游戏。
2.5 菜单界面函数
菜单界面函数为Pausemenu(),按下Esc键可暂停游戏并通过该函数进入菜单界面。该函数与游戏主界面函数(详见2.3)相类似,绘制界面并等待接收用户鼠标信息进行“继续游戏”、“重新开始”、“返回主界面”、“退出”等操作。
2.6 读取存档函数
读取存档函数为readRecordFile(),可以读取存档中的关卡是否解锁的信息数据,并写入到Access[]数组中,以延续之前的解锁进度继续游戏。
2.7 写入存档函数
写入存档函数为writeRecordFile(),与读取存档函数(详见2.6)相对应,功能是将当前关卡数据写入文档。
2.8 播放音效函数
播放音效函数为PlayMusicOnce(const TCHAR fileName[80]),每次发射小球时读取发射音效文件并播放一次。为了尽可能还原原来的游戏,我们将原游戏的音效录制为了MP4文件。
2.9 与关卡有关的数据更新
与关卡有关的数据更新函数为Levelupdate(int n),其中n为关卡数,该函数中记录每个关卡的数据。在确定需要加载的关卡数后调用此函数可以对本关所有数据进行初始化。
2.10 计算所有小球间距离的平方
计算所有小球间距离的平方函数为get_distance(struct Ball BALL[]),将已经发射的小球参数(x,y坐标)传入函数,通过遍历计算小球间两两距离的平方,得到该小球最近的小球距离及其下标,记录在小球参数结构体(struct Ball)中的distance[]数组中,方便小球间的碰撞判断。
2.11 判断游戏是否结束
判断游戏是否结束函数为isfinished(),顾名思义,该函数通过if语句判断单次游戏是否已经结束。如果框中小球数量达标或者所有小球都已落到框中或框外则当前关卡结束,函数返回值为1,否则函数返回值为0。
2.12 判断碰撞
判断碰撞函数为knock(struct Ball BALL[]),判断碰撞可分为以下部分:
对小球进行遍历先利用get_distance(struct Ball BALL[])函数(详见2.10)中计算出的两个小球间的距离的平方与四倍半径的平方进行比较,判断两小球是否碰撞。对所有碰撞的小球,通过两者的坐标判断它们的相对位置,最终实现两个小球的运动状态改变。
球与挡板碰撞后球的速度方向、大小改变,实现反弹效果。
框的形状是不规则的,因此在判断球与框碰撞的过程中我们对框进行了拆分,根据碰撞点的不同分成了多种情况进行设计,不断地调试数据达到理想的碰撞效果。
球与障碍物碰撞后小球将被击碎,通过更改坐标使其直接落出对话框外,再绘制分裂出的几个更小的小球,其中小小球的个数和每个小小球的大小、方向随机。
2.13 小球及挡板的运动
move()函数通过对小球及挡板速度、坐标的不断更新来实现它们的运动的描述,用结构体数组记录所有小球的状态。
2.14 与输入无关的更新
与输入无关的更新函数为updateWithoutInput(),在该函数中通过精确延时函数MyTimer()(详见2.17),每隔8帧执行move()函数,进行小球及挡板的位置更新。另外,此函数中还通过对小球遍历,分别判定它们所处的位置,从而更新框中小球的个数和失去小球的个数。
2.15 与输入有关的更新
与输入有关的更新函数为updateWithInput(),该函数可根据用户输入执行相应操作。实现点击鼠标发射小球并播放一次音效(详见2.8)的功能,并且按下“ESC”键可将游戏暂停,切换为菜单界面(详见2.5)。
2.16 进行绘制
进行绘制函数为show(),该函数通过判定游戏状态gameStatus来进行对应绘制:
当游戏在主界面状态时,通过调用游戏主界面函数Startmenu()(详见2.3)绘制主界面,并等待下一步操作。
当游戏在关卡选择界面状态时,调用关卡选择界面函数Maininterface()(详见2.4)绘制关卡选择界面,并等待下一步操作。
当游戏暂停状态时,调用菜单界面函数Pausemenu()(详见2.5)绘制菜单界面,并等待下一步操作。
当处于正常游戏状态且游戏未结束时,进行游戏中发射器、小球、挡板、框等各元素图像的绘制以及必要的文字提示。通过调用isfinished()函数(详见2.11)判断游戏是否结束。
当isfinished()函数(详见2.11)返回值为1,即游戏结束时,用if语句判断是否通关(框内小球数是否达标),并绘制对应结算界面,如果游戏胜利则继续通过损失的小球数判断并绘制对应星星。最后绘制对应按钮并等待用户鼠标操作,如果接收到有效的鼠标信息则更新关卡数据,进入对应关卡或改变游戏状态。
2.17 精确延时函数
精确延时函数MyTimer可以通过获取时钟来进行计时,这种方法起到的延时效果较为稳定,受电脑性能影响较小。
3.1 关卡数据
关卡数据影响着难度系数和玩家体验,为了增强玩家体验,我们反复测试关卡的难度是否合理,并邀请其他同学一起体验,不断改进,以达到最终合理的效果。
3.2 精确延时函数
在初始代码中,我们用了以下的延时函数:
static int waitIndex = 1; // 静态局部变量,初始化时为1
waitIndex++; // 每一帧+1
if (waitIndex == 15) // 如果等于15才执行
{
move(); // 调用移动函数
waitIndex = 1; // 再变成1
}
但是在运行游戏时发现,游戏一开始的速度很快,但由于电脑性能的原因,运行速度会逐渐慢下来。
面对这样一个严重的问题,我们最终通过查阅资料,发现可以用时钟的时间使程序运行帧率稳定。
所以在最终代码中,我们加入了
3.3 各函数间调用的逻辑关系协调
在每次完成一个游戏基本功能的设计和编写,并封装成相应函数后,我们时常会面临了一个难题:怎样将这些功能整合进游戏中去。为解决这一问题,我们先是梳理了各部分间的逻辑关系,发现函数的调用大都需要对具体情况进行判断。于是我们又新增了一些参数(如游戏状态gameStatus、关卡是否解锁Access[]、是否加载对应关卡isReloadLevel、关卡数Level)以及函数(如判断游戏是否结束isfinished())来判断具体情况。最终,在具体代码中,我们就能按照最初拟定的设想,将这些功能通过逻辑关系串联进去。当然,在这个过程中也少不了反复耐心地调试,因为一点点小小的逻辑错误就很有可能使得程序实现和最初设想大相径庭。
3.4 小球的碰撞效果——knock()函数的编写
小球间的碰撞判定一直是困扰我们的一大难题。由于小球发射下落与碰撞的不确定性使得这个问题看起来异常复杂。最终我们想到了一个可行的思路,那就是通过小球间距离的计算来判定小球间是否会碰撞。
在编写了计算所有小球间距离的平方函数get_distance(struct Ball BALL[])(详见2.10)和小球间碰撞的相应代码后,我们又发现了另一个重大问题:由于速度变化后极短时间内小球仍处于碰撞状态,有可能小球速度被再次交换。于是我们通过对小球距离重新赋值来解决这一问题,具体代码如下:
BALL[j].distance[0] = 99999999;
BALL[j].distance[1] = -1;
BALL[i].distance[0] = 99999999;
BALL[i].distance[1] = -1;
框是一个不规则图形,因此在判断碰撞时要将框进行拆分,如图所示,框被拆分成1、2、3三个部分。
第一部分是框的底部,球与框的底部碰撞后将落在框中。
第二部分是框的左右两侧,球碰撞后会反弹进框中,x方向速度改变,但不影响y方向速度。
第三部分是框的上面,球碰撞后将被弹起,y方向速度改变,但不影响x方向速度。
由于小球是逐帧移动,前后的位置并不连续,因而在反弹时要对小球的坐标进行校准。(需添加代码:BALL[i].y = HEIGHT - 257;)
在框内的小球碰撞时,我们适当减小了反弹的效果,以免小球被反弹出框外。然而这样也会导致一定的问题。同样也是由于逐帧移动而带来的位置不连续性,需要对小球位置进行校准。对应代码为:
while ((BALL[i].x - BALL[j].x) * (BALL[i].x - BALL[j].x) + (BALL[i].y - BALL[j].y) * (BALL[i].y - BALL[j].y) < 4 * Radius * Radius + 8){
if (BALL[i].y < BALL[j].y)
BALL[i].y -= 0.1;
if (BALL[i].y > BALL[j].y)
BALL[j].y -= 0.1;
if (BALL[i].x < BALL[j].x){
BALL[i].x -= 0.05;
BALL[j].x += 0.05;}
if (BALL[i].x > BALL[j].x){
BALL[j].x -= 0.05;
BALL[i].x += 0.05;}}
在碰撞情况中这样类似的细节还有很多,在此就不一一赘述了。
本次的游戏开发大作业耗时一个多月,从框架设计到代码的编写修改,再到最终的整合运行,这之中让我体会到了太多。代码的编写过程始终都需要保持代码的整洁规范,变量的命名也要具有意义,否则,一旦出错就有可能会造成崩盘,一些逻辑问题也无从找出,让人抓狂却又无能为力。而成百上千次的调试运行则更是对自我的一种考验,一条语句的修改带来的就可能是翻天覆地的变化。严谨细致,精益求精,也许这就是一个优秀的编程者所应具备的品质吧。
这次的游戏开发对我来说不仅是一次对编程能力的锻炼,更是一次对良好习惯与能力的培养。在与组员讨论、整合代码的过程中,我学会了交流合作;在尝试解决问题的过程中、我学会了自主学习与查询资料;在每个功能得以实现的那一刹那,我也体会到了无比的激动与喜悦……相信这次作业带给我的收获将会使我终生受用。