news 2026/6/10 11:39:17

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/6/10 11:39:56

Agentic AI的“责任边界”:提示工程架构师必须明确的5个问题

Agentic AI的“责任边界”&#xff1a;提示工程架构师必须明确的5个核心问题 摘要/引言&#xff1a;当AI从“工具”变成“代理人”&#xff0c;我们该如何划清责任&#xff1f; 清晨7点&#xff0c;你刚到公司&#xff0c;就收到用户的投诉邮件&#xff1a;“你们的AI助手帮我订…

作者头像 李华
网站建设 2026/6/10 2:22:33

降AI工具避坑指南:2026年真正好用的就这几款

降AI工具避坑指南&#xff1a;2026年真正好用的就这几款 我室友花了150块买了个降AI工具&#xff0c;结果AI率只降了5个百分点&#xff0c;客服还联系不上。 这种坑太多了。市面上降AI工具几十款&#xff0c;宣传都说「一键降AI」「效果显著」&#xff0c;实际试用下来&#…

作者头像 李华
网站建设 2026/6/10 11:35:43

2026毕业季必备:6款降AI率工具红黑榜

2026毕业季必备&#xff1a;6款降AI率工具红黑榜 花了两周时间&#xff0c;测试了十几款降AI工具&#xff0c;踩了不少坑。 今天把结果整理成红黑榜&#xff0c;告诉你哪些能用、哪些是坑。先说结论&#xff1a;嘎嘎降AI&#xff08;www.aigcleaner.com&#xff09;和比话降A…

作者头像 李华
网站建设 2026/6/10 11:38:50

【开题答辩全过程】以 基于ssm的电影推荐与分享平台的设计与实现为例,包含答辩的问题和答案

个人简介一名14年经验的资深毕设内行人&#xff0c;语言擅长Java、php、微信小程序、Python、Golang、安卓Android等开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。感谢大家的…

作者头像 李华
网站建设 2026/6/10 11:43:32

springboot评教高校在线教师教学学术能力评价系统

目录 系统概述技术架构核心功能创新点应用价值 开发技术源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01; 系统概述 SpringBoot评教系统是一款基于高校在线教师教学学术能力评价的管理平台&#xff0c;旨在通过数字化手段实现教学质量的…

作者头像 李华
网站建设 2026/6/10 13:24:24

毕业论文AI率从90%降到10%,这5款工具亲测有效

结论先行&#xff1a;从90%到10%&#xff0c;靠的是这两款工具 92%。 这是我用DeepSeek辅助写完初稿后&#xff0c;知网检测给我的AI率。当时离提交只剩一周&#xff0c;我整个人都慌了。 后来花了三天时间&#xff0c;把市面上能找到的降AI工具都试了一遍。最终结论&#x…

作者头像 李华