美文网首页Flutter圈子FlutterFlutter
开始用Flutter做游戏吧

开始用Flutter做游戏吧

作者: 何小有 | 来源:发表于2019-07-03 12:08 被阅读46次

    一点点基础

    游戏主循环(GameLoop

    游戏主循环是游戏的核心,计算机一次又一次运行的一组指令,用通俗的话来说,如果游戏有生命,那么游戏主循环就是游戏的心跳。

    同时为了更好的理解游戏主循环,还需要引入一个计算机图像领域的知识——FPS,FPS全称是“Frames Per Second”,翻译为“每秒传输帧数”,意思就是,如果游戏以60FPS运行,则计算机每秒运行60次游戏主循环。总结一下就是,1帧==游戏主循环的一次运行。

    通常来说,游戏主循环由两部分组成——更新(update)和渲染(render)。

    游戏更新与渲染

    如上图,更新(update)部分负责处理对象的移动,这里的对象可以是主角、NPC、敌人、障碍物、地图和其他需要更新的参数。你在游戏里能看到的大部分动作都在这部分发生,比如,计算主角的98K射出的子弹是否接触到敌人。

    而渲染(render)部分通常只负责一件事,在更新(update)部分发生变化时,绘制屏幕上的所有对象,以便玩家看到的一切都是同步的。

    游戏同步机制

    在游戏中,同步机制是非常重要的,可以想象一下,现在更新一个NPC的位置,NPC处于正常状态,所以,你让NPC开始移动。但是,此时有一个子弹距离NPC只有几个像素的距离,你更新了子弹,它会击中NPC。

    现在NPC已经死了,所以你不用绘制子弹。这个时候,你应该绘制NPC倒地动画的第一帧。

    然后,在下一个游戏主循环中,您将跳过更新NPC位置,因为NPC已经死了,所以您改为渲染NPC垂死动画的第一帧,而不是倒地动画第二帧。

    这会给玩家带来一种游戏不稳定的感觉,玩家在玩射击游戏,射击一个NPC的时候,NPC不会倒地,玩家再次射击,但是在子弹击中NPC之前,NPC就死了。

    非同步渲染的不稳定性能可能不易被察觉,特别是当每秒运行60帧的高帧频率下,但如果这种情况经常发生,玩家还是会感觉出来的,然后就骂辣鸡游戏了。

    所以,最好提前计算好所有内容,并且当计算完成后最终确定所有对象的状态时,再开始绘制屏幕。

    开始撸码

    使用Flame插件

    pubspec.yaml下添加flame插件,并通过flutter packages get命令下载插件,或者使用Visual Studio Code保存文件会自动下载插件。

    dependencies:
      flutter:
        sdk: flutter
    
      cupertino_icons: ^0.1.2
    
      flame: ^0.13.0
    

    Flame插件已经提供了一个完整的游戏开发框架,所以我们只需要专心编写实际的更新和渲染过程。首先,需要将应用程序转化为游戏模式,要做两个操作:全屏和纵向。而令人感到巴适的是,Flame插件已经封装好了这些实用的功能,我们只需要编写调用代码就可以了。

    我们先在main.dart的顶部添加以下引用。

    import 'package:flame/util.dart';
    import 'package:flutter/services.dart';
    

    然后在main.dartmain函数内部创建Flame的Util类的实例,调用其实例的全屏(fullScreen)和设置方向(setOrientation)函数,同时要注意,因为这些函数的返回值类型是未来(Future),所以要在这些函数前面添加等待(await)。

    未来(Future)、异步(async)和等待(await)是一种特殊的编码方法,它让那些需要长时间才能处理完成的代码在不同的线程上完成,而且不会阻塞主线程。

    为了能够等待(await)未来(Future)处理完成,相关的代码必须在异步(async)函数内,所以我们必须修改main函数,使它成为一个异步函数。

    void main() async {
      Util flameUtil = Util();
      await flameUtil.fullScreen();
      await flameUtil.setOrientation(DeviceOrientation.portraitUp);
    

    到这里为止,我们的main.dart里面应该有以下代码。

    import 'package:flutter/material.dart';
    
    import 'package:flame/util.dart';
    import 'package:flutter/services.dart';
    
    void main() async {
      Util flameUtil = Util();
      await flameUtil.fullScreen();
      await flameUtil.setOrientation(DeviceOrientation.portraitUp);
    }
    

    游戏主循环脚手架

    在开头,我们知道在一个游戏应用中,游戏是在游戏主循环里面运行的。Flame插件已经提供了可以直接使用的游戏主循环脚手架,要使用这个脚手架,就要用到Flame的游戏(Game)抽象类。

    创建一个名称为box-game.dart的新文件,然后开始编写BoxGame类,。

    import 'dart:ui';
    
    import 'package:flame/game.dart';
    
    class BoxGame extends Game {
      void render(Canvas canvas) {
        // TODO: 实现渲染
      }
    
      void update(double t) {
        // TODO: 实现更新
      }
    }
    

    上面的代码中,导入dart:ui库,这样的话,等一下我们就可以使用画布(Canvas)类和大小(Size)类。然后导入package:flame/game.dart库,这个库里面包括我们现在使用的游戏(Game)抽象类,这个类有两个方法:更新(update)和渲染(render),我们直接用同名方法覆盖了它们。

    Dart 2.x版本中,@override注释和new关键字是可选的,所以在这里也不需要写。

    接下来,我们在main.dart文件中创建BoxGame类的实例,并将其widget属性传递给runApp函数。同时,引用我们刚才创建的package:hello_flame/box-game.dart,让BoxGame类可以在main.dart中使用。

    ...
    import 'package:hello_flame/box-game.dart';
    
    void main() async {
      ...
      BoxGame game = BoxGame();
      runApp(game.widget);
    

    到这里为止,我们的main.dart里面应该有以下代码。

    import 'package:flutter/material.dart';
    
    import 'package:flame/util.dart';
    import 'package:flutter/services.dart';
    
    import 'package:hello_flame/box-game.dart';
    
    void main() async {
      Util flameUtil = Util();
      await flameUtil.fullScreen();
      await flameUtil.setOrientation(DeviceOrientation.portraitUp);
    
      BoxGame game = BoxGame();
      runApp(game.widget);
    }
    

    现在我们的应用程序可以被称为游戏了,运行游戏,会显示一个空白的黑屏,因为我们还没有在屏幕上绘制具体的内容。

    屏幕的大小和尺寸

    Flame这个游戏开发框架是以Flutter为基础的,而Flutter在屏幕上绘制时使用逻辑像素,因此,我们在Flame上调整游戏对象的大小时也是使用逻辑像素。

    实际上,游戏(Game)抽象类上有个调整(resize)方法,这个方法接受大小(Size)类参数,使用这个参数就可以确定设备的屏幕大小。

    首先在box-game.dart文件中,添加一个BoxGame类的实例变量screenSize,这个变量用于保持屏幕的大小,只有当屏幕的大小发生变化时才会更新,它也是Flame在屏幕上绘制对象时的基础。screenSizeSize类型的变量,与传递给调整(resize)方法的参数一致。

    类变量screenSize的初始值为null,可以用来判断渲染过程中是否已知屏幕大小。接下来,我们编写一个同名方法覆盖调整(resize)方法。

    class BoxGame extends Game {
      Size screenSize;
    
      ...
    
      void resize(Size size) {
        screenSize = size;
        super.resize(size);
      }
    

    到这里为止,我们的box-game.dart里面应该有以下代码。

    import 'dart:ui';
    
    import 'package:flame/game.dart';
    
    class BoxGame extends Game {
      Size screenSize;
    
      void render(Canvas canvas) {
        // TODO: 实现渲染
      }
    
      void update(double t) {
        // TODO: 实现更新
      }
    
      void resize(Size size) {
        screenSize = size;
        super.resize(size);
      }
    }
    

    绘制画布和背景

    到这一步,游戏主循环已经存在,可以开始绘制一些对象了。在渲染(render)方法中,我们可以访问画布(Canvas),这个画布(Canvas)是Flame提供的,在画布(Canvas)上绘制游戏图形之后,Flame会将其绘制并将整个画布绘制到屏幕上。

    在画布上绘图时,就像我们拿着画笔画画一样,先绘制最底层的背景对象,然后在上面绘制一些动物、植物或建筑物对象。

    现在我们可以开始绘制背景,这个例子中游戏背景只是一个黑屏,可以使用以下代码绘制。

      void render(Canvas canvas) {
        // TODO: 实现渲染
        // 在整个屏幕上绘制黑色背景
        Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
        Paint bgPaint = Paint();
        bgPaint.color = Color(0xff000000);
        canvas.drawRect(bgRect, bgPaint);
    

    上面代码中,第一行声明了一个与屏幕一样大小的矩形(Rect),坐标位于(0,0),即屏幕的左上角,我们就用这个当游戏背景了。

    然后,第二行声明一个绘制(Paint)类对象,其后尾随配置这个绘制(Paint)类对象的颜色(Color)。

    最后一行代码使用前面定义的矩形(Rect)和绘制(Paint)实例在画布(Canvas)上绘制一个矩形。

    绘制上层的对象

    接下来的步骤中,我们会在屏幕的中间绘制一个游戏对象,在当前游戏中,游戏对象是一个小矩形图案。

      void render(Canvas canvas) {
        ...
    
        // 画一个盒子,如果获胜则将其设为绿色,否则为白色
        double screenCenterX = screenSize.width / 2;
        double screenCenterY = screenSize.height / 2;
        Rect boxRect = Rect.fromLTWH(
          screenCenterX - 75,
          screenCenterY - 75,
          150,
          150,
        );
        Paint boxPaint = Paint();
        boxPaint.color = Color(0xffffffff);
        canvas.drawRect(boxRect, boxPaint);
      }
    

    上面代码中,前面2行代码声明两个变量,分别是用于保持屏幕中心坐标的变量,分别为屏幕宽度和高度的一半。

    接下来的6行代码声明了一个150x150个逻辑像素大小的矩形,它位于屏幕中间,但是会向左偏移75个像素,向上偏移75个像素。

    其余的代码前面绘制画布和背景的代码差不多,此时运行游戏,就可以看到黑色背景上有一个白色的矩形对象。

    处理输入和胜利条件

    到这里,我们已经完成了大部分内容,现在只需要接受玩家的输入了。在box-game.dart文件中,先导入Flutter的手势库(package:flutter/gestures.dart),然后还要添加点击操作的处理函数。

    ...
    import 'package:flutter/gestures.dart';
    
    class BoxGame extends Game {
      ...
    
      void onTapDown(TapDownDetails d) {
        // 处理点击
      }
    }
    

    然后回到main.dart文件中,注册一个手势识别器(GestureRecognizer)并将其点击(onTapDown)事件链接到游戏的点击(onTapDown)处理程序。同时,我们也不要忘记在这里导入Flutter的手势库(package:flutter/gestures.dart),以便在此文件中可以使用手势识别器(GestureRecognizer)类。

    再然后,定位到main函数内部,声明一个点击手势识别器(TapGestureRecognizer)并将其点击(onTapDown)事件分配给游戏的点击(onTapDown)处理程序。最后使用Flutter的工具库package:flame/util.dart中的添加手势识别器(addGestureRecognizer)函数注册手势识别器。

    ...
    import 'package:flutter/gestures.dart';
    
    void main() async {
      ...
    
      BoxGame game = BoxGame();
      TapGestureRecognizer tapper = TapGestureRecognizer();
      
      tapper.onTapDown = game.onTapDown;
      runApp(game.widget);
      flameUtil.addGestureRecognizer(tapper);
    }
    

    到这里为止,我们的main.dart里面应该有以下代码。

    import 'package:flutter/material.dart';
    
    import 'package:flame/util.dart';
    import 'package:flutter/services.dart';
    
    import 'package:hello_flame/box-game.dart';
    
    import 'package:flutter/gestures.dart';
    
    void main() async {
      Util flameUtil = Util();
      await flameUtil.fullScreen();
      await flameUtil.setOrientation(DeviceOrientation.portraitUp);
    
      BoxGame game = BoxGame();
      TapGestureRecognizer tapper = TapGestureRecognizer();
    
      tapper.onTapDown = game.onTapDown;
      runApp(game.widget);
      flameUtil.addGestureRecognizer(tapper);
    }
    

    现在,我们再回到box-game.dart文件中来,添加另一个实例变量hasWon来判断玩家是否胜利,定义一个布尔(bool)变量,默认为false表示玩家未取得胜利。

    然后在渲染(render)方法里面,写一个条件判断,如果玩家已经胜利,将boxPaint的颜色设置成绿色,否则为白色。

    class BoxGame extends Game {
      ...
      bool hasWon = false;
    
      void render(Canvas canvas) {
        ...
    
        Paint boxPaint = Paint();
        if (hasWon) {
          boxPaint.color = Color(0xff00ff00);
        } else {
          boxPaint.color = Color(0xffffffff);
        }
        canvas.drawRect(boxRect, boxPaint);
      }
    
      ...
    }
    

    最后我们还需要在游戏的点击(onTapDown)处理程序中添加逻辑代码,判断玩家是否点击了中间的矩形,如果是,就将hasWon变量的值转换为true,表示玩家已经取得胜利。

      void onTapDown(TapDownDetails d) {
        // 处理点击
        double screenCenterX = screenSize.width / 2;
        double screenCenterY = screenSize.height / 2;
        if (d.globalPosition.dx >= screenCenterX - 75 &&
            d.globalPosition.dx <= screenCenterX + 75 &&
            d.globalPosition.dy >= screenCenterY - 75 &&
            d.globalPosition.dy <= screenCenterY + 75) {
          hasWon = true;
        }
      }
    

    上面代码中,前面2行用来确定屏幕中心点的坐标,后面的5行多条件判断的if语句,用来判断点击坐标是否位于屏幕中间的150x150逻辑像素范围内。

    如果是,就转换hasWon变量的值,并在下次调用渲染(render)方法时反映在屏幕上。同时我们这里将更新(update)方法留空了,因为这个游戏里不会更新任何内容呀。

    到这里为止,我们的box-game.dart里面应该有以下代码。

    import 'dart:ui';
    
    import 'package:flutter/gestures.dart';
    import 'package:flame/game.dart';
    
    class BoxGame extends Game {
      Size screenSize;
      bool hasWon = false;
    
      void render(Canvas canvas) {
        // 在整个屏幕上绘制黑色背景
        Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
        Paint bgPaint = Paint();
        bgPaint.color = Color(0xff000000);
        canvas.drawRect(bgRect, bgPaint);
    
        // 画一个盒子,如果获胜则将其设为绿色,否则为白色
        double screenCenterX = screenSize.width / 2;
        double screenCenterY = screenSize.height / 2;
        Rect boxRect = Rect.fromLTWH(
          screenCenterX - 75,
          screenCenterY - 75,
          150,
          150,
        );
        Paint boxPaint = Paint();
        if (hasWon) {
          boxPaint.color = Color(0xff00ff00);
        } else {
          boxPaint.color = Color(0xffffffff);
        }
        canvas.drawRect(boxRect, boxPaint);
      }
    
      void update(double t) {
        // TODO: 实现更新
      }
    
      void resize(Size size) {
        screenSize = size;
        super.resize(size);
      }
    
      void onTapDown(TapDownDetails d) {
        // 处理点击
        double screenCenterX = screenSize.width / 2;
        double screenCenterY = screenSize.height / 2;
        if (d.globalPosition.dx >= screenCenterX - 75 &&
            d.globalPosition.dx <= screenCenterX + 75 &&
            d.globalPosition.dy >= screenCenterY - 75 &&
            d.globalPosition.dy <= screenCenterY + 75) {
          hasWon = true;
        }
      }
    }
    

    运行游戏,可以看到效果如下所示。

    开始用Flutter做游戏吧

    相关文章

      网友评论

        本文标题:开始用Flutter做游戏吧

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