Loading...
Loading...
Compare original and translation side by side
FlameGameHasCollisionDetectionHasKeyboardHandlerComponentsSpriteComponentPositionComponentSpriteAnimationComponentupdate(double dt)render(Canvas canvas)CollisionCallbacksCircleHitboxRectangleHitboxKeyboardHandlerTapCallbacksJoystickComponentHasCollisionDetectionHasKeyboardHandlerComponentsFlameGameSpriteComponentPositionComponentSpriteAnimationComponentupdate(double dt)render(Canvas canvas)CollisionCallbacksCircleHitboxRectangleHitboxKeyboardHandlerTapCallbacksJoystickComponentflutter create my_game
cd my_game
flutter pub add flameflutter pub add flame_audioflutter pub add flame_tiledflutter create my_game
cd my_game
flutter pub add flameflutter pub add flame_audioflutter pub add flame_tiledassets/
├── images/
│ ├── player.png
│ ├── enemy_spritesheet.png
│ └── background.png
├── audio/
│ ├── bgm.mp3
│ └── sfx_jump.wav
└── tiles/
└── map.tmxpubspec.yamlflutter:
assets:
- assets/images/
- assets/audio/
- assets/tiles/assets/
├── images/
│ ├── player.png
│ ├── enemy_spritesheet.png
│ └── background.png
├── audio/
│ ├── bgm.mp3
│ └── sfx_jump.wav
└── tiles/
└── map.tmxpubspec.yamlflutter:
assets:
- assets/images/
- assets/audio/
- assets/tiles/class MyGame extends FlameGame with HasCollisionDetection {
Future<void> onLoad() async {
await add(Player());
}
}class MyGame extends Game {
void render(Canvas canvas) { /* Custom rendering */ }
void update(double dt) { /* Custom logic */ }
}class MyGame extends FlameGame with HasCollisionDetection {
Future<void> onLoad() async {
await add(Player());
}
}class MyGame extends Game {
void render(Canvas canvas) { /* 自定义绘制 */ }
void update(double dt) { /* 自定义逻辑 */ }
}| Mixin | Purpose |
|---|---|
| Enables collision system |
| Allows keyboard input handling |
| Provides access to game instance |
| Mixin | 用途 |
|---|---|
| 启用碰撞系统 |
| 支持键盘输入处理 |
| 提供对游戏实例的访问 |
Component
├── PositionComponent
│ ├── SpriteComponent
│ ├── SpriteAnimationComponent
│ ├── TextComponent
│ └── ShapeComponent
├── ParticleSystemComponent
└── ParallaxComponentComponent
├── PositionComponent
│ ├── SpriteComponent
│ ├── SpriteAnimationComponent
│ ├── TextComponent
│ └── ShapeComponent
├── ParticleSystemComponent
└── ParallaxComponentclass MyComponent extends PositionComponent {
Future<void> onLoad() async {
// Called once when added to game
// Load assets, initialize state
}
void update(double dt) {
// Called every frame
// dt = delta time in seconds
}
void render(Canvas canvas) {
// Called every frame for drawing
}
void onRemove() {
// Cleanup before removal
}
}class MyComponent extends PositionComponent {
Future<void> onLoad() async {
// 添加到游戏时调用一次
// 加载资源,初始化状态
}
void update(double dt) {
// 每帧调用
// dt = 以秒为单位的增量时间
}
void render(Canvas canvas) {
// 每帧调用用于绘制
}
void onRemove() {
// 移除前清理资源
}
}class Player extends SpriteComponent {
Player() : super(size: Vector2(64, 64), position: Vector2(100, 100));
Future<void> onLoad() async {
sprite = await Sprite.load('player.png');
}
}class Player extends SpriteComponent {
Player() : super(size: Vector2(64, 64), position: Vector2(100, 100));
Future<void> onLoad() async {
sprite = await Sprite.load('player.png');
}
}class Enemy extends SpriteComponent {
Future<void> onLoad() async {
final spriteSheet = await SpriteSheet.load(
'enemies.png',
srcSize: Vector2(32, 32),
);
sprite = spriteSheet.getSprite(0, 0); // row, column
}
}class Enemy extends SpriteComponent {
Future<void> onLoad() async {
final spriteSheet = await SpriteSheet.load(
'enemies.png',
srcSize: Vector2(32, 32),
);
sprite = spriteSheet.getSprite(0, 0); // 行, 列
}
}class Circle extends PositionComponent {
final Paint paint = Paint()..color = Colors.red;
Circle() : super(size: Vector2.all(50));
void render(Canvas canvas) {
canvas.drawCircle(
size.toOffset() / 2,
size.x / 2,
paint,
);
}
}class Circle extends PositionComponent {
final Paint paint = Paint()..color = Colors.red;
Circle() : super(size: Vector2.all(50));
void render(Canvas canvas) {
canvas.drawCircle(
size.toOffset() / 2,
size.x / 2,
paint,
);
}
}class AnimatedPlayer extends SpriteAnimationComponent {
AnimatedPlayer() : super(size: Vector2(64, 64));
Future<void> onLoad() async {
final spriteSheet = await SpriteSheet.load(
'player_run.png',
srcSize: Vector2(64, 64),
);
animation = spriteSheet.createAnimation(
row: 0,
stepTime: 0.1, // seconds per frame
to: 8, // number of frames
);
}
}class AnimatedPlayer extends SpriteAnimationComponent {
AnimatedPlayer() : super(size: Vector2(64, 64));
Future<void> onLoad() async {
final spriteSheet = await SpriteSheet.load(
'player_run.png',
srcSize: Vector2(64, 64),
);
animation = spriteSheet.createAnimation(
row: 0,
stepTime: 0.1, // 每帧秒数
to: 8, // 帧数
);
}
}class Character extends SpriteAnimationComponent with HasGameReference {
late final SpriteAnimation runAnimation;
late final SpriteAnimation idleAnimation;
late final SpriteAnimation jumpAnimation;
Future<void> onLoad() async {
final spritesheet = await game.images.load('character.png');
runAnimation = _createAnimation(spritesheet, row: 0, frames: 8);
idleAnimation = _createAnimation(spritesheet, row: 1, frames: 4);
jumpAnimation = _createAnimation(spritesheet, row: 2, frames: 6);
animation = idleAnimation;
}
SpriteAnimation _createAnimation(
Image image, {
required int row,
required int frames,
}) {
return SpriteAnimation.fromFrameData(
image,
SpriteAnimationData.sequenced(
amount: frames,
amountPerRow: frames,
textureSize: Vector2(64, 64),
texturePosition: Vector2(0, row * 64),
stepTime: 0.1,
),
);
}
void run() => animation = runAnimation;
void idle() => animation = idleAnimation;
void jump() => animation = jumpAnimation;
}class Character extends SpriteAnimationComponent with HasGameReference {
late final SpriteAnimation runAnimation;
late final SpriteAnimation idleAnimation;
late final SpriteAnimation jumpAnimation;
Future<void> onLoad() async {
final spritesheet = await game.images.load('character.png');
runAnimation = _createAnimation(spritesheet, row: 0, frames: 8);
idleAnimation = _createAnimation(spritesheet, row: 1, frames: 4);
jumpAnimation = _createAnimation(spritesheet, row: 2, frames: 6);
animation = idleAnimation;
}
SpriteAnimation _createAnimation(
Image image, {
required int row,
required int frames,
}) {
return SpriteAnimation.fromFrameData(
image,
SpriteAnimationData.sequenced(
amount: frames,
amountPerRow: frames,
textureSize: Vector2(64, 64),
texturePosition: Vector2(0, row * 64),
stepTime: 0.1,
),
);
}
void run() => animation = runAnimation;
void idle() => animation = idleAnimation;
void jump() => animation = jumpAnimation;
}// Game level
class MyGame extends FlameGame with HasCollisionDetection {
Future<void> onLoad() async {
add(Player());
add(Enemy());
add(ScreenHitbox()); // Boundary collision
}
}
// Component level
class Player extends PositionComponent with CollisionCallbacks {
Future<void> onLoad() async {
add(CircleHitbox(radius: 20));
// OR
add(RectangleHitbox(size: Vector2(40, 40)));
// OR
add(PolygonHitbox([
Vector2(0, 0),
Vector2(20, 10),
Vector2(0, 20),
]));
}
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
if (other is Enemy) {
takeDamage();
}
}
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
// Called once when collision begins
}
void onCollisionEnd(PositionComponent other) {
// Called once when collision ends
}
}// 游戏层级
class MyGame extends FlameGame with HasCollisionDetection {
Future<void> onLoad() async {
add(Player());
add(Enemy());
add(ScreenHitbox()); // 边界碰撞
}
}
// 组件层级
class Player extends PositionComponent with CollisionCallbacks {
Future<void> onLoad() async {
add(CircleHitbox(radius: 20));
// 或者
add(RectangleHitbox(size: Vector2(40, 40)));
// 或者
add(PolygonHitbox([
Vector2(0, 0),
Vector2(20, 10),
Vector2(0, 20),
]));
}
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
if (other is Enemy) {
takeDamage();
}
}
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
// 碰撞开始时调用一次
}
void onCollisionEnd(PositionComponent other) {
// 碰撞结束时调用一次
}
}// Customize collision behavior
class MyHitbox extends CircleHitbox {
MyHitbox() : super(radius: 20) {
collisionType = CollisionType.passive; // Won't trigger collisions actively
// Options: active, passive, inactive
}
}// 自定义碰撞行为
class MyHitbox extends CircleHitbox {
MyHitbox() : super(radius: 20) {
collisionType = CollisionType.passive; // 不会主动触发碰撞
// 选项:active, passive, inactive
}
}class Player extends SpriteComponent with KeyboardHandler {
Vector2 velocity = Vector2.zero();
double speed = 200;
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
velocity = Vector2.zero();
if (keysPressed.contains(LogicalKeyboardKey.keyA)) {
velocity.x = -1;
}
if (keysPressed.contains(LogicalKeyboardKey.keyD)) {
velocity.x = 1;
}
if (keysPressed.contains(LogicalKeyboardKey.keyW)) {
velocity.y = -1;
}
if (keysPressed.contains(LogicalKeyboardKey.keyS)) {
velocity.y = 1;
}
return true;
}
void update(double dt) {
position += velocity.normalized() * speed * dt;
}
}class Player extends SpriteComponent with KeyboardHandler {
Vector2 velocity = Vector2.zero();
double speed = 200;
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
velocity = Vector2.zero();
if (keysPressed.contains(LogicalKeyboardKey.keyA)) {
velocity.x = -1;
}
if (keysPressed.contains(LogicalKeyboardKey.keyD)) {
velocity.x = 1;
}
if (keysPressed.contains(LogicalKeyboardKey.keyW)) {
velocity.y = -1;
}
if (keysPressed.contains(LogicalKeyboardKey.keyS)) {
velocity.y = 1;
}
return true;
}
void update(double dt) {
position += velocity.normalized() * speed * dt;
}
}class Button extends SpriteComponent with TapCallbacks {
void onTapDown(TapDownEvent event) {
scale = Vector2.all(0.9); // Visual feedback
}
void onTapUp(TapUpEvent event) {
scale = Vector2.all(1.0);
onPressed();
}
void onTapCancel(TapCancelEvent event) {
scale = Vector2.all(1.0);
}
void onPressed() {
// Handle button press
}
}class Button extends SpriteComponent with TapCallbacks {
void onTapDown(TapDownEvent event) {
scale = Vector2.all(0.9); // 视觉反馈
}
void onTapUp(TapUpEvent event) {
scale = Vector2.all(1.0);
onPressed();
}
void onTapCancel(TapCancelEvent event) {
scale = Vector2.all(1.0);
}
void onPressed() {
// 处理按钮点击
}
}class DraggableObject extends PositionComponent with DragCallbacks {
Vector2? dragStartPosition;
Vector2? objectStartPosition;
void onDragStart(DragStartEvent event) {
dragStartPosition = event.localPosition;
objectStartPosition = position.clone();
}
void onDragUpdate(DragUpdateEvent event) {
if (dragStartPosition != null && objectStartPosition != null) {
position = objectStartPosition! + event.localPosition - dragStartPosition!;
}
}
void onDragEnd(DragEndEvent event) {
dragStartPosition = null;
objectStartPosition = null;
}
}class DraggableObject extends PositionComponent with DragCallbacks {
Vector2? dragStartPosition;
Vector2? objectStartPosition;
void onDragStart(DragStartEvent event) {
dragStartPosition = event.localPosition;
objectStartPosition = position.clone();
}
void onDragUpdate(DragUpdateEvent event) {
if (dragStartPosition != null && objectStartPosition != null) {
position = objectStartPosition! + event.localPosition - dragStartPosition!;
}
}
void onDragEnd(DragEndEvent event) {
dragStartPosition = null;
objectStartPosition = null;
}
}class MyGame extends FlameGame with HasCollisionDetection {
late final Player player;
Future<void> onLoad() async {
final world = World();
player = Player();
await world.add(player);
await add(world);
camera = CameraComponent.withFixedResolution(
world: world,
width: 800,
height: 600,
);
await add(camera);
camera.follow(player);
}
}class MyGame extends FlameGame with HasCollisionDetection {
late final Player player;
Future<void> onLoad() async {
final world = World();
player = Player();
await world.add(player);
await add(world);
camera = CameraComponent.withFixedResolution(
world: world,
width: 800,
height: 600,
);
await add(camera);
camera.follow(player);
}
}camera.follow(
player,
maxSpeed: 300,
snap: false, // Smooth following
);
// Set world bounds
camera.setBounds(
Rectangle.fromLTRB(0, 0, mapWidth, mapHeight),
);camera.follow(
player,
maxSpeed: 300,
snap: false, // 平滑跟随
);
// 设置世界边界
camera.setBounds(
Rectangle.fromLTRB(0, 0, mapWidth, mapHeight),
);// Zoom in
camera.viewfinder.zoom = 2.0;
// Animated zoom
await camera.viewfinder.zoomTo(2.0, speed: 2.0);// 放大
camera.viewfinder.zoom = 2.0;
// 动画缩放
await camera.viewfinder.zoomTo(2.0, speed: 2.0);class MyGame extends FlameGame {
Future<void> onLoad() async {
final mapComponent = await TiledComponent.load(
'map.tmx',
Vector2(32, 32), // tile size
);
await add(mapComponent);
// Access object layer
final objectLayer = mapComponent.tileMap.getLayer<ObjectGroup>('objects');
for (final object in objectLayer?.objects ?? []) {
if (object.name == 'player_spawn') {
add(Player(position: Vector2(object.x, object.y)));
}
}
}
}class MyGame extends FlameGame {
Future<void> onLoad() async {
final mapComponent = await TiledComponent.load(
'map.tmx',
Vector2(32, 32), // 瓦片大小
);
await add(mapComponent);
// 访问对象层
final objectLayer = mapComponent.tileMap.getLayer<ObjectGroup>('objects');
for (final object in objectLayer?.objects ?? []) {
if (object.name == 'player_spawn') {
add(Player(position: Vector2(object.x, object.y)));
}
}
}
}// In Tiled editor, add custom property "collision" = true to tiles
// Then in code:
Future<void> onLoad() async {
final map = await TiledComponent.load('map.tmx', Vector2(32, 32));
await add(map);
// Generate collision blocks from tile layer
final collisionLayer = map.tileMap.getLayer<TileLayer>('ground');
final collisionBlocks = <PositionComponent>[];
for (var y = 0; y < collisionLayer!.height; y++) {
for (var x = 0; x < collisionLayer.width; x++) {
final tile = collisionLayer.tileData![y][x];
if (tile.tile > 0) {
collisionBlocks.add(
PositionComponent(
position: Vector2(x * 32.0, y * 32.0),
size: Vector2.all(32),
)..add(RectangleHitbox()),
);
}
}
}
addAll(collisionBlocks);
}// 在Tiled编辑器中,为瓦片添加自定义属性 "collision" = true
// 然后在代码中:
Future<void> onLoad() async {
final map = await TiledComponent.load('map.tmx', Vector2(32, 32));
await add(map);
// 从瓦片层生成碰撞块
final collisionLayer = map.tileMap.getLayer<TileLayer>('ground');
final collisionBlocks = <PositionComponent>[];
for (var y = 0; y < collisionLayer!.height; y++) {
for (var x = 0; x < collisionLayer.width; x++) {
final tile = collisionLayer.tileData![y][x];
if (tile.tile > 0) {
collisionBlocks.add(
PositionComponent(
position: Vector2(x * 32.0, y * 32.0),
size: Vector2.all(32),
)..add(RectangleHitbox()),
);
}
}
}
addAll(collisionBlocks);
}import 'package:flame_audio/flame_audio.dart';
class MyGame extends FlameGame {
Future<void> onLoad() async {
await FlameAudio.audioCache.load('bgm.mp3');
FlameAudio.bgm.initialize();
FlameAudio.bgm.play('bgm.mp3', volume: 0.5);
}
void onRemove() {
FlameAudio.bgm.dispose();
super.onRemove();
}
}import 'package:flame_audio/flame_audio.dart';
class MyGame extends FlameGame {
Future<void> onLoad() async {
await FlameAudio.audioCache.load('bgm.mp3');
FlameAudio.bgm.initialize();
FlameAudio.bgm.play('bgm.mp3', volume: 0.5);
}
void onRemove() {
FlameAudio.bgm.dispose();
super.onRemove();
}
}class Player extends PositionComponent {
void jump() {
FlameAudio.play('jump.wav', volume: 0.8);
velocity.y = -300;
}
void collectCoin() {
FlameAudio.play('coin.wav');
}
}class Player extends PositionComponent {
void jump() {
FlameAudio.play('jump.wav', volume: 0.8);
velocity.y = -300;
}
void collectCoin() {
FlameAudio.play('coin.wav');
}
}import 'package:flame/particles.dart';
class Explosion extends ParticleSystemComponent {
Explosion({required Vector2 position})
: super(
position: position,
particle: Particle.generate(
count: 20,
lifespan: 0.5,
generator: (i) => AcceleratedParticle(
acceleration: Vector2.random() * 100,
speed: Vector2.random() * 200,
child: CircleParticle(
radius: 4,
paint: Paint()..color = Colors.orange,
),
),
),
);
}import 'package:flame/particles.dart';
class Explosion extends ParticleSystemComponent {
Explosion({required Vector2 position})
: super(
position: position,
particle: Particle.generate(
count: 20,
lifespan: 0.5,
generator: (i) => AcceleratedParticle(
acceleration: Vector2.random() * 100,
speed: Vector2.random() * 200,
child: CircleParticle(
radius: 4,
paint: Paint()..color = Colors.orange,
),
),
),
);
}Particle.generate(
count: 12,
lifespan: 1.0,
generator: (i) {
final angle = (i / 12) * 2 * pi;
return ComputedParticle(
renderer: (canvas, particle) {
final progress = particle.progress;
final radius = 10 * (1 - progress);
final alpha = (1 - progress) * 255;
canvas.drawCircle(
Offset(cos(angle) * 50 * progress, sin(angle) * 50 * progress),
radius,
Paint()..color = Colors.red.withAlpha(alpha.toInt()),
);
},
);
},
)Particle.generate(
count: 12,
lifespan: 1.0,
generator: (i) {
final angle = (i / 12) * 2 * pi;
return ComputedParticle(
renderer: (canvas, particle) {
final progress = particle.progress;
final radius = 10 * (1 - progress);
final alpha = (1 - progress) * 255;
canvas.drawCircle(
Offset(cos(angle) * 50 * progress, sin(angle) * 50 * progress),
radius,
Paint()..color = Colors.red.withAlpha(alpha.toInt()),
);
},
);
},
)void main() {
runApp(
GameWidget(
game: MyGame(),
overlayBuilderMap: {
'PauseMenu': (context, game) => PauseMenuOverlay(),
'HUD': (context, game) => HudOverlay(game: game as MyGame),
},
initialActiveOverlays: const ['HUD'],
),
);
}void main() {
runApp(
GameWidget(
game: MyGame(),
overlayBuilderMap: {
'PauseMenu': (context, game) => PauseMenuOverlay(),
'HUD': (context, game) => HudOverlay(game: game as MyGame),
},
initialActiveOverlays: const ['HUD'],
),
);
}class MyGame extends FlameGame {
void showPauseMenu() {
overlays.add('PauseMenu');
pauseEngine();
}
void resumeGame() {
overlays.remove('PauseMenu');
resumeEngine();
}
}class MyGame extends FlameGame {
void showPauseMenu() {
overlays.add('PauseMenu');
pauseEngine();
}
void resumeGame() {
overlays.remove('PauseMenu');
resumeEngine();
}
}class HudOverlay extends StatelessWidget {
final MyGame game;
const HudOverlay({required this.game});
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: game.score,
builder: (context, score, child) {
return Positioned(
top: 20,
left: 20,
child: Text(
'Score: $score',
style: TextStyle(fontSize: 24, color: Colors.white),
),
);
},
);
}
}class HudOverlay extends StatelessWidget {
final MyGame game;
const HudOverlay({required this.game});
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: game.score,
builder: (context, score, child) {
return Positioned(
top: 20,
left: 20,
child: Text(
'分数: $score',
style: TextStyle(fontSize: 24, color: Colors.white),
),
);
},
);
}
}class MyGame extends FlameGame {
Future<void> onLoad() async {
final parallax = await loadParallaxComponent(
[
ParallaxImageData('bg_layer1.png'),
ParallaxImageData('bg_layer2.png'),
ParallaxImageData('bg_layer3.png'),
],
baseVelocity: Vector2(50, 0),
velocityMultiplierDelta: Vector2(1.5, 0),
);
add(parallax);
}
}class MyGame extends FlameGame {
Future<void> onLoad() async {
final parallax = await loadParallaxComponent(
[
ParallaxImageData('bg_layer1.png'),
ParallaxImageData('bg_layer2.png'),
ParallaxImageData('bg_layer3.png'),
],
baseVelocity: Vector2(50, 0),
velocityMultiplierDelta: Vector2(1.5, 0),
);
add(parallax);
}
}class MyGame extends FlameGame {
late final JoystickComponent joystick;
late final Player player;
Future<void> onLoad() async {
player = Player();
await add(player);
joystick = JoystickComponent(
knob: CircleComponent(
radius: 20,
paint: Paint()..color = Colors.red.withAlpha(200),
),
background: CircleComponent(
radius: 50,
paint: Paint()..color = Colors.black.withAlpha(100),
),
margin: const EdgeInsets.only(left: 40, bottom: 40),
);
await add(joystick);
}
void update(double dt) {
if (joystick.direction != JoystickDirection.idle) {
player.velocity = joystick.delta * 5;
}
super.update(dt);
}
}class MyGame extends FlameGame {
late final JoystickComponent joystick;
late final Player player;
Future<void> onLoad() async {
player = Player();
await add(player);
joystick = JoystickComponent(
knob: CircleComponent(
radius: 20,
paint: Paint()..color = Colors.red.withAlpha(200),
),
background: CircleComponent(
radius: 50,
paint: Paint()..color = Colors.black.withAlpha(100),
),
margin: const EdgeInsets.only(left: 40, bottom: 40),
);
await add(joystick);
}
void update(double dt) {
if (joystick.direction != JoystickDirection.idle) {
player.velocity = joystick.delta * 5;
}
super.update(dt);
}
}dt
void update(double dt) {
// Good - frame-rate independent
position.x += speed * dt;
// Bad - frame-rate dependent
position.x += speed;
}dt
void update(double dt) {
// 良好 - 帧率无关
position.x += speed * dt;
// 糟糕 - 依赖帧率
position.x += speed;
}class BulletPool {
final List<Bullet> _available = [];
final List<Bullet> _inUse = [];
Bullet acquire() {
if (_available.isEmpty) {
return Bullet();
}
final bullet = _available.removeLast();
_inUse.add(bullet);
return bullet;
}
void release(Bullet bullet) {
_inUse.remove(bullet);
_available.add(bullet);
}
}class BulletPool {
final List<Bullet> _available = [];
final List<Bullet> _inUse = [];
Bullet acquire() {
if (_available.isEmpty) {
return Bullet();
}
final bullet = _available.removeLast();
_inUse.add(bullet);
return bullet;
}
void release(Bullet bullet) {
_inUse.remove(bullet);
_available.add(bullet);
}
}class Player extends PositionComponent {
late final SpriteAnimationTicker ticker;
void onRemove() {
ticker.dispose();
super.onRemove();
}
}class Player extends PositionComponent {
late final SpriteAnimationTicker ticker;
void onRemove() {
ticker.dispose();
super.onRemove();
}
}