同学们实现的效果:
https://www.zhihu.com/video/1066249425780809728以下是开发同学的相关文档:
《Ekko》设计报告
本组设计并编写的游戏《Ekko》,是一款引用了当下红火的网络游戏《英雄联盟》中的游戏角色Ekko为主角,由本组三名成员使用C语言编写的一款横屏动作闯关游戏。
一 设计思路
在选取游戏题材作为最终的课程设计目标时,一如许多组的同学一样,我们曾考虑过将一个现成的游戏拿来改编,但最后我们决定自己做一款游戏。而我们要做的第一步,则是明确我们要做一款什么样的游戏。受一款动作游戏《Super Hot》灵感,本组成员想要做一款主角拥有控制时间能力的动作游戏。
在《Super Hot》中,玩家操控的角色拥有一种令人着迷的能力:放慢时间,让原本正常的世界的时间流动速度变慢,从而拥有更强、更从容的反应能力来应对敌人。我们认为这是一个非常酷炫的游戏概念,并想要做一款能体现这一游戏概念的游戏。
受限于我们的知识储备,我们只能使用C语言来编写游戏,而与此同时,我们也只能用EasyX来进行绘图。因此,我们最终决定设计一款2D的动作游戏,而这款游戏能展现出令人着迷的控制时间的能力。
而在设计我们的主角的时候,我们亦曾想过做一个“能控制时间的忍者”,而之后我们注意到了在现今的网络游戏中大红大紫的竞技游戏《英雄联盟》之中,就有一个类似的能控制时间的角色:艾克(Ekko)。艾克是一名生长于名为祖安的城市的少年,拥有控制时间的能力,并利用这个能力惩奸除恶,践行自己的价值观,守护着祖安城。艾克的人设展现出令人钦佩的人性光彩,和他桀骜不羁的个性。我们几乎一拍即合,决定采用Ekko作为我们游戏的主角。
而在游戏构架的构思中,我们为了最大程度体现“控制时间”这一能力,决定设计一种关卡难度极大的游戏——需要玩家细腻的操作,并且正确地使用“时间暂停”这一能力。
谈及动作游戏的难度,最容易想到的有两点:1、地图的复杂性;2、敌人攻击的危险性。因此,在最初的构思中,我们决定给予主角仅仅一条生命——如同《超级马里奥》一般,被敌人的攻击命中,便会死亡。同时,我们会设计复杂而多变的地形,以及大量的敌人、更大量的弹幕。
而我们不能只关注游戏难度是否够高,而应同时关注玩家的游戏体验。所以我们必须将主角的操作细节设计得足够细腻——以回应玩家细腻的操作。
综上,我们的游戏的基本雏形便构建完毕——在祖安城内,一位名为Ekko的少年,在大量的敌人面前把玩着自己的控制时间的能力,灵巧地躲避敌人的各种攻击并前进,这就是我们想做出来的一款原创跑酷游戏。
二 功能描述
在我们组的《Ekko》的设计中,由我来担任Ekko的运动的编写,以及Ekko与地图中各种各样的互动。
以下为艾克——这名酷炫的时间刺客,需要具有什么样的基础功能,以及什么样的“画龙点睛”,才能给玩家一个不错的游戏体验。
1:显示素材。不可否认的是,对于计科专业的同学来说,期末课设最为直接的素材收集方式为搜索互联网。但是这对于我们组而言不可行——因为我们要做的游戏中,需要的元素大部分在网上找不到,原因则为我们游戏虽题材非原创,但游戏方式与界面却与原素材的相去甚远(比如英雄联盟中的3D建模下的艾克是无法应用在我们的游戏中的)。因此,我们组只能自己去绘制我们的素材。
此外,绘制好的素材需要放在特定文件夹内,以便编程时用代码读取这些素材;此外,我们组还需要学会如何利用EasyX,在游戏中显示透明的图层。
2:人物移动。在课程中,我们学习了最基本的人物移动方法:利用getchar函数和几个简单的判断,来实现人物坐标的改变。但这对于我们的Ekko是不够的——这个函数的读取模式存在着“第一次和第二次之间存在停顿”的问题。因此,我需要寻求更加优秀的算法来实现人物的移动。
3:与场景的碰撞。在许多简单的游戏中,与场景的碰撞可以简单的归结为“和一两个物体之间的碰撞判定”,但这个判定方式在一个更加复杂的地图中时不成立的——我们需要编写大量碰撞判断,这让代码变得冗杂,难以阅读和编写,容易出错,且不易调试。因此,我需要写一套“碰撞法则”,以方便后期的地图编写。如此,只需要一套法则,就可以应对各种各样的地图而不易出现问题。
4:时间暂停。这个功能是我们的游戏中的亮点。Ekko正是因为能够控制自己的时间,才能成为“撕裂时空的少年”。这一功能的实现方法很多。
5:正确的死亡。我需要编写艾克在合何时会死亡的代码。
三 分步骤实现方法
第一阶段:绘制素材,准备好人物图片与对应的遮罩图;
第二阶段:架构基本游戏框架,采用不同的源文件以封装不同的功能。利用掩码图将人物放入,调试直至能够正常的演示图片,并加入最简单的人物移动功能。为了使游戏运行更加顺畅,我花费了大量的时间来构建人物的移动,代码量多达一千行左右。以下为代码的一部分:
if ((GetAsyncKeyState(0x41) & 0x8000)) // a
{
if (Ekko_Speed_x >= -2.5)
{
Acceleration_x = -0.1;//获得加速度
Ekko_Speed_x += Acceleration_x;
}
else if (Ekko_Speed_x <= -3)
{
Acceleration_x = 0.8;
Ekko_Speed_x += Acceleration_x;
}
if (Crash_Wall())//如果要撞墙
{
Acceleration_x = 0;
Ekko_Speed_x = 0;
goto loop1;
}
if (Ekko_Face == 1)//变换方向
{
Ekko_Face = -1;
}
Ekko_x += Ekko_Speed_x;
if (DropOrNot == 0)
{
Status_check_i = 1;
}
else if (DropOrNot == 1)//检测空中是否按过AD
{
AD_in_Air = 1;
}
}
需要注意的是,单独的这一段代码并不能看出什么直接意义。这些变量的功能穿插于许许多多的函数中,牵一发而动全身。许多变量不仅用来记录数据,还用来记录状态。
第三阶段:优化人物移动,加入人物和环境的碰撞的法则并测试。这一阶段的完成,意味着地图编写工作可以正式开始;然而该阶段为开发过程最为艰难的部分之一。没有现成的物理引擎的支持,自己从头开始写,并非一件容易的事情,需要大量的耐心、细心,以及长时间的测试,同时这个阶段面临的BUG是最多的。
以下为碰撞代码的核心内容:
int ABS(float A, float B) //float型绝对值。单独定义并且用在下面的函数中
{
float C;
C = A - B;
if (C >= 0)
{
return C;
}
else
{
C = (-1)*C;
return C;
}
}
int Crash_Wall()//判断是否即将和地形碰撞。即将 碰撞 返还 1 ,否则返还 0
{
for (int i = 0; i < Block_Number; i++)
{
if (Ekko_Speed_x > 0)
{
if (
Ekko_x + Ekko_Width / 2 <= Land_Left[i] && Ekko_x + Ekko_Speed_x + Ekko_Width / 2 > Land_Left[i]
&&
Ekko_y + Ekko_High / 2 > Land_Top[i] && Ekko_y - Ekko_High / 2 < Land_Bottom[i]
)
{
LockedOne_x = i;
return 1;
//break;
}
else if (i == LockedOne_x && ABS(Land_Left[i], Ekko_x + Ekko_Width / 2) < 4)
{
return 1;
}
else
continue;
}
else if (Ekko_Speed_x < 0)
{
if (
Ekko_x - Ekko_Width / 2 >= Land_Right[i] && Ekko_x + Ekko_Speed_x - Ekko_Width / 2 < Land_Right[i]
&&
Ekko_y + Ekko_High / 2 > Land_Top[i] && Ekko_y - Ekko_High / 2 < Land_Bottom[i]
)
{
LockedOne_x = i;
return 1;
//break;
}
else if (i == LockedOne_x && ABS(Ekko_x - Ekko_Width / 2, Land_Right[i]) < 3)
{
return 1;
}
else
continue;
}
}
return 0;
}
int Crash_Ground()//判断和地面是否碰撞,即将碰撞返还1,否则返还0 加入了踩到地刺的判定
{
for (int i = 0; i < Block_Number; i++)
{
if (Ekko_Speed_y > 0)
{
if (
Ekko_y + Ekko_High / 2 <= Land_Top[i] && Ekko_y + Ekko_Speed_y + 0.06 + Ekko_High / 2 > Land_Top[i] //0.06是Y轴方向加速度
&&
Ekko_x + Ekko_Width / 2 > Land_Left[i] && Ekko_x - Ekko_Width / 2 < Land_Right[i]
)
{
LockedOne_y = i;
if (LockedOne_y == 6||LockedOne_y == 7 || LockedOne_y == 8 || LockedOne_y == 11 || LockedOne_y == 12 || LockedOne_y == 44 || LockedOne_y == 45 || LockedOne_y == 46 || LockedOne_y == 64 || LockedOne_y == 65 || LockedOne_y == 66)
DeadOrNot = 1;
return 1;
//break;
}
else
continue;
}
else if (DropOrNot == 1 && Ekko_Speed_y == 0)//悬空时速度为0
{
return 0;
}
else if (DropOrNot == 0 && Ekko_Speed_y == 0 && LockedOne_y == i && (Ekko_x + Ekko_Width / 2 < Land_Left[i] || Ekko_x - Ekko_Width / 2 > Land_Right[i]))//落地后速度为0但是踩空
{
return 1;
}
else if (DropOrNot == 0 && Ekko_Speed_y == 0 && LockedOne_y == i && (Ekko_x - Ekko_Width / 2 < Land_Right[i] && Ekko_x + Ekko_Width / 2 > Land_Left[i]))//落地后速度为0,踩实
{
return 0;
}
else if (Ekko_Speed_y < 0)
{
return 0;
}
}
return 0;
}
int Crash_Top()
{
for (int i = 0; i < Block_Number; i++)
{
if (Ekko_Speed_y < 0)
{
if (
Ekko_y - Ekko_High / 2 >= Land_Bottom[i] && Ekko_y + Ekko_Speed_y - Ekko_High / 2 < Land_Bottom[i]
&&
Ekko_x + Ekko_Width / 2 > Land_Left[i] && Ekko_x - Ekko_Width / 2 < Land_Right[i]
)
{
LockedOne_y = i;
return 1;
//break;
}
}
else
break;
}
return 0;
}
简而言之,这些代码实现了主角和任意设置好的矩形都能发生正确的碰撞,使得地图的开发变得简单——只需要记录各个地图板块的坐标即可。
第四阶段:测试人物和地图的碰撞,并加入冲刺能力、时间减缓能力,并测试。
冲刺代码:
static float SIN, COS;//记录角度
AD_in_Air=0;
MOUSEMSG mouse;
mouse.uMsg = false;
if (MouseHit())
{
mouse = GetMouseMsg();
}
if (mouse.uMsg== WM_RBUTTONDOWN && Dash_Check == 0&&Dash_limit==0) //准备冲刺
{
Dash_Speed = 11;
COS = ((mouse.x - Screen_Center_x) / sqrt((mouse.y - Screen_Center_y)*(mouse.y - Screen_Center_y) + (mouse.x - Screen_Center_x)*(mouse.x - Screen_Center_x)));
SIN= ((mouse.y - Screen_Center_y) / sqrt((mouse.y - Screen_Center_y)*(mouse.y - Screen_Center_y) + (mouse.x - Screen_Center_x)*(mouse.x - Screen_Center_x)));
Ekko_Speed_x = Dash_Speed * COS;//鼠标位置决定冲刺方向
Ekko_Speed_y = Dash_Speed * SIN;
Dash_Check = 1;
Dash_limit = 1;
if (!Crash_Wall()&&!Crash_Top()&&!Crash_Ground())
{
Ekko_x += Ekko_Speed_x;
Ekko_y += Ekko_Speed_y;
}
else
{
Ekko_Speed_x = 0;
Ekko_Speed_y = 0;
Dash_Check = 0;
Dash_limit = 0;
}
if (Dash_Check==1)
{
Status_check_i = 3;//图形演示变为冲刺
if (Ekko_Speed_x > 0)
Ekko_Face = 1;
else
Ekko_Face = -1;
DropOrNot = 1;
w_check = 1;
JumpOrNot = 1;
jump_limit_check = 1;
}
}
else if (Dash_Check == 1) //冲刺过程不可控
{
if (Dash_Speed > 3)
{
Dash_Speed -= 0.2;
}
Ekko_Speed_x = Dash_Speed * COS;
Ekko_Speed_y = Dash_Speed * SIN;
if (Crash_Wall())
{
Ekko_Speed_x = 0;
Dash_Check = 0;
}
if (Crash_Top())
{
Ekko_Speed_y = 0;
Dash_Check = 0;
}
if (Crash_Ground())
{
Ekko_Speed_y = 0;
Dash_Check = 0;
Dash_limit = 0;
}
if(Dash_Speed<=5)
{
Dash_Check = 0;
}
Ekko_x += Ekko_Speed_x;
Ekko_y += Ekko_Speed_y;
}
else //非冲刺的情况
... ...(其他功能)
第五阶段:加入人物的死亡功能——被敌人的子弹击中时死亡,坠入地底时也会死亡,碰到地刺的时候亦会死亡。
第六阶段:优化、加入与Ekko语音的音乐素材。
四 体会与总结
在最后完成游戏的一瞬间,心中固然有着千般喜悦,但苦涩感依然无法散去。这一个月的开发过程并不总是一帆风顺,在面对众多的BUG和技术难关时,自己知识的匮乏彻彻底底地暴露出来了,这也迫使自己进一步去学习新的知识。
令我高兴的是,游戏的效果不错——拥有着相当流畅的游戏体验、自己制作的素材活灵活现地展现在游戏中、游戏本身有着诸多趣味等等,依然会让我会心一笑。在这样的过程中,我领悟到了编程带来的无穷乐趣,以及编程的深奥。这样宝贵的经历,必将促使我进行更加深入的学习,以做出让自己更为满意的作品!
分步骤代码、素材、开发难点讲解视频、报告文档,可以从百度网盘下载:
https://pan.baidu.com/s/15lDd1bAwBjsZm6oUi5NHhApan.baidu.com