Arduino教你制作 FC炸弹人游戏-Arduino中文社区 - Powered by Discuz! Archiver

createskyblue 发表于 2018-9-17 19:30

Arduino教你制作 FC炸弹人游戏

本帖最后由 createskyblue 于 2018-9-17 20:20 编辑


前排提示,游戏文件、在线模拟在2楼
1楼为制作教程
视频: https://www.bilibili.com/video/av31933044/

第一步 导入素材





第二步 地图生成器


void BuildMap() {
/*
   0空气
   1不可摧毁墙
   2普通墙
   3TNT
   4爆炸3
   5爆炸2
   6爆炸1
*/
//生成墙和怪物
LIFE = 3; //开局3条命
byte MN = 0; //重置怪物计数器
PP = 2; //设置玩家方向头朝下
首先要生成墙壁,先生成边框防止玩家跑出地图,最后生成边框里面墙壁阵列


for (byte y = 0; y < 15; y++) {
    for (byte x = 0; x < 31; x++) {
      if (y == 0 || y == 14) {
      MAP = 1;      生成y轴墙壁外围 下图橙色区域
      } else if (x == 0 || x == 30) {
      MAP = 1;      生成x轴墙壁外围 下图蓝色区域
      } else {
      if (x % 2 == 0 && y % 2 == 0) {
          MAP = 1;      判断当前x,y坐标是否为偶数,如果是则生成内部墙壁
      } else if (random(0, 4) == 0) {
          MAP = 2;      1/4概率生成可以破坏的墙
      } else if (random(0, 28) == 0) {   1/28 的概率生成怪物
          //生成怪物
          if (MN < LEVEL) {      确保怪物数量少于当前关卡数
            monster = x;    记录当前编号怪物的x,y坐标
            monster = y;
            MLRUD = 2; //设置怪物方向为头朝下 0上 1右 2下 3左
            MN++;            下一个怪物编号
          }
      } else MAP = 0;    假若不满足上边的条件则生成空气
      }
    }
}


//设置玩家出生点
for (byte py = 0; py < 3; py++) {
    for (byte px = 0; px < 3; px++) {
      //清空出生点附近的普通墙和怪物,确保出生点安全
      MAP = 0;
    }
}

清除玩家出生点附近3*3区域内所有方块,确保有较大的空间让玩家开一条路出去

PX = 15;设置玩家在地图上的出生点
PY = 7;
}




上图为生成器生成的初始游戏地图第三步 实现玩家移动 绘图部分
void loop() {
if (LIFE == 0) FAIL();//如果生命为0 游戏结束
key();   //扫描按键
Draw();    //绘图
logic();   //逻辑处理
}

/*=========================================================
按键扫描
=========================================================*/
void key() {
/*
      012345
      ↑ ↓← →AB
*/
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) {

用两个嵌套的for语句来扫描玩家视线内的方块,然后用switch语句显示出对应方块的位图下图灰色区域为玩家所能看到的区域 黑框内为128x64OLED的显示范围游戏中,无论怎样移动移动的都是地图而不是玩家本身,玩家是被固定在屏幕中心位置的,而且地图方块对应的位置不代表实际显示的位置,为了正确显示我们需要减去地图方块与玩家相对坐标 (参考下图紫色箭头) 为了直观告诉大家哪里做了处理,下列显示代码会把要坐标处理的部分染为紫色
switch (MAP) {
          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, 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 != 255) arduboy.drawSlowXYBitmap(monster * 8 - (PX - 15) * 8 - 64 + CSX, monster * 8- (PY - 7) * 8 - 32 + CSY, M_table)], 8, 8, 0);
    }
Man_table 存储的是炸弹人动作列表:{ 炸弹人_朝上_1, 炸弹人_朝上_2, 炸弹人_朝上_3, 炸弹人_朝右_1, 炸弹人_朝右_2, 炸弹人_朝右_3, 炸弹人_朝下_1, 炸弹人_朝下_2, 炸弹人_朝下_3, 炸弹人_朝左_1, 炸弹人_朝左_2, 炸弹人_朝左_3} 炸弹人有4个方向的贴图,每个方向拥有3个动作,加起来一共12幅图变量PP指的是炸弹人的方向,决定选择哪一组贴图变量PS为炸弹人当前动作帧,当每一帧动作显示后自加或者复位 实现炸弹人移动动画 因此得出Man_table这条式子当炸弹人受到伤害后将会被扣血,扣血后会进入一段时间的无敌状态避免短时间重复受到伤害而无敌模式中只会显示炸弹人某个方向的第一帧 忽略掉2、3帧,因此在无敌模式中炸弹人会有闪烁的效果
//渲染玩家
    if (millis() >= PIT + Invincible_Time) {
      arduboy.drawSlowXYBitmap(56, 24, Man_table , 8, 8, 0); //玩家图像为方向*3+动画帧
    } else if (PS == 0) arduboy.drawSlowXYBitmap(56, 24, Man_table , 8, 8, 0); //无敌模式的时候闪烁效果
    if (PMove == true || millis() < PIT + Invincible_Time) { //只有在玩家移动的时候或者无敌模式 才会有移动动画
      PS++;
      if (PS > 2) PS = 0;
    } else PS = 0;
}
}


/*=========================================================
逻辑
=========================================================*/
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;
    }
}
接下来研究炸弹人平滑移动,而不是直接瞬移到下一方块方法是按下方向键后先让背景平滑移动,当移动整整一格方块后背景复位并且玩家坐标移动到对应位置。给人一种平滑走动的感觉!上面移动代码中反复出现了以下类似的结构,我们取向右移动的代码来研究:

第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语句判断要检测的坐标

(X,Y-1)
(X-1,Y)玩家位置(X,Y)(X+1,Y)
(X,Y+1)


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;
}
第3步 获取列表长度获取障碍物列表最大的长度 默认障碍物列表有以下ID(1,2,3) 分别为坚固墙壁 可以摧毁的墙壁以及TNTbyte length = sizeof(SBDPL) / sizeof(SBDPL);第4步 检测是否为障碍物获用for逐个检测目标位置ID是否为障碍物,如果是则设定变量BMOVE为falsefor (byte i = 0; i < length; i++) {
    if (MAP == SBDPL) BMove = false;
}
}
第四步 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 爆炸的第一阶段

(X,Y-1)
(X-1,Y)TNT位置(X,Y)(X+1,Y)
(X,Y+1)

先用for检查X轴是否有可以摧毁的方块,这里可以修改代码扩大摧毁范围 for (byte BOOMx = 0; BOOMx < 3; BOOMx++) {if (MAP - 1 + BOOMx]] != 1 && MAP - 1 + BOOMx]] != 3) {
MAP - 1 + BOOMx]] = 4;
}
}最后for检查y轴是否有可以摧毁的方块
for (byte BOOMy = 0; BOOMy < 3; BOOMy++) {
          if (MAP] - 1 + BOOMy] != 1 && MAP - 1 + BOOMy]] != 3) {
            MAP] - 1 + BOOMy] = 4;
          }
      }第5步 在TNT列表注销以及爆炸的TNT //让TNT列表向前移位
      TNTN--; //减少一枚TNT
      for (byte TNTi = 0; TNTi < TNTN; TNTi++) {
让列表后边的TNT坐标和TNT放置时间移动到前面一位
TntList 对应编号TNT的X轴
TntList 对应编号TNT的Y轴
TntTime[编号]         对应编号TNT的放置时间

          TntList = TntList;
          TntList = TntList;
          TntTime = TntTime;
      }
      }
}现在TNT可以爆炸,接下来是爆炸动画

第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 == 4) MAP = 5; else if (MAP == 5) MAP = 6; else if (MAP == 6) MAP = 0; //让爆炸切换下一帧
    }
}

有动画还不够,TNT存在的意义是破坏和伤害

第1步 检查怪物列表
用for遍历怪物列表,每个地图怪物上限为10 很轻松就可以遍历一次
for (byte i = 0; i < 10; i++) {
      if (MAP]] >= 4 && MLRUD != 255) {
假若当前列表编号的怪物不是死亡状态并且脚下为ID-4 ~ ID-6 不同阶段的爆炸则继续执行
      

第2步 让怪物死亡
MLRUD = 255;
      }

第3步 检查玩家是否在无敌状态
      if (millis() >= PIT + Invincible_Time) { //玩家不在无敌状态


第4步 若不是在无敌状态下检查脚下是否为为ID-4 ~ ID-6 不同阶段的爆炸
      if (PX == monster && PY == monster || MAP >= 4) { //怪物伤害 或者 TNT伤害

第5步 扣生命 以及设置无敌状态开始的时间
          LIFE--;
          PIT = millis();
      }
      }
第五步 怪物AI要怪物有可用,为了被玩家攻击和攻击玩家给玩家带来难度,所以我们要会动的怪物而不是呆住不动的木头脑袋 在逻辑语句插入以下内容
/*
   怪物AI
*/
第1步 假设游戏通关 怪物被消灭
bool PWIN = true;

第2步 是否到了刷新时间 若是继续执行
if (millis() >= MMTime + MMTimeOut) {

重置刷新时间
      MMTime = millis();

第3步 遍历怪物列表
      for (byte n = 0; n < 10; n++) {

假若对应列表编号的怪物不是死亡状态
      if (MLRUD != 255) {
很遗憾,游戏还没有结束 设置游戏胜利状态为假
          PWIN = false;

第4步通过SBDP障碍物判断函数判断怪物坐标的对应方向是否有障碍物
          SBDP(MLRUD, monster, monster);

第5步 没有障碍物 进行移动
在对应方向的坐标走一步
          if (BMove == true) {
            //移动合法
            switch (MLRUD) {
            case 0:
                monster--;
                break;
            case 1:
                monster++;
                break;
            case 2:
                monster++;
                break;
            case 3:
                monster--;
                break;
            }
假若有障碍物,那么怪物方向随机更改(通过无数次尝试发现越简单的越好用,穷举法在这种情况下很好用)
          } else MLRUD = random(0, 4);
      } else if (PWIN == true) WIN(); 假若通过为真,调用通关函数
      }
    }

第六步 游戏菜单 以及 游戏结束


/*=========================================================
                     通关
=========================================================*/
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 , 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(); //显示第几关
}
}



/*=========================================================
                     显示关卡
=========================================================*/
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 = 4;
把整个地图设置为爆炸
    }
}

while (MAP >= 3) {
    Draw();
    logic();
delay(500);
切换爆炸下一帧,直到爆炸结束
}



Draw();
arduboy.drawSlowXYBitmap(56, 24, Man_table , 8, 8, 0);
在画面中间显示孤独的主角



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();
}
}



createskyblue 发表于 2018-9-17 19:41

本帖最后由 createskyblue 于 2018-10-3 21:31 编辑

更新:2018/9/21 怪物伤害玩家代码中,忘记判断怪物是否死亡,导致死亡并且隐藏的怪物伤害玩家,无缘无故扣血 GITHUB将会发布推送更新

如何正确运行游戏游戏文件:**** Hidden Message *****
传统的u8g系列的库包括u8g2 刷新有点慢,所以使用arduboy UNO移植库,刷新率比较快但是会导致运行游戏没有这么容易 参考方法一和二即使没有硬件,有手机或电脑也可以使用在线模拟器运行游戏,没关系的 看方法三
方法一 直接上传hex*Arduboy上传Ardubox.hex*UNO上传UNO.hex
1.先连接硬件https://www.arduino.cn/data/attachment/forum/201807/04/212143rw1nj0on1w53201z.png上键               ===>17 A3
下键               ===>2
左键               ===>15 A1
右键               ===>3
A键                ===>4
B键                ===>16 A2
OLED_SCL   ===>19 A5
OLED_SDA    ===>18 A4
按键一端接地 一端接到UNO上
2.下载Arduloader3.打开Arduloaderhttps://www.arduino.cn/data/attachment/forum/201808/22/181145o3tyo637yx7vddzv.png游戏hex文件在上面的游戏下载附件的压缩包里https://www.arduino.cn/data/attachment/forum/201808/22/181145fliywn1lwsk5n1ko.png该画面为成功上传,现在oled上应该有画面
方法二 编译安装    可以参考下面   假若有Arduboy可以直接编译
https://www.arduino.cn/forum.php?mod=viewthread&tid=80103&highlight=arduboy
方法三 在线模拟器 https://felipemanga.github.io/ProjectABE/?url=https://raw.githubusercontent.com/createskyblue/Bomberman/master/ARDUBOY.hex
**** Hidden Message *****

单片机菜鸟 发表于 2018-9-17 19:47

大佬6666

明娃子 发表于 2018-9-19 16:29

厉害啊啊

明娃子 发表于 2018-9-19 16:29

谢谢分享

科技工匠 发表于 2018-10-9 14:07

太棒了 终于找到了

Momo_HxJ 发表于 2018-10-29 15:50


太棒了 终于找到了

mxlwj 发表于 2018-10-29 18:25

好有意思

WYWD1234 发表于 2018-11-3 09:41

棒,学习到了!!!

图灵甜点 发表于 2018-11-3 10:45

楼主,很棒
页: [1] 2 3 4 5 6 7 8 9
查看完整版本: Arduino教你制作 FC炸弹人游戏