You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Flame平台游戏:实现乌龟敌人3秒刺收放状态切换及过渡动画

问题:Flutter Flame平台游戏中实现乌龟敌人尖刺定时收放

我刚学习Flutter和Flame,正在开发一款简易平台游戏,想要添加一个壳带尖刺的乌龟敌人,要求尖刺每3秒收放一次,且主角仅能在尖刺收起时击杀它。我尝试使用Future.delayed函数但未能成功实现,相关代码如下:

enum TurtleState { hit, idleSpikesIn, idleSpikesOut, spikesIn, spikesOut }

class Turtle extends SpriteAnimationGroupComponent
    with HasGameRef<PixelAdventure>, CollisionCallbacks {
  final double offNeg;
  final double offPos;
  Turtle({
    super.position,
    super.size,
    this.offPos = 0,
    this.offNeg = 0,
  });

  late final SpriteAnimation _hitAnimation;
  late final SpriteAnimation _idleSpikeInAnimation;
  late final SpriteAnimation _idleSpikeOutAnimation;
  late final SpriteAnimation _spikeOutAnimation;
  late final SpriteAnimation _spikeInAnimation;
  late final Player player;

  static const _bounceHeight = 260.0;
  static const stepTime = 0.05;
  static const tileSize = 16;

  final textureSize = Vector2(44, 26);

  double moveSpeed = 80;
  double moveDirection = -1;
  double rangeNeg = 0;
  double rangePos = 0;

  bool gotStomped = false;
  bool spikesIn = false;

  @override
  FutureOr<void> onLoad() {
    player = game.player;
    add(RectangleHitbox(
      position: Vector2.zero(),
      size: Vector2(44, 26),
    ));

    debugMode = true;

    _loadAllAnimations();
    _calculateRange();
    return super.onLoad();
  }

  @override
  void update(double dt) {
    if (!gotStomped) {
      _updateState();
      _movement(dt);
    }
    super.update(dt);
  }

  void _loadAllAnimations() {
    _idleSpikeOutAnimation = _spriteAnimation('Idle 1', 14);
    _hitAnimation = _spriteAnimation('Hit', 5)..loop = false;
    _idleSpikeInAnimation = _spriteAnimation('Idle 2', 8);
    _spikeOutAnimation = _spriteAnimation('Spikes out', 8)..loop = false;
    _spikeInAnimation = _spriteAnimation('Spikes in', 8)..loop = false;

    animations = {
      TurtleState.idleSpikesIn: _idleSpikeInAnimation,
      TurtleState.idleSpikesOut: _idleSpikeOutAnimation,
      TurtleState.hit: _hitAnimation,
      TurtleState.spikesOut: _spikeOutAnimation,
      TurtleState.spikesIn: _spikeInAnimation,
    };
    current = TurtleState.idleSpikesOut;
  }

  SpriteAnimation _spriteAnimation(String state, int amount) {
    return SpriteAnimation.fromFrameData(
      game.images.fromCache('Enemies/Turtle/$state (44x26).png'),
      SpriteAnimationData.sequenced(
        amount: amount,
        stepTime: stepTime,
        textureSize: textureSize,
      ),
    );
  }

  void _calculateRange() {
    rangeNeg = position.x - offNeg * tileSize;
    rangePos = position.x + width + offPos * tileSize;
  }

  void _updateState() {
    if ((moveDirection > 0 && scale.x > 0) ||
        (moveDirection < 0 && scale.x < 0)) {
      flipHorizontallyAroundCenter();
    }
    const spikeDuration = Duration(seconds: 3);
    Future.delayed(spikeDuration, () {
      current = TurtleState.idleSpikesOut;
      spikesIn = false;
    });
    Future.delayed(spikeDuration, () {
      spikesIn = true;
      current = TurtleState.spikesIn;
    });
  }

  void _movement(double dt) {
    if (position.x >= rangePos) {
      moveDirection = -1;
    } else if (position.x <= rangeNeg) {
      moveDirection = 1;
    }
    position.x += moveSpeed * moveDirection * dt;
  }

  void collidedWithPlayer() async {
    if (player.velocity.y > 0 && player.y + player.height > position.y) {
      gotStomped = true;
      current = TurtleState.hit;
      player.velocity.y = -_bounceHeight;
      await animationTicker?.completed;
      removeFromParent();
    } else {
      player.collidedWithEnemy();
    }
  }
}

请问该如何实现尖刺的定时收放状态切换及过渡动画?


解决方案

你的核心问题是在update方法里反复调用Future.delayed,导致每帧都会触发新的延迟任务,状态切换逻辑完全混乱。下面是修正后的实现方案:

1. 添加计时器与状态锁变量

在类中新增变量跟踪时间和状态切换的状态,避免重复触发:

// 跟踪尖刺状态切换的计时器
double spikeTimer = 0.0;
// 状态切换动画播放中时锁定,防止重复触发
bool isTransitioning = false;
// 尖刺状态切换的间隔时间(秒)
static const spikeToggleInterval = 3.0;

2. 重写状态更新与切换逻辑

移除原有的Future.delayed,改用计时器累计时间,到点后触发状态切换,并等待过渡动画完成后切换到 idle 状态:

void _updateState(double dt) {
  // 处理翻转逻辑(保留原代码)
  if ((moveDirection > 0 && scale.x > 0) ||
      (moveDirection < 0 && scale.x < 0)) {
    flipHorizontallyAroundCenter();
  }

  // 如果正在播放过渡动画或已被击杀,跳过计时器逻辑
  if (isTransitioning || gotStomped) return;

  // 累计时间
  spikeTimer += dt;

  // 到达切换间隔时触发状态切换
  if (spikeTimer >= spikeToggleInterval) {
    spikeTimer = 0.0;
    _toggleSpikeState();
  }
}

// 处理尖刺状态切换的逻辑
void _toggleSpikeState() async {
  isTransitioning = true;

  if (spikesIn) {
    // 从收起切换到伸出:先播放伸出动画,再切换到 idle 状态
    current = TurtleState.spikesOut;
    await animationTicker?.completed;
    current = TurtleState.idleSpikesOut;
    spikesIn = false;
  } else {
    // 从伸出切换到收起:先播放收起动画,再切换到 idle 状态
    current = TurtleState.spikesIn;
    await animationTicker?.completed;
    current = TurtleState.idleSpikesIn;
    spikesIn = true;
  }

  isTransitioning = false;
}

3. 修改击杀判断逻辑

只有当尖刺收起(spikesIn为true)且满足踩踏条件时,才能击杀乌龟:

void collidedWithPlayer() async {
  // 新增尖刺状态判断:只有尖刺收起时才能被击杀
  if (player.velocity.y > 0 && player.y + player.height > position.y && spikesIn) {
    gotStomped = true;
    current = TurtleState.hit;
    player.velocity.y = -_bounceHeight;
    await animationTicker?.completed;
    removeFromParent();
  } else {
    player.collidedWithEnemy();
  }
}

4. 完整修改后的代码

enum TurtleState { hit, idleSpikesIn, idleSpikesOut, spikesIn, spikesOut }

class Turtle extends SpriteAnimationGroupComponent
    with HasGameRef<PixelAdventure>, CollisionCallbacks {
  final double offNeg;
  final double offPos;
  Turtle({
    super.position,
    super.size,
    this.offPos = 0,
    this.offNeg = 0,
  });

  late final SpriteAnimation _hitAnimation;
  late final SpriteAnimation _idleSpikeInAnimation;
  late final SpriteAnimation _idleSpikeOutAnimation;
  late final SpriteAnimation _spikeOutAnimation;
  late final SpriteAnimation _spikeInAnimation;
  late final Player player;

  static const _bounceHeight = 260.0;
  static const stepTime = 0.05;
  static const tileSize = 16;
  // 尖刺状态切换间隔
  static const spikeToggleInterval = 3.0;

  final textureSize = Vector2(44, 26);

  double moveSpeed = 80;
  double moveDirection = -1;
  double rangeNeg = 0;
  double rangePos = 0;

  bool gotStomped = false;
  bool spikesIn = false;
  // 计时器与状态锁
  double spikeTimer = 0.0;
  bool isTransitioning = false;

  @override
  FutureOr<void> onLoad() {
    player = game.player;
    add(RectangleHitbox(
      position: Vector2.zero(),
      size: Vector2(44, 26),
    ));

    debugMode = true;

    _loadAllAnimations();
    _calculateRange();
    return super.onLoad();
  }

  @override
  void update(double dt) {
    if (!gotStomped) {
      _updateState(dt);
      _movement(dt);
    }
    super.update(dt);
  }

  void _loadAllAnimations() {
    _idleSpikeOutAnimation = _spriteAnimation('Idle 1', 14);
    _hitAnimation = _spriteAnimation('Hit', 5)..loop = false;
    _idleSpikeInAnimation = _spriteAnimation('Idle 2', 8);
    _spikeOutAnimation = _spriteAnimation('Spikes out', 8)..loop = false;
    _spikeInAnimation = _spriteAnimation('Spikes in', 8)..loop = false;

    animations = {
      TurtleState.idleSpikesIn: _idleSpikeInAnimation,
      TurtleState.idleSpikesOut: _idleSpikeOutAnimation,
      TurtleState.hit: _hitAnimation,
      TurtleState.spikesOut: _spikeOutAnimation,
      TurtleState.spikesIn: _spikeInAnimation,
    };
    current = TurtleState.idleSpikesOut;
  }

  SpriteAnimation _spriteAnimation(String state, int amount) {
    return SpriteAnimation.fromFrameData(
      game.images.fromCache('Enemies/Turtle/$state (44x26).png'),
      SpriteAnimationData.sequenced(
        amount: amount,
        stepTime: stepTime,
        textureSize: textureSize,
      ),
    );
  }

  void _calculateRange() {
    rangeNeg = position.x - offNeg * tileSize;
    rangePos = position.x + width + offPos * tileSize;
  }

  void _updateState(double dt) {
    // 处理翻转逻辑
    if ((moveDirection > 0 && scale.x > 0) ||
        (moveDirection < 0 && scale.x < 0)) {
      flipHorizontallyAroundCenter();
    }

    if (isTransitioning || gotStomped) return;

    spikeTimer += dt;

    if (spikeTimer >= spikeToggleInterval) {
      spikeTimer = 0.0;
      _toggleSpikeState();
    }
  }

  // 尖刺状态切换逻辑
  void _toggleSpikeState() async {
    isTransitioning = true;

    if (spikesIn) {
      current = TurtleState.spikesOut;
      await animationTicker?.completed;
      current = TurtleState.idleSpikesOut;
      spikesIn = false;
    } else {
      current = TurtleState.spikesIn;
      await animationTicker?.completed;
      current = TurtleState.idleSpikesIn;
      spikesIn = true;
    }

    isTransitioning = false;
  }

  void _movement(double dt) {
    if (position.x >= rangePos) {
      moveDirection = -1;
    } else if (position.x <= rangeNeg) {
      moveDirection = 1;
    }
    position.x += moveSpeed * moveDirection * dt;
  }

  void collidedWithPlayer() async {
    // 只有尖刺收起时才能击杀
    if (player.velocity.y > 0 && player.y + player.height > position.y && spikesIn) {
      gotStomped = true;
      current = TurtleState.hit;
      player.velocity.y = -_bounceHeight;
      await animationTicker?.completed;
      removeFromParent();
    } else {
      player.collidedWithEnemy();
    }
  }
}

关键说明

  • 使用spikeTimer累计时间,避免每帧触发新的延迟任务,确保3秒间隔的准确性
  • 添加isTransitioning锁,防止在过渡动画播放时重复触发状态切换
  • 状态切换时先播放过渡动画(spikesIn/spikesOut),动画完成后再切换到对应的 idle 状态,保证动画流畅
  • 修改击杀判断逻辑,只有尖刺收起时才能被主角踩踏击杀

内容的提问来源于stack exchange,提问作者The Naqvi Games

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 05:50:56