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

Flame框架中Bad state: No element错误排查求助

解决Flame中"Bad state: No element"拖拽错误

我通过Google Crash Analytics捕获到错误Bad state: No element. Error thrown,堆栈跟踪如下:

Fatal Exception: io.flutter.plugins.firebase.crashlytics.FlutterError
Bad state: No element. Error thrown .
List.last (dart:core)
DisplacementEvent.localEndPosition (displacement_event.dart:97)
DisplacementEvent.localDelta (displacement_event.dart:118)
Road.onDragUpdate (road.dart:189)
MultiDragDispatcher.onDragUpdate (multi_drag_dispatcher.dart:81)
MultiDragDispatcher.handleDragUpdate (multi_drag_dispatcher.dart:124)
FlameDragAdapter.update (flame_drag_adapter.dart:30)
MultiDragPointerState._move (multidrag.dart:100)
GestureBinding._handlePointerDataPacket (binding.dart:299)

环境信息

  • 使用Flame版本:flame: ^1.18.0
  • Flutter环境:
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.24.0, on Microsoft Windows [Version 10.0.22631.4037], locale en-US)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.10.6)
[√] Android Studio (version 2023.3)
[√] VS Code (version 1.92.2)
[√] Connected device (5 available)
[√] Network resources

相关代码

Road类的onDragUpdate方法

@override
void onDragUpdate(DragUpdateEvent event) {
  if (isDraggable) {
    position += event.localDelta;
    dropX = position.x + 64;
    dropY = position.y + 64;
  }
}

Road组件完整代码

import 'dart:async';
import 'dart:math';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:umfths/audio/sounds.dart';
import 'package:umfths/flame/components/cell.dart';
import 'package:umfths/flame/components/gnome.dart';
import 'package:umfths/flame/components/path_stop.dart';
import 'package:umfths/flame/components/treasure.dart';
import 'package:umfths/flame/umfth_game.dart';
import 'package:umfths/flame/umfth_world.dart';

class Road extends SpriteComponent
    with
        CollisionCallbacks,
        DragCallbacks,
        HasGameRef<UmfthGame>,
        HasWorldReference<UmfthWorld> {
  Road(this.roadType) : super(anchor: Anchor.topLeft);

  RoadTypes roadType;

  double dropX = 0;
  double dropY = 0;
  double startX = 0;
  double startY = 0;
  double gravity = 2;
  Vector2 velocity = Vector2(0, 20);
  bool isDraggable = false;
  bool isWaiting = false;
  bool isInTube = false;
  bool isDragging = false;
  bool isAvailable = false;
  bool isSlowZone = false;
  bool isSpecial = false;
  int cellId = 0;
  String mess = '';

  final textChild = TextComponent(position: Vector2(1, 20), priority: 99);

  @override
  Future<void> onLoad() async {
    add(textChild);

    add(RectangleHitbox(size: Vector2(128, 128), isSolid: true));

    if (roadType == RoadTypes.none) {
      roadType = gameRef.getRandomRoad();

      int special = Random().nextInt(20);

      if (special > 18) {
        isSpecial == true;
        setSpecial();
      }
    }

    String roadName = roadType.name;
    sprite =
        await Sprite.load('roads/$roadName.png', srcSize: Vector2(128, 128));

    /*  RoadBlock rbh = RoadBlock();
    RoadBlockVertical rbv = RoadBlockVertical();

    switch (roadType) {
      case RoadTypes.nw:
        add(rbh..position = Vector2(0, 124));
        add(rbv..position = Vector2(124, 0));
        break;
      case RoadTypes.ne:
        add(rbh..position = Vector2(0, 124));
        add(rbv..position = Vector2(0, 0));
        break;
      case RoadTypes.se:
        add(rbh..position = Vector2(0, 0));
        add(rbv..position = Vector2(0, 0));
        break;
      case RoadTypes.sw:
        add(rbh..position = Vector2(0, 0));
        add(rbv..position = Vector2(124, 0));
        break;
      case RoadTypes.horiz:
        add(rbh..position = Vector2(0, 0));
        RoadBlock rbh2 = RoadBlock();
        add(rbh2..position = Vector2(0, 124));
        break;

      case RoadTypes.vert:
        add(rbv..position = Vector2(0, 0));
        RoadBlockVertical rbv2 = RoadBlockVertical();
        add(rbv2..position = Vector2(124, 0));
        break;

      case RoadTypes.splitEast:
        add(rbv..position = Vector2(0, 0));
        break;
      case RoadTypes.splitWest:
        add(rbv..position = Vector2(124, 0));
        break;

      default:
    }

    if (roadType == RoadTypes.nw) {} */
  }

  @override
  void update(double dt) {
    super.update(dt);

    if (isInTube == true) {
      // Sitting in glass emitter tube
      if (!isWaiting) {
        // Not waiting so let it drop down emitter tube
        velocity.y += gravity;
        position.y += velocity.y * dt;
      } else {
        // Has colided with another road in emitter so waiting
        // to drop down
        velocity.y = 0;
      }
    }

    //textChild.text = mess;
  }

  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);

    if (other is PathStop) {
      velocity.y = 0;
      isAvailable = true;
      velocity.y = 0;
      opacity = 1;
      isInTube = false;
      isWaiting = false;
      isDraggable = true;
    }
    if (!isWaiting) {
      if (other is Road) {
        isWaiting = true;
      }
    }

    if (other is Gnome && UmfthWorld.isDragging == false) {
      setOccupied(cellId, false, false);

      for (Road rd in UmfthWorld.gridRoads) {
        if (rd.cellId == cellId) {
          UmfthWorld.gridRoads.remove(rd);
          break;
        }
      }

      UmfthWorld.gridRoads.remove(this);

      parent!.remove(this);
       game.playAudio(SfxType.steal, false);
    }
  }

  @override
  void onDragStart(DragStartEvent event) {
    super.onDragStart(event);
    if (isDraggable) {
      if (cellId > 0) {
        setOccupied(cellId, false, false);
      }

      priority = 1;
      isDragging = true;
      UmfthWorld.isDragging = true;
      velocity.y = 0;
    }
  }

  @override
  void onDragUpdate(DragUpdateEvent event) {
    /* if you are using DragUpdateEvent events, the devicePosition, canvasPosition, localPosition, and delta are deprecated as they are unclear.
    use xStartPosition to get the position at the start of the drag event ("from")
    use xEndPosition to get the position at the end of the drag event ("to")
    if you want the delta, use localDelta. it now already considers the camera zoom. no need to manually account for that
    now you keep receiving drag events for the same component even if the mouse leaves the component (breaking) */
    if (isDraggable) {
      position += event.localDelta;
      dropX = position.x + 64;
      dropY = position.y + 64;
      
    }
  }

  @override
  Future<void> onDragEnd(DragEndEvent event) async {
    super.onDragEnd(event);
    isDragging = false;
    UmfthWorld.isDragging = false;
    if (isDraggable) {
      Cell? cb = getDroppedOn(dropX, dropY, this);

      if (cb != null) {
        if (cb.occupied == false) {
          setOccupied(cb.id, true, true);
          cellId = cb.id;
          cb.ignoreCollisions = true;
          x = cb.x;
          y = cb.y;

          velocity = Vector2(0, 0);
          opacity = 1;
          isDraggable = true;
          gravity = 0;
          isInTube = false;

          for (Road rd in UmfthWorld.gridRoads) {
            if (rd.cellId == cellId) {
              UmfthWorld.gridRoads.remove(rd);
              break;
            }
          }

          UmfthWorld.gridRoads.add(this);
        } else {
          //not a cell
          parent!.remove(this);
        }
      } else {
        // No empty cell found
        parent!.remove(this);
      }

      if (isAvailable == true) {
        // Removes first road from queue after user drags and drops
        gameRef.roadQueue.removeFirst();

        double firstY = 830;

        for (var w in gameRef.roadQueue) {
          // move each road in queue down by road height
          w.position.y = firstY;
          firstY -= 135;
        }

        // Set first road in queue properties
        // first is the one user can drag
        if (gameRef.roadQueue.isNotEmpty) {
          gameRef.roadQueue.first.isDraggable = true;
          gameRef.roadQueue.first.opacity = 1;
          gameRef.roadQueue.first.velocity.y = 0;
          gameRef.roadQueue.first.isInTube = false;
          gameRef.roadQueue.first.isWaiting = false;
          gameRef.roadQueue.first.isAvailable = true;
        }
        isAvailable = false;
      }
    }
  }

  setSlow() async {
    isSlowZone = true;
    SpriteComponent slow = SpriteComponent();
    slow.sprite =
        await Sprite.load('slow-down.png', srcSize: Vector2(128, 128));
    add(slow);
  }

  setSpecial() async {
    SpriteAnimationComponent s = Treasure();
    add(s
      ..priority = 100
      ..position = Vector2(32, 32));
  }

  Cell? getDroppedOn(x, y, road) {
    List<Cell> blocks = UmfthWorld.gridCells;
    for (final cell in blocks) {
      if (x >= cell.position.x &&
          x <= cell.position.x + 128 &&
          y >= cell.position.y &&
          y <= cell.position.y + 128) {
        if (!cell.occupied) {
          return cell;
        }
      }
    }
    //No cell
    List<Cell> cells = UmfthWorld.gridCells;
    for (final cell in cells) {
      if (cell.occupied == false) {
        return cell;
      }
    }

    return null;
  }

  Cell? getCell(x, y, road) {
    List<Cell> blocks = UmfthWorld.gridCells;
    for (final cell in blocks) {
      if (x >= cell.position.x &&
          x <= cell.position.x + 128 &&
          y >= cell.position.y &&
          y <= cell.position.y + 128) {
        return cell;
      }
    }

    return null;
  }

  void setOccupied(int id, bool val, bool ignore) {
    List<Cell> cells = UmfthWorld.gridCells;

    findById(obj) => obj.id == id;
    var result = cells.where(findById);

    Cell? c = result.isNotEmpty ? result.first : null;
    if (c != null) {
      c.occupied = val;
      c.ignoreCollisions = ignore;
    }
  }
}

问题分析

从堆栈跟踪可见,错误是Flame内部DisplacementEvent类调用List.last时,列表为空导致的。这通常发生在拖拽事件异常中断(比如拖拽中组件被移除),或者旧版本Flame的拖拽事件处理存在bug。

修复方案

  1. 添加异常捕获
    修改onDragUpdate方法,捕获异常避免崩溃:
@override
void onDragUpdate(DragUpdateEvent event) {
  if (isDraggable) {
    try {
      position += event.localDelta;
      dropX = position.x + 64;
      dropY = position.y + 64;
    } catch (e) {
      // 可选:记录异常日志
      print('拖拽更新错误:$e');
    }
  }
}
  1. 增加组件存在性检查
    确保拖拽时组件未被移除,在拖拽相关方法中添加parent != null判断:
@override
void onDragStart(DragStartEvent event) {
  if (isDraggable && parent != null) {
    // 原有逻辑
  }
}

@override
void onDragUpdate(DragUpdateEvent event) {
  if (isDraggable && parent != null) {
    try {
      position += event.localDelta;
      dropX = position.x + 64;
      dropY = position.y + 64;
    } catch (e) {
      print('拖拽更新错误:$e');
    }
  }
}
  1. 升级Flame版本
    当前使用的flame: ^1.18.0版本较旧,建议升级到最新稳定版,这类底层事件处理bug通常已被修复。

  2. 优化组件移除逻辑
    在onDragEnd中移除组件前,先标记组件不可拖拽,避免后续事件触发:

@override
Future<void> onDragEnd(DragEndEvent event) async {
  super.onDragEnd(event);
  isDragging = false;
  UmfthWorld.isDragging = false;
  if (isDraggable) {
    Cell? cb = getDroppedOn(dropX, dropY, this);

    if (cb != null) {
      // 原有逻辑
    } else {
      isDraggable = false;
      parent!.remove(this);
    }

    // 队列处理逻辑
  }
}

内容的提问来源于stack exchange,提问作者Phil Ciraolo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 16:25:55