30天计划第19天-Flame环境搭建与基础

-
-
2025-12-02 18:41
  • 学习内容

    1. 安装 Flame 依赖(flame: ^1.14.0,按官网最新版本)

    2. Flame 核心:GameWidget(游戏容器)、FlameGame(游戏基类)

  • 实践任务

    1. 创建第一个 Flame 项目:运行一个空白游戏页面,显示 “我的第一个 Flame 游戏”

    2. 理解游戏循环(update 刷新逻辑、render 绘制画面)

一、环境搭建

pubspec.yamldependencies节点下添加依赖:

dependencies:
  flutter:
    sdk: flutter
  # 添加 Flame 依赖(从官网获取最新版)
  flame: ^1.34.0

二、Flame核心组件解析

1. GameWidget:游戏容器(UI层)

  • 作用

    GameWidget是Flame与Flutter UI交互的“桥梁”,本质是一个Flutter Widget,负责将Flame游戏内容嵌入到Flutter页面中,支持与其他Flutter组件(如:AppBar、Button)混合使用。

  • 核心属性

    属性名类型作用
    gameFlameGame必传参数,绑定对应的F辣么Game实例,将游戏逻辑与容器关联。
    backgroundBuilderWidgetBuilder?可选,用于构建游戏背景,支持纯色、图片等任意Flutter Widget
    sizeSize可选,指定游戏容器的固定大小,不设置则默认占满父组件空间。
    overlayBuilderMapMap<String, WidgetBuilder>可选,用于添加游戏覆盖层(如:游戏暂停界面、分数显示)

2. FlameGame:游戏基类(逻辑层)

  • 作用

    FlameGame是所有Flame游戏的“父类”,封装了游戏的核心生命周期(初始化、更新、绘制、销毁),提供了游戏循环(update + render)的默认实现,开发者只需要继承它并按需重写方法即可。

  • 核心方法(生命周期)

    方法名作用调用时机
    onLoad()游戏初始化(加载资源、初始化变量)游戏启动时调用一次,在update和render之前执行
    update(double dt)状态刷新(逻辑计算)每帧调用一次,dt为当前帧与上一帧的时间间隔(单位:秒),用于处理移动、碰撞等逻辑
    render(Canvas canvas)画面绘制(渲染内容)每帧调用一次,在update之后执行,通过Canvas绘制游戏元素(文字、图片、图形)
    onDetach()游戏销毁(释放资源)游戏容器(GameWidget)被销毁时调用,用于释放图片、音频等资源,避免内存泄露

三、深入理解Flame游戏循环

  • 循环的本质

    Flame游戏循环是“连续执行的帧序列”,每一帧包含两个核心步骤:update(逻辑刷新)→render(画面绘制),默认帧率为60帧/秒,即:每秒执行60次update和render,确保画面流畅。

  • update方法:状态刷新逻辑
    • 核心作用

      处理游戏的“动态逻辑”,不涉及画面绘制,例如:

      • 角色位置更新(根据速度和时间间隔计算新坐标)
      • 碰撞检测(判断角色是否与障碍物重叠)
      • 分数计算(每帧累加时间分)
      • 敌人AI行为(每帧判断敌人移动方向)
    • 关键参数dt(delta time)
      • dt表示“当前帧与上一帧的时间间隔”,单位为:秒,例如:60帧每秒时,dt≈0.0167
      • 为什么需要dt:因为不同设备帧率不同,用dt做“时间补偿”,确保游戏速度在不同设备上一致。
    • 示例:让文字随时间移动

      class MyFirstGame extends FlameGame {
        // 添加文字位置变量
        double _textX = 0;
        // 文字移动速度(像素/秒)
        final double _textSpeed = 100;
      
        @override
        Future<void> onLoad() async {
          super.onLoad();
          // 初始化文字 X 坐标为左侧(Y 坐标仍居中)
          _textX = 0;
        }
      
        @override
        void update(double dt) {
          super.update(dt);
          // 计算每帧文字移动的距离:速度 × 时间间隔
          final moveDistance = _textSpeed * dt;
          // 更新文字 X 坐标
          _textX += moveDistance;
          // 文字超出屏幕右侧后,重置到左侧
          if (_textX > size.x) {
            _textX = -200; // -200 是文字宽度(避免突然消失)
          }
        }
      
        @override
        void render(Canvas canvas) {
          super.render(canvas);
          // 绘制时使用更新后的 _textX
          final textStyle = TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold);
          final textPainter = TextPainter(
            text: TextSpan(text: "我的第一个 Flame 游戏", style: textStyle),
            textDirection: TextDirection.ltr,
          );
          textPainter.layout();
          // Y 坐标仍居中,X 坐标用 _textX
          final textY = (size.y - textPainter.height) / 2;
          textPainter.paint(canvas, Offset(_textX, textY));
        }
      }
  • render方法:画面绘制逻辑
    • 核心作用

      update后的状态“绘制”到屏幕上,通过Canvas对象实现,支持绘制:

      • 文字
      • 图形(矩形、圆形、线段,通过PaintPath实现)
      • 图片(通过Flame提供的Sprite组件加载和绘制)
      • 动画(通过SpriteAnimation实现序列帧动画)
    • 绘制顺序

      render方法中,代码执行顺序即“绘制层级”,先执行的代码绘制在下层,后执行的代码绘制在上层。

四、综合实战

  • 任务

    创建一个空白游戏页面,背景为黑色,居中显示白色文字“我的第一个Flame游戏”,无复杂交互,聚焦核心组件与游戏循环的理解。

  • 代码实现
    • 创建游戏类(my_first_game.dart

      在lib目录下新建my_first_game.dart文件,编写自定义FlameGame类

      import 'dart:async';
      
      import 'package:flame/game.dart';
      import 'package:flutter/material.dart';
      
      
      // 自定义游戏类,继承FlamGame
      class MyFirstGame extends FlameGame{
        // 游戏初始化:onLoad只执行一次
        @override
        Future<void> onLoad() async {
          super.onLoad();
          // 此处可执行资源架子逻辑
          print('游戏初始化完成,准备启动。');
        }
      
        // 游戏循环:update每帧执行(处理逻辑,不负责绘制) 非必须重载函数
        @override
        void update(double dt) {
          super.update(dt);
      
          // dt = 时间间隔(约1/60秒,因设备帧率而异)
        }
      
        // 游戏循环:render每帧执行(处理绘制,不负责逻辑)
        @override
        void render(Canvas canvas) {
          super.render(canvas);
      
          _drawGameText(canvas);
        }
      
        // 辅助方法:绘制游戏文字
        void _drawGameText(Canvas canvas) {
          // 自定义文字样式
          final textStyle = TextStyle(
            color: Colors.white,
            fontSize: 24,
            fontWeight: FontWeight.bold
          );
      
          // 创建TextPainter(Flame绘制文字依赖的工具)
          final textPainter = TextPainter(
            text: TextSpan(text: '我的第一个Flame游戏', style: textStyle),
            textDirection: TextDirection.ltr //文字方向:从左向右
          );
      
          // 布局文字(计算文字宽高)
          textPainter.layout();
          final textX = (size.x - textPainter.width) / 2; // 水平居中
          final textY = (size.y - textPainter.height) / 2; // 垂直居中
      
          // 绘制文字
          textPainter.paint(canvas, Offset(textX, textY));
        }
      }
    • 创建游戏页面(game_screen.dart

      在lib目录下新建game_screen.dart文件,编写包含GameWidget的Flutter页面

      import 'package:flutter/material.dart';
      import 'package:flame/game.dart';
      import 'my_first_game.dart';
      
      
      // 游戏展示页面
      class GameScreen extends StatelessWidget {
        const GameScreen({super.key});
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            // 页面标题栏
            appBar: AppBar(
              title: const Text('Flame游戏示例'),
              centerTitle: true,
              backgroundColor: Colors.blueAccent,
            ),
            // 页面主体:导入GameWidget
            body: GameWidget(
              // 绑定自定义游戏实例
              game: MyFirstGame(),
              // 设置游戏容器背景色为黑色
              backgroundBuilder: (context) => Container(
                color: Colors.black,
              ),
              // 可选:添加加载占位界面(游戏初始化时显示)
              loadingBuilder: (context) => const Center(
                child: CircularProgressIndicator(color: Colors.blueAccent,),
              ),
              // 可选,添加错误提示界面(游戏初始化失败时显示)
              errorBuilder: (context, error) => Center(
                child: Text(
                  '游戏加载失败:$error',
                  style: const TextStyle(color: Colors.red),
                ),
              ),
            ),
          );
        }
      }
    • 修改入口文件(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(),
          );
        }
      }
  • 运行效果


目录