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

TankCombat系列文章

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

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

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

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

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

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

效果图

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

image

开工

闲话你bo要讲——接上回,我们这次开始完成控制坦克的功能

角度pi的正负示意图

image
X轴从右到左,为从正向负
y轴从上到下,为从正向负
pi的范围是 (-pi,pi)

Tank代码结构

class Tank extends BaseComponent{

  final int tankId = 666;
  final TankGame game;
  Sprite bodySprite,turretSprite;

  Tank(this.game,{this.position}){
    turretSprite = Sprite('tank/t_turret_blue.webp');
    bodySprite= Sprite('tank/t_body_blue.webp');

  }


  //出生位置
  Offset position;
  //车体角度
  double bodyAngle = 0;
  //炮塔角度
  double turretAngle = 0;

  //车体目标角度
  double targetBodyAngle;
  //炮塔目标角度
  double targetTurretAngle;

  //tank是否存活
  bool isDead = false;

  final double ration = 0.7;
  
    @override
  void render(Canvas canvas) {
    if(isDead) return;
    drawBody(canvas);
  }


  @override
  void update(double t) {
    //时间增量t 旋转速率
    rotateBody(t);
    rotateTurret(t);
    moveTank(t);
  }
  
}

控制坦克

我们在Tank里面增加几个变量用于控制和计算坦克及炮塔的角度

     //车体角度
    double bodyAngle = 0;
    //炮塔角度
    double turretAngle = 0;

    //车体目标角度
    double targetBodyAngle;
    
    //炮塔目标角度
    double targetTurretAngle;

    //tank是否存活
    bool isDead = false;

然后我们更新一下drawBody方法,增加坦克身体和炮塔的旋转绘制,

  void drawBody(Canvas canvas) {
    //操作前保存一下状态
    canvas.save();
    canvas.translate(position.dx, position.dy);
    //车身旋转的角度
    canvas.rotate(bodyAngle);

    //绘制tank身体

    bodySprite.renderRect(canvas,Rect.fromLTWH(-20*ration, -15*ration, 38*ration, 32*ration));

    //旋转炮台
    canvas.rotate(turretAngle);
    // 绘制炮塔
    turretSprite.renderRect(canvas, Rect.fromLTWH(-1, -2*ration, 22*ration, 6*ration));

    canvas.restore();

  }

同时,之前空出的update方法,此时就派上用场了,我们在其中增加三个方法:

rotateBody(t);//旋转身体
rotateTurret(t);//旋转炮塔
moveTank(t);//移动坦克

注意这个t是double类型,表示的是时间增量,举个栗子,如果是60fps,那么这个t理论上=0.01666

源码如下:

  @override
  void update(double t) {
    //时间增量t 
    rotateBody(t);
    rotateTurret(t);
    moveTank(t);
  }

这三个方法比较长,且繁琐,我将说明加在注释里面以便于联系阅读

旋转车身

  void rotateBody(double t) {
    //旋转车身是不能 'chua'一下就转过去的,得有一个平滑的过程
    //因此这里根据pi * t 算出一个比较平均的旋转速率rotationRate 
    final double rotationRate = pi * t;
    //在做旋转之前,我们要确定 目标角度是不能为空的
    if (targetBodyAngle != null) {
        //车身角度 小于 目标角度
      if (bodyAngle < targetBodyAngle) {
        //车体角度和目标角度差额,判断是否大于/小于极值(-pi,pi)
        //正向或者逆向旋转
        if ((targetBodyAngle - bodyAngle).abs() > pi) {
            //大于,以rotationRate来减去车身角度
          bodyAngle = bodyAngle - rotationRate;
          //如果车身角度小于 负极值,那么给他额外+加一圈,
          //这样保证了值在集合内,同时坐标方位角不变
          //如果这里不好理解,可以看一下上面的示意图
          if (bodyAngle < -pi) {
            bodyAngle += pi * 2;
          }
        } else {
            //小于 (与上同理)
          bodyAngle = bodyAngle + rotationRate;
          if (bodyAngle > targetBodyAngle) {
            bodyAngle = targetBodyAngle;
          }
        }
      }
      //车身角度 大于 目标角度 
      //同理,只是操作相反
      if (bodyAngle > targetBodyAngle) {
        if ((targetBodyAngle - bodyAngle).abs() > pi) {
          bodyAngle = bodyAngle + rotationRate;
          if (bodyAngle > pi) {
            bodyAngle -= pi * 2;
          }
        } else {
          bodyAngle = bodyAngle - rotationRate;
          if (bodyAngle < targetBodyAngle) {
            bodyAngle = targetBodyAngle;
          }
        }
      }
    }
  }

旋转炮塔

  void rotateTurret(double t) {
    final double rotationRate = pi * t;

    if(targetTurretAngle != null){
    //这里的原理和旋转车身是一样的,唯一的区别就是我们的炮塔目标角度,并不是直接使用 变量 targetTurretAngle
    //而是通过targetTurretAngle - bodyAngle 来算出真正的目标角度
    //这是因为,在drawBody()方法中,我们先对 车身进行了旋转,所以这里要减去
      double localTargetTurretAngle = targetTurretAngle - bodyAngle;
      if(turretAngle < localTargetTurretAngle){
        if((localTargetTurretAngle -turretAngle).abs() > pi){
          turretAngle = turretAngle - rotationRate;
          //超出临界值,进行转换 即:小于-pi时,转换成(pi-差值)之后继续累加,具体参考 笛卡尔坐标,范围是(-pi,pi)
          if(turretAngle < -pi){
            turretAngle += pi*2;
          }
        }else{
          turretAngle = turretAngle + rotationRate;
          if(turretAngle > localTargetTurretAngle){
            turretAngle = localTargetTurretAngle;
          }
        }
      }
      if(turretAngle > localTargetTurretAngle){
        if((localTargetTurretAngle - turretAngle).abs() > pi){
          turretAngle = turretAngle + rotationRate;
          if(turretAngle > pi){
            turretAngle -= pi*2;
          }
        }else{
          turretAngle = turretAngle - rotationRate;
          if(turretAngle < localTargetTurretAngle){
            turretAngle = localTargetTurretAngle;
          }
        }
      }
    }

  }

移动坦克

    //这里方法比较简单,
    //100和50 分别是坦克的速度,因为转弯的时候慢,所以转弯速度为50,直行为100
  void moveTank(double t) {
    if(targetBodyAngle != null){
      if(bodyAngle == targetBodyAngle){
        //tank 直线时 移动速度快
        position = position + Offset.fromDirection(bodyAngle,100*t);//100 是像素
      }else{
        //tank旋转时 移动速度要慢
        position = position + Offset.fromDirection(bodyAngle,50*t);
      }
    }
  }

至此坦克控制系统就写完了,接下来就是连接摇杆和坦克了。

组合启动

还记得mian函数中的代码吗?

main()

    final TankGame tankGame = TankGame();

  runApp(Directionality(textDirection: TextDirection.ltr,
      child: Stack(
    children: [

      tankGame.widget,

      Column(
        children: [

          Spacer(),
          //发射按钮
          Row(
            children: [
              SizedBox(width: 48),
              FireButton(
                onTap: tankGame.onFireButtonTap,
              ),
              Spacer(),
              FireButton(
                onTap: tankGame.onFireButtonTap,
              ),
              SizedBox(width: 48),
            ],
          ),
          SizedBox(height: 20),
          //摇杆
          Row(
            children: [
              SizedBox(width: 48),
              JoyStick(
                onChange: (Offset delta)=>tankGame.onLeftJoypadChange(delta),
              ),
              Spacer(),
              JoyStick(
                onChange: (Offset delta)=>tankGame.onRightJoypadChange(delta),
              ),
              SizedBox(width: 48)
            ],
          ),
          SizedBox(height: 24)
        ],
      ),

    ],
  )));

可以看到我们将摇杆的回调中传出来的偏移量delta,通过game的两个方法

onLeftJoypadChange(delta)//车身
onRightJoypadChange(delta)//炮塔

传给了game,用来更新游戏component

TankGame

我们将tankgame的代码进行一下更新,首先增加一个玩家坦克

Tank tank;

在增加两个方法接收摇杆的delta,我们将delta的方向传给tank

  void onLeftJoypadChange(Offset offset){
    if(offset == Offset.zero){
      tank.targetBodyAngle = null;
    }else{
        //得到车身的目标角度
      tank.targetBodyAngle = offset.direction;//范围(pi,-pi)
    }
  }

  void onRightJoypadChange(Offset offset) {
    if (offset == Offset.zero) {
      tank.targetTurretAngle = null;
    } else {
    //得到炮塔目标角度
      tank.targetTurretAngle = offset.direction;
    }
  }

下面,我们在resize()方法中实例化一下tank,一般情况下这个方法只会在屏幕尺寸变化时才会调用

    if(tank == null){
      tank = Tank(
        //出生点在屏幕中央
        this,position: Offset(screenSize.width/2,screenSize.height/2),
      );
    }

好的,目前摇杆的控制已经与坦克连接到一起了,但是控制效果我们是看不到的,因为没有根据屏幕的刷新进行渲染和更新。
我们只需要在game的render和update调用tank对应的方法即可。

  @override
  void render(Canvas canvas) {
    if(screenSize == null)return;
    //绘制草坪
    bg.render(canvas);
    //tank
    tank.render(canvas);
    }
  @override
  void update(double t) {
    if(screenSize == null)return;
    tank.update(t);
    }

至此整个坦克控制系统就完成了,你可以运行一下,活动活动坦克了,下一章我们将进行炮弹和敌方坦克的设计,
谢谢你的阅读。 :)

DEMO

坦克大战

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