Flame - Fly Dash小游戏

文章目录[x]
  1. 1:游戏介绍
  2. 2:项目介绍
  3. 3:背景视差动画
  4. 4:Dash跳转逻辑
  5. 5:管道对
  6. 6:管道对的生成和移除
  7. 7:得分组件

在介绍完Flame的基本知识后,我们就可以开发一个小游戏来练练手了。

游戏介绍

就像之前风靡全球的Flappy Bird一样,我们可以开发一个Dart吉祥物Dash飞跃管道的小游戏。

项目介绍

此项目主要使用Bloc进行状态管理、Flame进行开发,主要使用如下组件:

  • BackgroundComponent 背景视差动画组件
  • PipelineComponent 管道精灵组件
  • PipelinePairComponent 管道对组件
  • GetScoreComponent 得分组件
  • DashComponent Dash精灵组件

在游戏未开始时,点击屏幕或者按下空格键,即可开始游戏,如果Dash成功从两个管道中间通过则会加1分,如果Dash碰撞到管道,或者超出屏幕,则会视为游戏结束,弹出游戏结束的Overlay,可以点击重新开始游戏。

背景视差动画

我们可以继承ParallaxComponent来实现背景移动的效果。

class BackgroundComponent extends ParallaxComponent<FlyBirdGame>
with FlameBlocReader<GameControlBloc, GameControlState> {
@override
Future<void> onLoad() async {
super.onLoad();
anchor = Anchor.center;
parallax = await game.loadParallax(
[
ParallaxImageData(Assets.images.background.layer1Sky.gamePath),
ParallaxImageData(Assets.images.background.layer2Clouds.gamePath),
ParallaxImageData(Assets.images.background.layer3Clouds.gamePath),
ParallaxImageData(Assets.images.background.layer4Clouds.gamePath),
ParallaxImageData(Assets.images.background.layer5HugeClouds.gamePath),
ParallaxImageData(Assets.images.background.layer6Bushes.gamePath),
ParallaxImageData(Assets.images.background.layer7Bushes.gamePath),
],
baseVelocity: Vector2(1, 0),
velocityMultiplierDelta: Vector2(1.8, 0),
);
}

@override
void update(double dt) {
if (bloc.state.status != GameStatus.gameOver) {
super.update(dt);
}
}
}

Dash跳转逻辑

Dash默认置于屏幕中间,如果游戏开始后给他提供持续一个默认向下位移的距离,如果用户点击屏幕则会将Dash的position减去jump的距离

class DashComponent extends SpriteComponent
with
FlameBlocReader<GameControlBloc, GameControlState>,
CollisionCallbacks,
HasGameReference {
DashComponent() : super(size: Vector2.all(80), anchor: Anchor.center);

///默认下拉的移动的距离
final Vector2 _gravity = Vector2(0, 1400);

///垂直方向的移动距离
Vector2 _vertical = Vector2(0, 0);

///跳转的距离
final Vector2 _jump = Vector2(0, -500);

@override
Future<void> onLoad() async {
super.onLoad();
sprite = await Sprite.load(Assets.images.dash.gamePath);
await add(
FlameBlocListener<DashControlBloc, DashControlState>(
onNewState: (state) {
if (state is DashControlJumpState) {
jump();
}
},
),
);
add(
FlameBlocListener<GameControlBloc, GameControlState>(
listenWhen: (previous, current) => current.status == GameStatus.none,
onNewState: (GameControlState state) {
if (state.status == GameStatus.none) {
position = Vector2.all(0);
_vertical = Vector2(0, 0);
}
},
),
);
//碰撞类型为主动
add(CircleHitbox()..collisionType = CollisionType.active);
}

@override
void update(double dt) {
if (bloc.state.status == GameStatus.playing) {
_vertical += _gravity * dt;
position += _vertical * dt;
if (position.y > (game.camera.viewport.size.y / 2 + size.y / 2)) {
bloc.add(GameControlStatusChangedEvent(status: GameStatus.gameOver));
}
}
}

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is PipelineComponent && bloc.state.status == GameStatus.playing) {
bloc.add(GameControlStatusChangedEvent(status: GameStatus.gameOver));
}
}

@override
void onCollisionEnd(PositionComponent other) {
if (other is GetScoreComponent) {
bloc.add(GameControlGetScoreEvent());
}
super.onCollisionEnd(other);
}

void jump() {
_vertical = _jump;
}
}

管道对

管道对会放置3个组件,分别为上管道、中间间隔(对用户不可见的得分组件)、下管道

class PipelinePairComponent extends PositionComponent
with FlameBlocListenable<GameControlBloc, GameControlState> {
///管道对中间的距离
final double gap;

///管道对向左移动的速度
final double speed;

PipelinePairComponent({this.gap = 300, this.speed = 100,super.position});

@override
FutureOr<void> onLoad() {
addAll([
PipelineComponent(position: Vector2(0, -gap / 2), isFlip: true),
PipelineComponent(position: Vector2(0, gap / 2)),
GetScoreComponent(gap: gap, position: Vector2(pipelineWidth/2, -gap / 2)),
]);
}

@override
void update(double dt) {
switch (bloc.state.status) {
case GameStatus.playing:
position.x += -speed * dt;
break;
default:
}
}
}

管道对的生成和移除

在根组件中,游戏开始后,当最后一个管道距离视角右侧距离大于distance/2 批量生成一部分管道对放置在后面,在一个管道移动到左侧的视角外后就会从组件中移除。

///批量构建管道对
///
/// - [count] 管道对数量
/// - [fromX] 管道对开始位置
/// - [distance] 管道对之间的距离
_buildPipes({int count = 5, double fromX = 0, double distance = 400}) {
for (var i = 0; i < count; i++) {
const area = 600;
final y = (_random.nextDouble() * area) - area / 2;
final pair = PipelinePairComponent(
position: Vector2(fromX + i * distance, y),
);
add(pair);
if (i == count - 1) {
_lastPipe = pair;
}
}
}

@override
void update(double dt) {
//无限生成
if (_lastPipe != null) {
//当最后一个管道距离右侧距离大于distance/2 批量生成一部分
if (game.camera.viewport.size.x / 2 - _lastPipe!.x > 400 / 2) {
_buildPipes(fromX: _lastPipe!.x + 400);
}
}
_removePipelinePair();
}

///移动到相机视角外部就移除管道对
_removePipelinePair() {
final child = children.whereType<PipelinePairComponent>();
for (var element in child) {
if (element.x < -game.camera.viewport.size.x / 2 - pipelineWidth / 2) {
remove(element);
}
}
}

得分组件

放置在管道对中间的一条线,对用户不可见,在与dash碰撞结束后,视为得分

class GetScoreComponent extends PositionComponent
with FlameBlocReader<GameControlBloc, GameControlState> {
final double gap;

GetScoreComponent({this.gap = 300, super.position});

@override
Future<void> onLoad() async {
super.onLoad();
size = Vector2(1, gap);

add(RectangleHitbox()..collisionType = CollisionType.passive);
}
}

如上,我们就可以用Flame来开发一款小游戏了,如果要了解其他逻辑可查看Github源码

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00