当前位置: 首页 > news >正文

HarmonyOS 小游戏《对战五子棋》开发第18篇 - 棋盘设计

棋盘是游戏的"脸面"——Canvas让每一颗棋子都栩栩如生

设计棋盘如下:

代码如下:

/** * ChessBoardView.ets - 可复用的五子棋棋盘组件 * 使用Canvas绘制棋盘、网格、棋子 */import{BOARD_SIZE,EMPTY,BLACK,WHITE}from'./GameConstants';constBOARD_BG_COLOR:string='#D4A868';constLINE_COLOR:string='#5C4033';constSTAR_COLOR:string='#3B2F2F';constLAST_MOVE_COLOR:string='#E63946';@Componentexportstruct ChessBoardView{/** 棋盘数据(扁平数组,row*15+col) */@Prop@Watch('onDataChange')boardData:number[]=[];/** 最后一手行 */@ProplastMoveRow:number=-1;/** 最后一手列 */@ProplastMoveCol:number=-1;/** 点击回调 */onCellClick:(row:number,col:number)=>void=()=>{};privatecontext:CanvasRenderingContext2D=newCanvasRenderingContext2D(newRenderingContextSettings(true));privatecanvasSize:number=320;privateisReady:boolean=false;build(){Canvas(this.context).width('100%').aspectRatio(1).onReady(()=>{this.isReady=true;this.drawBoard();}).onClick((event:ClickEvent)=>{this.handleClick(event);}).onAreaChange((_oldValue:Area,newValue:Area)=>{constw=newValue.width;if(typeofw==='number'){this.canvasSize=w;if(this.isReady){this.drawBoard();}}})}/** 数据变化时重绘 */onDataChange():void{if(this.isReady){this.drawBoard();}}/** 处理点击事件 */privatehandleClick(event:ClickEvent):void{constcellSize=this.canvasSize/BOARD_SIZE;constx=event.x;consty=event.y;constcol=Math.round((x-cellSize/2)/cellSize);constrow=Math.round((y-cellSize/2)/cellSize);if(row>=0&&row<BOARD_SIZE&&col>=0&&col<BOARD_SIZE){this.onCellClick(row,col);}}/** 绘制棋盘 */privatedrawBoard():void{constctx=this.context;constsize=this.canvasSize;if(size<=0)return;constcellSize=size/BOARD_SIZE;consthalfCell=cellSize/2;// 清空画布ctx.clearRect(0,0,size,size);// 绘制背景ctx.fillStyle=BOARD_BG_COLOR;ctx.fillRect(0,0,size,size);// 绘制网格线ctx.strokeStyle=LINE_COLOR;ctx.lineWidth=1;for(leti=0;i<BOARD_SIZE;i++){constpos=halfCell+i*cellSize;// 水平线ctx.beginPath();ctx.moveTo(halfCell,pos);ctx.lineTo(size-halfCell,pos);ctx.stroke();// 垂直线ctx.beginPath();ctx.moveTo(pos,halfCell);ctx.lineTo(pos,size-halfCell);ctx.stroke();}// 绘制星位conststars:number[][]=[[3,3],[3,11],[7,7],[11,3],[11,11]];ctx.fillStyle=STAR_COLOR;for(conststarofstars){constsx=halfCell+star[1]*cellSize;constsy=halfCell+star[0]*cellSize;ctx.beginPath();ctx.arc(sx,sy,3,0,Math.PI*2);ctx.fill();}// 绘制棋子for(leti=0;i<BOARD_SIZE;i++){for(letj=0;j<BOARD_SIZE;j++){constidx=i*BOARD_SIZE+j;if(idx<this.boardData.length){constpiece=this.boardData[idx];if(piece!==EMPTY){constpx=halfCell+j*cellSize;constpy=halfCell+i*cellSize;constradius=cellSize*0.42;this.drawPiece(ctx,px,py,radius,piece);}}}}// 绘制最后一手标记if(this.lastMoveRow>=0&&this.lastMoveCol>=0){constpx=halfCell+this.lastMoveCol*cellSize;constpy=halfCell+this.lastMoveRow*cellSize;ctx.strokeStyle=LAST_MOVE_COLOR;ctx.lineWidth=2;ctx.beginPath();ctx.arc(px,py,cellSize*0.16,0,Math.PI*2);ctx.stroke();}}/** 绘制单个棋子(带渐变效果) */privatedrawPiece(ctx:CanvasRenderingContext2D,x:number,y:number,radius:number,player:number):void{if(player===BLACK){constgradient=ctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,x,y,radius);gradient.addColorStop(0,'#666666');gradient.addColorStop(0.5,'#2A2A2A');gradient.addColorStop(1,'#111111');ctx.fillStyle=gradient;}else{constgradient=ctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,x,y,radius);gradient.addColorStop(0,'#FFFFFF');gradient.addColorStop(0.7,'#F0F0F0');gradient.addColorStop(1,'#C8C8C8');ctx.fillStyle=gradient;}ctx.beginPath();ctx.arc(x,y,radius,0,Math.PI*2);ctx.fill();// 棋子边框ctx.strokeStyle=player===BLACK?'#000000':'#999999';ctx.lineWidth=0.5;ctx.stroke();}}

组件声明

@Componentexportstruct ChessBoardView{@Prop@Watch('onDataChange')boardData:number[]=[];@ProplastMoveRow:number=-1;@ProplastMoveCol:number=-1;onCellClick:(row:number,col:number)=>void=()=>{};privatecontext:CanvasRenderingContext2D=newCanvasRenderingContext2D(newRenderingContextSettings(true));privatecanvasSize:number=320;privateisReady:boolean=false;build(){Canvas(this.context).width('100%').aspectRatio(1).onReady(()=>{this.isReady=true;this.drawBoard();}).onClick((event:ClickEvent)=>{this.handleClick(event);}).onAreaChange((_oldValue:Area,newValue:Area)=>{constw=newValue.width;if(typeofw==='number'){this.canvasSize=w;if(this.isReady){this.drawBoard();}}})}}

组件接口设计

ChessBoardView是一个纯表现组件——只负责显示和事件传递:

输入(@Prop)

@Prop@Watch('onDataChange')boardData:number[]=[];// 棋盘数据@ProplastMoveRow:number=-1;// 最后一手行@ProplastMoveCol:number=-1;// 最后一手列

输出(回调)

onCellClick:(row:number,col:number)=>void=()=>{};

内部状态

privatecontext:CanvasRenderingContext2D;// Canvas绘制上下文privatecanvasSize:number=320;// 实际绘制尺寸privateisReady:boolean=false;// Canvas是否就绪

Canvas初始化

privatecontext:CanvasRenderingContext2D=newCanvasRenderingContext2D(newRenderingContextSettings(true));

RenderingContextSettings(true)中的true表示开启抗锯齿,让棋子和线条更加平滑。

Canvas组件的生命周期

组件创建 ↓ Canvas挂载到组件树 ↓ onReady触发 → isReady = true → drawBoard() ↓ onAreaChange触发 → canvasSize更新 → drawBoard() ↓ @Prop boardData变化 → @Watch onDataChange → drawBoard()

onReady

.onReady(()=>{this.isReady=true;this.drawBoard();})

Canvas组件创建后需要等待onReady回调才能开始绘制。isReady标志位防止在Canvas未就绪时调用绘制方法。

onAreaChange

.onAreaChange((_oldValue:Area,newValue:Area)=>{constw=newValue.width;if(typeofw==='number'){this.canvasSize=w;if(this.isReady){this.drawBoard();}}})

当组件尺寸变化时(如屏幕旋转、窗口调整),重新获取尺寸并重绘。typeof w === 'number'检查是因为Area的width可能是number或string类型。

@Watch数据驱动重绘

@Prop@Watch('onDataChange')boardData:number[]=[];onDataChange():void{if(this.isReady){this.drawBoard();}}

当父组件传入的boardData发生变化时,@Watch自动触发onDataChange,进而调用drawBoard重绘。

注意:ArkTS中@Prop数组变化需要创建新引用才能触发@Watch。父组件中的做法:

privaterefreshBoardData():void{this.boardData=this.engine.toFlatArray();// 创建新数组}

toFlatArray()每次返回新数组,确保引用变化触发@Watch

drawBoard绘制流程

privatedrawBoard():void{constctx=this.context;constsize=this.canvasSize;if(size<=0)return;constcellSize=size/BOARD_SIZE;consthalfCell=cellSize/2;// 1. 清空画布ctx.clearRect(0,0,size,size);// 2. 绘制背景ctx.fillStyle=BOARD_BG_COLOR;ctx.fillRect(0,0,size,size);// 3. 绘制网格线ctx.strokeStyle=LINE_COLOR;ctx.lineWidth=1;for(leti=0;i<BOARD_SIZE;i++){constpos=halfCell+i*cellSize;ctx.beginPath();ctx.moveTo(halfCell,pos);ctx.lineTo(size-halfCell,pos);ctx.stroke();ctx.beginPath();ctx.moveTo(pos,halfCell);ctx.lineTo(pos,size-halfCell);ctx.stroke();}// 4. 绘制星位// 5. 绘制棋子// 6. 绘制最后一手标记}

颜色常量

constBOARD_BG_COLOR:string='#D4A868';// 棋盘木色constLINE_COLOR:string='#5C4033';// 网格线深棕constSTAR_COLOR:string='#3B2F2F';// 星位黑色constLAST_MOVE_COLOR:string='#E63946';// 最后一手红色

这些颜色定义在组件文件顶部,没有放入资源文件——因为它们是组件内部实现细节,不需要国际化或主题适配。

组件复用

ChessBoardView被两个页面复用:

// TwoPlayerPageChessBoardView({boardData:this.boardData,lastMoveRow:this.lastMoveRow,lastMoveCol:this.lastMoveCol,onCellClick:(row:number,col:number)=>{this.onCellClick(row,col);}})// AIBattlePage — 完全相同的用法ChessBoardView({boardData:this.boardData,lastMoveRow:this.lastMoveRow,lastMoveCol:this.lastMoveCol,onCellClick:(row:number,col:number)=>{this.onCellClick(row,col);}})

组件不关心数据来源(双人对战还是AI对战),只负责渲染和事件传递。

总结

ChessBoardView的设计展示了ArkUI组件的最佳实践:

  1. @Prop + @Watch:数据驱动重绘
  2. Canvas生命周期:onReady后才绘制
  3. onAreaChange:响应式适配屏幕尺寸
  4. 纯表现组件:只负责显示和回调,不含业务逻辑
  5. 可复用:被两个页面无缝复用
http://www.cnnetsun.cn/news/3231724.html

相关文章:

  • HarmonyKit | 鸿蒙新特性实践:进制转换器 2/8/10/16 四进制通用互转
  • DeepSeek V3 vs Claude 3.5 Sonnet:程序员日常开发场景深度横向对比
  • SFT数据质量评估:3种自动化方法筛选千条指令数据,PPL降低20%
  • 2026抖音动图去水印免费方法,无水印保存抖音GIF技巧
  • Transformer 与 RNN/LSTM 对比:5 个维度解析长序列建模效率差异
  • YOLOv8 训练数据配置避坑:3个常见 data.yaml 错误与修复方案
  • Agent Harness 十二大模块完全解析:为什么生产级智能体的胜负手不在模型,而在“脚手架”
  • matlab和simulink的入门联合仿真-入门级!保姆级!包成功
  • 计算机大数据毕设实战-基于 Django 框架的电商用户消费行为画像分析系统的设计与实现 基于多源数据融合的电商用户精准画像可视化系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • VMware新增EXSI虚拟机
  • 围绕MonetaMarkets提醒机制看,表现如何?
  • 容器网络与存储:lxcfs-tools如何协同其他容器技术栈
  • Codex CLI一次改多个文件安全吗?提交前检查清单与PR验证流程
  • MCP 协议从零实现:打造 Agent 的“应用商店“——工具热插拔与标准化生态
  • Android 7系统网络(二)内核层—netfilter_iptables与路由策略
  • 线段树 Beats 技巧在复杂区间问题中的实战解读
  • 我和 Fable 5 聊了一晚上 AI,得出的结论大吃一惊
  • EfficientNet-B0 到 B3 复合缩放实战:4 个模型在 224x224 输入下的精度/速度权衡
  • 134.标准化 PLC 工程落地!扫描周期优化 + 防抖处理 + 状态机防死锁
  • BsMax插件:让3ds Max用户在Blender中无缝工作的终极解决方案
  • 计算机毕业设计之民办高校二手物品交易网的设计与实现
  • 如何快速集成移动端PDF预览:pdfh5.js的完整解决方案
  • 5分钟完全解锁索尼相机隐藏功能:OpenMemories-Tweak终极指南
  • BRT vs 随机森林:3个R包性能对比与5万条数据实战分析
  • 数据库规范化理论实战:从1NF到BCNF的5个经典模式分解案例
  • VisualCppRedist AIO:告别DLL错误,Windows软件运行的终极救星![特殊字符]
  • KITTI 数据集转 ROS Bag 实战:lidar2rosbag_KITTI 工具 3 步配置与 A-LOAM 适配要点
  • 如何在SketchUp中轻松实现3D打印:STL导入导出完全指南
  • Oracle CASE WHEN 表达式 7 种实战用法:从 SELECT 到 ORDER BY 与 GROUP BY
  • STM32与CMT-8540S音频模块开发实战