Flame是一款根据Flutter的2D游戏引擎,今天我将运用它制造一款简略的小游戏Flappy Bird

Flutter Flame实战 - 制造一个Flappy Bird

为游戏添加背景

游戏的的背景分为2个部分,前景和近处的平台,咱们能够运用ParallaxComponent来进行展示

final bgComponent = await loadParallaxComponent(
    [ParallaxImageData("background-day.png")],
    baseVelocity: Vector2(5, 0), images: images);
add(bgComponent);
_pipeLayer = PositionComponent();
add(_pipeLayer);
final bottomBgComponent = await loadParallaxComponent(
    [ParallaxImageData("base.png")],
    baseVelocity: Vector2(gameSpeed, 0),
    images: images,
    alignment: Alignment.bottomLeft,
    repeat: ImageRepeat.repeatX,
    fill: LayerFill.none);
add(bottomBgComponent);

第一个bgComponent为前景,中间的_pipeLayer是为了后续的管道占位,bottomBgComponent 则是下面的平台。bgComponent作为前景,缓慢移动,速度为Vector2(5, 0)bottomBgComponent则是运用了规则的游戏速度Vector2(gameSpeed, 0),这是为了后续和管道保持同步的移动速度,终究会得到如下的效果

Flutter Flame实战 - 制造一个Flappy Bird

主角登场

接下来进行角色的制造,第一步咱们需求一个扑腾着翅膀的小鸟,运用SpriteAnimationComponent能够很方便的得到它

List<Sprite> redBirdSprites = [
  await Sprite.load("redbird-downflap.png", images: images),
  await Sprite.load("redbird-midflap.png", images: images),
  await Sprite.load("redbird-upflap.png", images: images)
];
final anim = SpriteAnimation.spriteList(redBirdSprites, stepTime: 0.2);
_birdComponent = Player(animation: anim);
add(_birdComponent);

为了后续更好的进行磕碰检测,这儿运用了承继自SpriteAnimationComponentPlayer

class Player extends SpriteAnimationComponent with CollisionCallbacks {
  Player({super.animation});
  @override
  FutureOr<void> onLoad() {
    add(RectangleHitbox(size: size));
    return super.onLoad();
  }
}

PlayeronLoad中为自己添加了一个矩形磕碰框

Flutter Flame实战 - 制造一个Flappy Bird

玩过游戏的都知道,正常情况下小鸟是自由下落的,要做到这一点只需求简略的重力模拟

_birdYVelocity += dt * _gravity;
final birdNewY = _birdComponent.position.y + _birdYVelocity * dt;
_birdComponent.position = Vector2(_birdComponent.position.x, birdNewY);

_gravity规则了重力加速度的大小,_birdYVelocity表明当时小鸟在Y轴上的速度,dt则是模拟的时间距离,这段代码会在Flame引擎每次update时调用,继续更新小鸟的速度和方位。

Flutter Flame实战 - 制造一个Flappy Bird

然后就是游戏的操作核心了,点击屏幕小鸟会跳起,这一步十分简略,只需求将小鸟的Y轴速度忽然变大即可

@override
void onTap() {
    super.onTap();
    _birdYVelocity = -120;
}

onTap事件中,将_birdYVelocity修改为-120,这样小鸟就会得到一个向上的速度,一起还会受到重力作用,发生一次小幅跳动。

Flutter Flame实战 - 制造一个Flappy Bird

最终看起来还缺陷什么,咱们的小鸟并没有视点改变,现在需求的是在小鸟坠落时鸟头朝下,反之鸟头朝上,完成也是很简略的,让视点跟从速度改变即可

_birdComponent.anchor = Anchor.center;
final angle = clampDouble(_birdYVelocity / 180, -pi * 0.25, pi * 0.25);
_birdComponent.angle = angle;

这儿将anchor设置为center,是为了在旋转时环绕小鸟的中心点,angle则运用clampDouble进行了约束,不然你会得到一个疯狂旋转的小鸟

Flutter Flame实战 - 制造一个Flappy Bird

反派管道登场

管道的烘托

游戏选手已就位,该反派登场了,创立一个承继自PositionComponent的管道组件PipeComponent

class PipeComponent extends PositionComponent with CollisionCallbacks {
  final bool isUpsideDown;
  final Images? images;
  PipeComponent({this.isUpsideDown = false, this.images, super.size});
  @override
  FutureOr<void> onLoad() async {
    final nineBox = NineTileBox(
        await Sprite.load("pipe-green.png", images: images))
      ..setGrid(leftWidth: 10, rightWidth: 10, topHeight: 60, bottomHeight: 60);
    final spriteCom = NineTileBoxComponent(nineTileBox: nineBox, size: size);
    if (isUpsideDown) {
      spriteCom.flipVerticallyAroundCenter();
    }
    spriteCom.anchor = Anchor.topLeft;
    add(spriteCom);
    add(RectangleHitbox(size: size));
    return super.onLoad();
  }
}

由于游戏资料图片管道长度有限,这儿运用了NineTileBoxComponent而不是SpriteComponent来进行管道的展示,NineTileBoxComponent能够让管道无限长而不拉伸。为了让管道能够在顶部,经过flipVerticallyAroundCenter来对顶部管道进行翻转,最终和Player一样,添加一个矩形磕碰框RectangleHitbox

管道的创立

每一组管道包含顶部和底部两个,首要随机出来缺口的方位

const pipeSpace = 220.0; // the space of two pipe group
const minPipeHeight = 120.0; // pipe min height
const gapHeight = 90.0; // the gap length of two pipe 
const baseHeight = 112.0; // the bottom platform height
const gapMaxRandomRange = 300; // gap position max random range
final gapCenterPos = min(gapMaxRandomRange,
            size.y - minPipeHeight * 2 - baseHeight - gapHeight) *
        Random().nextDouble() +
    minPipeHeight +
    gapHeight * 0.5;

经过pipe的最小高度,缺口的高度,底部平台的高度能够核算出缺口方位随机的规模,一起经过gapMaxRandomRange约束随机的规模上限,防止缺口方位改变的太离谱。接下来经过缺口方位核算管道的方位,并创立出对应的管道

PipeComponent topPipe =
    PipeComponent(images: images, isUpsideDown: true, size: pipeFullSize)
      ..position = Vector2(
          lastPipePos, (gapCenterPos - gapHeight * 0.5) - pipeFullSize.y);
_pipeLayer.add(topPipe);
_pipes.add(topPipe);
PipeComponent bottomPipe =
    PipeComponent(images: images, isUpsideDown: false, size: pipeFullSize)
      ..size = pipeFullSize
      ..position = Vector2(lastPipePos, gapCenterPos + gapHeight * 0.5);
_pipeLayer.add(bottomPipe);
_pipes.add(bottomPipe);

lastPipePos是管道的x坐标方位,经过最终一个管道x坐标方位(不存在则为屏幕宽度)加上pipeSpace核算可得

var lastPipePos = _pipes.lastOrNull?.position.x ?? size.x - pipeSpace;
lastPipePos += pipeSpace;

管道的更新

管道需求依照规则的速度向左匀速移动,完成起来很简略

updatePipes(double dt) {
    for (final pipe in _pipes) {
      pipe.position =
          Vector2(pipe.position.x - dt * gameSpeed, pipe.position.y);
    }
}

不过除此之外还有些杂事需求处理,比方脱离屏暗地自动毁掉

_pipes.removeWhere((element) {
  final remove = element.position.x < -100;
  if (remove) {
    element.removeFromParent();
  }
  return remove;
});

最终一个管道呈现后需求创立下一个

if ((_pipes.lastOrNull?.position.x ?? 0) < size.x) {
  createPipe();
}

管道的磕碰检测

最终需求让管道发挥他的反派作用了,如果小鸟碰到管道,需求让游戏当即完毕,在Player的磕碰回调中,进行如下判断

@override
void onCollisionStart(
  Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PipeComponent) {
      isDead = true;
    }
}

isDead是新增的属性,表明小鸟是否阵亡,如果磕碰到PipeComponentisDead则被设置为true。在游戏循环中,发现小鸟阵亡,则直接完毕游戏

@override
void update(double dt) {
    super.update(dt);
    ...
    if (_birdComponent.isDead) {
      gameOver();
    }
}

Flutter Flame实战 - 制造一个Flappy Bird

经过管道的奖励

如何断定小鸟正常经过了管道呢?有一个简略的办法就是在管道缺口添加一个通明的磕碰体,发生磕碰则移除去它,而且分数加1,新建一个BonusZone组件来做这件工作

class BonusZone extends PositionComponent with CollisionCallbacks {
  BonusZone({super.size});
  @override
  FutureOr<void> onLoad() {
    add(RectangleHitbox(size: size));
    return super.onLoad();
  }
  @override
  void onCollisionEnd(PositionComponent other) {
    super.onCollisionEnd(other);
    if (other is Player) {
      other.score++;
      removeFromParent();
    }
  }
}

onLoad中为自己添加磕碰框,与Player磕碰完毕时,移除本身,而且给Player分数加1。BonusZone需求被放置在缺口处,代码如下

..
PipeComponent bottomPipe =
    PipeComponent(images: images, isUpsideDown: false, size: pipeFullSize)
      ..size = pipeFullSize
      ..position = Vector2(lastPipePos, gapCenterPos + gapHeight * 0.5);
_pipeLayer.add(bottomPipe);
_pipes.add(bottomPipe);
final bonusZone = BonusZone(size: Vector2(pipeFullSize.x, gapHeight))
  ..position = Vector2(lastPipePos, gapCenterPos - gapHeight * 0.5);
add(bonusZone);
_bonusZones.add(bonusZone);
...

显现当时的分数

游戏资料中每一个数字是一张图片,也就是说需求将不同数字的图片组合起来显现,咱们能够运用ImageComposition来进行图片的拼接

final scoreStr = _birdComponent.score.toString();
final numCount = scoreStr.length;
double offset = 0;
final imgComposition = ImageComposition();
for (int i = 0; i < numCount; ++i) {
  int num = int.parse(scoreStr[i]);
  imgComposition.add(
      _numSprites[num], Vector2(offset, _numSprites[num].size.y));
  offset += _numSprites[num].size.x;
}
final img = await imgComposition.compose();
_scoreComponent.sprite = Sprite(img);

_numSprites是加载好的数字图片列表,索引则代表其显现的数字,从数字最高位开始拼接出一个新图片,最终显现在_scoreComponent

Flutter Flame实战 - 制造一个Flappy Bird

添加一些音效

最终给游戏添加一些音效,咱们分别在点击,小鸟撞击,死亡,取得分数添加对应音效

@override
void onTap() {
    super.onTap();
    FlameAudio.play("swoosh.wav");
    _birdYVelocity = -120;
}
![image](https://note.youdao.com/yws/res/1/WEBRESOURCE136045f72f1f0dc0fdaef9919b55d3f1)
...
@override
void onCollisionStart(
  Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PipeComponent) {
      FlameAudio.play("hit.wav");
      isDead = true;
    }
}
...
@override
void update(double dt) {
    super.update(dt);
    updateBird(dt);
    updatePipes(dt);
    updateScoreLabel();
    if (_birdComponent.isDead) {
      FlameAudio.play("die.wav");
      gameOver();
    }
}
...
@override
void onCollisionEnd(PositionComponent other) {
    super.onCollisionEnd(other);
    if (other is Player) {
      other.score++;
      removeFromParent();
      FlameAudio.play("point.wav");
    }
}

接下来…

访问 github.com/BuildMyGame… 能够获取完整代码,更多细节阅览代码就能够知道了哦~