簡介
最近大火了一個小游戲火遍朋友圈,我們就一起看看如何能用OpenHarmony學(xué)習(xí)做個”羊了個羊“逼纸。本文中引用的圖片資源均來自:https://github.com/Jetereting/ylgy洋措。
開發(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頁面如下:
頁面顯示前會調(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)
}
}
遺留問題:
點擊選擇沒有判斷圖層:可以在卡片數(shù)據(jù)結(jié)構(gòu)里增加圖層標(biāo)識,最下面的卡片為圖層標(biāo)識為1羊娃,上面的多一層加1埃跷,點中選擇的時候可以判斷,增加是否可以選中的邏輯垃帅;
消除區(qū)域布局可靈活配置:增加布局配置邏輯贸诚,使用數(shù)據(jù)結(jié)構(gòu)設(shè)定布局邏輯窗宦,可規(guī)定卡片種類赴涵,數(shù)量髓窜,布局行數(shù),列數(shù)以及層級
游戲聲音問題:目前ohos不支持音頻播放資源音頻鳖敷,看之后版本是否支持
5. 獲取源碼
倉庫地址:https://gitee.com/wshikh/ohosylgy.git
總結(jié)
本文主要介紹了小游戲的開發(fā)定踱,畫布功能的使用