1. 項(xiàng)目的背景
Flappy Bird是一款2013年發(fā)布并在2014年特別流行的一款橫向卷抽游戲。玩家控制一只鳥(niǎo),試圖在綠色管道之間飛行而不撞到它們。
因?yàn)?a target="_blank">OpenHarmony具備多設(shè)備的窗口框架能力,可以支持不同設(shè)備類型的圖形界面的靈活性。今天我們就一起看看如何能用OpenHarmony學(xué)習(xí)做個(gè)FlappyBird。本文中引用的圖片資源均來(lái)自與Github。游戲的效果如下:
項(xiàng)目源碼如下:
https://gitee.com/wshikh/ohosflappybird
2. HAP應(yīng)用建立2.1HAP簡(jiǎn)介
HAP文件是在OpenHarmony系統(tǒng)下編譯生成的可執(zhí)行文件。HAP 包是由代碼、資源、第三方庫(kù)以及應(yīng)用配置文件打包生成的模塊包,主要分為兩種類型:entry 和 feature。
OpenHarmony 用戶應(yīng)用程序包可以只包含一個(gè)基礎(chǔ)的 entry 包,也可以包含一個(gè)基礎(chǔ)的 entry 包和一個(gè)或多個(gè)功能型的 feature 包。
entry:應(yīng)用的主模塊,作為 OpenHarmony 應(yīng)用的入口,提供了應(yīng)用的基礎(chǔ)功能。
feature:應(yīng)用的動(dòng)態(tài)特性模塊,作為應(yīng)用能力的擴(kuò)展,可以根據(jù)用戶的需求和設(shè)備的類型進(jìn)行選擇性安裝。
2.2HAP開(kāi)發(fā)工具
本開(kāi)發(fā)采用:DevEco Studio 3.0 Beta4
HUAWEI DevEco Studio For OpenHarmony是基于IntelliJ IDEA Community開(kāi)源版本打造,面向OpenHarmony全場(chǎng)景多設(shè)備的一站式集成開(kāi)發(fā)環(huán)境(IDE),DevEco Studio 3.0支持在HarmonyOS 3.0 Beta版上開(kāi)發(fā)應(yīng)用及服務(wù),并已適配ArkUI聲明式編程范式、ArkCompiler方舟編譯,同時(shí)提供低代碼開(kāi)發(fā)、雙向預(yù)覽、全新構(gòu)建工具、模擬器、調(diào)試調(diào)優(yōu)、信息中心等功能,為開(kāi)發(fā)者提供工程模板創(chuàng)建、開(kāi)發(fā)、編譯、調(diào)試、發(fā)布等E2E的OpenHarmony應(yīng)用/服務(wù)開(kāi)發(fā)。
下載鏈接:
https://developer.harmonyos.com/cn/develop/deveco-studio#download_beta
2.3ETS
ETS是基于TS擴(kuò)展的聲明式開(kāi)發(fā)范式的方舟開(kāi)發(fā)框架是一套開(kāi)發(fā)極簡(jiǎn)、高性能、跨設(shè)備應(yīng)用的UI開(kāi)發(fā)框架,支持開(kāi)發(fā)者高效的構(gòu)建跨設(shè)備應(yīng)用UI界面。
基于TS擴(kuò)展的聲明式開(kāi)發(fā)范式提供了一系列基礎(chǔ)組件,這些組件以聲明方式進(jìn)行組合和擴(kuò)展來(lái)描述應(yīng)用程序的UI界面,并且還提供了基本的數(shù)據(jù)綁定和事件處理機(jī)制,幫助開(kāi)發(fā)者實(shí)現(xiàn)應(yīng)用交互邏輯。
//用@Entry裝飾的自定義組件用作頁(yè)面的默認(rèn)入口組件,也可以理解為頁(yè)面的根節(jié)點(diǎn)。 一個(gè)頁(yè)面有且僅能有一個(gè)@Entry,只有被@Entry修飾的組件或者其子組件,才會(huì)在頁(yè)面上顯示。
//@Component裝飾的struct表示該結(jié)構(gòu)體具有組件化能力,能夠成為一個(gè)獨(dú)立的組件,這種類型的組件也稱為自定義組件,在build方法里描述UI結(jié)構(gòu)。
struct Hello { //在聲明式UI中,所有的頁(yè)面都是由組件構(gòu)成。組件的數(shù)據(jù)結(jié)構(gòu)為struct
string = 'World'
myText: build() { //build函數(shù)用于定義組件的聲明式UI描述,在build方法中以聲明式方式進(jìn)行組合自定義組件或系統(tǒng)內(nèi)置組件。
Column() { //Column:沿垂直方向布局的容器。
Text('Hello') //Text:顯示一段文本的組件。
.fontSize(30)
Text(this.myText)
.fontSize(32)
Divider() //Divider:提供分隔器組件,分隔不同內(nèi)容塊/內(nèi)容元素。
Button() { //Button:按鈕組件,可快速創(chuàng)建不同樣式的按鈕,通常用于響應(yīng)用戶的點(diǎn)擊操作。
Text('Click me')
.fontColor(Color.Red)
}.onClick(() => {
this.myText = 'UI'
})
.width(500)
.height(200)
}
}
}
2.4HAP基本概念
-
裝飾器:方舟開(kāi)發(fā)框架定義了一些具有特殊含義的裝飾器,用于裝飾類、結(jié)構(gòu)、方法和變量。裝飾器就是某一種修飾,給被裝飾的對(duì)象賦予某一種能力,比如@Entry就是頁(yè)面入口的能力,@Component就是組件化能力。
-
自定義組件:可重用的UI單元,可以與其他組件組合,如@Component裝飾的struct Hello。
-
UI描述:聲明性描述UI結(jié)構(gòu),例如build()方法中的代碼塊。
-
內(nèi)置組件:框架中默認(rèn)內(nèi)置的基本組件和布局組件,開(kāi)發(fā)者可以直接調(diào)用,僅用于解釋UI描述規(guī)范。如Column、Text、Divider、Button等。
-
屬性方法:用于配置組件屬性,如fontSize()、width()、height()、color()等。
-
事件方法:在事件方法的回調(diào)中添加組件響應(yīng)邏輯。例如,為Button組件添加onClick方法,在onClick方法的回調(diào)中添加點(diǎn)擊響應(yīng)邏輯。
2.5page文件
基于TS擴(kuò)展的聲明式開(kāi)發(fā)范式提供了一系列基礎(chǔ)組件,如:基礎(chǔ)組件,容器組件,媒體組件,繪制組件和畫布組件等,本節(jié)我們主要使用畫布組件。
這里我們就不贅述Hap項(xiàng)目的建立過(guò)程,以下就是基礎(chǔ)的Hap的page文件:index.ets
build() {
Row() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.onClick((ev: ClickEvent) => {
console.info("click!!")
//響應(yīng)鼠標(biāo)左擊
this.doClick()
})
.onReady(() =>{
//繪制基礎(chǔ)
this.context.imageSmoothingEnabled = false
this.drawBlock()
})
}
.width('100%')
}
.height('100%')
.backgroundImage($r("app.media.backgroundday"))
.backgroundImageSize(ImageSize.Cover)
}
build是基礎(chǔ)頁(yè)面的構(gòu)造函數(shù),用于界面的元素構(gòu)造,其他的頁(yè)面的生命周期函數(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;
}
3. Canvas畫布介紹canvas是畫布組件用于自定義繪制圖形,具體的API頁(yè)面如下:
https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-components-canvas-canvas-0000001333641081
頁(yè)面顯示前會(huì)調(diào)用aboutToAppear()函數(shù),此函數(shù)為頁(yè)面生命周期函數(shù)。
canvas組件初始化完畢后會(huì)調(diào)用onReady()函數(shù),函數(shù)內(nèi)部實(shí)現(xiàn)小游戲的初始頁(yè)面的繪制。
3.1初始化頁(yè)面數(shù)據(jù)
drawBlock() {
this.context.clearRect(0,0,this.context.width,this.context.height)
this.context.drawImage( this.baseImg,this.baseX,this.baseY,500,300)
switch(this.flappyState) {
case 0:
this.context.drawImage( this.messageImg,this.startX,this.startY,300,500)
this.drawBird()
break;
case 1:
this.drawBird()
this.context.drawImage( this.pipegreenImg,this.pipeX,this.pipeY,50,150)
break;
case 2:
this.context.drawImage( this.gameoverImg,this.startX,this.startY*3,300,90)
break
}
}
頁(yè)面狀態(tài)有三:
-
0:等待開(kāi)始界面
-
1:游戲進(jìn)行
-
2:游戲結(jié)束
3.2繪制Bird
drawBird() {
switch(this.birdType) {
case 0:
this.context.drawImage( this.midbirdImg,this.slotX,this.slotY,this.birdH,this.birdW)
break
case 1:
this.context.drawImage( this.upbirdImg,this.slotX,this.slotY,this.birdH,this.birdW)
break;
case 2:
this.context.drawImage( this.downbirdImg,this.slotX,this.slotY,this.birdH,this.birdW)
break;
default:
break;
}
}
小鳥(niǎo)飛行狀態(tài)有三種:
-
翅膀在中間:0
-
翅膀在上:1
-
翅膀在下:2
4.1 主體游戲邏輯:
簡(jiǎn)單的小游戲主體游戲邏輯為:等待開(kāi)始,開(kāi)始,結(jié)束流程圖如下:
graph LR
等待開(kāi)始 --> click[點(diǎn)擊]
click[點(diǎn)擊] --> 游戲開(kāi)始
游戲開(kāi)始 --> 點(diǎn)擊 --> |游戲開(kāi)始|小鳥(niǎo)飛,水管動(dòng) --> |小鳥(niǎo)碰到水管| 游戲結(jié)束 --> 點(diǎn)擊 --> |游戲結(jié)束| 等待開(kāi)始
小鳥(niǎo)飛,水管動(dòng) --> |小鳥(niǎo)沒(méi)碰到水管| 游戲繼續(xù) --> 點(diǎn)擊
doClick() {
switch (this.flappyState) {
case 0:
{
// 開(kāi)始
this.flappyState = 1
break
}
case 1:
{
//上下飛
// this.flappyState = 2
this.slotY -= this.flyHeight
console.log(this.slotY.toString())
break
}
case 2:
{
//由結(jié)束到待開(kāi)始
this.flappyState = 0
this.slotY = this.slotStartY
this.pipeX = this.pipeStartX
break
}
default:
break
}
this.drawBlock()
}
4.2 完整游戲邏輯:
struct Index {
'Hello World'
message: string = private baseImg:ImageBitmap = new ImageBitmap("common/images/base.png")
private messageImg:ImageBitmap = new ImageBitmap("common/images/message.png")
private zeroImg:ImageBitmap = new ImageBitmap("common/images/0.png")
private gameoverImg:ImageBitmap = new ImageBitmap("common/images/gameover.png")
private upbirdImg:ImageBitmap = new ImageBitmap("common/images/bluebirdupflap.png")
private midbirdImg:ImageBitmap = new ImageBitmap("common/images/bluebirdmidflap.png")
private downbirdImg:ImageBitmap = new ImageBitmap("common/images/bluebirddownflap.png")
private pipegreenImg:ImageBitmap = new ImageBitmap("common/images/pipegreen.png")
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private flappyState: number = 0
private startX = 30;
private startY = 100;
private slotStartY = 410;
private slotX = 50;
private slotY = this.slotStartY;
private baseX = 0;
private baseY = 650;
private pipeStartX = 330;
private pipeX = this.pipeStartX;
private pipeY = 500;
private birdH = 60;
private birdW = 50;
private birdTimer: number;
private birdType: number = 0;
private count = 1;
private flyHeight = 20;
private pipeMove = 10;
drawBird() {
switch(this.birdType) {
case 0:
this.context.drawImage( this.midbirdImg,this.slotX,this.slotY,this.birdH,this.birdW)
break
case 1:
this.context.drawImage( this.upbirdImg,this.slotX,this.slotY,this.birdH,this.birdW)
break;
case 2:
this.context.drawImage( this.downbirdImg,this.slotX,this.slotY,this.birdH,this.birdW)
break;
default:
break;
}
}
drawBlock() {
this.context.clearRect(0,0,this.context.width,this.context.height)
this.context.drawImage( this.baseImg,this.baseX,this.baseY,500,300)
switch(this.flappyState) {
case 0:
this.context.drawImage( this.messageImg,this.startX,this.startY,300,500)
this.drawBird()
break;
case 1:
this.drawBird()
this.context.drawImage( this.pipegreenImg,this.pipeX,this.pipeY,50,150)
break;
case 2:
this.context.drawImage( this.gameoverImg,this.startX,this.startY*3,300,90)
break
}
}
doClick() {
switch (this.flappyState) {
case 0:
{
// 開(kāi)始
this.flappyState = 1
break
}
case 1:
{
//上下飛
// this.flappyState = 2
this.slotY -= this.flyHeight
console.log(this.slotY.toString())
break
}
case 2:
{
//由結(jié)束到待開(kāi)始
this.flappyState = 0
this.slotY = this.slotStartY
this.pipeX = this.pipeStartX
break
}
default:
break
}
this.drawBlock()
}
doFly(): void {
console.log("dofly ------ !!")
this.birdType += 1
if (this.birdType/5 == 0) {
this.message = "dofly ---555--- !!"
}
}
async sleep(ms: number) {
return new Promise((r) => {
setInterval(() => {
this.birdType += 1
this.message = this.birdType.toString()
if (this.birdType == 3) {
this.birdType = 0
}
console.log(this.message)
if (this.flappyState == 1) {
this.pipeX -= this.pipeMove
if (this.pipeX < 0) {
this.pipeX = 330
}
this.slotY += this.flyHeight/5
}
if ((((this.pipeX-this.slotX) <= this.birdW) && ((this.pipeY-this.slotY) <= this.birdH)) ||
this.pipeY >= this.baseY) {
this.flappyState = 2
}
this.drawBlock()
}, ms)
})
}
aboutToDisappear() {
}
aboutToAppear() {
this.sleep(200)
}
build() {
Row() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.onClick((ev: ClickEvent) => {
console.info("click!!")
this.doClick()
})
.onReady(() =>{
this.context.imageSmoothingEnabled = false
this.drawBlock()
})
}
.width('100%')
}
.height('100%')
.backgroundImage($r("app.media.backgroundday"))
.backgroundImageSize(ImageSize.Cover)
}
}
5. 游戲的瑕疵-
水管只在下層顯示:可以在上層顯示;
-
地面沒(méi)有讓動(dòng)
-
游戲聲音問(wèn)題:目前ohos不支持音頻播放資源音頻,看之后版本是否支持
-
DevEcoy用setInterval重繪canvas會(huì)導(dǎo)致ide崩潰
本文完
寫在最后我們最近正帶著大家玩嗨OpenHarmony。如果你有好玩的東東,歡迎投稿,讓我們一起嗨起來(lái)!有點(diǎn)子,有想法,有Demo,立刻聯(lián)系我們:合作郵箱:zzliang@atomsource.org
原文標(biāo)題:玩嗨OpenHarmony:基于OpenHarmony的小游戲:一起學(xué)做FlappyBird
文章出處:【微信公眾號(hào):開(kāi)源技術(shù)服務(wù)中心】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
開(kāi)源技術(shù)
+關(guān)注
關(guān)注
0文章
389瀏覽量
7891 -
OpenHarmony
+關(guān)注
關(guān)注
25文章
3611瀏覽量
15964
原文標(biāo)題:玩嗨OpenHarmony:基于OpenHarmony的小游戲:一起學(xué)做FlappyBird
文章出處:【微信號(hào):開(kāi)源技術(shù)服務(wù)中心,微信公眾號(hào):共熵服務(wù)中心】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論