30天计划第21天-游戏输入控制(触屏 / 键盘)

-
-
2025-12-04 19:56
  • 学习内容

  1. 触屏控制:TapDetector(点击)、PanDetector(拖动)

  2. 键盘控制:KeyboardHandler(上下左右键移动)

  • 实践任务

    1. 实现 “拖动玩家”:触屏拖动时,玩家精灵跟随手指移动

    2. 桌面 / Web 端支持键盘:按方向键,玩家精灵向对应方向移动(速度可配置)

一、核心概念解析

1. 触屏输入检测器

Flame提供Detector系列mixin用于监听触屏事件,核心两类:

检测器作用核心回调方法
TapDetector监听点击/双击事件(适合点击交互,如:按钮、攻击)onTapDown(按下)、onTapUp(抬起)
PanDetector监听拖动事件(适合精灵随手指移动)onPanUpdate(拖动中)
  • 使用方式:通过mixin混入到FlameGame或Component中,重写回调方法即可监听事件。

2. 键盘事件处理器

KeyboardHandler是Flame提供的键盘事件处理mixin,适配桌面/Web端键盘输入

核心回调作用
onKeyEvent监听所有键盘事件(按下/抬起)
LogicalKeyboardKey枚举类,定义所有键盘按键(如:方向键:arrowLeft/arrowRight/arrowUp/arrowDown)
  • 关键特性:支持多按键同时按下(如:同时按上+右实现斜向移动)。

3. 移动速度控制逻辑

为了保证不同帧率设备(如:30fps/120fps)移动速度一致,需基于dt(帧间隔时间,单位:秒)计算移动距离

// 核心公式:移动距离 = 移动速度(像素/秒) × 帧间隔时间(dt)
final moveDistance = speed * dt;
// 示例:速度 200 像素/秒,60fps 下 dt≈0.0167,每帧移动约 3.34 像素

4. 坐标系统

Flame以游戏容器左上角为原点,x轴向右为正方向,y轴向下为正方向。

二、综合实战

  • 任务目标
    • 加载玩家精灵,初始显示在屏幕中间
    • 移动端:手指拖动精灵,精灵跟随手指实时移动
    • 桌面/Web端:按方向键(↑↓←→),精灵向对应方向移动(速度可配置)
    • 边界限制:精灵不超出游戏容器范围(避免移出屏幕)
  • 项目准备
    • 图片路径:项目根目录/assets/images/dog.png(推荐尺寸100X100)
    • pubsepec.yaml依赖及资源声明

      dependencies:
        flutter:
          sdk: flutter
        flame: ^1.34.0 # 请使用官网最新版本
        
      flutter:
        uses-material-design: true
        assets:
          - assets/images/
  • 代码实现
    • 游戏类(player_control_game.dart

      集成触屏检测器、键盘处理器,实现移动逻辑

      import 'dart:async';
      
      import 'package:flame/game.dart';
      import 'package:flame/components.dart';
      import 'package:flame/cache.dart';
      import 'package:flame/input.dart';
      import 'package:flame/events.dart';
      import 'package:flutter/material.dart';
      import 'package:flutter/services.dart';
      
      
      
      // 自定义游戏类:混入PanDetector(拖动)、KeyboardHandler(键盘)
      class PlayerControlGame extends FlameGame with PanDetector, KeyboardEvents {
        final Images _images = Images();
        late SpriteComponent _player;
        final double _moveSpeed = 200;
        bool _moveLeft = false;
        bool _moveRight = false;
        bool _moveUp = false;
        bool _moveDown = false;
      
        @override
        Future<void> onLoad() async {
          await super.onLoad();
      
          final playerImage = await _images.load('dog.png');
          final playerSprite = Sprite(playerImage);
      
          _player = SpriteComponent(
            sprite: playerSprite,
            size: Vector2(100, 100),
            anchor: Anchor.center,
            position: Vector2(size.x / 2, size.y / 2)
          );
      
          add(_player);
        }
      
        // ------------- 触屏拖动逻辑 ----------------------------
        @override
        void onPanUpdate(DragUpdateInfo info) {
          super.onPanUpdate(info);
      
          // 获取拖动的偏移量(屏幕坐标 -> 游戏场景坐标)
          final delta = info.delta.global;
      
          // 更新玩家位置(跟随手指移动)
          _player.position += delta;
      
          // 边界限制,防止精灵移出屏幕
          _limitPlayerBounds();
        }
      
        // ------------- 键盘控制逻辑 ----------------------------
        @override
        KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
          // 更新方向键状态(按下/抬起)
          _moveLeft = keysPressed.contains(LogicalKeyboardKey.arrowLeft);
          _moveRight = keysPressed.contains(LogicalKeyboardKey.arrowRight);
          _moveUp = keysPressed.contains(LogicalKeyboardKey.arrowUp);
          _moveDown = keysPressed.contains(LogicalKeyboardKey.arrowDown);
      
          return super.onKeyEvent(event, keysPressed);
        }
      
        // ------------- 游戏循环:更新玩家位置 ------------------------
        @override
        void update(double dt) {
          super.update(dt);
      
          // 基于方向键盘状态计算移动偏移量
          Vector2 moveDelta = Vector2.zero();
          if (_moveLeft) moveDelta.x -= _moveSpeed * dt;
          if (_moveRight) moveDelta.x += _moveSpeed * dt;
          if (_moveUp) moveDelta.y -= _moveSpeed * dt;
          if (_moveDown) moveDelta.y += _moveSpeed * dt;
      
          // 更新玩家位置(键盘控制)
          if (moveDelta != Vector2.zero()) {
            _player.position += moveDelta;
            _limitPlayerBounds();
          }
        }
      
        // ------------- 辅助方法:边界限制 ---------------------------
        void _limitPlayerBounds() {
          // 左边界:精灵左边缘不小于0
          if (_player.position.x - _player.size.x / 2 < 0) {
            _player.position.x = _player.size.x / 2;
          }
      
          // 右边界:精灵右边缘不大于容器宽度
          if (_player.position.x + _player.size.x / 2 > size.x) {
            _player.position.x = size.x - _player.size.x / 2;
          }
      
          // 上边界:精灵上边缘不小于0
          if (_player.position.y - _player.size.y / 2 < 0) {
            _player.position.y = _player.size.y / 2;
          }
      
          // 下边界:精灵下边缘不大于容器高度
          if (_player.position.y + _player.size.y / 2 > size.y) {
            _player.position.y = size.y - _player.size.y / 2;
          }
        }
      }
    • 游戏页面(game_screen.dart

       import 'package:flutter/material.dart';
      import 'package:flame/game.dart';
      import 'player_control_game.dart';
      
      
      class GameScreen extends StatelessWidget {
        const GameScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          final game = PlayerControlGame();
      
          return Scaffold(
            appBar: AppBar(
              title: const Text('Flame输入控制:触屏 + 键盘'),
              centerTitle: true,
              backgroundColor: Colors.blueAccent,
            ),
            body: GameWidget(
              game: game,
              backgroundBuilder: (context) => Container(color: Colors.black,),
              loadingBuilder: (context) => const Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CircularProgressIndicator(color: Colors.blueAccent,),
                    SizedBox(height: 20,),
                    Text(
                      '游戏加载中...',
                      style: TextStyle(color: Colors.white, fontSize: 16),
                    )
                  ],
                ),
              ),
              errorBuilder: (context, error) => Center(
                child: Text(
                  '加载失败:$error',
                  style: TextStyle(color: Colors.red, fontSize: 16),
                  textAlign: TextAlign.center,
                ),
              ),
              // 自动聚焦,监听键盘事件
              autofocus: true,
            ),
          );
        }
      }
    • 入口文件(main.dart

      import 'package:flutter/material.dart';
      import 'game_screen.dart';
      
      void main() => runApp(const App());
      
      class App extends StatelessWidget {
        const App({super.key});
      
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            title: 'Flame输入控制示例',
            theme: ThemeData(primarySwatch: Colors.blue),
            home: const GameScreen(),
          );
        }
      }
  • 运行效果

三、拓展

1. mixin核心定义

mixin是Dart中的一种特殊类型,用于定义“可复用的方法和属性集合”,通过with关键字可以将一个或多个mixin混入到类中,被混入的类会自动拥有这些方法和属性,无需继承。

2. mixin解决的核心问题

Dart是单继承语言(即:一个类只能extends一个父类),如果用传统继承实现“触屏+键盘”功能,会面临以下问题:

// 错误:Dart 不支持多继承
class PlayerControlGame extends FlameGame, PanDetector, KeyboardHandler { ... }

3. mixin基础语法

(1)定义

mixin关键字定义(可包含属性、方法,甚至异步方法)

// 定义一个“移动能力”的 mixin
mixin Movable {
  // 可复用属性
  double speed = 200;

  // 可复用方法
  void move(double dx, double dy) {
    print("移动偏移:x=$dx, y=$dy");
  }
}

// 定义一个“边界检测”的 mixin
mixin Bounded {
  void checkBounds(double x, double y, double maxX, double maxY) {
    if (x < 0 || x > maxX || y < 0 || y > maxY) {
      print("超出边界!");
    }
  }
}

(2)使用mixin(with关键字)

通过with将mixin混入类中,可混入多个,用逗号分隔

// 主类继承 FlameGame,同时混入 Movable + Bounded
class PlayerControlGame extends FlameGame with Movable, Bounded {
  @override
  void update(double dt) {
    super.update(dt);
    // 直接使用 mixin 中的属性和方法
    move(10 * dt, 20 * dt); // 调用 Movable 的 move 方法
    checkBounds(0, 0, size.x, size.y); // 调用 Bounded 的 checkBounds 方法
    print("当前速度:$speed"); // 访问 Movable 的 speed 属性
  }
}

(3)带约束的mixin(on关键字)

有时希望mixin只能被特定类/父类的子类混入(如:Flame的PanDector只能混入FlameGame或Component),用on约束

// 约束:该 mixin 只能被 FlameGame 或其子类混入
mixin PanDetector on FlameGame {
  // 触屏拖动回调(只有 FlameGame 子类能使用)
  void onPanUpdate(DragUpdateInfo info) {
    // 核心逻辑...
  }
}

// 合法:PlayerControlGame 继承 FlameGame
class PlayerControlGame extends FlameGame with PanDetector { ... }

// 非法:MyWidget 不是 FlameGame 子类,编译报错
class MyWidget extends StatelessWidget with PanDetector { ... }

4. mixin、继承、接口的核心区别

特性mixin(混入)继承(extends)接口(implements)
核心目的复用多组独立的功能逻辑复用父类的核心逻辑(单继承)约束类的方法签名(无实现)
方法实现可包含完整实现(直接用)可包含完整实现(可重写)仅定义方法签名(必须重写)
多重复用支持(多个with)不支持(单继承)支持(多个implements)
与原类的关系注入功能,不改变继承关系是“is-a”关系(子类是父类)是“has-a”关系(类实现接口)

 


目录