阅读 2607

基于Flutter Canvas的飞机大战(一)

说明

小编也是初学者,为了了解flutter动画的使用与效果, 决定亲自定手用flutte写一款小游戏出来. 并将过程中的跳过的坑记录下来.

开发准备

具体参考flutter环境搭建, 笔者环境信息

  • Android Studio 3.2
  • macOS 10.14
  • flutter v1.1.10-pre.136
  • dart 2.0

New Flutter Project

我们大概目录为:

  • assets
    • images
  • lib
    • main.dart
    • src
      • enter.dart

main.dart是我们代码的主入口. lib.src用来存放我们整个游戏的逻辑代码文件. assets则是用来存放我们的图片资源文件.

项目入口 main.dart

在这个文件中, 我们可以制作一个菜单, 先保留着. 我们只留一下按钮, 点击按钮后. 将我们的游戏界面, 入栈到Router中, 开始我们的游戏.部份代码如下:

 RaisedButton(
      onPressed: () {
        Navigator.push(context, MaterialPageRoute(builder: (_) {
          return GameEnter();
        }));
      },
      child: Text("开始游戏")
)
复制代码

游戏入口enter.dart

enter.dart是我们整个游戏的主入口. 在这个入口中, 我们加载资源, 进行整体的绘图操作. 我们在enter.dart中定义我们的主画板, 关于CustomPaint的说明参考: 官方DOC, MainPainter 继承自 CustomPainter, 按官方的说明我们继承并实现他的二个方法 paintshouldRepaint, 在当前状态下. 整个界面是空白的, 什么都没有. 接下来我们定义我们的游戏背景.

// CustomPaint.painter
class MainPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return oldDelegate != this;
  }
}
// GameEnter.build
Widget build(BuildContext context) {
    return CustomPaint(
      painter: MainPainter(),
    );
}
复制代码

游戏背景

我们在 src 下, 新建一个叫做bg.dart的文件, 并新建一个 Background的类, init函数, 是用来在Enter 中加载我们游戏所需要的资源文件, paint 函数是用来在 MainPainter 中绘制我们的背景动画.

class Background {
  // 初始背景的偏移量
  double offsetY = -100.0;
  // 屏幕的宽度
  double screenWidth;
  // 屏幕的高度
  double screenHeight;
  // 画布滚动的速度
  double speed = 10;
  // 加载的背景图片
  ui.Image image;
  // 二张背景图的纵坐标点
  double y1 = 100.0;
  double y2 = 0.0;
  
  // 构造函数
  Background();
  
  // 初始化, 各种资源
  Future<VoidCallback> init() async {
    return null;
  }
  
  // 绘图函数
  paint(Canvas canvas, Size size) async {
    Rect screenWrap = Offset(0.0, 0.0) & Size(screenWidth, screenHeight);
    Paint screenWrapPainter = new Paint();
    screenWrapPainter.color = Colors.red;
    screenWrapPainter.style = PaintingStyle.fill;
    canvas.drawRect(screenWrap, screenWrapPainter);
  }
}

复制代码

Enter入口绘制背景

接下来我们要将我们的游戏背景真正的绘制在我们的手机上. 我们在 Enter 的初始化函数 initeState 中 初始化 Background 实例, 并进行资源初始化. 然后在 MainPainter 的绘图接口上, 增加我们的绘图逻辑

void paint(Canvas canvas, Size size) {
    background.paint(canvas, size);
}
复制代码

运行效果如下

运行效果

接下来我们需要将游戏的背景图绘制到背景中, 这里我们调用的是

Canvas.drawImage(Image image, Offset offset, Painter paint) API

Background.paint 函数中我们增加以下代码, 然后执行

Paint paint = new Paint();
canvas.drawImage(image, Offset(0, 0), paint);
复制代码

效果如下:

静态图背景

让背景动起来

在本次探动画探究中, 我使用 AnimationControllerCurvedAnimation 完成我们的效果. 有关这二个类的具体文档参考AnimationControllerCurvedAnimation. 我们先在 EnteriniteState 中声明二个实例,

 animation = CurvedAnimation(
  parent: controller,
  curve: Curves.linear,
);
animation.addListener(() {
  setState(() {});
});
animation.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    controller.repeat();
  }
});
复制代码

controller在有几个控制动画的方法

  • forward() 向前运动
  • stop() 停止
  • reverse() 向后运动 (这个概念, 我也没懂. 暂时搁这)

我们去监听 animation 每次动画值的改变, 增加监听函数, 通过 setState 去触发当前视图的刷新.

我们去监听 animation 动画状态, 判断是否动画结束,从而调用 repeat 方法, 使动画一直循环下去. 通过以上代码. 运行后发现, 每一帧都会触发 Background的重绘, 通过这点我们每次更改背景图起绘点的坐标, 就可以达到动画的效果.

paint(Canvas canvas, Size size) async {
    ...
    y1 += 10;
}
复制代码

因为录屏的原因, 所以显示比较卡顿.

让背景循环起来

在正常的2D飞机类游戏中, 游戏的背景是循环滚动的 ,常见的处理方法是, 二张背景图,头尾相连循环绘制, 当其中某个背景图, 滚出屏幕视野, 将其重新定位到上一张背景图的正上方, 来回往复, 从而达到背景循环滚动的效果. 在这里我们为二张图景图, 起绘点坐标增加以下的逻辑,

y1 = y1 + 1 * speed;
y2 = y2 + 1 * speed;
if (y2 > image.height) {
  y2 = y1 - image.height;
}
if (y1 > image.height) {
  y1 = y2 - image.height;
}
复制代码

在这次项目中, 由于我找到的背景图比较小, 没有办法撑满整个屏幕, 所以我在绘制的时候, 将Canvas进行了缩放操作.

canvas.scale(1, screenHeight / image.height);
复制代码

最后让我们看一下效果:

总结

第一部份, 大工告成. 在接下来几天. 我会把其他的元素的相关逻辑加上. git传送门