FlutterFlame

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

2020-08-12  本文已影响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

坦克大战

上一篇下一篇

猜你喜欢

热点阅读