FlutterFlame

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

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

坦克大战

上一篇下一篇

猜你喜欢

热点阅读