Flame是一款基于Flutter的2D游戲引擎,今天我將使用它制作一款簡單的小游戲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
是為了后續(xù)的管道占位束莫,bottomBgComponent
則是下面的平臺。bgComponent
作為遠景御吞,緩慢移動麦箍,速度為Vector2(5, 0)
,bottomBgComponent
則是使用了規(guī)定的游戲速度Vector2(gameSpeed, 0)
陶珠,這是為了后續(xù)和管道保持同步的移動速度挟裂,最終會得到如下的效果
主角登場
接下來進行角色的制作,第一步我們需要一個撲騰著翅膀的小鳥揍诽,使用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ù)更好的進行碰撞檢測诀蓉,這里使用了繼承自SpriteAnimationComponent
的Player
class Player extends SpriteAnimationComponent with CollisionCallbacks {
Player({super.animation});
@override
FutureOr<void> onLoad() {
add(RectangleHitbox(size: size));
return super.onLoad();
}
}
Player
在onLoad
中為自己增加了一個矩形碰撞框
玩過游戲的都知道,正常情況下小鳥是自由下落的暑脆,要做到這一點只需要簡單的重力模擬
_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ù)更新小鳥的速度和位置。
然后就是游戲的操作核心了碟联,點擊屏幕小鳥會跳起妓美,這一步非常簡單,只需要將小鳥的Y軸速度突然變大即可
@override
void onTap() {
super.onTap();
_birdYVelocity = -120;
}
在onTap
事件中鲤孵,將_birdYVelocity
修改為-120壶栋,這樣小鳥就會得到一個向上的速度,同時還會受到重力作用普监,產(chǎn)生一次小幅跳躍贵试。
最后看起來還缺點什么琉兜,我們的小鳥并沒有角度變化,現(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
進行了限制,否則你會得到一個瘋狂旋轉的小鳥
反派管道登場
管道的渲染
游戲選手已就位施符,該反派登場了,創(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
是新增的屬性钝鸽,表示小鳥是否陣亡汇恤,如果碰撞到PipeComponent
,isDead
則被設置為true
寞埠。在游戲循環(huán)中屁置,發(fā)現(xiàn)小鳥陣亡,則直接結束游戲
@override
void update(double dt) {
super.update(dt);
...
if (_birdComponent.isDead) {
gameOver();
}
}
通過管道的獎勵
如何判定小鳥正常通過了管道呢仁连?有一個簡單的方法就是在管道缺口增加一個透明的碰撞體蓝角,發(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
上
添加一些音效
最后給游戲增加一些音效拓挥,我們分別在點擊,小鳥撞擊袋励,死亡侥啤,獲得分數(shù)增加對應音效
@override
void onTap() {
super.onTap();
FlameAudio.play("swoosh.wav");
_birdYVelocity = -120;
}

...
@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é)閱讀代碼就可以知道了哦~