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

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

作者: 吉哈达 | 来源:发表于2020-08-11 17:06 被阅读0次

    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

    坦克大战

    相关文章

      网友评论

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

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