前言
最近在学习基于Flutter的游戏引擎Flame,并尝试自己编写一个飞机大战。本文将记录工程搭建和玩家战机创建的过程。强烈推荐在阅读文章前,优先食用以下内容,这将会对你有一定的Flame知识基础。当然,Flutter的基础也是必要的。
环境及资源搭建
新建项目后,在pubspec.yaml中添加Flame的核心依赖。ps:这里笔者使用的是当前最新的1.2.0版本。
environment:
sdk: ">=2.17.5 <3.0.0"
dependencies:
flutter:
sdk: flutter
flame: ^1.2.0
还有就是常规的Flutter静态资源配置了。ps:这些资源不全是在本文中使用,图片资源都是在网上找到的,是微信飞机大战的资源仅为学习所用。
创建战机
游戏环境
void main() {
runApp(GameWidget(game: Game()));
}
class Game extends FlameGame with HasDraggables {
@override
Future<void> onLoad() async {}
}
在Flutter的main函数中,runApp传入一个GameWidget,实际是一个StatefulWidget。Game继承自FlameGame,它是Flame定义的一个Component。Flame依赖于Component进行展开,类比于Flutter依赖于Widget。后续在游戏里的对象都会是在FlameGame对象中添加Component实现。
// game_widget.dart
class GameWidget<T extends Game> extends StatefulWidget {
final T? game;
...
onLoad是Component中的生命周期之一,学习过Android的话可以和Activity的onCreate进行类比。
玩家战机
class Player extends SpriteAnimationComponent {
Player({required Vector2 initPosition, required Vector2 size})
: super(position: initPosition, size: size);
@override
Future<void> onLoad() async {
List<Sprite> sprites = [];
for (int i = 1; i <= 2; i++) {
sprites.add(await Sprite.load('player/me$i.png'));
}
final spriteAnimation = SpriteAnimation.spriteList(sprites, stepTime: 0.15);
animation = spriteAnimation;
add(RectangleHitbox()..debugMode = true);
}
创建类Player:
- 继承自
SpriteAnimationComponent,这是一个可以加载动画的Component,也可以理解为序列帧。这里战机共两帧,设置切换间隔为0.15s。ps:默认是循环播放的,这里没有特殊需求循环即可。
// sprite_animation.dart
factory SpriteAnimation.spriteList(
List<Sprite> sprites, {
required double stepTime,
bool loop = true,
}) {
return SpriteAnimation(
sprites.map((sprite) => SpriteAnimationFrame(sprite, stepTime)).toList(),
loop: loop,
);
}
- 构造方法需要传入
initPosition,size,即初始位置和大小。两者都为Vector2。ps:由于SpriteAnimationComponent继承自PositionComponent,所以会有anchor锚点的概念,默认是左上角的,锚点的变更会影响position的值,以及后续在布局上的参考位置。 - 添加一个
RectangleHitbox,可以观察战机当前的位置。
控制战机
在手机上操作战机一般会使用拖拽,或者是拖动的交互。可以在Player中加入HasGameRef和Draggable的混入。前者是用于获取最上层FlameGame对象,即我们自定义的Game对象(ps:加入的Component也是一个树状结构);后者用于实现单个对象的拖拽效果。
class Player extends SpriteAnimationComponent with HasGameRef, Draggable {
Player({required Vector2 initPosition, required Vector2 size})
: super(position: initPosition, size: size);
@override
Future<void> onLoad() async {
// 。。。
}
@override
bool onDragUpdate(DragUpdateInfo info) {
final willToPosition = position + info.delta.global;
double x = willToPosition.x;
double y = willToPosition.y;
if (x < 0) {
x = 0;
} else if (x > gameRef.size.x - size.x) {
x = gameRef.size.x - size.x;
}
if (y < 0) {
y = 0;
} else if (y > gameRef.size.y - size.y) {
y = gameRef.size.y - size.y;
}
position = Vector2(x, y);
return true;
}
onDragUpdate方法会返回一个DragUpdateInfo对象,这里取info.delta.global,即为相对于当前位置的移动差值。正常情况下只需要叠加到position即可。考虑到边界问题,所以需要获取Game对象的大小进行计算,再更新position。
ps:Draggable还有onDragStart、onDragEnd等方法可实现,机制上类似原生的触摸事件机制。
在Component最外层也就是Game需要添加HasDraggables的混入,这样事件才能往下传递。
class Game extends FlameGame with HasDraggables {
实现效果
最后来看看实现效果,这里还加了一个视差组件作为背景。
最后
本文记录了基本的Flame环境搭建以及飞机大战中的玩家战机创建。后续会记录有关战机子弹的逻辑。