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
是为了后续的管道占位,bottomBgComponent
则是下面的平台。bgComponent
作为前景,缓慢移动,速度为Vector2(5, 0)
,bottomBgComponent
则是运用了规则的游戏速度Vector2(gameSpeed, 0)
,这是为了后续和管道保持同步的移动速度,终究会得到如下的效果
主角登场
接下来进行角色的制造,第一步咱们需求一个扑腾着翅膀的小鸟,运用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);
为了后续更好的进行磕碰检测,这儿运用了承继自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
规则了重力加速度的大小,_birdYVelocity
表明当时小鸟在Y轴上的速度,dt
则是模拟的时间距离,这段代码会在Flame引擎每次update时调用,继续更新小鸟的速度和方位。
然后就是游戏的操作核心了,点击屏幕小鸟会跳起,这一步十分简略,只需求将小鸟的Y轴速度忽然变大即可
@override
void onTap() {
super.onTap();
_birdYVelocity = -120;
}
在onTap
事件中,将_birdYVelocity
修改为-120,这样小鸟就会得到一个向上的速度,一起还会受到重力作用,发生一次小幅跳动。
最终看起来还缺陷什么,咱们的小鸟并没有视点改变,现在需求的是在小鸟坠落时鸟头朝下,反之鸟头朝上,完成也是很简略的,让视点跟从速度改变即可
_birdComponent.anchor = Anchor.center;
final angle = clampDouble(_birdYVelocity / 180, -pi * 0.25, pi * 0.25);
_birdComponent.angle = angle;
这儿将anchor设置为center,是为了在旋转时环绕小鸟的中心点,angle则运用clampDouble
进行了约束,不然你会得到一个疯狂旋转的小鸟
反派管道登场
管道的烘托
游戏选手已就位,该反派登场了,创立一个承继自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
是新增的属性,表明小鸟是否阵亡,如果磕碰到PipeComponent
,isDead
则被设置为true
。在游戏循环中,发现小鸟阵亡,则直接完毕游戏
@override
void update(double dt) {
super.update(dt);
...
if (_birdComponent.isDead) {
gameOver();
}
}
经过管道的奖励
如何断定小鸟正常经过了管道呢?有一个简略的办法就是在管道缺口添加一个通明的磕碰体,发生磕碰则移除去它,而且分数加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
上
添加一些音效
最终给游戏添加一些音效,咱们分别在点击,小鸟撞击,死亡,取得分数添加对应音效
@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… 能够获取完整代码,更多细节阅览代码就能够知道了哦~