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

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

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

    TankCombat系列文章

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

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

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

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

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

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

    游戏介绍

    玩法

    我们要实现一个坦克大战:

    玩家控制蓝色坦克,出生于屏幕中间
    绿色和黄色为敌军坦克,出生于屏幕四角(随机)
    发射的炮弹可以击毁坦克
    敌军坦克在被摧毁后,会随机重生,但总体敌军数量保持4个
    坦克可以发射炮弹,并分别旋转坦克身体和炮塔
    

    更多功能待发现...

    效果图

    tankCombat2020831533281.gif

    开工

    一口吃不了一个胖子,我们将项目拆分,先实现背景、摇杆和绘制一辆坦克

    摇杆主要借鉴自官方,如果你已经在官方的教程里学会了,可以略过此章
    

    准备

    首先我们引入Flame插件

    flame: ^0.24.0
    

    之后添加背景图片资源文件:

    assets/images/
    

    [图片上传失败...(image-3818fa-1597136568141)]

    开始代码部分,我们将main函数清空,如下:

    main()async{
    }
    

    添加如下代码,(还是老规矩,代码多时我会将说明添加到注解里。)

    void main()async{
        //确保flutter启动成功
      WidgetsFlutterBinding.ensureInitialized();
        //为flame加载资源文件
      loadAssets();
        ///设置横屏
        await SystemChrome.setPreferredOrientations([
          DeviceOrientation.landscapeRight,
          DeviceOrientation.landscapeLeft
        ]);
    
        ///全面屏
        await SystemChrome.setEnabledSystemUIOverlays([]);
        
        //这个稍后解释
        final TankGame tankGame = TankGame();
        
        runApp(...)//这里下面详细交代
      
    }
    

    loadAssets();的代码如下,主要是加载图片资源以备开发时候的使用

    void loadAssets(){
      Flame.images.loadAll([
        'new_map.webp',
      ]);
    }
    

    接下来的tankgame,我们需要说一下它的父类Game

    Game

    Game是Flame的核心,也是我们游戏的驱动力,它内部有两个主要的方法,就是上篇文章提到的

    render(Canvas c)和update(double t)
    

    这里再贴一下官方的流程图:

    image

    我们实际开发时,需要继承它并在上面两个方法中做我们自己的处理,如这里的TankGame:

    class TankGame extends Game{
    
        @override
      void render(Canvas canvas) {}
      
      @override
      void update(double t) {}
      
      ///resize 这里的方法在屏幕尺寸变动和第一次初始化时会调用,
      ///我们可以在这里获取到屏幕的尺寸
      @override
      void resize(Size size) {}
    
    }
    

    接下来看最后一行的runApp(...)

    runApp(...)

    这里如app开发一样,是我们要加载widget的地方,可以看一下game里面有个widget变量,就是在这里面用的,不过现在我们先考虑一下布局。

    通过观察,可以发现摇杆是悬浮于地图上方的,所以这里用stack比较合适。代码如下:

      runApp(Directionality(textDirection: TextDirection.ltr,
          child: Stack(
        children: [
            ///我们将游戏内容如tank,地图等放在最底层
          tankGame.widget,
            
            //上层放摇杆
          Column(
            children: [
                //这个widget可以将摇杆挤在底部,内部是一个Expanded
              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)
            ],
          ),
    
        ],
      )));
    

    这样我们的基本布局就算完成了,先对布局结构有一个了解,具体内部什么样子,我们一步一步来。

    Component&Sprite

    在游戏开发前,我们需要先简单了解一下两个东西

    component : 组件(我觉得它跟游戏开发中的 刚体 很像),如子弹、坦克等游戏角色都属于component
    sprite  : 这个内部方法很简单,主要是将图形绘制在游戏界面上
    

    由component的定义可以知道,它与游戏的每帧都有关系,因此需要增加两个与game的update和render对应的方法,为了便于理解,我们依然为component的这两个方法命名为:update和render,同时抽出来:

    abstract class BaseComponent{
      void render(Canvas canvas);
      void update(double t);
    }
    
    

    搞定! 下面再来布置一下我们的Game(TankGame)

    TankGame

    TankGame继承自Game,我们从这里可以获得游戏场景大小,同时通过update和render驱动各个component,代码如下:

    class TankGame extends Game{
        //用来保存游戏场景尺寸
        Size screenSize;
        //游戏背景
        BattleBackground bg;
    
        @override
      void render(Canvas canvas) {
        //没有初始化成功的话,不进行绘制
        if(screenSize == null)return;
        //绘制背景
        bg.render(canvas);
      }
      
      @override
      void update(double t) {
        if(screenSize == null)return;
      }
      
      ///resize 这里的方法在屏幕尺寸变动和第一次初始化时会调用,
      ///我们可以在这里获取到屏幕的尺寸
      @override
      void resize(Size size) {
        screenSize = size;
        //初始化一个背景sprite
        if(bg == null){
          bg = BattleBackground(this);
        }
        
      }
    
    }
    
    

    我们在game里保存下场景尺寸,并且初始化一个bg,同时在render里调用bg的render方法,将背景绘制到游戏上,让我们看一下BattleBackground

    背景

    背景(BattleBackground)实现非常简单,它的代码如下:

    class BattleBackground with BaseComponent{
    
      final TankGame game;
    
      Sprite bgSprite;
      Rect bgRect;
    
      BattleBackground(this.game){
        //将bgSprite初始化,并将地图图片引入进来
        bgSprite = Sprite('new_map.webp');
        //根据游戏场景尺寸确定一个rect,用来告诉sprite绘制区域
        bgRect = Rect.fromLTWH(0, 0, game.screenSize.width, game.screenSize.height);
      }
    
      @override
      void render(Canvas canvas) {
        bgSprite.renderRect(canvas, bgRect);
      }
    
      @override
      void update(double t) {
    
      }
    
    }
    

    因为咱们的地图目前并没有什么变化,所以update方法可以不管,只需要render里绘制一下即可。

    这里的大致流程是,game启动后,会循环调用下面的方法:

    (TankGame)update->render->update->...
    

    我们在game的render中调用背景的render方法,就可以绘制图片了。

    至此,背景就添加成功了,下面我们制作摇杆

    摇杆

    我们这里要用到widget,起名叫JoyStick。如果你会flutter开发,那么接下来的代码是非常简单的。

    首先声明一个JoyStick

    class JoyStick extends StatefulWidget{
        
        //用于回传摇杆移动的方位
      final void Function(Offset) onChange;
    
      const JoyStick({Key key, this.onChange}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() {
        return JoyStickState();
      }
    
    }
    
    class JoyStickState extends State<JoyStick> {}
    

    state内部实现如下,代码比较多我将说明写在注释里

    class JoyStickState extends State<JoyStick> {
    
      //摇杆中间的圆的位置,简称 摇杆头
      Offset delta = Offset.zero;
    
      //更新 摇杆头的位置,并将位置传出去(这样就可以控制坦克了)
      void updateDelta(Offset newD){
        widget.onChange(newD);
        setState(() {
          delta = newD;
        });
      }
        
        //这个是根据用户移动摇杆头时的控制计算,主要是确保摇杆头的活动范围不能超出 外层白圈
      void calculateDelta(Offset offset){
        Offset newD = offset - Offset(bgSize/2,bgSize/2);
        updateDelta(Offset.fromDirection(newD.direction,min(bgSize/4, newD.distance)));//活动范围控制在bgSize之内
      }
        
        //摇杆外层的白圈尺寸,摇杆头的尺寸跟这个也有关系
      final double bgSize = 120;
    
      @override
      Widget build(BuildContext context) {
        return SizedBox(
          width: bgSize,height: bgSize,
          
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(bgSize/2)
            ),
            //监听用户手势
            child: GestureDetector(
              ///摇杆底部白圈
              child: Container(
                decoration: BoxDecoration(
                  color: Color(0x88ffffff),
                  borderRadius: BorderRadius.circular(bgSize/2),
                ),
                child: Center(
                  child: Transform.translate(offset: delta,
                    ///摇杆头
                    child: SizedBox(
                      width: bgSize/2,height: bgSize/2,
                      child: Container(
                        decoration: BoxDecoration(
                          color: Color(0xccffffff),
                          borderRadius: BorderRadius.circular(30),
                        ),
                      ),
                    ),),
                ),
              ),
              onPanDown: onDragDown,
              onPanUpdate: onDragUpdate,
              onPanEnd: onDragEnd,
            ),
          ),
        );
      }
        //三个方法主要用于获取用户触摸位置的数据
      void onDragDown(DragDownDetails d) {
        calculateDelta(d.localPosition);
      }
    
      void onDragUpdate(DragUpdateDetails d) {
        calculateDelta(d.localPosition);
      }
    
      void onDragEnd(DragEndDetails d) {
        updateDelta(Offset.zero);
      }
    }
    

    这样摇杆部分就完了,回看runApp内的方法,这个时候运行一下就可以看到屏幕上面有个摇杆了。

    image

    按钮

    就是俩白圈,我直接上代码了:

    class FireButton extends StatelessWidget {
      final void Function() onTap;
    
      const FireButton({Key key, this.onTap}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return SizedBox(
          height: 64,width: 64,
          child: Container(
            decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(32)
            ),
            child: GestureDetector(
              child:Container(
                decoration: BoxDecoration(
                  color: Color(0x88ffffff),
                  borderRadius: BorderRadius.circular(32),
                ),
              ),
              onTap: onTap,
            ),
          ),
        );
      }
    }
    

    接下来我们开始绘制坦克

    绘制坦克

    首先我们需要坦克的图片,并加载进flame.

    image
    别忘了在pub中添加,并get一下
    

    之后回到main函数中的loadAssets()方法,加载刚才的图片资源:

    void loadAssets(){
      Flame.images.loadAll([
        'new_map.webp',
        'tank/t_body_blue.webp',
        'tank/t_turret_blue.webp',
        'tank/t_body_green.webp',
        'tank/t_turret_green.webp',
        'tank/t_body_sand.webp',
        'tank/t_turret_sand.webp',
        'tank/bullet_blue.webp',
        'tank/bullet_green.webp',
        'tank/bullet_sand.webp',
        'explosion/explosion1.webp',
        'explosion/explosion2.webp',
        'explosion/explosion3.webp',
        'explosion/explosion4.webp',
        'explosion/explosion5.webp',
      ]);
    }
    

    ok,资源加载完毕,开始代码部分。

    以玩家坦克为例我们先要继承一下baseComponent,同时我们需要分别控制身体和炮塔,所以需要分别进行绘制,即两个Sprite。

    class Tank extends BaseComponent{
      final TankGame game;
      Sprite bodySprite,turretSprite;
      
        //坦克出生位置
      Offset position;
    
      Tank(this.game,{this.position}){
        //炮塔
        turretSprite = Sprite('tank/t_turret_blue.webp');
        //坦克身体
        bodySprite= Sprite('tank/t_body_blue.webp');
    
      }
      
        //调整坦克整体大小的系数
      final double ratio = 0.7;
      
      @override
      void render(Canvas canvas){
        drawBody(Canvas canvas);
      }
      @override
      void update(double t){}
      
    }
    

    我们在render方法中添加一个drawBody()方法,来绘制坦克 :

    void drawBody(Canvas canvas){
        //对画布操作前要先保存一下
        canvas.save();
        canvas.translate(position.dx, position.dy);
        //绘制tank身体
        bodySprite.renderRect(canvas,Rect.fromLTWH(-20*ratio, -15*ratio, 38*ratio, 32*ratio));
        // 绘制炮塔
        turretSprite.renderRect(canvas, Rect.fromLTWH(-1, -2*ratio, 22*ratio, 6*ratio));
        canvas.restore();
    }
    
    坦克大小我是直接写的数值,而后面的ratio,是我用来调整大小用的。
    

    现在我们的‘不会动’坦克就绘制完成了。

    后面我们需要将摇杆和坦克联系起来已达到控制坦克的目的,不过碍于篇幅(我现在滑动页面都已经卡顿了)且控制坦克这三个方法需要详尽的说一下,因此我将挪到下一篇再讲,谢谢大家阅读。

    再次感谢官方的文档及其贡献者,给我提供了很大的帮助,如果你很着急可以直接查阅官方文档
    

    Demo

    坦克大战

    相关文章

      网友评论

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

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