Flutter Flame實戰(zhàn) - 制作一個Flappy Bird

Flame是一款基于Flutter的2D游戲引擎,今天我將使用它制作一款簡單的小游戲Flappy Bird

flappy_bird_preview.gif

為游戲添加背景

游戲的的背景分為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是為了后續(xù)的管道占位束莫,bottomBgComponent 則是下面的平臺。bgComponent作為遠景御吞,緩慢移動麦箍,速度為Vector2(5, 0)bottomBgComponent則是使用了規(guī)定的游戲速度Vector2(gameSpeed, 0)陶珠,這是為了后續(xù)和管道保持同步的移動速度挟裂,最終會得到如下的效果

flappy_bird_bg.gif

主角登場

接下來進行角色的制作,第一步我們需要一個撲騰著翅膀的小鳥揍诽,使用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);

為了后續(xù)更好的進行碰撞檢測诀蓉,這里使用了繼承自SpriteAnimationComponentPlayer

class Player extends SpriteAnimationComponent with CollisionCallbacks {
  Player({super.animation});

  @override
  FutureOr<void> onLoad() {
    add(RectangleHitbox(size: size));
    return super.onLoad();
  }
}

PlayeronLoad中為自己增加了一個矩形碰撞框

flappy_bird_bird_anim.gif

玩過游戲的都知道,正常情況下小鳥是自由下落的暑脆,要做到這一點只需要簡單的重力模擬

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

_gravity規(guī)定了重力加速度的大小渠啤,_birdYVelocity表示當前小鳥在Y軸上的速度,dt則是模擬的時間間隔添吗,這段代碼會在Flame引擎每次update時調用沥曹,持續(xù)更新小鳥的速度和位置。

flappy_bird_bird_gravity.gif

然后就是游戲的操作核心了碟联,點擊屏幕小鳥會跳起妓美,這一步非常簡單,只需要將小鳥的Y軸速度突然變大即可

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

onTap事件中鲤孵,將_birdYVelocity修改為-120壶栋,這樣小鳥就會得到一個向上的速度,同時還會受到重力作用普监,產(chǎn)生一次小幅跳躍贵试。

flappy_bird_bird_jump.gif

最后看起來還缺點什么琉兜,我們的小鳥并沒有角度變化,現(xiàn)在需要的是在小鳥墜落時鳥頭朝下毙玻,反之鳥頭朝上豌蟋,實現(xiàn)也是很簡單的,讓角度跟隨速度變化即可

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

這里將anchor設置為center淆珊,是為了在旋轉時圍繞小鳥的中心點夺饲,angle則使用clampDouble進行了限制,否則你會得到一個瘋狂旋轉的小鳥

flappy_bird_bird_jump_full.gif

反派管道登場

管道的渲染

游戲選手已就位施符,該反派登場了,創(chuàng)建一個繼承自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塘雳。

管道的創(chuàng)建

每一組管道包含頂部和底部兩個陆盘,首先隨機出來缺口的位置

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限制隨機的范圍上限,避免缺口位置變化的太離譜妻顶。接下來通過缺口位置計算管道的位置酸员,并創(chuàng)建出對應的管道

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;

管道的更新

管道需要按照規(guī)定的速度向左勻速移動讳嘱,實現(xiàn)起來很簡單

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;
});

最后一個管道出現(xiàn)后需要創(chuàng)建下一個

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

管道的碰撞檢測

最后需要讓管道發(fā)揮他的反派作用了,如果小鳥碰到管道沥潭,需要讓游戲立即結束邀泉,在Player的碰撞回調中,進行如下判斷

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

isDead是新增的屬性钝鸽,表示小鳥是否陣亡汇恤,如果碰撞到PipeComponentisDead則被設置為true寞埠。在游戲循環(huán)中屁置,發(fā)現(xiàn)小鳥陣亡,則直接結束游戲

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

通過管道的獎勵

如何判定小鳥正常通過了管道呢仁连?有一個簡單的方法就是在管道缺口增加一個透明的碰撞體蓝角,發(fā)生碰撞則移除掉它阱穗,并且分數(shù)加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分數(shù)加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);

...

顯示當前的分數(shù)

游戲素材中每一個數(shù)字是一張圖片,也就是說需要將不同數(shù)字的圖片組合起來顯示裁厅,我們可以使用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是加載好的數(shù)字圖片列表冰沙,索引則代表其顯示的數(shù)字,從數(shù)字最高位開始拼接出一個新圖片执虹,最后顯示在_scoreComponent

flappy_bird_num.gif

添加一些音效

最后給游戲增加一些音效拓挥,我們分別在點擊,小鳥撞擊袋励,死亡侥啤,獲得分數(shù)增加對應音效

@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");
    }
}

接下來...

訪問 https://github.com/BuildMyGame/FlutterFlameGames 可以獲取完整代碼,更多細節(jié)閱讀代碼就可以知道了哦~

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末茬故,一起剝皮案震驚了整個濱河市盖灸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌磺芭,老刑警劉巖赁炎,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異徘跪,居然都是意外死亡甘邀,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門垮庐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來松邪,“玉大人,你說我怎么就攤上這事哨查《阂郑” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵寒亥,是天一觀的道長邮府。 經(jīng)常有香客問我,道長溉奕,這世上最難降的妖魔是什么褂傀? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮加勤,結果婚禮上仙辟,老公的妹妹穿的比我還像新娘同波。我一直安慰自己,他們只是感情好叠国,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布未檩。 她就那樣靜靜地躺著,像睡著了一般粟焊。 火紅的嫁衣襯著肌膚如雪冤狡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天项棠,我揣著相機與錄音悲雳,去河邊找鬼。 笑死香追,一個胖子當著我的面吹牛怜奖,可吹牛的內容都是我干的。 我是一名探鬼主播翅阵,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼迁央!你這毒婦竟也來了掷匠?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤岖圈,失蹤者是張志新(化名)和其女友劉穎讹语,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜂科,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡顽决,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了导匣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片才菠。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖贡定,靈堂內的尸體忽然破棺而出赋访,到底是詐尸還是另有隱情,我是刑警寧澤缓待,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布蚓耽,位于F島的核電站,受9級特大地震影響旋炒,放射性物質發(fā)生泄漏步悠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一瘫镇、第九天 我趴在偏房一處隱蔽的房頂上張望鼎兽。 院中可真熱鬧答姥,春花似錦、人聲如沸接奈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽序宦。三九已至睁壁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間互捌,已是汗流浹背潘明。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留秕噪,地道東北人钳降。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像腌巾,于是被迫代替她去往敵國和親遂填。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內容