news 2026/4/18 5:44:22

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter for OpenHarmony 实战:CustomPainter游戏画面渲染详解

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

文章目录

  • Flutter for OpenHarmony 实战:CustomPainter游戏画面渲染详解
    • 一、前言
    • 二、从GridView到CustomPainter的演进
      • 2.1 GridView方案的问题
      • 2.2 为什么选择CustomPainter
      • 2.3 两种方案对比
    • 三、CustomPainter基础
      • 3.1 CustomPainter类介绍
      • 3.2 paint()方法详解
      • 3.3 shouldRepaint()性能优化
    • 四、游戏画面绘制实现
      • 4.1 网格线绘制
      • 4.2 蛇身圆角绘制
      • 4.3 蛇头眼睛绘制技巧
      • 4.4 食物圆角矩形绘制
    • 五、长方形网格适配
      • 5.1 宽高比计算公式
      • 5.2 动态尺寸适配
      • 5.3 cellWidth/cellHeight计算
    • 六、完整GamePainter代码
    • 七、性能分析
    • 八、总结
    • 社区支持

一、前言

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

二、从GridView到CustomPainter的演进

2.1 GridView方案的问题

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

GridView.builder(gridDelegate:constSliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:gridWidth,// 30列),itemCount:gridWidth*gridHeight,// 600个格子itemBuilder:(context,index){int x=index%gridWidth;int y=index~/gridWidth;returnContainer(margin:constEdgeInsets.all(1),// 问题所在!decoration:BoxDecoration(color:isSnake?Colors.green:Colors.grey[800],),);},)

问题分析:

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

2.2 为什么选择CustomPainter

CustomPainter优势:

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

选择理由:

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

2.3 两种方案对比

GridView方案:

// 600个Container WidgetContainer(margin:EdgeInsets.all(1),...)Container(margin:EdgeInsets.all(1),...)...// 598个更多

CustomPainter方案:

// 1个CustomPainter WidgetCustomPaint(painter:GamePainter(...),)

性能对比:

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

三、CustomPainter基础

3.1 CustomPainter类介绍

classGamePainterextendsCustomPainter{finalList<Point>snake;finalPoint?food;finalint gridWidth;finalint gridHeight;GamePainter({requiredthis.snake,requiredthis.food,requiredthis.gridWidth,requiredthis.gridHeight,});@overridevoidpaint(Canvascanvas,Sizesize){// 绘制逻辑}@overrideboolshouldRepaint(GamePainteroldDelegate){returntrue;}}

核心方法:

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

3.2 paint()方法详解

@overridevoidpaint(Canvascanvas,Sizesize){finalcellWidth=size.width/gridWidth;finalcellHeight=size.height/gridHeight;// 使用canvas绘制canvas.drawRect(...);canvas.drawRRect(...);canvas.drawLine(...);}

Canvas常用方法:

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

3.3 shouldRepaint()性能优化

@overrideboolshouldRepaint(GamePainteroldDelegate){returntrue;// 总是重绘}

优化版本:

@overrideboolshouldRepaint(GamePainteroldDelegate){returnoldDelegate.snake!=snake||oldDelegate.food!=food;}

说明:

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

四、游戏画面绘制实现

4.1 网格线绘制

// 绘制背景finalbgPaint=Paint()..color=Colors.grey[800]!;canvas.drawRect(Rect.fromLTWH(0,0,size.width,size.height),bgPaint,);// 绘制网格线finalgridPaint=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 蛇身圆角绘制

for(int i=0;i<snake.length;i++){finalsegment=snake[i];finalisHead=i==0;finalsnakePaint=Paint()..color=isHead?Colors.green[700]!:Colors.green;finalsegmentRect=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,);}

圆角半径计算:

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

4.3 蛇头眼睛绘制技巧

if(isHead){finaleyePaint=Paint()..color=Colors.white;finaleyeSize=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,);// 瞳孔finalpupilPaint=Paint()..color=Colors.black;finalpupilSize=eyeSize*0.5;// 0.9pxcanvas.drawCircle(...,pupilSize,pupilPaint);canvas.drawCircle(...,pupilSize,pupilPaint);}

眼睛位置计算:

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

4.4 食物圆角矩形绘制

if(food!=null){finalfoodPaint=Paint()..color=Colors.red;finalfoodRect=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 宽高比计算公式

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 动态尺寸适配

finalcellWidth=size.width/gridWidth;// 每格宽度finalcellHeight=size.height/gridHeight;// 每格高度

适配原理:

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

不同屏幕适配:

屏幕宽度Canvas宽度Canvas高度格子尺寸
360px328px219px10.9×10.9px
390px358px239px11.9×11.9px
414px382px255px12.7×12.7px

5.3 cellWidth/cellHeight计算

finalcellWidth=size.width/gridWidth;finalcellHeight=size.height/gridHeight;// 坐标→像素转换double pixelX=point.x*cellWidth;double pixelY=point.y*cellHeight;// 格子矩形Rectrect=Rect.fromLTWH(pixelX+1,pixelY+1,cellWidth-2,cellHeight-2,);

+1和-2的作用:

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

【图片3:网格尺寸计算示意图】
(建议绘制:展示Canvas尺寸、格子尺寸、坐标到像素的转换关系)

六、完整GamePainter代码

classGamePainterextendsCustomPainter{finalList<Point>snake;finalPoint?food;finalint gridWidth;finalint gridHeight;GamePainter({requiredthis.snake,requiredthis.food,requiredthis.gridWidth,requiredthis.gridHeight,});@overridevoidpaint(Canvascanvas,Sizesize){finalcellWidth=size.width/gridWidth;finalcellHeight=size.height/gridHeight;// 1. 绘制背景finalbgPaint=Paint()..color=Colors.grey[800]!;canvas.drawRect(Rect.fromLTWH(0,0,size.width,size.height),bgPaint,);// 2. 绘制网格线finalgridPaint=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){finalfoodPaint=Paint()..color=Colors.red;finalfoodRect=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++){finalsegment=snake[i];finalisHead=i==0;finalsnakePaint=Paint()..color=isHead?Colors.green[700]!:Colors.green;finalsegmentRect=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){finaleyePaint=Paint()..color=Colors.white;finaleyeSize=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,);finalpupilPaint=Paint()..color=Colors.black;finalpupilSize=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. 绘制边框finalborderPaint=Paint()..color=Colors.green..strokeWidth=2..style=PaintingStyle.stroke;canvas.drawRect(Rect.fromLTWH(0,0,size.width,size.height),borderPaint,);}@overrideboolshouldRepaint(GamePainteroldDelegate){returntrue;}}

七、性能分析

绘制性能:

  • 背景矩形: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 跨平台社区,获取更多技术支持和资源:

  • 社区论坛:开源 OpenHarmony 跨平台开发者社区
  • 技术交流:参与社区讨论,分享开发经验

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 5:38:36

函数补充/数据存储

目录 1 函数的深入理解 1.1 函数的返回值&#xff08;return&#xff09; 1.1.1 基本语法 1.1.2 使用要点 1.1.3 返回值类型 1.1.4 流程控制对比 1.2 arguments 对象 1.2.1 特性 1.2.2 应用场景 1.2.3 arguments.callee 1.3 函数方法对比 1.3.1 传参方式 1.3.2 th…

作者头像 李华
网站建设 2026/4/18 5:42:44

清华大学等多所顶尖院校联手揭秘智能数据准备革命

这项由清华大学、上海交通大学、微软研究院、麻省理工学院等多所世界顶尖学府和科研机构联合完成的研究发表于2025年1月的《IEEE知识与数据工程汇刊》&#xff0c;论文编号为IEEE TRANSACTIONS ON KNOWLEDGE AND DATA ENGINEERING, VOL. 0, NO. 0, JANUARY 2025&#xff0c;详细…

作者头像 李华
网站建设 2026/4/15 20:49:44

苏州大学突破:AI评判官能评估人工智能的记忆管理能力吗?

这项由苏州大学LCM实验室联合中国移动(苏州)共同完成的突破性研究发表于2026年1月&#xff0c;论文编号为arXiv:2601.11969v1。有兴趣深入了解的读者可以通过该编号查询完整论文。 想象一下&#xff0c;当我们看一部长达三小时的电影时&#xff0c;大脑需要不断记忆和管理信息—…

作者头像 李华
网站建设 2026/4/7 12:01:34

近屿智能发现:年终奖背后的IT赛道秘密

大家好&#xff0c;这里是近屿智能。盼望着盼望着&#xff0c;发年终奖的季节终于临近。近期&#xff0c;已有不少人在网络中晒出第一波收获&#xff0c;金额之丰厚&#xff0c;令人羡慕不已。而提起年终奖&#xff0c;互联网大厂始终是话题的焦点。大厂年终奖盘点&#xff1a;…

作者头像 李华
网站建设 2026/4/16 15:39:22

上市传闻再起,“平头哥”将如何搅动AI芯片市场?

文/王慧莹 编辑/子夜 1月23日&#xff0c;阿里巴巴港股开盘站上171港元/股高位&#xff0c;创下去年11月以来的新高。 让阿里股价应声大涨的&#xff0c;是旗下低调了八年的芯片制作业务——平头哥。1月 22 日&#xff0c;彭博社披露&#xff0c;阿里计划将平头哥重组为员工…

作者头像 李华
网站建设 2026/4/16 14:58:42

ARM架构——ADC 模数转换器

目录 一、ADC 基础概念 二、逐次逼近型 ADC 三、关键概念解析 2.1 量程 2.2 分辨率 2.3 精度 2.4 实际应用选择指南 四、IMX6ULL 中的 ADC 模块 4.1 硬件配置 4.2 关键寄存器配置 五、驱动代码实现 5.1 自动校准函数 5.2 初始化函数 5.3 采样与电压转换函数 5.4…

作者头像 李华