美文网首页FlutterFlame
Flutter&Flame——TankCombat游戏开发(三)

Flutter&Flame——TankCombat游戏开发(三)

作者: 吉哈达 | 来源:发表于2020-08-12 10:41 被阅读0次

    TankCombat系列文章

    如果你还不了解Flame可以看这里:

    见微知著,Flutter在游戏开发的表现及跨平台带来的优势

    Flutter&Flame——TankCombat游戏开发(一)

    Flutter&Flame——TankCombat游戏开发(二)

    Flutter&Flame——TankCombat游戏开发(三)

    Flutter&Flame——TankCombat游戏开发(四)

    效果图

    蛮好看的,我再加一下,让大家整体有个印象自己在做什么 :)

    image

    开工

    本章节,我们开始制作发射炮弹和敌方坦克的设计

    开火

    还记得这段代码吗?

              //发射按钮
              Row(
                children: [
                  SizedBox(width: 48),
                  FireButton(
                    onTap: tankGame.onFireButtonTap,
                  ),
                  Spacer(),
                  FireButton(
                    onTap: tankGame.onFireButtonTap,
                  ),
                  SizedBox(width: 48),
                ],
              ),
    

    在main函数中的runApp方法中,这两个是我们的开火按钮,可以看到,点击事件触发了game中的onFireButtonTap方法,我们来看看具体实现:

      void onFireButtonTap(){
        if(blueBulletNum < 20){
          bullets.add(Bullet(this,BulletColor.BLUE,tank.tankId
              ,position: tank.getBulletOffset(),angle: tank.getBulletAngle()));
        }
    
      }
    

    为了避免子弹过多,导致的卡顿,这里加了个玩家子弹上限,下面就是往bullets(list)加了一颗子弹,同时传给了这颗子弹坦克的位置和game对象,另外两个参数先不用管。我们先看看bullet这个类

    Bullet

    首先我们还是让bullet集成baseComponent,并创建一些变量,你可以将子弹抽象成坦克,这样看他们本质就没啥区别了,甚至更简单一些

    如下:

    class Bullet extends BaseComponent{
            
          final TankGame game;
          final double speed;//子弹速度
          Offset position;//子弹位置
          double angle = 0;//子弹角度
          bool isOffScreen = false;//是否飞出屏幕
          //玩家坦克的子弹图片
          final Sprite blueSprite = Sprite('tank/bullet_blue.webp'),
            //是否击中
          bool isHit = false;
          
    
    }
    

    这样一些子弹的基础属性就声明完成了,接下来我们在render方法和update方法中操纵子弹即可。
    先看update:

      @override
      void update(double t) {
        //我们首先判断是否已经飞出屏幕/击中敌人,这样我们就没必要操作它了,
        if(isHit) return;
        if(isOffScreen)return;
        //之后我们按照既定角度和速度来更新子单位置以达到飞行的效果
        //子弹角度是由坦克炮塔角度决定的
        position = position + Offset.fromDirection(angle,speed * t);
        //下面的方法就比较容易理解了,判断是不是飞出了屏幕,并更新isOffScreen
        if (position.dx < -50) {
          isOffScreen = true;
        }
        if (position.dx > game.screenSize.width + 50) {
          isOffScreen = true;
        }
        if (position.dy < -50) {
          isOffScreen = true;
        }
        if (position.dy > game.screenSize.height + 50) {
          isOffScreen = true;
        }
      }
    

    再看render:

      @override
      void render(Canvas canvas) {
        //理论上讲这里不写也没事,我个人倾向不写的,大家可以看一下flame的流程图就明白了
        if(isHit) return;
        if(isOffScreen)return;
        canvas.save();
        //方法很简单,将画布移动到子单位制和旋转对应角度
        canvas.translate(position.dx, position.dy);
        canvas.rotate(angle);
        //然后绘制子弹即可
        blueSprite.renderRect(canvas, Rect.fromLTWH(-4, -2, 8, 4));
    
        canvas.restore();
      }
    

    ok,这样子弹就处理完了,现在我们回到game中

    TankGame

    所有的component都需要与game联系起来,不然是没法进行更新和渲染上屏的(仅指游戏)。

    因为我们肯定不止一发子弹,所以我们创建一个list

     List<Bullet> bullets; //炮弹
    

    接着在resize中实例化它

      @override
      void resize(Size size) {
        screenSize = size;
        //initEnemyTank();
        if(bg == null){
          bg = BattleBackground(this);
        }
        if(tank == null){
          tank = Tank(
            this,position: Offset(screenSize.width/2,screenSize.height/2),
          );
        }
        if(bullets == null){
          bullets = List();
        }
    
    
      }
    

    然后在update中我们将关键参数 t 传给它,并调用子弹的update方法

          @override
      void update(double t) {
        bullets.forEach((element) {
            //子弹
            element.update(t);
        
        }
            //移除飞出屏幕的
        bullets.removeWhere((element) => element.isHit || element.isOffScreen);
      }
    
    

    我们在render方法中调用子弹的render方法,并将canvas传给它

    @override
      void render(Canvas canvas) {
         bg.render(canvas);
        //tank
        tank.render(canvas);
        //bullet
        bullets.forEach((element) {
          element.render(canvas);
        });
      }
    

    这样我们就完成了坦克发射炮弹的功能,我们来梳理一下大致流程:

    image

    以上图片也可以帮助你理解component/sprite 在game中的工作流程。

    现在我们虽然可以发射炮弹,但是没法打到人,换言之,我们需要先添加一些敌人。

    敌军坦克TankModel

    敌军坦克和玩家坦克有很多功能可以共用,我们先给敌军坦克抽象出来一个模型 TankModel :

    abstract class TankModel{
    
      final int id;
    
      final TankGame game;
      Sprite bodySprite,turretSprite;
      //出生位置
      Offset position;
    
      TankModel(this.game,this.bodySprite,this.turretSprite,this.position):
          id = DateTime.now().millisecondsSinceEpoch+Random().nextInt(100);
    
      ///随机生成路线用到
      final int seedNum = 50;
      final int seedRatio = 2;
    
      //移动的路线
      double movedDis = 0;
    
      //直线速度
      final double speed = 80;
      //转弯速度
      final double turnSpeed = 40;
    
      //车体角度
      double bodyAngle = 0;
      //炮塔角度
      double turretAngle = 0;
    
      //车体目标角度
      double targetBodyAngle;
      //炮塔目标角度
      double targetTurretAngle;
    
      //tank是否存活
      bool isDead = false;
    
      //移动到目标位置
      Offset targetOffset;
    
      final double ration = 0.7;
    
    
      ///获取炮弹发射位置
      Offset getBulletOffset() ;
     ///炮弹角度
      double getBulletAngle();
    }
    

    都是一堆属性,没啥好说的。现在我们根据模型开始造坦克了,这里我们就造一个绿色的敌方坦克吧

    GreenTank

    首先我们继承tankModel,然后混入baseComponent,如下:

    class GreenTank extends TankModel with BaseComponent{
    
      //坦克身体
      Rect bodyRect ;
      //坦克炮管
      Rect turretRect;
      
        GreenTank(TankGame game, Sprite bodySprite, Sprite turretSprite,Offset position)
          : super(game, bodySprite, turretSprite,position){
        bodyRect = Rect.fromLTWH(-20*ration, -15*ration, 38*ration, 32*ration);
        turretRect = Rect.fromLTWH(-1, -2*ration, 22*ration, 6*ration);
        generateTargetOffset();
      }
      
        void generateTargetOffset(){
        double x = Random().nextDouble() * (game.screenSize.width - (seedNum * seedRatio));
        double y = Random().nextDouble() * (game.screenSize.height - (seedNum * seedRatio));
    
        targetOffset = Offset(x,y);
    
        Offset temp = targetOffset - position;
        targetBodyAngle = temp.direction;
        targetTurretAngle = temp.direction;
    
      }
      
        @override
      void render(Canvas canvas) {
        if(isDead) return;
        drawBody(canvas);
      }
      
        @override
      void update(double t) {
        rotateBody(t);
        rotateTurret(t);
        moveTank(t);
    
    
      }
      
    }
    

    构造函数我们初始化了一些基本属性,这个在上文已经介绍过,不再赘述。我们看多出的这个方法

      void generateTargetOffset(){
        double x = Random().nextDouble() * (game.screenSize.width - (seedNum * seedRatio));
        double y = Random().nextDouble() * (game.screenSize.height - (seedNum * seedRatio));
    
        targetOffset = Offset(x,y);
    
        Offset temp = targetOffset - position;
        targetBodyAngle = temp.direction;
        targetTurretAngle = temp.direction;
    
      }
    

    这个方法用于生成一个随机的目标点,然后让坦克开过去,根据目标点,我们把目标角度(炮塔和车身)保存下来。

    render和update方法中的函数跟之前基本一样,唯一区别在moveTank(t)这个方法,代码如下:

      void moveTank(double t) {
        if(targetBodyAngle != null){
          if(targetOffset != null){
            //可以看到这里多了一个 movedDis, 用来存储走了多少距离
            movedDis += speed * t;
            if(movedDis < 100){
              if(bodyAngle == targetBodyAngle){
                //tank 直线时 移动速度快
                position = position + Offset.fromDirection(bodyAngle,speed*t);//100 是像素
              }else{
                //tank旋转时 移动速度要慢
                position = position + Offset.fromDirection(bodyAngle,turnSpeed*t);
              }
            }else{
                //当行驶距离超出100时我们重新计算新的目标点
              movedDis = 0;
              generateTargetOffset();
    
            }
          }
    
        }
      }
    

    经过了上面的开动,我们的敌军坦克就不再是‘头铁直奔南墙’了,而是走一段距离就会自己转弯,更为灵活生动了。

    ok,敌军坦克完成了,我们开始将他们和game组合

    组合启动

    TankGame

    我们在game中增加两个list分别管理两种颜色的敌军坦克

      List<GreenTank> gTanks = [];
      List<SandTank> sTanks = [];
    

    之后我们在tankGame构造函数初始化中初始化4个敌军坦克

      TankGame(){
        observer = GameObserver(this);
        initEnemyTank();
      }
      
        ///初始化敌军
      void initEnemyTank() {
        var turretSprite = Sprite('tank/t_turret_green.webp');
        var bodySprite= Sprite('tank/t_body_green.webp');
        gTanks.add(GreenTank(this,bodySprite,turretSprite, Offset(100,100)));
        gTanks.add(GreenTank(this,bodySprite,turretSprite, Offset(100,screenSize.height*0.8)));
    
    
        ///sand
        var turretSpriteS = Sprite('tank/t_turret_sand.webp');
        var bodySpriteS = Sprite('tank/t_body_sand.webp');
        sTanks.add( SandTank(this,bodySpriteS,turretSpriteS,
            Offset(screenSize.width-100,100)));
        sTanks.add( SandTank(this,bodySpriteS,turretSpriteS,
                Offset(screenSize.width-100,screenSize.height*0.8)));
      }
    
    
    

    经过上面的操作我们的仓库gTanks和sTanks里面就各有两台整装待发的坦克了,现在开动它们!

    在update和render方法中我们增加下面的代码:

    update

        gTanks.forEach((element) {
          element.update(t);
        });
        sTanks.forEach((element) {
          element.update(t);
        });
            //移除死亡tank
        gTanks.removeWhere((element) => element.isDead);
        sTanks.removeWhere((element) => element.isDead);
    

    render

        gTanks.forEach((element) {
          element.render(canvas);
        });
        sTanks.forEach((element) {
          element.render(canvas);
        });
    

    功能上文已经说过了。

    现在运行一下就可以看到4个敌军小坦克满地图跑了,不过还不会开炮,我们来增加一下这个功能。

    电脑开炮功能

    首先我们考虑蓝、绿、黄三个坦克炮弹不同,且后期可能加别的功能,我们为了区分,先给bullet类文件增加一个枚举:

    enum BulletColor{
      BLUE,GREEN,SAND
    }
    

    之后我们在game中增加一个敌军坦克开火的方法:

      void enemyTankFire<T extends TankModel>(BulletColor color,T tankModel){
        bullets.add(Bullet(this,color,tankModel.id
            ,position: tankModel.getBulletOffset(),angle: tankModel.getBulletAngle()));
      }
    

    原理和玩家坦克开火一样,为了避免炮弹过多造成卡顿(打不过电脑),我们给敌军增加一下子弹上限

    game中增加两个变量

      //黄色炮弹数量
      int sandBulletNum = 0;
      //蓝色炮弹数量
      int blueBulletNum = 0;
    

    game的update方法中统计一下在屏的炮弹数量

        blueBulletNum = 0;
        greenBulletNum = 0;
        sandBulletNum = 0;
        bullets.forEach((element) {
          switch(element.bulletColor){
    
            case BulletColor.BLUE:
              blueBulletNum ++;
              break;
            case BulletColor.GREEN:
              greenBulletNum ++;
              break;
            case BulletColor.SAND:
              sandBulletNum ++;
              break;
          }
          element.update(t);
        });
    

    之后我们在敌军坦克 greenTank的update方法中增加两行代码:

      @override
      void update(double t) {
        rotateBody(t);
        rotateTurret(t);
        moveTank(t);
        
        //当没达到上限时,我们就发射一枚炮弹
        if(game.greenBulletNum < 10){
          game.enemyTankFire(BulletColor.GREEN, this);
        }
    
      }
    

    现在我们运行一下,就会看到满地飞奔,四处转动炮塔开火的敌军小坦克了!

    ok,大功过半,马上告成,在下一章我们将增加炮弹击毁坦克的功能和爆炸效果以及GameObserver的设计。

    多谢阅读,喜欢的点个赞吧 :)
    

    DEMO

    坦克大战

    相关文章

      网友评论

        本文标题:Flutter&Flame——TankCombat游戏开发(三)

        本文链接:https://www.haomeiwen.com/subject/ptuydktx.html