鸿蒙征文|使用鸿蒙Next复刻一个上古小游戏:《是男人就坚持100秒》 ...

张春  论坛元老 | 2024-10-5 03:43:31 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1031|帖子 1031|积分 3093




  • 介绍
  • 情况搭建
  • 代码布局解读
  • canvas交互开辟原理简介
  • 构建应用界面
  • canvas(游戏焦点功能)组件的实现
  • 绑定TextTimer的时间信息
  • 订阅角度传感器
  • 数据长期化存储
  • 总结
介绍

本篇Codelab将介绍如何使用canvas结合sensor(手机传感器服务),实现一个简单的小游戏。要求完成以下功能:
游戏内容是玩家操作一个小球(黄色小球,上方有红色箭头作为标志)在屏幕中移动,躲避其他仇人小球的追击,如果触碰到仇人小球则游戏结束,在菜单界面显示本次的生存时间与最高记录的生存时间。
游戏包含四个差别的难度,同屏的仇人小球的数目上限与移动速率根据难度依次递增,差别难度下的最高记录相互独立,玩家可以反复嬉戏,以挑战更久的生存时长记录。
主要学习内容为:

  • 学习如何基于变化的参数开辟简单的canvas交互动画结果。
  • 学习如何使用TextTimer组件显示计时器时间,并将时间通报给其他变量。
  • 学习如何订阅传感器,获取传感器监听信息。
  • 通过长期化存储生存数据。
相干概念:


  • Canvas:提供画布组件,用于自定义绘制图形。
  • CanvasRenderingContext2D:使用RenderingContext可以在Canvas组件上进行绘制,绘制对象可以是矩形、文本、图片等。
  • TextTimer:通过文本显示计时信息并控制其计时器状态的组件。
  • 方向传感器:可以提供手机在xyz轴方向的旋转角度信息,订阅方向传感器数据,系统返回相干数据。
  • setInterval:(定时器接口)重复调用一个函数,在每次调用之间具有固定的时间延迟。通过定时器与canvas擦除重绘的结合就能实现雷同动画的图像结果。
  • PersistentStorage:长期化存储UI状态 。state与appstorage生存的变量在应用重启后会丢失。将PersistentStorage与appStorage协同使用,就可以方便实现变量的长期化存储。【适用于数据布局简单的轻量级数据,否则大概会数据长期化失败】
相干权限

情况搭建

我们首先需要完成HarmonyOS开辟情况搭建,可参照如下步骤进行。
软件要求
DevEco Studio版本:DevEco Studio NEXT Developer Preview1及以上。
HarmonyOS SDK版本:HarmonyOS NEXT Developer Preview1 SDK及以上。
硬件要求
设备类型:华为手机。
HarmonyOS系统:HarmonyOS NEXT Developer Preview1及以上。
情况搭建
安装DevEco Studio,详情请参考下载和安装软件。
设置DevEco Studio开辟情况,DevEco Studio开辟情况需要依靠于网络情况,需要连接上网络才能确保工具的正常使用,详情请参考配置开辟情况。
开辟者可以参考以下链接,完成设备调试的相干配置:
使用真机进行调试
代码布局解读

本篇Codelab只对焦点代码进行讲解,对于完备代码,我们会在源码下载中提供。
本codelab的有效功能代码布局如下:
  1. entry/src/main/ets                 // 代码区
  2.   ├──utils
  3.   │  └──StepsUtil.ets              // 小球的属性类、角度管理类
  4.   └──pages
  5.      └──Index.ets                  // 应用代码
复制代码
canvas交互开辟原理简介:

canvas工作原理简介:

canvas的本质是一个画板,在绑定了CanvasRenderingContext2D对象或OffscreenCanvasRenderingContext2D对象以后,可以通过输入指令的方式往画板上绘制内容,绘制的内容类型可以是底子形状、文本、图片等。
在代码逻辑中增长对指令的选择,就可以差异化的天生各种差别的画面,而且由于canvas内容的绘制是基于坐标系来定位的,我们可以对这个画板进行像素级的内容定制,。
Canvas提供画布组件,用于自定义绘制图形,
开辟者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制,。
canvas动画的原理:

动画的本质是一连播放的图像,通过定时器与canvas的结合,不断擦除并重画图像,就能做出动画结果。
开辟者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制。
canvas坐标系:

canvas中的元素巨细和位置是以canvas坐标系为底子确定的。
在默认情况下,canvas画布的左上角未坐标原点(0,0)。水平方向为X轴(向右为正值),垂直方向为Y轴(向下为正值)。
使用clearRect(x: number, y: number, w: number, h: number)清除指定区域内的绘制内容。
本案例中使用的canvas指令汇总:

  1. fillStyle  指定绘制的填充色。
  2. clearRect(x: number, y: number, w: number, h: number): void    删除指定区域内的绘制内容。
  3. translate(x: number, y: number): void  移动当前坐标系的原点。
  4. beginPath(): void  创建一个新的绘制路径。
  5. arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void   绘制弧线路径。
  6. closePath(): void  结束当前路径形成一个封闭路径。
  7. stroke(path?: Path2D): void  进行边框绘制操作。
  8. fill(fillRule?: CanvasFillRule): void  对封闭路径进行填充。
  9. moveTo(x: number, y: number): void  路径从当前点移动到指定点。
  10. lineTo(x: number, y: number): void  从当前点到指定点进行路径连接。
复制代码
构建应用界面

应用界面以stack组件为根节点,依次堆叠四个组件构成,分别是:canvas(游戏焦点功能)组件、游戏计时器器组件、(开始)菜单组件、暂停菜单组件。
  1. build() {
  2.   Stack(){
  3.     //canvas(游戏核心功能)组件
  4.     Canvas(this.context).width("100%").height("100%").onReady(() => {
  5.         this.isGameOver=true
  6.         this.canvasReady()
  7.         this.randomBackBalls()
  8.     })
  9.     .border({width:1,color:Color.Red})
  10.     .flexGrow(1)
  11.     .onClick(()=>{
  12.         this.gamePause()
  13.     })
  14.     //游戏计时器器组件
  15.     TextTimer({ isCountDown: false,  controller: this.textTimerController })
  16.     .format('mm:ss.SS')
  17.     .fontColor(Color.Black)
  18.     .position({x:0,y:0})
  19.     .fontSize(30)
  20.     .onTimer((utc: number, elapsedTime: number) => {
  21.         console.info('textTimer notCountDown utc is:' + utc + ', elapsedTime: ' + elapsedTime)
  22.         this.liveTime[this.level]=elapsedTime
  23.         if(this.maxLiveTime[this.level]<this.liveTime[this.level]){
  24.             this.maxLiveTime[this.level]=this.liveTime[this.level]
  25.         }
  26.     })
  27.   if (this.isGameOver == true) {
  28.     //开始菜单的代码
  29.     Column() {
  30.       Column({space:15}) {
  31.         Text(`最高纪录:${Math.floor(this.maxLiveTime[this.level] / 1000)}:${Math.floor(this.maxLiveTime[this.level] % 1000 / 10)}`)
  32.         Text(`本次生存时长:${Math.floor(this.liveTime[this.level] / 1000)}:${Math.floor(this.liveTime[this.level] % 1000 / 10)}`)
  33.         Text(`难度:${this.difficultyName[this.level]}`)
  34.         List(){
  35.           ForEach(this.difficultyName,(res:string,index)=>{
  36.             ListItem(){
  37.               Text(res)
  38.                 .fontColor(this.level==index?Color.White:Color.Black)
  39.                 .backgroundColor(this.level==index?Color.Blue:Color.Gray)
  40.                 .padding(5)
  41.             }
  42.             .onClick(()=>{this.level=index})
  43.           })
  44.         }
  45.         .listDirection(Axis.Horizontal)
  46.         .height(30)
  47.         Button('开始游戏')
  48.           .onClick(() => {
  49.             this.gameRestart()
  50.           })
  51.       }
  52.       .width('80%')
  53.       .backgroundColor(Color.White)
  54.       .alignItems(HorizontalAlign.Center)
  55.       .justifyContent(FlexAlign.Center)
  56.       .padding(10)
  57.     }
  58.     .height('100%')
  59.     .width('100%')
  60.     .backgroundColor('rgba(0,0,0,0.4)')
  61.     .alignItems(HorizontalAlign.Center)
  62.     .justifyContent(FlexAlign.Center)
  63.     .border({width:1,color:Color.Orange})
  64.     }
  65.     if(this.gamePausing==true){
  66.       ...//暂停菜单的代码
  67.     }
  68.   }.width("100%").height("100%").alignContent(Alignment.Center)
  69. }
复制代码
开辟canvas(游戏焦点功能)组件

初始化canvas画布

清理画布并将canvas画布的中心点设置为坐标系原点
  1. canvasReady() {//初始化canvas画布
  2.     this.context.clearRect(-this.context.width, -this.context.height,this.context.width*2,this.context.height*2)
  3.     this.context.translate(this.context.width/2, this.context.height/2)
  4. }
复制代码
天生随机背景

为了增长用户体验,在游戏处于菜单界面时,天生30个随机活动的小球作为背景。
  1. randomBackBalls(){//游戏等待时间随机生成的背景动画
  2.     //确认小球数量
  3.     if(this.backgroundList.length!=30){
  4.         this.backgroundList=[]
  5.         for (let index = 0; index < 30; index++) {
  6.             let X=Math.floor(this.context.width/2-Math.random()*this.context.width)
  7.             let Y=Math.floor(this.context.height/2-Math.random()*this.context.height)
  8.             let size=Math.ceil(Math.random()*10)
  9.             let XSpeed=Math.ceil(Math.random()*10)
  10.             let YSpeed=Math.ceil(Math.random()*10)
  11.             this.backgroundList.push(new ballInfo(X,Y,size,XSpeed,YSpeed))
  12.         }
  13.     }
  14.     //定时自动更新画布
  15.     this.readyTimerId=setInterval(()=>{
  16.       //计算元素位置
  17.       for (let index = 0; index < 30; index++) {
  18.         this.backgroundList[index].x+=this.backgroundList[index].XSpeed
  19.         this.backgroundList[index].y+=this.backgroundList[index].YSpeed
  20.         if(this.backgroundList[index].x>=this.context.width/2 || this.backgroundList[index].x<=-this.context.width/2){
  21.           this.backgroundList[index].XSpeed = -this.backgroundList[index].XSpeed
  22.         }
  23.         if(this.backgroundList[index].y>=this.context.height/2 || this.backgroundList[index].y<=-this.context.height/2){
  24.           this.backgroundList[index].YSpeed = -this.backgroundList[index].YSpeed
  25.         }
  26.       }
  27.       //清理画布
  28.       this.context.clearRect(-this.context.width, -this.context.height,this.context.width*2,this.context.height*2)
  29.       //依次绘制元素
  30.       for (let index = 0; index < this.backgroundList.length; index++) {
  31.         this.context.fillStyle = 'rgba(90,60,90,1)';
  32.         this.context.beginPath()
  33.         this.context.arc(this.backgroundList[index].x, this.backgroundList[index].y, this.backgroundList[index].size, 0, 6.28)
  34.         this.context.closePath()
  35.         this.context.stroke()
  36.         this.context.fill()
  37.       }
  38.     },100)
  39. }  
复制代码
计算游戏中下一帧中每个元素所处的位置

玩家小球根据角度传感器传来的方向信息进行移动,仇人小球不断地向玩家小球所处的位置靠近。
  1. NextFrame(){//单帧的移动
  2.   this.player.x +=this.player.XSpeed;
  3.   this.player.y +=this.player.YSpeed
  4.   for (let index = 0; index < this.enemyList.length; index++) {
  5.     if (this.enemyList[index].x > this.player.x) {
  6.       if(this.enemyList[index].x-1*(this.level+1)<= this.player.x){
  7.         this.enemyList[index].x=this.player.x
  8.       }else{
  9.         this.enemyList[index].x -= 1*(this.level+1)
  10.       }
  11.     } else if (this.enemyList[index].x < this.player.x) {
  12.       if(this.enemyList[index].x+  1*(this.level+1)>= this.player.x){
  13.         this.enemyList[index].x=this.player.x
  14.       }else{
  15.         this.enemyList[index].x += 1*(this.level+1)
  16.       }
  17.     }
  18.     if (this.enemyList[index].y > this.player.y) {
  19.       if(this.enemyList[index].y-1*(this.level+1)<= this.player.y){
  20.         this.enemyList[index].y=this.player.y
  21.       }else{
  22.         this.enemyList[index].y -= 1*(this.level+1)
  23.       }
  24.     } else if (this.enemyList[index].y < this.player.y) {
  25.       if(this.enemyList[index].y+  1*(this.level+1)>= this.player.y){
  26.         this.enemyList[index].y=this.player.y
  27.       }else{
  28.         this.enemyList[index].y += 1*(this.level+1)
  29.       }
  30.     }
  31.     if ((this.enemyList[index].x - this.player.x)**2 + (this.enemyList[index].y - this.player.y)**2 < (this.enemyList[index].size + this.player.size)**2) {
  32.       this.gameOver()
  33.     }
  34.   }
  35.   this.redrawCanvas()//将当前帧绘制到画布上
  36. }
复制代码
革新页面

将画布的内容全部清除并根据数据画出下一帧的内容
  1. redrawCanvas(){//游戏过程中每一帧的更新内容
  2.   //清理画布
  3.   this.context.clearRect(-this.context.width, -this.context.height,this.context.width*2,this.context.height*2)
  4.   //绘制玩家小球
  5.   if(this.player.size!=0){
  6.     this.context.fillStyle = 'rgba(255,215,0,1)'; // 设置玩家操纵小球为金色
  7.     this.context.beginPath()
  8.     this.context.arc(this.player.x, this.player.y, this.player.size, 0, 6.28)
  9.     this.context.closePath()
  10.     this.context.stroke()
  11.     this.context.fill()
  12.     this.context.fillStyle = 'rgba(250,0,0,1)'; // 指示玩家位置的红色箭头
  13.     this.context.beginPath()
  14.     this.context.moveTo(this.player.x, this.player.y-8)
  15.     this.context.lineTo(this.player.x-5, this.player.y-15)
  16.     this.context.lineTo(this.player.x+5, this.player.y-15)
  17.     this.context.closePath()
  18.     this.context.stroke()
  19.     this.context.fill()
  20.   }
  21.   //绘制敌人
  22.   for (let index = 0; index < this.enemyList.length; index++) {
  23.     this.context.fillStyle = 'rgba(90,60,90,1)'; // 敌人的颜色
  24.     this.context.beginPath()
  25.     this.context.arc(this.enemyList[index].x, this.enemyList[index].y, this.enemyList[index].size, 0, 6.28)
  26.     this.context.closePath()
  27.     this.context.stroke()
  28.     this.context.fill()
  29.   }
  30. }
复制代码
绑定TextTimer的时间信息

TextTimer是文本形式的计时器组件,通过start/pause/reset来控制计时器的启停。可在onTimer事件中获取计时器的最新时间数据,同步给其他变量使用。
  1.   TextTimer({ isCountDown: false,  controller: this.textTimerController })
  2.     .format('mm:ss.SS')
  3.     .fontColor(Color.Black)
  4.     .position({x:0,y:0})
  5.     .fontSize(30)
  6.     .onTimer((utc: number, elapsedTime: number) => {//文本jishuqi
  7.        console.info('textTimer notCountDown utc is:' + utc + ', elapsedTime: ' + elapsedTime)
  8.            this.liveTime[this.level]=elapsedTime
  9.           if(this.maxLiveTime[this.level]<this.liveTime[this.level]){
  10.             this.maxLiveTime[this.level]=this.liveTime[this.level]
  11.         }
  12.     })
复制代码
订阅角度传感器

通过订阅获取到计步传感器数据,解析后将beta/gamma两个参数用于游戏控制。
  1.   sensor.on(sensor.SensorId.ORIENTATION, (data: sensor.OrientationResponse) => {
  2.     if(this.originalDirection.x==0){
  3.       this.originalDirection.x=data.alpha//沿侧边的旋转//本案例中不使用
  4.     }
  5.     if(this.originalDirection.y==0){
  6.       this.originalDirection.y=data.beta//沿屏幕垂直方向的旋转
  7.     }
  8.     if(this.originalDirection.z==0){
  9.       this.originalDirection.z=data.gamma//沿屏幕水平方向的旋转
  10.     }
  11.     this.player.YSpeed=(this.originalDirection.y-data.beta)/3
  12.     this.player.XSpeed=(this.originalDirection.z-data.gamma)/3
  13.     if(this.player.x+this.player.XSpeed>=this.context.width/2  || (this.player.x+this.player.XSpeed)<=-this.context.width/2){
  14.       this.player.XSpeed=0
  15.     }
  16.     if(this.player.y+this.player.YSpeed>=this.context.height/2  || (this.player.y+this.player.YSpeed)<=-this.context.height/2){
  17.       this.player.YSpeed=0
  18.     }
  19.     console.info('Succeeded in invoking on. Scalar quantity: ' + data.alpha);
  20.   }, { interval: 100000 });
复制代码



数据长期化存储

在使用AppStorage之前调用PersistentStorage.persistProp,可从设备中获取到之前长期化的对应变量。
之后修改同名的appStorage变量,即可将最新的变量值长期化存储到本地磁盘。
  1. PersistentStorage.persistProp<number[]>('maxLiveTime',[0,0,0,0])
  2. @Entry
  3. @Component
  4. export struct miniGame{
  5.     @StorageLink('maxLiveTime') maxLiveTime:number[]=[0,0,0,0]//历史分数
  6.     ...
  7.    
  8.     build{
  9.     ...
  10.     }
  11. }
复制代码
总结

您已经完成了本次Codelab的学习,并了解到以下知识点:

  • 学习如何基于变化的参数开辟简单的canvas交互动画结果。
  • 学习如何使用TextTimer组件显示计时器时间,并将时间通报给其他变量。
  • 学习如何订阅传感器,获取传感器监听信息。
  • 通过长期化存储生存数据。
作者:陈胜歌
坚果派成员,南京星梦之舟联合创始人
关于坚果派
坚果派由坚果创建,团队拥有12个华为HDE,以及若干其他领域的三十余位万粉博主运营。专注于分享的技能包括HarmonyOS/OpenHarmony,ArkUI-X,元服务,服务卡片,华为自研语言,AI、BlueOS操作系统、团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,接待合作。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张春

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表