#跟著小白一起學(xué)鴻蒙# [番外]一起學(xué)做“羊了個羊”

簡介

最近大火了一個小游戲火遍朋友圈,我們就一起看看如何能用OpenHarmony學(xué)習(xí)做個”羊了個羊“逼纸。本文中引用的圖片資源均來自:https://github.com/Jetereting/ylgy洋措。

ylgy.gif

開發(fā)

1. HAP應(yīng)用建立

《#跟著小白一起學(xué)鴻蒙#[六]如何編寫一個hap應(yīng)用》里我們介紹了簡單的Hap應(yīng)用的開發(fā)以及基礎(chǔ)控件的介紹,這里我們就不贅述Hap項目的建立過程杰刽,以下就是基礎(chǔ)的Hap的page文件:index.ets

build() {
    Row() {
      Column() {
        Canvas(this.context)
          .width('100%')
          .height('100%')
          .onClick((ev: ClickEvent) => {
            console.log("screen.xy:"+ev.screenX+":"+ev.screenY)
            console.log("xy:"+ev.x+":"+ev.y)
          })
          .onReady(() =>{
            this.context.imageSmoothingEnabled = false
            this.drawBlock()
          })
      }
      .height("80%")
      .width("100%")
    }
    .height('100%')
    .width('100%')
    .backgroundImage($r("app.media.grass"))
    .backgroundImageSize(ImageSize.Cover)
}

build是基礎(chǔ)頁面的構(gòu)造函數(shù)菠发,用于界面的元素構(gòu)造,其他的頁面的生命周期函數(shù)如下:

declare class CustomComponent {
  /**
   * Customize the pop-up content constructor.
   * @since 7
   */
  build(): void;

  /**
   * aboutToAppear Method
   * @since 7
   */
  aboutToAppear?(): void;

  /**
   * aboutToDisappear Method
   * @since 7
   */
  aboutToDisappear?(): void;

  /**
   * onPageShow Method
   * @since 7
   */
  onPageShow?(): void;

  /**
   * onPageHide Method
   * @since 7
   */
  onPageHide?(): void;

  /**
   * onBackPress Method
   * @since 7
   */
  onBackPress?(): void;
}

2. Canvas介紹

canvas是畫布組件用于自定義繪制圖形贺嫂,具體的API頁面如下:

https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-components-canvas-canvas-0000001333641081

頁面顯示前會調(diào)用aboutToAppear()函數(shù)滓鸠,此函數(shù)為頁面生命周期函數(shù)

canvas組件初始化完畢后會調(diào)用onReady()函數(shù),函數(shù)內(nèi)部實現(xiàn)小游戲的初始頁面的繪制

2.1 初始化頁面數(shù)據(jù)
initBlocks() {
    for (let i=0;i<this.avaliableCnt;i++) {
      let lineCn = Math.floor(i/3)
      let rowCn = Math.floor(i%3)
      if (lineCn == 0) {
        this.blockList[i] = {
          img: "censer",
          isShow: true,
          x: this.startX+rowCn*30,
          y: this.startY,
          w: 55,
          h: 53,
        }
      } else if (lineCn == 1) {
        this.blockList[i] = {
          img: "cloud",
          isShow: true,
          x: this.startX+rowCn*30,
          y: this.startY+lineCn*90,
          w: 55,
          h: 53,
        }
      } else if (lineCn == 2) {
        this.blockList[i] = {
          img: "knif",
          isShow: true,
          x: this.startX+rowCn*30,
          y: this.startY+lineCn*90,
          w: 55,
          h: 53,
        }
      }
    }
  }

小游戲的每個卡片都是用canvas繪制的圖片資源第喳,用于進行排列以及點擊判斷所以在此設(shè)計了個數(shù)據(jù)結(jié)構(gòu)

{
    img: 卡片資源類型糜俗,用于圖片渲染和相似圖片消除
    isShow: 卡片是否顯示標(biāo)志,用于渲染的時候進行判斷
    x:卡片渲染左上角橫坐標(biāo)
    y:卡片渲染左上角縱坐標(biāo)
    w:卡片渲染寬度
    h: 卡片渲染高度
}

現(xiàn)在制作的是用固定方法初始化卡片的方法即渲染3行,每行3個圖片悠抹,之后改進可以改成明確一個區(qū)域珠月,然后采用隨機算法進行位置和卡片類型生成。

2.2 初始化頁面繪制
drawBlock() {
    //初始化消除區(qū)域的卡片
    this.blockList.forEach((block)=>{
      if (block.isShow) {
        let imgItem:ImageBitmap = null
        switch(block.img) {
          case "censer":
            imgItem = this.censerImg
            break
          case "cloud":
            imgItem = this.cloudImg
            break
          case "knif":
            imgItem = this.knifImg
            break
          default:
            imgItem = this.censerImg
            break
        }
        this.context.drawImage( this.cardImg,block.x,block.y,this.blockw,this.blockh)
        this.context.drawImage( imgItem,block.x+5,block.y+5,block.w,block.h)
      }
    })
    //初始化選擇卡片區(qū)域
    this.context.drawImage( this.slotImg,this.slotX,this.slotY,300,39)
    let pos = 0
    for (let i=0;i<5;i++) {
      this.context.drawImage( this.cardImg,this.slotX + pos,this.slotY+40,61,69)
      if (i < this.emptyList.length) {
        let emptyText = this.emptyList[i]
        let pItem = null;
        switch (emptyText) {
          case "censer":
            pItem = this.censerImg;
            break;
          case "cloud":
            pItem = this.cloudImg;
            break;
          case "knif":
            pItem = this.knifImg;
            break;
          default:
            break;
        }
        if (pItem) {
          this.context.drawImage(pItem,this.slotX + pos + 3,this.slotY+40,55,59)
        }
      }
      pos += 60
    }
  }

整個繪制區(qū)域分兩個區(qū)域:

  • 消除區(qū)域:繪制卡片背景和卡片類型锌钮,利用初始化的卡片數(shù)據(jù)進行卡片繪制桥温;
  • 選擇區(qū)域:繪制欄桿,卡片背景梁丘,以及選擇的卡片

3. 游戲邏輯

簡單的小游戲主體游戲邏輯為:初始化(之前的章節(jié)已經(jīng)介紹)侵浸,點擊(選中,選不中氛谜,消除掏觉,選擇區(qū)域滿,消除區(qū)域空)流程圖如下:

graph LR
init[初始化] --> click[點擊]
click[點擊] --> isSelect{是否點中}
isSelect -->|點中| yes[點中]
isSelect -->|沒點中| no[沒點中]
yes --> isEmpty{是否選擇區(qū)域滿}
isEmpty -->|滿| full[無法消除]
isEmpty -->|不滿| notfull[加入選擇區(qū)域]
notfull --> canClear{有3個相同}
canClear -->|能消除| clear[消除]
canClear -->|不能消除| append[進入選擇區(qū)域]
append --> 重繪
.onClick((ev: ClickEvent) => {
            if (this.needRestart) {
              this.needRestart = false
              this.emptyList.splice(0, this.emptyList.length)
              this.blockList.splice(0, this.blockList.length)
              this.emptyCnt = 5
              this.avaliableCnt = 9
              this.initBlocks()
              this.context.clearRect(0,0,this.context.width,this.context.height)
              this.drawBlock()
              return
            }
            console.log("screen.xy:"+ev.screenX+":"+ev.screenY)
            console.log("xy:"+ev.x+":"+ev.y)
            //判斷是否點中方塊
            let flag = this.isSelect(ev.x, ev.y)
            console.info("flag:"+flag)
            if (flag == 1) {
              //如果可以移動或消除則清空重填
              this.context.clearRect(0,0,this.context.width,this.context.height)
              this.drawBlock();
            } else if (flag == 2) {
              //如果清空顯示勝利畫面
              this.context.drawImage( this.blackImg,0,0,this.context.width,this.context.height)
              this.context.drawImage( this.winImg,this.slotX+50,this.slotY-300,200,200)
              this.context.font="100px bold"
              this.context.fillText("歡迎你加入羊群", this.slotX+50,this.slotY-350,500)
              this.needRestart = true
            } else if (flag == 3) {
              this.context.clearRect(0,0,this.context.width,this.context.height)
              this.drawBlock();
              this.context.drawImage( this.blackImg,0,0,this.context.width,this.context.height)
              this.context.drawImage( this.ylgyImg,this.slotX+50,this.slotY-300,200,100)
              this.context.font="100px bold"
              this.context.fillText("加入羊群失敗", this.slotX+50,this.slotY-350,500)
              this.needRestart = true
            }
          })

4. 完整邏輯

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  @State _translate: TranslateOptions = {
    x: 0,
    y: 0,
    z: 0
  }
  @State _scale: ScaleOptions = {
    x: 1,
    y: 1,
    z: 1
  }
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private cardImg:ImageBitmap = new ImageBitmap("common/images/iback.png")
  private slotImg:ImageBitmap = new ImageBitmap("common/images/lan.png")
  private ylgyImg:ImageBitmap = new ImageBitmap("common/images/ylgy.png")
  private blackImg:ImageBitmap = new ImageBitmap("common/images/black.png")
  private censerImg:ImageBitmap = new ImageBitmap("common/images/censer.png")
  private cloudImg:ImageBitmap = new ImageBitmap("common/images/cloud.png")
  private knifImg:ImageBitmap = new ImageBitmap("common/images/knif.png")
  private winImg:ImageBitmap = new ImageBitmap("common/images/win.png")
  private startX = 50;
  private startY = 10;
  private slotX = 20;
  private slotY = 450;
  private blockw = 62;
  private blockh = 69;
  private blockList = []
  private emptyList = []
  private emptyCnt =  5;
  private avaliableCnt = 9;
  private clearLen = 3;
  private needRestart = false;
  animationStep(value: AnimateParam, event: () => void) {
    return () => {
      return new Promise((resolve) => {
        let onFinish = value.onFinish
        value.onFinish = () => {
          if(onFinish) onFinish()
          resolve(true)
        }
        animateTo(value, event)
      })
    }
  }
  async pulse(time) {
    // 0% - 50%
    let step1 = this.animationStep({
      duration: time * 0.5, // 動畫時長
      tempo: 0.5, // 播放速率
      curve: Curve.EaseInOut, // 動畫曲線
      delay: 0, // 動畫延遲
      iterations: 1, // 播放次數(shù)
      playMode: PlayMode.Normal, // 動畫模式
    }, () => {
      this._scale = {
        x: 1.05,
        y: 1.05,
        z: 1.05
      }
    })

    // 50% - 100%
    let step2 = this.animationStep({
      duration: time * 0.5, // 動畫時長
      tempo: 0.5, // 播放速率
      curve: Curve.EaseInOut, // 動畫曲線
      delay: 0, // 動畫延遲
      iterations: 1, // 播放次數(shù)
      playMode: PlayMode.Normal, // 動畫模式
    }, () => {
      this._scale = {
        x: 1,
        y: 1,
        z: 1
      }
    })

    await step1()
    await step2()
  }
  initBlocks() {
    for (let i=0;i<this.avaliableCnt;i++) {
      let lineCn = Math.floor(i/3)
      let rowCn = Math.floor(i%3)
      if (lineCn == 0) {
        this.blockList[i] = {
          img: "censer",
          isShow: true,
          x: this.startX+rowCn*30,
          y: this.startY,
          w: 55,
          h: 53,
        }
      } else if (lineCn == 1) {
        this.blockList[i] = {
          img: "cloud",
          isShow: true,
          x: this.startX+rowCn*30,
          y: this.startY+lineCn*90,
          w: 55,
          h: 53,
        }
      } else if (lineCn == 2) {
        this.blockList[i] = {
          img: "knif",
          isShow: true,
          x: this.startX+rowCn*30,
          y: this.startY+lineCn*90,
          w: 55,
          h: 53,
        }
      }
    }
  }
  aboutToAppear() {
    this.initBlocks()
    let audioPlayer = media.createAudioPlayer();
    audioPlayer.on('dataLoad', () => {            //設(shè)置'dataLoad'事件回調(diào)值漫,src屬性設(shè)置成功后澳腹,觸發(fā)此回調(diào)
      console.info('audio set source success');
      audioPlayer.play();                       //開始播放,并觸發(fā)'play'事件回調(diào)
    });
//    audioPlayer.src = $r("app.media.background")
  }
  clearEmpty() {
    let emptyMap:Map<string, number> = new Map()
    console.info("emptylen:"+this.emptyList.length)
    for (let i=0;i<this.emptyList.length;i++) {
      let txt = this.emptyList[i]
      if (emptyMap[txt]) {
        let num = emptyMap[txt]
        emptyMap[txt] = num + 1
        if (emptyMap[txt] == 3) {
          for (let j=0;j<3;j++) {
            this.emptyList.splice(this.emptyList.indexOf(txt), 1)
          }
          this.emptyCnt += 3
          console.info("key:"+txt+"   n:"+this.emptyList.length)
        }
      } else {
        emptyMap[txt] = 1
      }
    }
  }
  isSelect(x, y) : number {
    let noshowCnt = 0
    let nofind = 0
    for (let i=0;i<this.blockList.length;i++) {
//    this.blockList.forEach((block)=>{
      let block = this.blockList[i]
      noshowCnt += 1
      x = Math.ceil(x)
      y = Math.ceil(y)
//      console.info("x:"+x+"y:"+y)
//      console.info("blockx:"+block.x+"block.y:"+block.y)
      let endx = block.x+this.blockw
      let endy = block.y+this.blockh

      if ((block.x <= x && endx >= x) &&
        (block.y <= y && endy >= y)) {
        console.info("isFind")
        if (block.isShow == true && this.emptyCnt > 0) {
          block.isShow = false;
          this.emptyCnt -= 1;
          this.avaliableCnt -= 1;
          this.emptyList.push(block.img)
          this.clearEmpty()
          //找到block
          if (this.avaliableCnt == 0) {
            return 2
          } else {
            if (this.emptyList.length == 5) {
              return 3
            } else {
              return 1
            }
          }
        } else if (this.emptyCnt == 0) {
          //沒有空閑空間
          return 3
        } else if (block.isShow == false) {
          nofind += 1
        }
      } else {
        console.info("noFind")
        nofind += 1
      }
    }
    if (nofind == this.blockList.length) {
      //沒有點中
      return 0
    }
    if (noshowCnt == this.blockList.length) {
      //沒有block
      return 2
    }

  }
  drawBlock() {
    this.blockList.forEach((block)=>{
      if (block.isShow) {
        let imgItem:ImageBitmap = null
        switch(block.img) {
          case "censer":
            imgItem = this.censerImg
            break
          case "cloud":
            imgItem = this.cloudImg
            break
          case "knif":
            imgItem = this.knifImg
            break
          default:
            imgItem = this.censerImg
            break
        }
        this.context.drawImage( this.cardImg,block.x,block.y,this.blockw,this.blockh)
        this.context.drawImage( imgItem,block.x+5,block.y+5,block.w,block.h)
      }
    })

    this.context.drawImage( this.slotImg,this.slotX,this.slotY,300,39)
    let pos = 0
    for (let i=0;i<5;i++) {
      this.context.drawImage( this.cardImg,this.slotX + pos,this.slotY+40,61,69)
      if (i < this.emptyList.length) {
        let emptyText = this.emptyList[i]
        let pItem = null;
        switch (emptyText) {
          case "censer":
            pItem = this.censerImg;
            break;
          case "cloud":
            pItem = this.cloudImg;
            break;
          case "knif":
            pItem = this.knifImg;
            break;
          default:
            break;
        }
        if (pItem) {
          this.context.drawImage(pItem,this.slotX + pos + 3,this.slotY+40,55,59)
        }
      }
      pos += 60
    }
  }
  build() {
    Row() {
      Column() {
        Canvas(this.context)
          .width('100%')
          .height('100%')
          .onClick((ev: ClickEvent) => {
            if (this.needRestart) {
              this.needRestart = false
              this.emptyList.splice(0, this.emptyList.length)
              this.blockList.splice(0, this.blockList.length)
              this.emptyCnt = 5
              this.avaliableCnt = 9
              this.initBlocks()
              this.context.clearRect(0,0,this.context.width,this.context.height)
              this.drawBlock()
              return
            }
            console.log("screen.xy:"+ev.screenX+":"+ev.screenY)
            console.log("xy:"+ev.x+":"+ev.y)
            //判斷是否點中方塊
            let flag = this.isSelect(ev.x, ev.y)
            console.info("flag:"+flag)
            if (flag == 1) {
              //如果可以移動或消除則清空充填
              this.context.clearRect(0,0,this.context.width,this.context.height)
              this.drawBlock();
            } else if (flag == 2) {
              //如果清空顯示勝利畫面
              this.context.drawImage( this.blackImg,0,0,this.context.width,this.context.height)
              this.context.drawImage( this.winImg,this.slotX+50,this.slotY-300,200,200)
              this.context.font="100px bold"
              this.context.fillText("歡迎你加入羊群", this.slotX+50,this.slotY-350,500)
              this.needRestart = true
            } else if (flag == 3) {
              this.context.clearRect(0,0,this.context.width,this.context.height)
              this.drawBlock();
              this.context.drawImage( this.blackImg,0,0,this.context.width,this.context.height)
              this.context.drawImage( this.ylgyImg,this.slotX+50,this.slotY-300,200,100)
              this.context.font="100px bold"
              this.context.fillText("加入羊群失敗", this.slotX+50,this.slotY-350,500)
              this.needRestart = true
            }
          })
          .onReady(() =>{
            this.context.imageSmoothingEnabled = false
            this.drawBlock()
          })
      }
      .height("80%")
      .width("100%")
    }
    .height('100%')
    .width('100%')
    .backgroundImage($r("app.media.grass"))
    .backgroundImageSize(ImageSize.Cover)
  }
}
win.png

遺留問題:

  1. 點擊選擇沒有判斷圖層:可以在卡片數(shù)據(jù)結(jié)構(gòu)里增加圖層標(biāo)識,最下面的卡片為圖層標(biāo)識為1羊娃,上面的多一層加1埃跷,點中選擇的時候可以判斷,增加是否可以選中的邏輯垃帅;

  2. 消除區(qū)域布局可靈活配置:增加布局配置邏輯贸诚,使用數(shù)據(jù)結(jié)構(gòu)設(shè)定布局邏輯窗宦,可規(guī)定卡片種類赴涵,數(shù)量髓窜,布局行數(shù),列數(shù)以及層級

  3. 游戲聲音問題:目前ohos不支持音頻播放資源音頻鳖敷,看之后版本是否支持

5. 獲取源碼

倉庫地址:https://gitee.com/wshikh/ohosylgy.git


總結(jié)

本文主要介紹了小游戲的開發(fā)定踱,畫布功能的使用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末崖媚,一起剝皮案震驚了整個濱河市畅哑,隨后出現(xiàn)的幾起案子水由,更是在濱河造成了極大的恐慌,老刑警劉巖泥张,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件媚创,死亡現(xiàn)場離奇詭異筝野,居然都是意外死亡粤剧,警方通過查閱死者的電腦和手機抵恋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門弧关,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人别瞭,你說我怎么就攤上這事蝙寨。” “怎么了墙歪?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵虹菲,是天一觀的道長毕源。 經(jīng)常有香客問我,道長郑藏,這世上最難降的妖魔是什么瘩欺? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任歌粥,我火速辦了婚禮拍埠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嬉探。我一直安慰自己涩堤,他們只是感情好分瘾,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布德召。 她就那樣靜靜地躺著,像睡著了一般福荸。 火紅的嫁衣襯著肌膚如雪逞姿。 梳的紋絲不亂的頭發(fā)上捆等,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音谒养,去河邊找鬼买窟。 笑死薯定,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的亏推。 我是一名探鬼主播年堆,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼芽狗,長吁一口氣:“原來是場噩夢啊……” “哼痒蓬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起顾复,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捕透,失蹤者是張志新(化名)和其女友劉穎乙嘀,沒想到半個月后破喻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡婴噩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年几莽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片站欺。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡矾策,死狀恐怖贾虽,靈堂內(nèi)的尸體忽然破棺而出吼鱼,到底是詐尸還是另有隱情,我是刑警寧澤庆尘,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布驶忌,位于F島的核電站笑跛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏几苍。R本人自食惡果不足惜陈哑,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一惊窖、第九天 我趴在偏房一處隱蔽的房頂上張望界酒。 院中可真熱鬧,春花似錦庇谆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春桶唐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欣簇。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工熊咽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留横殴,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓衫仑,卻偏偏與公主長得像文狱,于是被迫代替她去往敵國和親缘挽。 傳聞我的和親對象是個殘疾皇子壕曼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容