Flutter for OpenHarmony 实战:CustomPainter游戏画面渲染详解

Flutter for OpenHarmony 实战:CustomPainter游戏画面渲染详解

文章目录

  • [Flutter for OpenHarmony 实战:CustomPainter游戏画面渲染详解](#Flutter for OpenHarmony 实战:CustomPainter游戏画面渲染详解)

一、前言

在贪吃蛇游戏开发过程中,我们遇到了一个棘手的问题:使用GridView实现游戏画面时,蛇和食物几乎看不见。本文将详细分析这个问题,讲解CustomPainter解决方案,以及如何绘制游戏画面。

二、从GridView到CustomPainter的演进

2.1 GridView方案的问题

最初我们使用GridView.builder来渲染游戏画面:

dart 复制代码
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: gridWidth,  // 30列
  ),
  itemCount: gridWidth * gridHeight,  // 600个格子
  itemBuilder: (context, index) {
    int x = index % gridWidth;
    int y = index ~/ gridWidth;

    return Container(
      margin: const EdgeInsets.all(1),  // 问题所在!
      decoration: BoxDecoration(
        color: isSnake ? Colors.green : Colors.grey[800],
      ),
    );
  },
)

问题分析:

  1. AspectRatio强制正方形
dart 复制代码
AspectRatio(
  aspectRatio: 1,  // 强制1:1,但网格是30x20
  child: GridView.builder(...),
)
  1. 格子太小
  • 30列 × 20列 = 600个格子
  • 假设宽度360px,每格仅12px
  • margin占2px,实际可见只有10px
  1. Margin占用过多
dart 复制代码
margin: const EdgeInsets.all(1),  // 四边各1px,共2px
  • 格子12px,margin 2px
  • 实际内容10px
  • 蛇和食物几乎看不见!

2.2 为什么选择CustomPainter

CustomPainter优势:

特性 GridView CustomPainter
性能 600个Widget 1个Widget
灵活性 受限于网格布局 完全自定义
长方形支持 需要技巧 原生支持
绘制精度 受Widget限制 像素级控制

选择理由:

  1. 游戏画面需要频繁重绘(每200ms一次)
  2. 需要精确控制每个像素
  3. 长方形网格需要自定义比例

2.3 两种方案对比

GridView方案:

dart 复制代码
// 600个Container Widget
Container(margin: EdgeInsets.all(1), ...)
Container(margin: EdgeInsets.all(1), ...)
...  // 598个更多

CustomPainter方案:

dart 复制代码
// 1个CustomPainter Widget
CustomPaint(
  painter: GamePainter(...),
)

性能对比:

  • GridView:600个Widget树节点
  • CustomPainter:1个Widget + Canvas绘制

三、CustomPainter基础

3.1 CustomPainter类介绍

dart 复制代码
class GamePainter extends CustomPainter {
  final List<Point> snake;
  final Point? food;
  final int gridWidth;
  final int gridHeight;

  GamePainter({
    required this.snake,
    required this.food,
    required this.gridWidth,
    required this.gridHeight,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制逻辑
  }

  @override
  bool shouldRepaint(GamePainter oldDelegate) {
    return true;
  }
}

核心方法:

  • paint():绘制方法,每帧调用
  • shouldRepaint():判断是否需要重绘

3.2 paint()方法详解

dart 复制代码
@override
void paint(Canvas canvas, Size size) {
  final cellWidth = size.width / gridWidth;
  final cellHeight = size.height / gridHeight;

  // 使用canvas绘制
  canvas.drawRect(...);
  canvas.drawRRect(...);
  canvas.drawLine(...);
}

Canvas常用方法:

方法 用途
drawRect 绘制矩形
drawRRect 绘制圆角矩形
drawCircle 绘制圆形
drawLine 绘制线条

3.3 shouldRepaint()性能优化

dart 复制代码
@override
bool shouldRepaint(GamePainter oldDelegate) {
  return true;  // 总是重绘
}

优化版本:

dart 复制代码
@override
bool shouldRepaint(GamePainter oldDelegate) {
  return oldDelegate.snake != snake ||
         oldDelegate.food != food;
}

说明:

  • 返回true:需要重绘
  • 返回false:复用缓存
  • 游戏场景:总是返回true(每帧都变化)

四、游戏画面绘制实现

4.1 网格线绘制

dart 复制代码
// 绘制背景
final bgPaint = Paint()..color = Colors.grey[800]!;
canvas.drawRect(
  Rect.fromLTWH(0, 0, size.width, size.height),
  bgPaint,
);

// 绘制网格线
final gridPaint = Paint()
  ..color = Colors.grey[700]!
  ..strokeWidth = 0.5;

// 垂直线
for (int i = 0; i <= gridWidth; i++) {
  canvas.drawLine(
    Offset(i * cellWidth, 0),
    Offset(i * cellWidth, size.height),
    gridPaint,
  );
}

// 水平线
for (int i = 0; i <= gridHeight; i++) {
  canvas.drawLine(
    Offset(0, i * cellHeight),
    Offset(size.width, i * cellHeight),
    gridPaint,
  );
}

绘制效果:

  • 30条垂直线(分隔30列)
  • 20条水平线(分隔20行)
  • 形成30×20网格

4.2 蛇身圆角绘制

dart 复制代码
for (int i = 0; i < snake.length; i++) {
  final segment = snake[i];
  final isHead = i == 0;

  final snakePaint = Paint()
    ..color = isHead ? Colors.green[700]! : Colors.green;

  final segmentRect = Rect.fromLTWH(
    segment.x * cellWidth + 1,
    segment.y * cellHeight + 1,
    cellWidth - 2,
    cellHeight - 2,
  );

  canvas.drawRRect(
    RRect.fromRectAndRadius(
      segmentRect,
      Radius.circular(cellWidth * 0.15)
    ),
    snakePaint,
  );
}

圆角半径计算:

dart 复制代码
Radius.circular(cellWidth * 0.15)
  • cellWidth = 12px
  • 圆角半径 = 1.8px
  • 视觉效果:柔和的圆角

4.3 蛇头眼睛绘制技巧

dart 复制代码
if (isHead) {
  final eyePaint = Paint()..color = Colors.white;
  final eyeSize = cellWidth * 0.15;  // 1.8px

  // 左眼
  canvas.drawCircle(
    Offset(
      segment.x * cellWidth + cellWidth * 0.3,
      segment.y * cellHeight + cellHeight * 0.35
    ),
    eyeSize,
    eyePaint,
  );

  // 右眼
  canvas.drawCircle(
    Offset(
      segment.x * cellWidth + cellWidth * 0.7,
      segment.y * cellHeight + cellHeight * 0.35
    ),
    eyeSize,
    eyePaint,
  );

  // 瞳孔
  final pupilPaint = Paint()..color = Colors.black;
  final pupilSize = eyeSize * 0.5;  // 0.9px

  canvas.drawCircle(..., pupilSize, pupilPaint);
  canvas.drawCircle(..., pupilSize, pupilPaint);
}

眼睛位置计算:

  • 左眼:x偏移30%,y偏移35%
  • 右眼:x偏移70%,y偏移35%
  • 瞳孔:眼睛中心,大小50%

4.4 食物圆角矩形绘制

dart 复制代码
if (food != null) {
  final foodPaint = Paint()..color = Colors.red;

  final foodRect = Rect.fromLTWH(
    food!.x * cellWidth + 1,
    food!.y * cellHeight + 1,
    cellWidth - 2,
    cellHeight - 2,
  );

  canvas.drawRRect(
    RRect.fromRectAndRadius(
      foodRect,
      Radius.circular(cellWidth * 0.2)
    ),
    foodPaint,
  );
}

食物特点:

  • 红色填充
  • 圆角半径20%(比蛇身圆一点)
  • 尺寸与蛇身相同

五、长方形网格适配

5.1 宽高比计算公式

dart 复制代码
CustomPaint(
  size: Size(
    MediaQuery.of(context).size.width - 32,  // 宽度
    (MediaQuery.of(context).size.width - 32) * gridHeight / gridWidth,  // 高度
  ),
  painter: GamePainter(...),
)

计算示例:

  • 屏幕宽度:360px
  • 减去padding:360 - 32 = 328px
  • 高度:328 × 20 / 30 = 218.67px
  • 宽高比:328 : 218.67 ≈ 1.5 : 1(符合30:20比例)

5.2 动态尺寸适配

dart 复制代码
final cellWidth = size.width / gridWidth;    // 每格宽度
final cellHeight = size.height / gridHeight; // 每格高度

适配原理:

  • Canvas尺寸动态计算
  • 格子尺寸随Canvas变化
  • 保持30:20比例

不同屏幕适配:

屏幕宽度 Canvas宽度 Canvas高度 格子尺寸
360px 328px 219px 10.9×10.9px
390px 358px 239px 11.9×11.9px
414px 382px 255px 12.7×12.7px

5.3 cellWidth/cellHeight计算

dart 复制代码
final cellWidth = size.width / gridWidth;
final cellHeight = size.height / gridHeight;

// 坐标→像素转换
double pixelX = point.x * cellWidth;
double pixelY = point.y * cellHeight;

// 格子矩形
Rect rect = Rect.fromLTWH(
  pixelX + 1,
  pixelY + 1,
  cellWidth - 2,
  cellHeight - 2,
);

+1和-2的作用:

  • +1:留出1px间隙
  • -2:左右各1px,共2px
  • 视觉效果:格子之间有间隔

【图片3:网格尺寸计算示意图】

(建议绘制:展示Canvas尺寸、格子尺寸、坐标到像素的转换关系)

六、完整GamePainter代码

dart 复制代码
class GamePainter extends CustomPainter {
  final List<Point> snake;
  final Point? food;
  final int gridWidth;
  final int gridHeight;

  GamePainter({
    required this.snake,
    required this.food,
    required this.gridWidth,
    required this.gridHeight,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final cellWidth = size.width / gridWidth;
    final cellHeight = size.height / gridHeight;

    // 1. 绘制背景
    final bgPaint = Paint()..color = Colors.grey[800]!;
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      bgPaint,
    );

    // 2. 绘制网格线
    final gridPaint = Paint()
      ..color = Colors.grey[700]!
      ..strokeWidth = 0.5;

    for (int i = 0; i <= gridWidth; i++) {
      canvas.drawLine(
        Offset(i * cellWidth, 0),
        Offset(i * cellWidth, size.height),
        gridPaint,
      );
    }

    for (int i = 0; i <= gridHeight; i++) {
      canvas.drawLine(
        Offset(0, i * cellHeight),
        Offset(size.width, i * cellHeight),
        gridPaint,
      );
    }

    // 3. 绘制食物
    if (food != null) {
      final foodPaint = Paint()..color = Colors.red;
      final foodRect = Rect.fromLTWH(
        food!.x * cellWidth + 1,
        food!.y * cellHeight + 1,
        cellWidth - 2,
        cellHeight - 2,
      );
      canvas.drawRRect(
        RRect.fromRectAndRadius(foodRect, Radius.circular(cellWidth * 0.2)),
        foodPaint,
      );
    }

    // 4. 绘制蛇
    for (int i = 0; i < snake.length; i++) {
      final segment = snake[i];
      final isHead = i == 0;

      final snakePaint = Paint()
        ..color = isHead ? Colors.green[700]! : Colors.green;

      final segmentRect = Rect.fromLTWH(
        segment.x * cellWidth + 1,
        segment.y * cellHeight + 1,
        cellWidth - 2,
        cellHeight - 2,
      );

      canvas.drawRRect(
        RRect.fromRectAndRadius(segmentRect, Radius.circular(cellWidth * 0.15)),
        snakePaint,
      );

      // 5. 绘制蛇头眼睛
      if (isHead) {
        final eyePaint = Paint()..color = Colors.white;
        final eyeSize = cellWidth * 0.15;

        canvas.drawCircle(
          Offset(segment.x * cellWidth + cellWidth * 0.3, segment.y * cellHeight + cellHeight * 0.35),
          eyeSize,
          eyePaint,
        );
        canvas.drawCircle(
          Offset(segment.x * cellWidth + cellWidth * 0.7, segment.y * cellHeight + cellHeight * 0.35),
          eyeSize,
          eyePaint,
        );

        final pupilPaint = Paint()..color = Colors.black;
        final pupilSize = eyeSize * 0.5;
        canvas.drawCircle(
          Offset(segment.x * cellWidth + cellWidth * 0.3, segment.y * cellHeight + cellHeight * 0.35),
          pupilSize,
          pupilPaint,
        );
        canvas.drawCircle(
          Offset(segment.x * cellWidth + cellWidth * 0.7, segment.y * cellHeight + cellHeight * 0.35),
          pupilSize,
          pupilPaint,
        );
      }
    }

    // 6. 绘制边框
    final borderPaint = Paint()
      ..color = Colors.green
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      borderPaint,
    );
  }

  @override
  bool shouldRepaint(GamePainter oldDelegate) {
    return true;
  }
}

七、性能分析

绘制性能:

  • 背景矩形:1次绘制调用
  • 网格线:50次绘制调用(30+20)
  • 食物:1次绘制调用
  • 蛇身:n次绘制调用(n=蛇长度)
  • 眼睛:4次绘制调用

总计:约55 + n次绘制调用

  • 蛇长10节:65次
  • 蛇长50节:105次

帧率分析:

  • 目标帧率:60 FPS
  • 每帧时间:16.67ms
  • 实际绘制:< 5ms
  • 性能:完全满足

八、总结

本文详细讲解了CustomPainter游戏渲染:

  1. GridView问题:格子太小、margin占用过多
  2. CustomPainter优势:性能好、灵活性高
  3. 绘制层次:背景→网格→食物→蛇→边框
  4. 长方形适配:动态计算cellWidth/cellHeight

关键要点:

  • CustomPainter比GridView更适合游戏场景
  • 绘制顺序影响视觉效果
  • 动态尺寸计算确保多屏适配

下篇预告:《Flutter for OpenHarmony 实战:双控制系统实现(按钮+键盘)》

社区支持

欢迎加入开源 OpenHarmony 跨平台社区,获取更多技术支持和资源:

如果本文对您有帮助,欢迎点赞、收藏和评论。您的支持是我持续创作的动力!

相关推荐
ghie90902 小时前
基于C#实现俄罗斯方块游戏
开发语言·游戏·c#
灰灰勇闯IT2 小时前
Flutter for OpenHarmony:调用原生能力与平台特定组件集成
flutter
ujainu2 小时前
Flutter + OpenHarmony 卡片式布局:Card 与 ListTile 在信息聚合界面(如服务卡片)中的应用
flutter·组件
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——读书笔记工具APP的开发流程
flutter·华为·harmonyos·鸿蒙
kirk_wang3 小时前
Flutter video_thumbnail库在鸿蒙(OpenHarmony)端的完整适配实践
flutter·移动开发·跨平台·arkts·鸿蒙
●VON3 小时前
Flutter for OpenHarmony:基于软删除状态机与双轨数据管理的 TodoList 回收站安全体系实现
安全·flutter·交互·openharmony·跨平台开发·von
九 龙3 小时前
Flutter框架跨平台鸿蒙开发——生日礼物推荐APP的开发流程
flutter·华为·harmonyos·鸿蒙
雨季6663 小时前
构建 OpenHarmony 简易数字猜谜游戏:用随机与反馈打造轻量级互动体验
javascript·flutter·游戏·ui·自动化·dart
[H*]3 小时前
Flutter框架跨平台鸿蒙开发——Hero共享元素动画详解
flutter·华为·harmonyos