#冲刺创作新星#一起学做鸿蒙“羊了个羊” 原创 精华
简介
最近大火了一个小游戏火遍朋友圈,我们就一起看看如何能用OpenHarmony学习做个”羊了个羊“。本文中引用的图片资源均来自:https://github.com/Jetereting/ylgy。
开发
1. HAP应用建立
《#跟着小白一起学鸿蒙#[六]如何编写一个hap应用》里我们介绍了简单的Hap应用的开发以及基础控件的介绍,这里我们就不赘述Hap项目的建立过程,以下就是基础的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)
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
build是基础页面的构造函数,用于界面的元素构造,其他的页面的生命周期函数如下:
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;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
2. Canvas介绍
canvas是画布组件用于自定义绘制图形,具体的API页面如下:
页面显示前会调用aboutToAppear()函数,此函数为页面生命周期函数
canvas组件初始化完毕后会调用onReady()函数,函数内部实现小游戏的初始页面的绘制
2.1 初始化页面数据
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,
}
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
小游戏的每个卡片都是用canvas绘制的图片资源,用于进行排列以及点击判断所以在此设计了个数据结构
{
img: 卡片资源类型,用于图片渲染和相似图片消除
isShow: 卡片是否显示标志,用于渲染的时候进行判断
x:卡片渲染左上角横坐标
y:卡片渲染左上角纵坐标
w:卡片渲染宽度
h: 卡片渲染高度
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
现在制作的是用固定方法初始化卡片的方法即渲染3行,每行3个图片,之后改进可以改成明确一个区域,然后采用随机算法进行位置和卡片类型生成。
2.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
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
整个绘制区域分两个区域:
- 消除区域:绘制卡片背景和卡片类型,利用初始化的卡片数据进行卡片绘制;
- 选择区域:绘制栏杆,卡片背景,以及选择的卡片
3. 游戏逻辑
简单的小游戏主体游戏逻辑为:初始化(之前的章节已经介绍),点击(选中,选不中,消除,选择区域满,消除区域空)流程图如下:
graph LR
init[初始化] --> click[点击]
click[点击] --> isSelect{是否点中}
isSelect -->|点中| yes[点中]
isSelect -->|没点中| no[没点中]
yes --> isEmpty{是否选择区域满}
isEmpty -->|满| full[无法消除]
isEmpty -->|不满| notfull[加入选择区域]
notfull --> canClear{有3个相同}
canClear -->|能消除| clear[消除]
canClear -->|不能消除| append[进入选择区域]
append --> 重绘
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
.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
}
})
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
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, // 播放次数
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, // 播放次数
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', () => { //设置'dataLoad'事件回调,src属性设置成功后,触发此回调
console.info('audio set source success');
audioPlayer.play(); //开始播放,并触发'play'事件回调
});
// 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)
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
遗留问题:
-
点击选择没有判断图层:可以在卡片数据结构里增加图层标识,最下面的卡片为图层标识为1,上面的多一层加1,点中选择的时候可以判断,增加是否可以选中的逻辑;
-
消除区域布局可灵活配置:增加布局配置逻辑,使用数据结构设定布局逻辑,可规定卡片种类,数量,布局行数,列数以及层级
-
游戏声音问题:目前ohos不支持音频播放资源音频,看之后版本是否支持
5. 获取源码
仓库地址:https://gitee.com/wshikh/ohosylgy.git
总结
本文主要介绍了小游戏的开发,画布功能的使用
微信扫码分享
不错,学习后做个更高难度的发给室友。
这篇厉害!正在学习
大神
牛....
期待第二关
执行力MAX
学习下如何实现的
不错不错,学习了
我还没通关,游戏都已经做出来了