Doodle Jump — 使用FlutterFlame开发游戏真不错!

前言

最近网上冲浪的时候,我偶然发现了一个国外的游戏网站,里面聚集了各种有趣的小游戏,类似于国内的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);
}

这样我们就完成了对玩家的定义。

游戏背景定义

如果是长图/多图背景,则可以通过baseVelocityvelocityMultiplierDelta启用视差效果,这里只有单一的一张,就用静态的效果啦。

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

计分、暂停、开始菜单、难度选择这些简单的页面就不在文章里说明了,有兴趣的朋友可以自行查看git仓库~那么到这里我们就完成了整个游戏的编写!😘

关于我

Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 ,也可以通过掘金的新的私信功能联系到我。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万一哪天我进步了呢?😝

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/535376.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024.4.11

1.思维导图 2.指针形式验证大小端存储 #include<myhead.h>int main(int argc, const char *argv[]) {int num 0x12345678;char* ptr (char *)&num;if(*ptr 0x12){printf("big endian\n");}else if(*ptr 0x78){printf("little endian\n");}r…

LangChain-10 Agents langchainhub 共享的提示词Prompt

LangChainHub 的思路真的很好&#xff0c;通过Hub的方式将Prompt 共享起来&#xff0c;大家可以通过很方便的手段&#xff0c;短短的几行代码就可以使用共享的Prompt。 我个人非常看好这个项目。 官方推荐使用LangChainHub&#xff0c;但是它在GitHub已经一年没有更新了&#x…

Linux函数学习 epoll

1、Linux epoll函数 1.1、创建epoll实例 int epoll_create1(int flag); 返回值&#xff1a;-1 失败&#xff0c;非负数 成功 flag &#xff1a;默认传入0 1.2、管理epoll对象 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epfd &#xff1a;e…

.a和.so库文件是什么?

我们在编译开源代码后&#xff0c;通常会生成.a和.so这两个库文件&#xff0c;这两个文件有什么区别&#xff1f;又如何使用&#xff1f; 在 Linux 中&#xff0c;.a 和 .so 文件都是库文件&#xff0c;但它们有一些区别&#xff1a; 静态库文件&#xff08;.a&#xff09;&am…

PaddleDetection 项目使用说明

PaddleDetection 项目使用说明 PaddleDetection 项目使用说明数据集处理相关模块环境搭建 PaddleDetection 项目使用说明 https://github.com/PaddlePaddle/PaddleDetection/blob/release/2.7/configs/ppyoloe/README_cn.md 自己项目&#xff1a; https://download.csdn.net/d…

融中财经专访 | 欧科云链:从跟随行业到引领行业

导读 THECAPITAL 新行业中的经验“老兵”。 本文4089字&#xff0c;约5.8分钟 作者 | 吕敬之 编辑 | 吾人 来源 | 融中财经 &#xff08;ID&#xff1a;thecapital&#xff09; 一个新兴行业从发展到成熟需要几个必要的推手&#xff1a;人才、产品、制度。 Web3.0&…

每天五分钟深度学习:逻辑回归算法的损失函数和代价函数是什么?

本文重点 前面已经学习了逻辑回归的假设函数,训练出模型的关键就是学习出参数w和b,要想学习出这两个参数,此时需要最小化逻辑回归的代价函数才可以训练出w和b。那么本节课我们将学习逻辑回归算法的代价函数是什么? 为什么不能平方差损失函数 线性回归的代价函数我们使用…

Hystrix:实现分布式系统的延迟处理和容错保护机制

文章目录 一、Hystrix的概念与作用1.1、资源隔离1.2、熔断器模式1.3、命令模式1.4、监控和报警 二、Hystrix的使用方法三、总结 一、Hystrix的概念与作用 Hystrix是Netflix开源的一个库&#xff0c;用于处理分布式系统中的延迟和容错。它通过在服务调用之间添加保护机制&#…

Leetcode刷题-字符串详细总结(Java)

字符串 字符串可能在算法处理上面和数组是类似的&#xff0c;但是String和数组的数据结构还是有一些不一样的 1、反转字符串 344. 反转字符串 - 力扣&#xff08;LeetCode&#xff09; 双指针的经典应用&#xff0c;两个指针同时向中间移动 public void reverseString(char[…

VMware启动显示“打开虚拟机时出错: 获取该虚拟机的所有权失败”

提示框&#xff08;忘截图了&#xff09;里提示目录C:\Users\mosep\Documents\Virtual Machines\VM-Win10 x64\中的某个文件&#xff08;在我这里好像是VM-Win10 x64.vmx&#xff0c;VM-Win10 x64是我给虚拟机取的名字&#xff09;在被使用中。 找到这个目录&#xff0c;删除.…

Python+Selenium+Unittest 之Unittest4(断言)

在unittest框架的TestCase类也提供了多种断言的方法。 断言常用方法 断言方法检查内容assertEqual(a,b)判断a是否等于b&#xff08;判断两个是不是同一个值&#xff09;assertNotEqual(a, b)判断a是否不等于b&#xff08;判断两个是不是同一个值&#xff09;assertTrue(a)判断a…

RAG应用开发实战(01)-RAG应用框架和解析器

1 开源解析和拆分文档 第三方的工具去对文件解析拆分&#xff0c;去将我们的文件内容给提取出来&#xff0c;并将我们的文档内容去拆分成一个小的chunk。常见的PDF word mark down, JSON、HTML。都可以有很好的一些模块去把这些文件去进行一个东西去提取。 优势 支持丰富的文…

[RK3399 Linux] 移植Linux 5.2.8内核详解

背景是在RK3399上面移植Rockchip官方提供的u-boot 2017.09 一、linux内核 1.1 源码下载 内核源码下载地址为:《https://www.kernel.org/》: 也可以到内核镜像网址下载https://mirrors.edge.kernel.org/pub/linux/kernel/,这里下载速度更快。 如果下载速度太慢,无法下载,…

2024.4.12蚂蚁庄园今日答案:豆腐在烹调时容易碎有什么办法可以避免?

原文来源&#xff1a;蚂蚁庄园今日答案 - 词令 蚂蚁庄园是一款爱心公益游戏&#xff0c;用户可以通过喂养小鸡&#xff0c;产生鸡蛋&#xff0c;并通过捐赠鸡蛋参与公益项目。用户每日完成答题就可以领取鸡饲料&#xff0c;使用鸡饲料喂鸡之后&#xff0c;会可以获得鸡蛋&…

【数学建模】机器人避障问题

已知&#xff1a; 正方形5的左下顶点坐标 ( 80 , 60 ) (80,60) (80,60)&#xff0c;边长 150 150 150机器人与障碍物的距离至少超过 10 10 10个单位规定机器人的行走路径由直线段和圆弧组成&#xff0c;其中圆弧是机器人转弯路径。机器人不能折线转弯&#xff0c;转弯路径由与…

【C++算法】线性DP详解:数字三角形、最长上升子序列、最长公共子序列、最长公共子串、字符串编辑距离

文章目录 1&#xff09;数字三角形1&#xff1a;顺推2&#xff1a;逆推 2&#xff09;最长上升子序列1&#xff1a;线性DP做法2&#xff1a;二分优化 3&#xff09;最长公共子序列4&#xff09;最长公共子串5&#xff09;字符串编辑距离 1&#xff09;数字三角形 1&#xff1a…

git修改本地提交历史邮箱地址

1、Git&#xff08;Git&#xff09; 2、修改Git本地提交历史中的邮箱地址 使用 git rebase 命令进行交互式重置。 具体步骤如下&#xff1a;&#xff08;https://git-scm.com/docs/git-rebase&#xff09; 1、查看提交历史&#xff1a; 使用 git log 命令列出提交历史&#x…

HCIE考试第三题:业务容器化及割接

文章目录 业务容器化及割接题目和做题步骤如下3.1业务容器化及割接3.1创建CCE集群solo3.2创建NAT网关3.2.1申请EIP3.2.2创建NAT网关3.2.3添加SNAT规则3.3创建节点池3.3.1 创建namespace3.3.2创建节点池3.4 安装命令行工具kubectl3.4.1上传kubectl3.4.2上传kubeconfig配置文件3.…

Linux文件IO(3):使用文件IO进行文件的打开、关闭、读写、定位等相关操作

目录 1. 文件IO的概念 2. 文件描述符概念 3. 函数介绍 3.1 文件IO-open函数 3.2 文件IO-close函数 3.3 文件IO-read函数 3.4 文件IO-write函数 3.5 文件IO-lseek函数 4. 代码练习 4.1 要求 4.2 具体实现代码 4.3 测试结果 5. 总结 1. 文件IO的概念 posix(可移植操作系统接…

【React】路由鉴权

需求 未登录状态下&#xff0c;某些页面不可访问&#xff0c;白名单中的页面可以。未登录状态下&#xff0c;拦截通过修改url直接访问页面。判断是否有权访问某些页面。路由规则中每个页面都需要调用某个接口。 前提 使用的react-router-dom6 &#xff0c;这里只是举例&…