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

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

坦克大战

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353