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
相关产品推荐
相关产品推荐

