【基于Flutter&Flame 的飞机大战开发笔记】重构敌机


theme: cyanosis

highlight: xcode

前言

Component实现碰撞检测,添加了碰撞的反馈效果后,整个效果就暂时闭环了。本文会着重敌机Component的重构工作。借此机会将其余类型的敌机Component添加进来

抽象

之前的类Enemy1是一种类型的敌机Component,它具备了敌机在飞机大战中的基本功能:

  • 无碰撞情况下,从屏幕最上方匀速移动到屏幕最下方,最终从Component树中移除。
  • 具备碰撞检测的能力,与战机Component/子弹Component发生碰撞时,会有生命值减少情况。
  • 生命值为0时,产生销毁/击毁效果。

其实这里还缺了一个与战机Component/子弹Component发生碰撞时,减少生命值的效果。结合上述几点,我们需要添加多种敌机Component到屏幕上。所以需要将敌机Component的特性进一步抽象出来。

结合上述的特性,定义了抽象类Enemy

SpriteAnimationComponent

先来说说不同状态的动画帧播放方案。前文我们利用SpriteAnimationComponent的播放能力,在敌机Component被击毁时设置playing = true。但此时敌机Component至少有3种状态,分别是正常、被攻击、销毁,可以定义一个枚举

  1. enum EnemyState {
  2. idle,
  3. hit,
  4. down,
  5. }

这里可以用SpriteAnimationGroupComponent代替SpriteAnimationComponent,通过设置参数current来切换当前的状态,从而切换对应的动画效果。大概瞄一下源码吧

  1. // sprite_animation_group_component.dart
  2. class SpriteAnimationGroupComponent<T> extends PositionComponent
  3. with HasPaint
  4. implements SizeProvider {
  5. /// Key with the current playing animation
  6. T? current;
  7. /// Map with the mapping each state to the flag removeOnFinish
  8. final Map<T, bool> removeOnFinish;
  9. /// Map with the available states for this animation group
  10. Map<T, SpriteAnimation>? animations;

这里的范型T可以设置成上面定义的EnemyStateanimations不同状态对应的SpriteAnimation。还有一个removeOnFinish,可以理解是哪个状态播放完成后,Component自动移除

  1. // sprite_animation_group_component.dart
  2. SpriteAnimation? get animation => animations?[current];
  3. @mustCallSuper
  4. @override
  5. void render(Canvas canvas) {
  6. animation?.getSprite().render(
  7. canvas,
  8. size: size,
  9. overridePaint: paint,
  10. );
  11. }
  12. @mustCallSuper
  13. @override
  14. void update(double dt) {
  15. animation?.update(dt);
  16. if ((removeOnFinish[current] ?? false) && (animation?.done() ?? false)) {
  17. removeFromParent();
  18. }
  19. }

render方法会获取对应状态的SpriteAnimation来渲染update检测动画是否完成,该状态是否需要自动移除。ps:render方法为每帧绘制的回调。

状态应用

构造方法中,初始的状态为idle,设置down状态播放完成后自动移除

  1. // class Enemy
  2. Enemy(
  3. {required Vector2 initPosition,
  4. required Vector2 size,
  5. required this.life,
  6. required this.speed})
  7. : super(
  8. position: initPosition,
  9. size: size,
  10. current: EnemyState.idle,
  11. removeOnFinish: {EnemyState.down: true}) {
  12. animations = <EnemyState, SpriteAnimation>{};
  13. }

定义三个抽象方法,用于加载不同状态下的SpriteAnimation,由于hit状态并非所有敌机Component都有,所以这里定义为可空。

  1. // class Enemy
  2. Future<SpriteAnimation> idleSpriteAnimation();
  3. Future<SpriteAnimation?> hitSpriteAnimation();
  4. Future<SpriteAnimation> downSpriteAnimation();

onLoad中加载。注意这里hit状态播放完成后,需要将状态重置到idle状态

  1. // abstract class Enemy
  2. @override
  3. Future<void> onLoad() async {
  4. animations?[EnemyState.idle] = await idleSpriteAnimation();
  5. final hit = await hitSpriteAnimation();
  6. hit?.onComplete = () {
  7. _enemyState = EnemyState.idle;
  8. };
  9. if (hit != null) animations?[EnemyState.hit] = hit;
  10. animations?[EnemyState.down] = await downSpriteAnimation();
  11. 。。。

在碰撞检测中

  • 如果已经是down状态了,就无需触发等待动画播放完自动移除。
  • 如果碰撞目标为Player/Bullect1,则需要处理,生命值未到达0前状态更改为hit,否则为downhit播放完需要变更回idle,与上述逻辑对应上。
    1. // class Enemy
    2. @override
    3. void onCollisionStart(
    4. Set<Vector2> intersectionPoints, PositionComponent other) {
    5. super.onCollisionStart(intersectionPoints, other);
    6. if (current == EnemyState.down) return;
    7. if (other is Player || other is Bullet1) {
    8. if (current == EnemyState.idle) {
    9. if (life > 1) {
    10. _enemyState = EnemyState.hit;
    11. life--;
    12. } else {
    13. _enemyState = EnemyState.down;
    14. life = 0;
    15. }
    16. 。。。

状态变成前,需要重置将要变更状态的SpriteAnimation。这是为了保证每次变更都是从第一帧开始,不会造成画面异常。

  1. // class Enemy
  2. set _enemyState(EnemyState state) {
  3. if (state == EnemyState.hit) {
  4. animations?[state]?.reset();
  5. }
  6. current = state;
  7. }

Component的移动

之前是通过s = v * t,在update方法回调中更新position的方式实现移动的。这里改成使用MoveEffect实现

  1. // class Enemy
  2. add(MoveEffect.to(
  3. Vector2(position.x, gameRef.size.y), EffectController(speed: speed),
  4. onComplete: () {
  5. removeFromParent();
  6. }));

传入speed,会使用SpeedEffectController,默认是线性移动的。

  1. // effect_contorller.dart
  2. final isLinear = curve == Curves.linear;
  3. if (isLinear) {
  4. items.add(
  5. duration != null
  6. ? LinearEffectController(duration)
  7. : SpeedEffectController(LinearEffectController(0), speed: speed!),
  8. );
  9. }

新一代敌机Component

简单说一下重构后的敌机Component,这里以第二个类型类Enemy2为例,因为它的生命值高可以触发hit状态。

  1. // class Enemy2
  2. @override
  3. Future<SpriteAnimation?> hitSpriteAnimation() async {
  4. List<Sprite> sprites = [];
  5. sprites.add(await Sprite.load(enemy/enemy2_hit.png));
  6. sprites.add(await Sprite.load(enemy/enemy2.png));
  7. final spriteAnimation =
  8. SpriteAnimation.spriteList(sprites, stepTime: 0.15, loop: false);
  9. return spriteAnimation;
  10. }
  11. @override
  12. RectangleHitbox rectangleHitbox() {
  13. return RectangleHitbox(
  14. size: Vector2(size.x, size.y * 0.9), position: Vector2(0, 0));
  15. }
  • 以重写hit状态的SpriteAnimation加载为例,这里有一帧的被击中效果。
  • 还需要输出一个RectangleHitbox,由于不同素材的尺寸有误差,所以这里单独作碰撞箱的修正。

大部分逻辑都在父类Enemy实现,这里基本只需要实现抽象方法即可。

敌机生成器适配

还记得之前有一个敌机生成器EnemyCreator,用于定时创建敌机Component吗?由于添加了不同类型的敌机,所以它的定时触发方法_createEnemy要作相应修改。在此之前,我们需要定义每款敌机的属性,1、2、3分别代表了类Enemy1、Enemy2、Enemy3,即小中大类型。属性值就见文知意吧。

  1. // class EnemyCreator
  2. final enemyAttrMapping = {
  3. 1: EnemyAttr(size: Vector2(45, 45), life: 1, speed: 50.0),
  4. 2: EnemyAttr(size: Vector2(50, 60), life: 2, speed: 30.0),
  5. 3: EnemyAttr(size: Vector2(100, 150), life: 4, speed: 20.0)
  6. };

_createEnemy中,我们通过区间控制每个类型的生成概率

  1. void _createEnemy() {
  2. final width = gameRef.size.x;
  3. double x = _random.nextDouble() * width;
  4. final double random = _random.nextDouble();
  5. final EnemyAttr attr;
  6. final Enemy enemy;
  7. if (random < 0.5) {
  8. // load Enemy1
  9. } else if (random >= 0.5 && random < 0.8) {
  10. // load Enemy2
  11. } else {
  12. // load Enemy3
  13. }
  14. add(enemy);
  15. }

至此,敌机Component的重构就告一段落了,后续还会有一些小改动。先来看看目前的效果吧

Record_2022-07-11-16-39-13_13914082904e1b7ce2b619733dc8fcfe_.gif

总结

敌机Component的重构就完成了,定时生成的规则可能有点粗糙,这个后续可能会考虑优化。关于敌机的属性,目前是写死的,后续可以考虑做成本地配置。


文章标签:

原文连接:https://juejin.cn/post/7119038284608569352

相关推荐

[教你做小游戏] 只用几行原生JS,写一个函数,播放音效、播放BGM、切换BGM

我们用48h,合作创造了一款Web游戏:Dice Crush,参加国际赛事

【小程序】快来开发你的第一个微信小游戏(详细流程)

还记得当年的超级玛丽么?来吧,动手设计一款小霸王游戏机

Unity中文版?脚本也用中文?用中文写了个剧情小游戏,真好玩~

网易游戏 Flink SQL 平台化实践

初学JS—JavaScript实现像素鸟小游戏

从零开始完整开发基于websocket的在线对弈游戏【五子棋】,只用几十行代码完成全部逻辑。

关于我仿做了个steam很火的《Helltaker》游戏

Unity制作 小球吃金币 游戏

NC20566 [SCOI2010]游戏

生命游戏(信息学堂2022)

[教你做小游戏] 展示斗地主扑克牌,支持按出牌规则排序!支持按大小排序!

【基于Flutter&Flame 的飞机大战开发笔记】展示面板及重新开始菜单

【基于Flutter&Flame 的飞机大战开发笔记】利用bloc管理游戏状态

软件设计实战:基于Java的俄罗斯方块游戏【完整版】

【基于Flutter&Flame 的飞机大战开发笔记】子弹升级和补给

【基于Flutter&Flame 的飞机大战开发笔记】重构敌机

Python做游戏很难吗—来看看我做的多有意思~

LeetCode-消除游戏