前言
最近网上冲浪的时候,我偶然发现了一个国外的游戏网站,里面聚集了各种有趣的小游戏,类似于国内的4399。在浏览时,我遇到了一款经典的小游戏:Doodle Jump。上一次玩还是在上小学的时候,那时候父母在厨房做饭,我就偷摸拿他们的手机玩...回到正题,我猜很多人小时候都有着做一款游戏的想法,那今天就让我用flutter带大家一起实现下这款童年游戏经典!
仓库地址:https://github.com/taxze6/flutter_game_collection/tree/main/flutter_dash_doodle_jump
游戏素材地址:https://i.imgur.com
游戏演示
演示的GIF两倍速加速了。
导入Flame
这次和之前游戏编写的文章有所不同,使用上了flame。很多游戏通用逻辑都不用在自己写了。
flame: 1.7.3
游戏主体框架
分为三块来进行游戏的开发:
- 游戏页面
- 开局菜单页面
- 游戏结束页面
Scaffold( body: Center( child: GameWidget( game: game, overlayBuilderMap: <String, Widget Function(BuildContext, Game)>{ "gameOverlay": (context, game) => GameOverlay(game: game), "mainMenuOverlay": (context, game) => MainMenuOverlay(game: game), "gameOverOverlay": (context, game) => GameOverOverlay(game: game), }, ), ), );
游戏状态控制器
结合游戏主体框架,分为三个状态:1.菜单页面 2. 游戏中 3.游戏结束
enum GameState { menu, playing, gameOver }
再额外记录一个游戏分数。我们的游戏状态控制器就成型啦!
class GameManager extends Component with HasGameRef<FlutterDashDoodleJump> { GameManager(); ValueNotifier<int> score = ValueNotifier(0); GameState gameState = GameState.menu; bool get isPlaying => gameState == GameState.playing; bool get isGameOver => gameState == GameState.gameOver; bool get isMenu => gameState == GameState.menu; ///重置、 初始化 void reset() { score.value = 0; gameState = GameState.menu; } ///增加分数 void increaseScore() { score.value++; } }
游戏难度控制器
大部分“跑酷类”游戏都会随着游戏进行的时间/获得的分数来增加难度,增加趣味,那么让我们也来实现一个游戏难度控制器。
class Difficulty { final double minDistance; final double maxDistance; final double jumpSpeed; final int score; const Difficulty({ required this.minDistance, required this.maxDistance, required this.jumpSpeed, required this.score, }); } class LevelManager extends Component with HasGameRef<FlutterDashDoodleJump> { LevelManager({this.selectedLevel = 1, this.level = 1}); //玩家在一开始选择的难度 int selectedLevel; //游戏中的难度 int level; //不同难度的配置,当达到对应分数,则启用对应难度,玩家跳跃的高度也要相对应增加 final Map<int, Difficulty> levelsConfig = { 1: const Difficulty( minDistance: 200, maxDistance: 300, jumpSpeed: 600, score: 0), 2: const Difficulty( minDistance: 200, maxDistance: 400, jumpSpeed: 650, score: 20), 3: const Difficulty( minDistance: 200, maxDistance: 500, jumpSpeed: 700, score: 40), 4: const Difficulty( minDistance: 200, maxDistance: 600, jumpSpeed: 750, score: 80), 5: const Difficulty( minDistance: 200, maxDistance: 700, jumpSpeed: 800, score: 100), }; double get minDistance { return levelsConfig[level]!.minDistance; } double get maxDistance { return levelsConfig[level]!.maxDistance; } double get jumpSpeed { return levelsConfig[level]!.jumpSpeed; } Difficulty get difficulty { return levelsConfig[level]!; } ///判断是否还能加大难度(最高5级) bool shouldLevelUp(int score) { int nextLevel = level + 1; if (levelsConfig.containsKey(nextLevel)) { return levelsConfig[nextLevel]!.score == score; } return false; } List<int> get levels { return levelsConfig.keys.toList(); } ///难度增加 void increaseLevel() { if (level < levelsConfig.keys.length) { level++; } } /// 开始游戏前,设置难度 void setLevel(int newLevel) { if (levelsConfig.containsKey(newLevel)) { level = newLevel; } } void selectLevel(int selectLevel) { if (levelsConfig.containsKey(selectLevel)) { selectedLevel = selectLevel; } } ///重置难度为刚开始的难度 void reset() { level = selectedLevel; } }
⚙玩家、平台、道具、敌人、游戏背景的定义
有了游戏状态和游戏难度的控制器,我们还需要对游戏中随机生成的平台、道具进行控制。那么先让我们来定义对应的信息。
定义平台
首先,定义平台通用的抽象类。
abstract class Platform<T> extends SpriteGroupComponent<T> with HasGameRef<FlutterDashDoodleJump>, CollisionCallbacks { //碰撞监测 final hitBox = RectangleHitbox(); //是否移动 bool isMoving = false; Platform({ super.position, }) : super( size: Vector2.all(100), // 确保平台始终在Dash的后面 priority: 2, ); @override Future<void> onLoad() async { await super.onLoad(); // 添加碰撞监测 await add(hitBox); // 这个平台是否会移动(大于80,代表20%的概率移动) final int rand = Random().nextInt(100); if (rand > 80) isMoving = true; } double direction = 1; final Vector2 velocity = Vector2.zero(); //移动速度 double speed = 35; void move(double deltaTime) { if (!isMoving) return; //获取游戏场景的宽度 final double gameWidth = gameRef.size.x; // 根据平台的位置来确定移动的方向。 // 如果平台的 x 坐标小于等于 0,说明平台到达了左边界,将 direction 设置为 1,表示向右移动; // 如果平台的 x 坐标大于等于游戏宽度减去平台宽度,说明平台到达了右边界,将 direction 设置为 -1,表示向左移动。 if (position.x <= 0) { direction = 1; } else if (position.x >= gameWidth - size.x) { direction = -1; } velocity.x = direction * speed; position += velocity * deltaTime; } //这个注解是 Dart 语言中的一个元数据注解(metadata annotation),用于标记方法,表示子类在覆盖这个方法时必须调用父类的同名方法。 //在这里,@mustCallSuper 注解告诉子类在覆盖 update 方法时必须调用父类的 update 方法。 @mustCallSuper @override void update(double dt) { move(dt); //确保调用父类的 update 方法 super.update(dt); } }
定义普通的平台方块
///默认的普通平台(方块) enum NormalPlatformState { only } class NormalPlatform extends Platform<NormalPlatformState> { NormalPlatform({super.position}); final Map<String, Vector2> spriteOptions = { 'platform1': Vector2(106, 52), 'platform2': Vector2(106, 52), 'platform3': Vector2(106, 52), 'platform4': Vector2(106, 52), }; @override Future<void> onLoad() async { var randSpriteIndex = Random().nextInt(spriteOptions.length); String randSprite = spriteOptions.keys.elementAt(randSpriteIndex); sprites = { NormalPlatformState.only: await gameRef.loadSprite('game/$randSprite.png') }; current = NormalPlatformState.only; size = spriteOptions[randSprite]!; await super.onLoad(); } }
定义跳一下就会破碎的方块
enum BrokenPlatformState { cracked, broken } class BrokenPlatform extends Platform<BrokenPlatformState> { BrokenPlatform({super.position}); @override Future<void> onLoad() async { await super.onLoad(); sprites = <BrokenPlatformState, Sprite>{ BrokenPlatformState.cracked: await gameRef.loadSprite('game/platform_cracked_monitor.png'), BrokenPlatformState.broken: await gameRef.loadSprite('game/platform_monitor_broken.png'), }; current = BrokenPlatformState.cracked; size = Vector2(113, 48); } void breakPlatform() { current = BrokenPlatformState.broken; } }
定义带弹簧的方块
///弹簧床 enum SpringState { down, up } class SpringBoard extends Platform<SpringState> { SpringBoard({ super.position, }); @override Future<void> onLoad() async { await super.onLoad(); sprites = <SpringState, Sprite>{ SpringState.down: await gameRef.loadSprite('game/platform_trampoline_down.png'), SpringState.up: await gameRef.loadSprite('game/platform_trampoline_up.png'), }; current = SpringState.up; size = Vector2(100, 45); } @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other) { super.onCollisionStart(intersectionPoints, other); //通过计算交点的垂直差值,判断是否发生垂直碰撞、 //如果垂直差值小于 5,则将当前状态 current 设置为 SpringState.down,表示向下弹簧状态 bool isCollidingVertically = (intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5; if (isCollidingVertically) { current = SpringState.down; } } @override void onCollisionEnd(PositionComponent other) { //这个方法在碰撞结束时被调用。 //首先调用super.onCollisionEnd(other),以确保调用父类的碰撞结束方法。 //然后,将当前状态 current 设置为 SpringState.up,表示向上弹簧状态。 super.onCollisionEnd(other); current = SpringState.up; } }
定义敌人
可以把敌人当作平台,因为大部分的特性和平台是一样的,只是碰撞事件不同,图片不同。
///敌人 enum EnemyPlatformState { only } class EnemyPlatform extends Platform<EnemyPlatformState> { EnemyPlatform({super.position}); @override Future<void> onLoad() async { var randBool = Random().nextBool(); var enemySprite = randBool ? 'enemy_heart' : 'enemy_e'; sprites = <EnemyPlatformState, Sprite>{ EnemyPlatformState.only: await gameRef.loadSprite('game/$enemySprite.png'), }; current = EnemyPlatformState.only; if (enemySprite == "enemy_heart") { size = Vector2(100, 45); } else { //雷电 size = Vector2(100, 32); } return super.onLoad(); } }
定义道具
和平台一样,我们先定义通用的抽象类。
abstract class PowerUp extends SpriteComponent with HasGameRef<FlutterDashDoodleJump>, CollisionCallbacks { PowerUp({ super.position, }) : super( size: Vector2.all(50), priority: 2, ); final hitBox = RectangleHitbox(); double get jumpSpeedMultiplier; @override Future<void>? onLoad() async { await super.onLoad(); // 添加碰撞检测逻辑 await add(hitBox); } @override void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) { if (other is Player && !other.isInvincible && !other.isWearingHat) { removeFromParent(); } super.onCollision(intersectionPoints, other); } }
定义火箭道具(直接变身成为一团火,起飞+无敌,但是不能移动)
///火箭 class Rocket extends PowerUp { @override double get jumpSpeedMultiplier => 3.5; Rocket({ super.position, }); @override Future<void>? onLoad() async { await super.onLoad(); sprite = await gameRef.loadSprite('game/rocket.png'); size = Vector2(50, 70); } }
定义起飞魔法帽(直接起飞,可以左右移动,但是不无敌)
///起飞魔法帽 class Hat extends PowerUp { @override double get jumpSpeedMultiplier => 2.5; Hat({ super.position, }); final int activeLengthInMS = 5000; @override Future<void>? onLoad() async { await super.onLoad(); sprite = await gameRef.loadSprite('game/hat.png'); size = Vector2(75, 50); } }
定义玩家
玩家相对于平台或者道具都更复杂一些,让我们一点点来实现。
定义玩家的状态
一共有7种状态,分别是,1.正常 2.向左移动 3.向右移动 4.吃了道具火箭 5.吃到了道具魔法帽然后向左移动 6.吃到了道具魔法帽正常状态 7.吃到了道具魔法帽向右移动。
enum PlayerState { left, right, center, rocket, hatCenter, hatLeft, hatRight }
定义玩家的尺寸
class Player extends SpriteGroupComponent<PlayerState> with HasGameRef<FlutterDashDoodleJump>, KeyboardHandler, CollisionCallbacks { Player({super.position, this.jumpSpeed = 600}) : super( size: Vector2(79, 109), anchor: Anchor.center, priority: 1, ); Vector2 velocity = Vector2.zero(); }
标识玩家的移动方向、移动速度
计算用户是向左(-1)还是向右(1)移动,当向左移动时,x轴速度乘以-1,得到一个负数。在flutter中,x轴上的数字从左向右增加,因此负数向左移动。当向右移动时,结果将是一个正数,如果数字是0,Dash是垂直移动的,也就是正常模式。
int hAxisInput = 0; final int movingLeftInput = -1; final int movingRightInput = 1; //用于计算水平移动速度 final double gravity = 9; //垂直速度 double jumpSpeed;
标识用户当前的状态
//吃到了道具(火箭和起飞帽子) bool get hasPowerUp => current == PlayerState.rocket || current == PlayerState.hatLeft || current == PlayerState.hatRight || current == PlayerState.hatCenter; //处于无敌状态(在火箭里) bool get isInvincible => current == PlayerState.rocket; //是否戴着帽子(处于起飞状态) bool get isWearingHat => current == PlayerState.hatLeft || current == PlayerState.hatRight || current == PlayerState.hatCenter;
跳跃
在flutter中,因为左上角是(0,0),所以向上是负的。
void jump({double? specialJumpSpeed}) { velocity.y = specialJumpSpeed != null ? -specialJumpSpeed : -jumpSpeed; }
碰撞检测
检测玩家和平台、道具、敌人的碰撞。
//玩家与游戏中另一个组件碰撞的回调 @override void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) { super.onCollision(intersectionPoints, other); //碰到敌人且不是无敌状态,直接嗝屁 if (other is EnemyPlatform && !isInvincible) { gameRef.onLose(); return; } //计算碰撞点的垂直差值,是否小于5 bool isCollidingVertically = (intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5; bool enablePowerUp = false; //是否可以激活道具 if (!hasPowerUp && (other is Rocket || other is Hat)) { enablePowerUp = true; } //如果玩家正在向下移动且发生了垂直碰撞,根据碰撞的对象类型,执行相应的操作。 if (isMovingDown && isCollidingVertically) { current = PlayerState.center; //普通平台 if (other is NormalPlatform) { jump(); return; } //弹簧板 else if (other is SpringBoard) { jump(specialJumpSpeed: jumpSpeed * 2); return; } //起飞 else if (other is BrokenPlatform && other.current == BrokenPlatformState.cracked) { jump(); other.breakPlatform(); return; } if (other is Rocket || other is Hat) { enablePowerUp = true; } } if (!enablePowerUp) return; if (other is Rocket) { current = PlayerState.rocket; //基础跳跃速度 jumpSpeed 乘以火箭的跳跃速度倍数 other.jumpSpeedMultiplier jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier); return; } else if (other is Hat) { //根据Dash的当前位置,判断要显示的图标 if (current == PlayerState.center) current = PlayerState.hatCenter; if (current == PlayerState.left) current = PlayerState.hatLeft; if (current == PlayerState.right) current = PlayerState.hatRight; removePowerUpAfterTime(other.activeLengthInMS); jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier); return; } }
键盘控制
当方向键被按下时,改变玩家的移动方向。
@override bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) { //默认情况下不向左或向右 hAxisInput = 0; // 向左移动 if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) { if (isWearingHat) { current = PlayerState.hatLeft; } else if (!hasPowerUp) { current = PlayerState.left; } hAxisInput += movingLeftInput; } // 向右移动 if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) { if (isWearingHat) { current = PlayerState.hatRight; } else if (!hasPowerUp) { current = PlayerState.right; } hAxisInput += movingRightInput; } // 测试用,一直跳,不过撞到敌人还是会嗝屁 //if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) { // jump(); //} return true; }
游戏进行中的刷新检测
@override void update(double dt) { //判断是否是游戏中状态 if (gameRef.gameManager.isMenu || gameRef.gameManager.isGameOver) return; final double dashHorizontalCenter = size.x / 2; //玩家的水平速度 velocity.x = hAxisInput * jumpSpeed; //玩家的垂直速度 velocity.y += gravity; // 如果玩家移动到不在屏幕上(位置从中心开始),则从另一侧出现 if (position.x < dashHorizontalCenter) { position.x = gameRef.size.x - (dashHorizontalCenter); } if (position.x > gameRef.size.x - (dashHorizontalCenter)) { position.x = dashHorizontalCenter; } //玩家的速度除以经过的时间 //计算当前位置 position += velocity * dt; super.update(dt); }
这样我们就完成了对玩家的定义。
游戏背景定义
如果是长图/多图背景,则可以通过baseVelocity
和velocityMultiplierDelta
启用视差效果,这里只有单一的一张,就用静态的效果啦。
class World extends ParallaxComponent<FlutterDashDoodleJump> { @override Future<void> onLoad() async { parallax = await gameRef.loadParallax( [ ParallaxImageData('game/background/background.png'), ], fill: LayerFill.width, repeat: ImageRepeat.repeat, ); } }
🎯平台/道具生成控制器
现在,让我来对游戏中随机生成的平台、道具进行控制吧!
定义生成平台之间的距离
class ObjectManager extends Component with HasGameRef<FlutterDashDoodleJump> { ObjectManager({ this.minVerticalDistanceToNextPlatform = 200, this.maxVerticalDistanceToNextPlatform = 300, }); //到下一个平台的最小垂直距离 double minVerticalDistanceToNextPlatform; //到下一个平台的最大垂直距离 double maxVerticalDistanceToNextPlatform; }
定义存放平台、道具、敌人的列表
//存放Dash可以踩的平台 final List<Platform> platforms = []; final List<PowerUp> powerUps = []; final List<EnemyPlatform> enemies = [];
生成平台
// 返回随机类型的平台 // 各类平台出现的概率都是不同的 Platform _semiRandomPlatform(Vector2 position) { if (specialPlatforms['spring'] == true && probGen.generateWithProbability(15)) { // 15%的机会得到跳板 return SpringBoard(position: position); } if (specialPlatforms['broken'] == true && probGen.generateWithProbability(10)) { // 10%的机会出现只能跳一次的平台 return BrokenPlatform(position: position); } // 默认为普通平台 return NormalPlatform(position: position); }
生成道具
void _maybeAddPowerUp() { //20%的概率出现起飞魔法帽 if (specialPlatforms['hat'] == true && probGen.generateWithProbability(20)) { // 生成帽子道具 var hat = Hat( position: Vector2(_generateNextX(75), _generateNextY()), ); add(hat); powerUps.add(hat); return; } // 15%的概率出现火箭 if (specialPlatforms['rocket'] == true && probGen.generateWithProbability(15)) { var rocket = Rocket( position: Vector2(_generateNextX(50), _generateNextY()), ); add(rocket); powerUps.add(rocket); } }
生成怪物
void _maybeAddEnemy() { // 判断有没有到能生成怪物的游戏难度 if (specialPlatforms['enemy'] != true) { return; } if (probGen.generateWithProbability(20)) { var enemy = EnemyPlatform( position: Vector2(_generateNextX(100), _generateNextY()), ); add(enemy); enemies.add(enemy); _cleanup(); } }
手动删除道具/怪物
因为道具和敌人的生成依赖于概率,所以不存在将它们从游戏中移除的最佳时机。我们需要定期检查是否有可以删除的。
void _cleanup() { final screenBottom = gameRef.player.position.y + (gameRef.size.x / 2) + gameRef.screenBufferSpace; while (enemies.isNotEmpty && enemies.first.position.y > screenBottom) { remove(enemies.first); enemies.removeAt(0); } while (powerUps.isNotEmpty && powerUps.first.position.y > screenBottom) { if (powerUps.first.parent != null) { remove(powerUps.first); } powerUps.removeAt(0); } }
计算生成组件的x轴和y轴
double _generateNextX(int platformWidth) { // 确保下一个平台不会重叠 final previousPlatformXRange = Range( platforms.last.position.x, platforms.last.position.x + platformWidth, ); double nextPlatformAnchorX; // 如果前一个平台和下一个平台重叠,尝试一个新的随机X do { nextPlatformAnchorX = random.nextInt(gameRef.size.x.floor() - platformWidth).toDouble(); } while (previousPlatformXRange.overlaps( Range(nextPlatformAnchorX, nextPlatformAnchorX + platformWidth))); return nextPlatformAnchorX; } // 用于确定下一个平台应该放置的位置 // 它返回minVerticalDistanceToNextPlatform和maxVerticalDistanceToNextPlatform之间的随机距离 double _generateNextY() { // 添加platformHeight(单个平台的高度)可以防止平台重叠。 final currentHighestPlatformY = platforms.last.center.y + tallestPlatformHeight; final distanceToNextY = minVerticalDistanceToNextPlatform.toInt() + random .nextInt((maxVerticalDistanceToNextPlatform - minVerticalDistanceToNextPlatform) .floor()) .toDouble(); return currentHighestPlatformY - distanceToNextY; }
初始化游戏时生成组件
@override void onMount() { super.onMount(); var currentX = (gameRef.size.x.floor() / 2).toDouble() - 50; //第一个平台总是在初始屏幕的底部三分之一处 var currentY = gameRef.size.y - (random.nextInt(gameRef.size.y.floor()) / 3) - 50; //生成10个随机x, y位置的平台,并添加到平台列表。 for (var i = 0; i < 9; i++) { if (i != 0) { currentX = _generateNextX(100); currentY = _generateNextY(); } platforms.add( _semiRandomPlatform( Vector2( currentX, currentY, ), ), ); add(platforms[i]); } }
刷新游戏
游戏更新控制,分数计算。
@override void update(double dt) { //增加平台高度可以确保两个平台不会重叠。 final topOfLowestPlatform = platforms.first.position.y + tallestPlatformHeight; final screenBottom = gameRef.player.position.y + (gameRef.size.x / 2) + gameRef.screenBufferSpace; //当平台往下移动离开屏幕时,可以将其移除并放置一个新平台 if (topOfLowestPlatform > screenBottom) { // 生成一个新跳板 var newPlatformX = _generateNextX(100); var newPlatformY = _generateNextY(); final nextPlatform = _semiRandomPlatform(Vector2(newPlatformX, newPlatformY)); add(nextPlatform); platforms.add(nextPlatform); //移除屏幕外的平台 final lowestPlat = platforms.removeAt(0); lowestPlat.removeFromParent(); //增加分数,移除一个屏幕加一分 gameRef.gameManager.increaseScore(); _maybeAddPowerUp(); _maybeAddEnemy(); } super.update(dt); }
🏓游戏控制器
完成了所有需要的组件内容,现在是最后一步,编写游戏的运行逻辑!
class FlutterDashDoodleJump extends FlameGame with HasKeyboardHandlerComponents, HasCollisionDetection { FlutterDashDoodleJump({super.children}); }
定义所有需要的控制器
late Player player; final World _world = World(); GameManager gameManager = GameManager(); LevelManager levelManager = LevelManager(); ObjectManager objectManager = ObjectManager();
加载游戏
@override Future<void> onLoad() async { await add(_world); // 添加游戏管理器 await add(gameManager); // 添加暂停按钮和记分器的UI overlays.add('gameOverlay'); // 添加关卡/难度管理器 await add(levelManager); }
初始化游戏
void initializeGameStart() { //重新计分 gameManager.reset(); if (children.contains(objectManager)) objectManager.removeFromParent(); levelManager.reset(); player.reset(); // 设置摄像机的世界边界将允许摄像机“向上移动” // 但要保持水平固定,这样玩家就可以从屏幕的一边走出去,然后在另一边重新出现。 camera.worldBounds = Rect.fromLTRB( 0, -_world.size.y, camera.gameSize.x, _world.size.y + screenBufferSpace, // 确保游戏的底部边界低于屏幕底部 ); camera.followComponent(player); player.position = Vector2( (_world.size.x - player.size.x) / 2, (_world.size.y - player.size.y) / 2, ); objectManager = ObjectManager( minVerticalDistanceToNextPlatform: levelManager.minDistance, maxVerticalDistanceToNextPlatform: levelManager.maxDistance); add(objectManager); objectManager.configure(levelManager.level, levelManager.difficulty); }
开始/重新开始游戏
///开始游戏 void startGame() { addPlayer(); initializeGameStart(); gameManager.gameState = GameState.playing; overlays.remove('mainMenuOverlay'); } void addPlayer() { player = Player(); player.setJumpSpeed(levelManager.jumpSpeed); add(player); } ///再来一次 void resetGame() { startGame(); overlays.remove('gameOverOverlay'); } ///回到主页面 void backMenu() { overlays.remove('gameOverOverlay'); overlays.add('mainMenuOverlay'); }
游戏暂停
void togglePauseState() { if (paused) { resumeEngine(); } else { pauseEngine(); } }
玩家死亡
//嗝屁了 void onLose() { gameManager.gameState = GameState.gameOver; player.removeFromParent(); overlays.add('gameOverOverlay'); }
计分、暂停、开始菜单、难度选择这些简单的页面就不在文章里说明了。那么到这里我们就完成了整个游戏的编写!