SpriteWidget

SpriteWidget是一个用于使用Flutter构建复杂、高性能动画和2D游戏的工具包。您的精灵渲染树位于一个与Flutter和其他Material组件无缝集成的Widget内。您可以使用SpriteWidget创建从动画图标到完整游戏的任何内容。

本指南假定您对Flutter和Dart有基本了解。通过在StackOverflow上标记spritewidget提出问题来获得支持。

您可以在example目录中找到示例,或查看完整的Space Blast游戏。

将SpriteWidget添加到您的项目

SpriteWidget作为标准包提供。只需将其添加为pubspec.yaml的依赖项,即可开始使用。

dependencies:
  flutter:
    sdk: flutter
  spritewidget:

创建SpriteWidget

要使用SpriteWidget,您首先需要设置一个用于绘制其内容的根节点。添加到根节点的任何精灵节点都将由SpriteWidget渲染。通常,您的根节点是您应用程序状态的一部分。下面是一个如何使用SpriteWidget设置自定义有状态Widget的示例:

import 'package:flutter/material.dart';
import 'package:spritewidget/spritewidget.dart';

class MyWidget extends StatefulWidget {
  @override
  MyWidgetState createState() => new MyWidgetState();
}

class MyWidgetState extends State<MyWidget> {
  NodeWithSize rootNode;

  @override
  void initState() {
    super.initState();
    rootNode = new NodeWithSize(const Size(1024.0, 1024.0));
  }

  @override
  Widget build(BuildContext context) {
  	return new SpriteWidget(rootNode);
  }
}

传递给SpriteWidget的根节点是一个NodeWithSize,根节点的大小定义了SpriteWidget使用的坐标系。默认情况下,SpriteWidget使用letterboxing来显示其内容。这意味着您为根节点提供的大小将决定SpriteWidget的内容将如何缩放以适应。如果它不能完美地适应Widget的区域,则其顶部和底部或左侧和右侧将被裁剪。您可以根据需要选择性地为SpriteWidget传递一个参数以获得其他缩放选项。

当您将SpriteWidget添加到应用程序的build方法后,它将自动开始运行动画并处理用户输入。无需任何其他额外设置。

将对象添加到节点图

您的SpriteWidget管理着一个节点图,根节点是创建SpriteWidget时传递给它的NodeWithSize。要渲染精灵、粒子系统或任何其他对象,只需将它们添加到节点图中。

节点图中的每个节点都有一个变换。变换会由其子节点继承,这使得通过将对象分组为节点的子节点,然后操作父节点来构建更复杂的结构成为可能。例如,以下代码创建了一个带有两个附加轮子的汽车精灵。汽车被添加到根节点。

Sprite car = new Sprite.fromImage(carImage);
Sprite frontWheel = new Sprite.fromImage(wheelImage);
Sprite rearWheel = new Sprite.fromImage(wheelImage);

frontWheel.position = const Offset(100, 50);
rearWheel.position = const Offset(-100, 50);

car.addChild(frontWheel);
car.addChild(rearWheel);

rootNode.addChild(car);

您可以通过设置position、rotation、scale和skew属性来操作变换。

精灵、纹理和精灵表

要加载图像资源,最简单的方法是使用ImageMap类。ImageMap可以一次加载一个或多个图像。

Image类不会通过flutter/material自动导入,因此您可能需要在文件顶部添加一个import语句。

import 'dart:ui' as ui show Image;

现在您可以使用ImageMap加载图像了。请注意,加载方法是异步的,因此此示例代码需要放在一个异步方法中。有关加载图像的完整示例,请参阅天气演示

ImageMap images = new ImageMap(rootBundle);

// Load a single image
ui.Image image = await images.loadImage('assets/my_image.png');

// Load multiple images
await images.load(<String>[
  'assets/image_0.png',
  'assets/image_1.png',
  'assets/image_2.png',
]);

// Access a loaded image from the ImageMap
image = images['assets/image_0.png'];

最常见的节点类型是Sprite节点。Sprite只是将图像绘制到屏幕上。Sprite可以从Image对象或SpriteTexture对象绘制。纹理是Image的一部分。使用SpriteSheet,您可以将多个纹理元素打包在单个图像中。这可以节省设备GPU内存空间,还可以加快绘图速度。目前SpriteWidget支持JSON格式的精灵表,由TexturePacker等工具生成。手动编辑精灵表文件是不常见的。您可以创建一个具有JSON定义和图像的SpriteSheet

SpriteSheet sprites = new SpriteSheet(myImage, jsonCode);
SpriteTexture texture = sprites['texture.png'];

帧周期

每次将新帧渲染到屏幕时,SpriteWidget将执行一系列操作。有时,在创建更高级的交互式动画或游戏时,这些操作的执行顺序可能很重要。

事情发生的顺序如下:

  1. 处理输入事件
  2. 运行动画操作
  3. 调用节点上的update函数
  4. 应用约束
  5. 将帧渲染到屏幕

下面可以阅读有关每个不同阶段的更多信息。

处理用户输入

您可以继承任何节点类型来处理触摸。要接收触摸,您需要将userInteractionEnabled属性设置为true并覆盖handleEvent方法。如果您继承的节点没有大小,您还需要覆盖isPointInside方法。

class EventHandlingNode extends NodeWithSize {
  EventHandlingNode(Size size) : super(size) {
    userInteractionEnabled = true;
  }

  @override handleEvent(SpriteBoxEvent event) {
    if (event.type == PointerDownEvent)
      ...
    else if (event.type == PointerMoveEvent)
      ...

    return true;
  }
}

如果您希望您的节点接收多个触摸,请将handleMultiplePointers属性设置为true。每次触摸按下或拖动触摸都会为handleEvent方法生成一个单独的调用,您可以通过pointer属性区分每次触摸。

使用操作进行动画

SpriteWidget提供了易于使用的函数,通过操作来为节点设置动画。您可以组合简单的操作块来创建更复杂的动画。

要执行操作动画,您首先构建操作本身,然后将其传递给节点的action manager的run方法(有关示例,请参阅下面的Tweens部分)。

Tweens

Tweens是创建动画的最简单的构建块。它将在指定的持续时间内插值一个值或属性。您为ActionTween类提供一个setter函数、其开始和结束值以及tween的持续时间。

创建tween后,通过节点的action manager运行它来执行它。

Node myNode = new Node();

ActionTween myTween = new ActionTween<Offset> (
  (a) => myNode.position = a,
  Offset.zero,
  const Offset(100.0, 0.0),
  1.0
);

myNode.actions.run(myTween);

您可以设置不同类型的值,如浮点数、点、矩形甚至颜色。您还可以选择性地为ActionTween类提供一个缓动函数。

序列

当您需要按顺序播放两个或多个操作时,请使用ActionSequence类。

ActionSequence sequence = new ActionSequence([
  firstAction,
  middleAction,
  lastAction
]);

使用ActionGroup并行播放操作。

ActionGroup group = new ActionGroup([
  action0,
  action1
]);

重复

您可以循环播放任何操作,无论是固定的次数,还是直到永远。

ActionRepeat repeat = new ActionRepeat(loopedAction, 5);

ActionRepeatForever longLoop = new ActionRepeatForever(loopedAction);

组合

可以通过以任何方式组合它们来创建更复杂的操作。

ActionSequence complexAction = new ActionSequence([
  new ActionRepeat(myLoop, 2),
  new ActionGroup([
  	action0,
  	action1
  ])
]);

处理更新事件

每一帧,更新事件都会被发送到当前节点树中的每个节点。覆盖update方法以手动执行动画或执行游戏逻辑。

MyNode extends Node {
  @override
  update(double dt) {
    // Move the node at a constant speed
  	position += new Offset(dt * 1.0, 0.0);
  }
}

定义约束

约束用于约束节点属性。它们可用于相对于其他节点定位节点,或调整旋转或缩放。您可以将多个约束应用于单个节点。

例如,您可以使用约束使一个节点以特定距离平滑跟随另一个节点。平滑效果将使跟随节点运动更加顺畅。

followingNode.constraints = [
  new ConstraintPositionToNode(
    targetNode,
    offset: const Offset(0.0, 100.0),
    dampening: 0.5
  )
];

约束在帧周期结束时应用。如果您需要在其他时间应用它们,您可以直接调用Node对象的applyConstraints方法。

执行自定义绘图

SpriteWidget提供了一组默认的绘图图元,但在某些情况下,您可能需要执行自定义绘图。要做到这一点,您需要继承Node或NodeWithSize类并覆盖paint方法。

class RedCircle extends Node {
  RedCircle(this.radius);

  double radius;

  @override
  void paint(Canvas canvas) {
    canvas.drawCircle(
      Offset.zero,
      radius,
      new Paint()..color = const Color(0xffff0000)
    );
  }
}

如果您正在覆盖NodeWithSize,您可能需要调用applyTransformForPivot,然后再开始绘图,以考虑节点的支点。调用后,坐标系已设置好,您可以从原点开始绘制到节点的大小。

@override
void paint(Canvas canvas) {
  applyTransformForPivot(canvas);

  canvas.drawRect(
    new Rect.fromLTWH(0.0, 0.0, size.width, size.height),
    myPaint
  );
}

使用粒子系统添加效果

粒子系统非常适合创建雨、烟或火等效果。设置粒子系统很容易,但有很多属性可以调整。了解它们工作原理的最佳方法是自己尝试。

这是一个如何创建、配置粒子系统并将其添加到场景的示例:

ParticleSystem particles = new ParticleSystem(
  particleTexture,
  posVar: const Point(100, 100.0),
  startSize: 1.0,
  startSizeVar: 0.5,
  endSize: 2.0,
  endSizeVar: 1.0,
  life: 1.5 * distance,
  lifeVar: 1.0 * distance
);

rootNode.addChild(particles);

GitHub

https://github.com/spritewidget/spritewidget