本帖最后由 createskyblue 于 2018-9-17 20:20 编辑
前排提示,游戏文件、在线模拟在2楼
1楼为制作教程
视频: https://www.bilibili.com/video/av31933044/
第一步 导入素材
第二步 地图生成器
[mw_shl_code=cpp,true]void BuildMap() {
/*
0 空气
1 不可摧毁墙
2 普通墙
3 TNT
4 爆炸3
5 爆炸2
6 爆炸1
*/
//生成墙和怪物
LIFE = 3; //开局3条命
byte MN = 0; //重置怪物计数器
PP = 2; //设置玩家方向头朝下[/mw_shl_code]
首先要生成墙壁,先生成边框防止玩家跑出地图,最后生成边框里面墙壁阵列
[mw_shl_code=cpp,true]for (byte y = 0; y < 15; y++) {
for (byte x = 0; x < 31; x++) {
if (y == 0 || y == 14) {
MAP[x][y] = 1; 生成y轴墙壁外围 下图橙色区域
} else if (x == 0 || x == 30) {
MAP[x][y] = 1; 生成x轴墙壁外围 下图蓝色区域
} else {
if (x % 2 == 0 && y % 2 == 0) {
MAP[x][y] = 1; 判断当前x,y坐标是否为偶数,如果是则生成内部墙壁
} else if (random(0, 4) == 0) {
MAP[x][y] = 2; 1/4概率生成可以破坏的墙
} else if (random(0, 28) == 0) { 1/28 的概率生成怪物
//生成怪物
if (MN < LEVEL) { 确保怪物数量少于当前关卡数
monster[MN][0] = x; 记录当前编号怪物的x,y坐标
monster[MN][1] = y;
MLRUD[MN] = 2; //设置怪物方向为头朝下 0上 1右 2下 3左
MN++; 下一个怪物编号
}
} else MAP[x][y] = 0; 假若不满足上边的条件则生成空气
}
}
}[/mw_shl_code]
[mw_shl_code=cpp,true]//设置玩家出生点
for (byte py = 0; py < 3; py++) {
for (byte px = 0; px < 3; px++) {
//清空出生点附近的普通墙和怪物,确保出生点安全
MAP[15 - 1 + px][7 - 1 + py] = 0;
}
}
清除玩家出生点附近3*3区域内所有方块,确保有较大的空间让玩家开一条路出去
PX = 15; 设置玩家在地图上的出生点
PY = 7;
}[/mw_shl_code]
上图为生成器生成的初始游戏地图 第三步 实现玩家移动 绘图部分
[mw_shl_code=cpp,true]void loop() {
if (LIFE == 0) FAIL(); //如果生命为0 游戏结束
key(); //扫描按键
Draw(); //绘图
logic(); //逻辑处理
}
/*=========================================================
按键扫描
=========================================================*/
void key() {
/*
0 1 2 3 4 5
↑ ↓← → A B
*/
KeyBack = 255;
if (arduboy.pressed(UP_BUTTON)) KeyBack = 0;
if (arduboy.pressed(DOWN_BUTTON)) KeyBack = 1;
if (arduboy.pressed(LEFT_BUTTON)) KeyBack = 2;
if (arduboy.pressed(RIGHT_BUTTON)) KeyBack = 3;
if (arduboy.pressed(A_BUTTON)) KeyBack = 4;
if (arduboy.pressed(B_BUTTON)) KeyBack = 5;
}
/*===================================================================
绘图
===================================================================*/
void Draw() {
DrawMap(); //渲染地图
DrawEntity(); //渲染实体
arduboy.display(); //发送画面到屏幕
arduboy.fillRect(0, 56, 128, 8, 0);
for (byte ni = 0; ni < LIFE; ni++) {
arduboy.drawSlowXYBitmap(ni * 9, 56, LOVE, 8, 8, 1); //生命条
}
arduboy.display(); //发送画面到屏幕
}
/*=================================================================
渲染地图
==================================================================*/
void DrawMap() {
arduboy.fillRect(0, 0, 128, 64, 1);
for (char y = PY - 4; y < PY + 5; y++) {
for (char x = PX - 8; x < PX + 10; x++) {
if (x >= 0 && y >= 0 && x <= 30 && y <= 14) {[/mw_shl_code]
用两个嵌套的for语句来扫描玩家视线内的方块,然后用switch语句显示出对应方块的位图 下图灰色区域为玩家所能看到的区域 黑框内为128x64OLED的显示范围 游戏中,无论怎样移动移动的都是地图而不是玩家本身,玩家是被固定在屏幕中心位置的,而且地图方块对应的位置不代表实际显示的位置,为了正确显示我们需要减去地图方块与玩家相对坐标 (参考下图紫色箭头) 为了直观告诉大家哪里做了处理,下列显示代码会把要坐标处理的部分染为紫色
[mw_shl_code=cpp,true] switch (MAP[x][y]) {
case 1:
arduboy.drawSlowXYBitmap(x * 8 - (PX - 15) * 8 - 64 + CSX, y * 8 - (PY - 7) * 8 - 32 + CSY, WALL_1, 8, 8, 0);
break;
case 2:
arduboy.drawSlowXYBitmap(x * 8 - (PX - 15) * 8 - 64 + CSX, y * 8 - (PY - 7) * 8 - 32 + CSY, WALL_2, 8, 8, 0);
break;
case 3:
arduboy.drawSlowXYBitmap(x * 8 - (PX - 15) * 8 - 64 + CSX, y * 8 - (PY - 7) * 8 - 32 + CSY, TNT_table[TNTS], 8, 8, 0);
break;
case 4:
arduboy.drawSlowXYBitmap(x * 8 - (PX - 15) * 8 - 64 + CSX, y * 8 - (PY - 7) * 8 - 32 + CSY, BOOM_1, 8, 8, 0);
break;
case 5:
arduboy.drawSlowXYBitmap(x * 8 - (PX - 15) * 8 - 64 + CSX, y * 8 - (PY - 7) * 8 - 32 + CSY, BOOM_2, 8, 8, 0);
break;
case 6:
arduboy.drawSlowXYBitmap(x * 8 - (PX - 15) * 8 - 64 + CSX, y * 8 - (PY - 7) * 8 - 32 + CSY, BOOM_3, 8, 8, 0);
break;
}
}
}
}
TNTS++;
if (TNTS >= 2) TNTS = 0;
}
/*====================================================================
渲染实体
====================================================================*/
void DrawEntity() {
if (LIFE > 0) {
//渲染怪物
for (byte n = 0; n < 10; n++) {
if (MLRUD[n] != 255) arduboy.drawSlowXYBitmap(monster[n][0] * 8 - (PX - 15) * 8 - 64 + CSX, monster[n][1] * 8 - (PY - 7) * 8 - 32 + CSY, M_table[byte(MLRUD[n])], 8, 8, 0);
}[/mw_shl_code]
Man_table 存储的是炸弹人动作列表:{ 炸弹人_朝上_1, 炸弹人_朝上_2, 炸弹人_朝上_3, 炸弹人_朝右_1, 炸弹人_朝右_2, 炸弹人_朝右_3, 炸弹人_朝下_1, 炸弹人_朝下_2, 炸弹人_朝下_3, 炸弹人_朝左_1, 炸弹人_朝左_2, 炸弹人_朝左_3} 炸弹人有4个方向的贴图,每个方向拥有3个动作,加起来一共12幅图 变量PP指的是炸弹人的方向,决定选择哪一组贴图 变量PS为炸弹人当前动作帧,当每一帧动作显示后自加或者复位 实现炸弹人移动动画 因此得出Man_table[PP * 3 + PS]这条式子 当炸弹人受到伤害后将会被扣血,扣血后会进入一段时间的无敌状态避免短时间重复受到伤害 而无敌模式中只会显示炸弹人某个方向的第一帧 忽略掉2、3帧,因此在无敌模式中炸弹人会有闪烁的效果
[mw_shl_code=cpp,true]//渲染玩家
if (millis() >= PIT + Invincible_Time) {
arduboy.drawSlowXYBitmap(56, 24, Man_table[PP * 3 + PS] , 8, 8, 0); //玩家图像为方向*3+动画帧
} else if (PS == 0) arduboy.drawSlowXYBitmap(56, 24, Man_table[PP * 3 + PS] , 8, 8, 0); //无敌模式的时候闪烁效果
if (PMove == true || millis() < PIT + Invincible_Time) { //只有在玩家移动的时候或者无敌模式 才会有移动动画
PS++;
if (PS > 2) PS = 0;
} else PS = 0;
}
}[/mw_shl_code]
[mw_shl_code=cpp,true]/*=========================================================
逻辑
=========================================================*/
void logic() {
/*
控制移动 以及移动相关动画
*/
switch (KeyBack) {
case 0:
PP = 0;
SBDP(PP, PX, PY);
if (PY > 1 && BMove == true) {
PMove = true;
for (CSY = 1; CSY <= 7; CSY += 3) Draw();
PY--;
}
break;
case 1:
PP = 2;
SBDP(PP, PX, PY);
if (PY < 13 && BMove == true) {
PMove = true;
for (CSY = -1; CSY >= -7; CSY -= 3) Draw();
PY++;
}
break;
case 2:
PP = 3;
SBDP(PP, PX, PY);
if (PX > 1 && BMove == true) {
PMove = true;
for (CSX = 1; CSX <= 7; CSX += 3) Draw();
PX--;
}
break;
case 3:
PP = 1;
SBDP(PP, PX, PY);
if (PX < 29 && BMove == true) {
PMove = true;
for (CSX = -1; CSX >= -7; CSX -= 3) Draw();
PX++;
}
break;
}
if (PMove == true) {
CSX = 0;
CSY = 0;
PMove = false;
}
}[/mw_shl_code]
接下来研究炸弹人平滑移动,而不是直接瞬移到下一方块 方法是按下方向键后先让背景平滑移动,当移动整整一格方块后背景复位并且玩家坐标移动到对应位置。给人一种平滑走动的感觉! 上面移动代码中反复出现了以下类似的结构,我们取向右移动的代码来研究:
第1步 PP = 1; 设置炸弹人朝向右边 第2步 SBDP(PP, PX, PY); 调用SBDP障碍物判断函数,判断在炸弹人位置对应方向前是否有障碍物,如果没有障碍物判断函数会把变量BMove设置为true 第3步 if (PX < 29 && BMove == true) { 判断障碍物判断函数返回值 前方是否没障碍物,并且前面是否小于x轴地图最大范围 第4步 PMove = true; 允许移动 第5步 for (CSX = -1; CSX >= -7; CSX -= 3) Draw(); 让背景往左边方向平滑移动 每次移动3个像素 如果想要更加平滑可以把-3改为-1 并且调用绘图函数背景显示部分,这里拿墙壁显示片段举例 arduboy.drawSlowXYBitmap(x * 8 - (PX - 15) * 8 - 64 + CSX, y * 8 - (PY - 7) * 8 - 32 + CSY, WALL_1, 8, 8, 0); 第6步 PX++; 让炸弹人坐标真正往前一步,不过不用马上调用绘图函数刷新 } 第6步 背景复位 if (PMove == true) { CSX = 0; CSY = 0; PMove = false; } 这一步也没有立即调用绘图函数来刷新,尽管背景位置复位了,不过玩家位置已经向前,所以在下一次刷新的时候画面不会变化
/*=================================================================== 障碍物判断===================================================================*/ void SBDP(byte SBP, byte sx, byte sy) { 第1步 BMove = true; char SX, SY; 初始化变量 第2步 判断返回值 调用障碍物判断函数的时候还要传递3个数据 分别是 方向 和玩家位置x和y 用一个SWITCH语句判断要检测的坐标
[mw_shl_code=cpp,true]switch (SBP) {
case 3:
SX = -1;
SY = 0;
break;
case 1:
SX = +1;
SY = 0;
break;
case 0:
SX = 0;
SY = -1;
break;
case 2:
SX = 0;
SY = +1;
break;
}[/mw_shl_code]
第3步 获取列表长度 获取障碍物列表最大的长度 默认障碍物列表有以下ID(1,2,3) 分别为坚固墙壁 可以摧毁的墙壁以及TNT [mw_shl_code=cpp,true]byte length = sizeof(SBDPL) / sizeof(SBDPL[0]);[/mw_shl_code] 第4步 检测是否为障碍物 获用for逐个检测目标位置ID是否为障碍物,如果是则设定变量BMOVE为false [mw_shl_code=cpp,true]for (byte i = 0; i < length; i++) {
if (MAP[sx + SX][sy + SY] == SBDPL) BMove = false;
}
}[/mw_shl_code]
第四步 TNT放置 爆炸效果 爆炸伤害
在上边第二步中检测按键返回值控制移动的switch语句中加多以下内容 case 4: //放TNT 第1步 检查要放TNT的位置是否已经有TNT 并且这一刻全地图内是否少于10个TNT if (TNTN < 10 && MAP[PX][PY] != 3) { //注意0为没有TNT 范围1-10 第2步 可以放置TNT 让全地图TNT数量+1 TNTN++;
第3步 在TNT列表中设置好当前编号的TNT位置 TntList[TNTN - 1][0] = PX; TntList[TNTN - 1][1] = PY;
第4步 在地图对应位置写入TNT的ID MAP[PX][PY] = 3;
第5步 记录下放置TNT的时间 用于计算什么时候起爆 TntTime[TNTN - 1] = millis(); } break; 现在TNT已经放置了,可是还需要让它到时间后起爆,在逻辑语句中加入以下部分 /* 计算TNT爆炸 */
第1步 判断地图内是否存在TNT,有的话继续 if (TNTN != 0) { //存在炸弹 第2步 检查TNT列表第一个也就是最接近起爆时间的TNT是否到时间 if (millis() >= TntTime[0] + BOOMTime) {
第3步 如果到了起爆时间那么在地图TNT所在的位置上把ID-3 准备爆炸的TNT 替换为ID-4爆炸第一阶段
MAP[TntList[0][0]][TntList[0][1]] = 4; //引爆 //摧毁附近的非坚固实体或者方块
第4步 检查起爆的TNT十字范围内是否有可以摧毁的东西,有的话把ID替换为ID-4 爆炸的第一阶段
先用for检查X轴是否有可以摧毁的方块,这里可以修改代码扩大摧毁范围 [mw_shl_code=cpp,true] for (byte BOOMx = 0; BOOMx < 3; BOOMx++) { if (MAP[TntList[0][0] - 1 + BOOMx][TntList[0][1]] != 1 && MAP[TntList[0][0] - 1 + BOOMx][TntList[0][1]] != 3) {
MAP[TntList[0][0] - 1 + BOOMx][TntList[0][1]] = 4;
}
}[/mw_shl_code] 最后for检查y轴是否有可以摧毁的方块
[mw_shl_code=cpp,true]for (byte BOOMy = 0; BOOMy < 3; BOOMy++) {
if (MAP[TntList[0][0]][TntList[0][1] - 1 + BOOMy] != 1 && MAP[TntList[0][0] - 1 + BOOMy][TntList[0][1]] != 3) {
MAP[TntList[0][0]][TntList[0][1] - 1 + BOOMy] = 4;
}
}[/mw_shl_code] 第5步 在TNT列表注销以及爆炸的TNT [mw_shl_code=cpp,true] //让TNT列表向前移位
TNTN--; //减少一枚TNT
for (byte TNTi = 0; TNTi < TNTN; TNTi++) {
让列表后边的TNT坐标和TNT放置时间移动到前面一位
TntList[TNT编号][0] 对应编号TNT的X轴
TntList[TNT编号][1] 对应编号TNT的Y轴
TntTime[编号] 对应编号TNT的放置时间
TntList[TNTi][0] = TntList[TNTi + 1][0];
TntList[TNTi][1] = TntList[TNTi + 1][1];
TntTime[TNTi] = TntTime[TNTi + 1];
}
}
}[/mw_shl_code] 现在TNT可以爆炸,接下来是爆炸动画
[mw_shl_code=cpp,true]第1步 两个嵌套的for遍历地图
for (byte y = 0; y < 15; y++) {
for (byte x = 0; x < 31; x++) {
第2步 如果是4替换为5 5替换为6 6替换为0(空气)
if (MAP[x][y] == 4) MAP[x][y] = 5; else if (MAP[x][y] == 5) MAP[x][y] = 6; else if (MAP[x][y] == 6) MAP[x][y] = 0; //让爆炸切换下一帧
}
}
有动画还不够,TNT存在的意义是破坏和伤害
第1步 检查怪物列表
用for遍历怪物列表,每个地图怪物上限为10 很轻松就可以遍历一次
for (byte i = 0; i < 10; i++) {
if (MAP[monster[0]][monster[1]] >= 4 && MLRUD != 255) {
假若当前列表编号的怪物不是死亡状态并且脚下为ID-4 ~ ID-6 不同阶段的爆炸则继续执行
第2步 让怪物死亡
MLRUD = 255;
}
第3步 检查玩家是否在无敌状态
if (millis() >= PIT + Invincible_Time) { //玩家不在无敌状态
第4步 若不是在无敌状态下检查脚下是否为为ID-4 ~ ID-6 不同阶段的爆炸
if (PX == monster[0] && PY == monster[1] || MAP[PX][PY] >= 4) { //怪物伤害 或者 TNT伤害
第5步 扣生命 以及设置无敌状态开始的时间
LIFE--;
PIT = millis();
}
}[/mw_shl_code]
第五步 怪物AI要怪物有可用,为了被玩家攻击和攻击玩家给玩家带来难度,所以我们要会动的怪物而不是呆住不动的木头脑袋 在逻辑语句插入以下内容
[mw_shl_code=cpp,true] /*
怪物AI
*/
第1步 假设游戏通关 怪物被消灭
bool PWIN = true;
第2步 是否到了刷新时间 若是继续执行
if (millis() >= MMTime + MMTimeOut) {
重置刷新时间
MMTime = millis();
第3步 遍历怪物列表
for (byte n = 0; n < 10; n++) {
假若对应列表编号的怪物不是死亡状态
if (MLRUD[n] != 255) {
很遗憾,游戏还没有结束 设置游戏胜利状态为假
PWIN = false;
第4步 通过SBDP障碍物判断函数判断怪物坐标的对应方向是否有障碍物
SBDP(MLRUD[n], monster[n][0], monster[n][1]);
第5步 没有障碍物 进行移动
在对应方向的坐标走一步
if (BMove == true) {
//移动合法
switch (MLRUD[n]) {
case 0:
monster[n][1]--;
break;
case 1:
monster[n][0]++;
break;
case 2:
monster[n][1]++;
break;
case 3:
monster[n][0]--;
break;
}
假若有障碍物,那么怪物方向随机更改(通过无数次尝试发现越简单的越好用,穷举法在这种情况下很好用)
} else MLRUD[n] = random(0, 4);
} else if (PWIN == true) WIN(); 假若通过为真,调用通关函数
}
}[/mw_shl_code]
第六步 游戏菜单 以及 游戏结束
[mw_shl_code=cpp,true]/*=========================================================
通关
=========================================================*/
void WIN() {
第1步 假若关卡为10 则通关画面
if (LEVEL == 10) {
第2步 清屏并显示文字
arduboy.clear();
arduboy.setCursor(16, 0);
arduboy.println(F("CONGRATULATIONS"));
arduboy.println(F(" BOMBER MAN BECOMES"));
arduboy.println(F(" RUNNER"));
arduboy.println(F("SEE YOU AGAIN IN LODE"));
arduboy.println(F(" RUNNER"));
第3步 显示一个面向右面的炸弹人
arduboy.drawSlowXYBitmap(56, 48, Man_table[3] , 8, 8, 1);
第4步 在最下方砖块显示的区域画一个实心长方体,用于画布底层
arduboy.fillRect(0, 56, 128, 8, 1);
通过for显示16个转头
for (byte x = 0; x < 128; x += 8) {
arduboy.drawSlowXYBitmap(x, 56, WALL_2, 8, 8, 0);
}
第5步 在屏幕上显示
arduboy.display();
while (1) {}
} else {
假若关卡小于10
LEVEL++; //关卡+1
BuildMap(); //构建地图
ShowLevel(); //显示第几关
}
}[/mw_shl_code]
[mw_shl_code=cpp,true]/*=========================================================
显示关卡
=========================================================*/
void ShowLevel() {
arduboy.clear();
arduboy.setCursor(52, 16);
arduboy.println(F("LEVEL"));
arduboy.setCursor(64, 32);
arduboy.println(LEVEL);
arduboy.display();
delay(1000);
}
/*=========================================================
玩家死亡
=========================================================*/
void FAIL() {
for (byte y = 0; y < 15; y++) {
for (byte x = 0; x < 31; x++) {
MAP[x][y] = 4;
把整个地图设置为爆炸
}
}
while (MAP[0][0] >= 3) {
Draw();
logic();
delay(500);
切换爆炸下一帧,直到爆炸结束
}[/mw_shl_code]
[mw_shl_code=cpp,true] Draw();
arduboy.drawSlowXYBitmap(56, 24, Man_table[3] , 8, 8, 0);
在画面中间显示孤独的主角[/mw_shl_code]
[mw_shl_code=cpp,true]arduboy.display();
delay(5000);
resetFunc(); //重启游戏
}
/*=========================================================
主菜单
=========================================================*/
void MENU() {
bool POA = false;
while (POA == true || KeyBack != 4) {
key();
switch (KeyBack) {
case 0:
POA = false;
break;
case 1:
POA = true;
break;
case 4:
if (POA == true) {
KeyBack = 255;
arduboy.clear();
arduboy.setCursor(0, 0);
arduboy.println(F(" >About"));
arduboy.println(F(""));
arduboy.println(F("LHW programming"));
arduboy.println(F("LHW Art"));
arduboy.println(F("E-mail"));
arduboy.println(F("1281702594@qq.com"));
arduboy.println(F(""));
arduboy.println(F("Any key back..."));
arduboy.display();
delay(200);
while (KeyBack == 255) key();
delay(200);
}
break;
}
arduboy.clear();
arduboy.drawSlowXYBitmap(39, 1, START_TITLE , 87, 39, 1); //大标题
arduboy.drawSlowXYBitmap(0, 23, TITLE_TNT , 37, 41, 1); //TNT图标
arduboy.drawSlowXYBitmap(65, 58, LHW , 39, 5, 1); //作者信息
arduboy.setCursor(70, 39);
arduboy.println(F("PLAY"));
arduboy.setCursor(70, 47);
arduboy.println(F("ABOUT"));
if (POA == false) arduboy.setCursor(62, 39); else arduboy.setCursor(62, 47);
arduboy.println(F("*"));
arduboy.display();
}
}[/mw_shl_code]
|