news 2026/7/9 1:27:09

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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. 可复用:被两个页面无缝复用
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 1:26:56

HarmonyKit | 鸿蒙新特性实践:进制转换器 2/8/10/16 四进制通用互转

HarmonyKit | 鸿蒙新特性实践&#xff1a;进制转换器 2/8/10/16 四进制通用互转 引言 进制转换是计算机科学的基础运算之一。虽然操作系统自带的计算器都提供了进制转换功能&#xff0c;但在移动端开发调试场景中&#xff0c;需要快速在二进制、八进制、十进制和十六进制之间…

作者头像 李华
网站建设 2026/7/9 1:25:53

DeepSeek V3 vs Claude 3.5 Sonnet:程序员日常开发场景深度横向对比

适用人群&#xff1a;正在做AI编程助手选型的开发者、想在日常开发中提效的前后端工程师、对DeepSeek V3和Claude 3.5 Sonnet代码能力感兴趣的技术人 核心结论前置&#xff1a;经过三个真实开发场景的实测&#xff0c;DeepSeek V3在中英文混合注释生成、中文文档编写方面优势明…

作者头像 李华
网站建设 2026/7/9 1:25:19

SFT数据质量评估:3种自动化方法筛选千条指令数据,PPL降低20%

SFT数据质量评估&#xff1a;3种自动化方法筛选千条指令数据&#xff0c;PPL降低20%在大型语言模型&#xff08;LLM&#xff09;的监督微调&#xff08;SFT&#xff09;过程中&#xff0c;数据质量往往比数据数量更为关键。低质量的指令数据不仅会拖慢训练进程&#xff0c;还可…

作者头像 李华
网站建设 2026/7/9 1:25:01

2026抖音动图去水印免费方法,无水印保存抖音GIF技巧

日常整理抖音素材、收藏GIF表情包时&#xff0c;平台自带的边角水印、动态飘字水印&#xff0c;往往会破坏画面整体质感&#xff0c;影响素材整洁度。市面上多数专业去水印工具需要付费解锁功能&#xff0c;普通用户仅做个人收藏、学习使用&#xff0c;无需花费成本购置会员。本…

作者头像 李华
网站建设 2026/7/9 1:23:08

Transformer 与 RNN/LSTM 对比:5 个维度解析长序列建模效率差异

Transformer 与 RNN/LSTM 对比&#xff1a;5 个维度解析长序列建模效率差异在自然语言处理领域&#xff0c;序列建模一直是核心挑战之一。传统循环神经网络&#xff08;RNN&#xff09;及其变体长短期记忆网络&#xff08;LSTM&#xff09;曾长期主导这一领域&#xff0c;直到2…

作者头像 李华
网站建设 2026/7/9 1:21:49

YOLOv8 训练数据配置避坑:3个常见 data.yaml 错误与修复方案

YOLOv8训练数据配置实战&#xff1a;从路径陷阱到自动化校验数据配置文件的隐藏陷阱初次接触YOLOv8训练自定义数据集时&#xff0c;多数开发者会低估data.yaml配置文件的重要性。这个看似简单的YAML文件实则暗藏玄机——90%的训练失败案例都源于此文件的配置错误。不同于其他深…

作者头像 李华