CocosCreator物理小游戏实战-别离开碗(四)完结!

摘要

阔阔录制的物理小游戏视频教程更新到了26集!完结撒花!文章总结下后面部分,去 GitHub 给阔阔点个 Star 就是最大的认可。

正文

使用版本

CocosCreator 版本 2.3.4

视频链接为:https://www.bilibili.com/video/BV1ck4y167mR

GitHub地址:https://github.com/KuoKuo666/NotLevelTheBowl

碗的眨眼动作(第15集)

使用 tween 实现一个永远重复的纹理切换:

// 眨眼动作
cc.tween(this.bowl).repeatForever(
    cc.tween()
        .delay(2)
        .call(() => this.bowl.getComponent(cc.Sprite).spriteFrame = this.closeEyeBowl)
        .delay(0.3)
        .call(() => this.bowl.getComponent(cc.Sprite).spriteFrame = this.openEyeBowl)
).start()

其中 this.closeEyeBowlthis.openEyeBowl 就是对应闭眼与睁眼的图片。

点击下落(第16集)

在游戏开始时会用 this.midConfig.node 来指向中心生成的待下落物体,下落逻辑后,将其改为动态类型的刚体,然后给个初速度,因为在检测停止逻辑时使用的是线速度检测,不给与初速度会导致最开始符合“停止”条件。

onClickDownFood() {
    if (!this.midConfig.node) { return }
    PhysicsManager.setRigidBoyDynamic(this.midConfig.node)
    PhysicsManager.setRigidBoyLinearVelocity(this.midConfig.node, cc.v2(0, -5))
    this.midConfig.node = undefined
}
关卡配置信息(第17集)

我们有 6 个关卡需要设计,每个关卡对应着要生成的食物,食物可以从 0 开始对应,从 0-5 对应六种食物,采用了一个数组的模式:

const GameConfig = [
    // 0 无数据
    [],
    // 第一关:2个蛋糕
    [2, 2],
    // 第二关:3个鸡翅
    [0, 0, 0],
    // 第三关:2个蛋糕 2个鸡蛋
    [2, 2, 1, 1],
    // 第四关:4个汉堡
    [5, 5, 5, 5],
    // 第五关:4个薯条
    [4, 4, 4, 4],
    // 第六关:5个饭团
    [3, 3, 3, 3, 3]
]
根据数据创建食物&关卡信息(第18-19集)

在导入这个配置数据后,根据开始游戏时设置的变量,写出这样一个 get 属性,方便的获取食物类型:

import GameConfig from "./config/GameConfig"

get nowFoodType(): number {
    return GameConfig[this.midConfig.level][this.midConfig.count]
}

在食物完成下落后会改变这个 midConfig 中数量属性,然后渲染到界面上,下面是数据结构类型与渲染逻辑:

interface IMidConfig {
    level: number,
    count: number,
    node: cc.Node | undefined
}

updateFoodCountUi() {
    StaticInstance.uiManager.setLevelInfo(this.midConfig.level, this.midConfig.count)
}
判断食物静止&过关条件(第20-21集)

其实这个物理小游戏的核心逻辑就是这块了,如何判断所有刚体都静止了,物理系统是有误差的,而且有时在两个动态刚体接触时有可能会抖动,虽然线速度很小,但是不会 ===0,所以这里有个允许的判断误差,食物都在 foods 节点下:

get allBodyStop(): boolean {
    for (let i = 0; i < this.foods.childrenCount; i++) {
        const node = this.foods.children[i]
        const body = node.getComponent(cc.RigidBody)
        if (!body.linearVelocity.fuzzyEquals(cc.v2(0, 0), 0.1)) {
            return false
        }
    }
    return true
}

判断所有刚体线速度与 0 之间的距离是否都符合要求,如果 0.1 精度不够,可以设置 0.02 左右。然后判断过关的条件就是所有的食物都下落完毕,其实就是相当于所有食物都是动态的刚体即可,因为只要没过关,最上方必然会生成一个新的静态刚体:

get someBodyStatic(): boolean {
    for (let i = 0; i < this.foods.childrenCount; i++) {
        const node = this.foods.children[i]
        const body = node.getComponent(cc.RigidBody)
        if (body.type === cc.RigidBodyType.Static) {
            return true
        }
    }
    return false
}

然后检测系统每 0.2 秒工作一次,进行判断:

update(dt: number) {
    this.time += dt
    if (this.time > this.checkCD) {
        this.time = 0
        this.checkAllBody()
        this.checkFall()
    }
}

checkAllBody() {
    if (!this.isPlaying || this.someBodyStatic || !this.allBodyStop ) { return }
    if (!this.canAddFood) {
        this.isPlaying = false
        this.gameWin()
        return
    }
    this.midConfig.node = this.addFood(this.nowFoodType)
}
胜利与失败逻辑(第22-23集)

this.checkFall() 这个检测方法中,判断有没有刚体下落到屏幕外,如果有就是失败了,如果一直到最后无静态刚体就是胜利了,根据胜利与失败弹出做好的两个 UI 界面,其中有三个按钮需要处理事件逻辑:

// 下一关
onClickNextLevel() {
    this.midConfig.level += 1
    this.clearAllFood()
    StaticInstance.uiManager.gameStart(this.midConfig.level)
}

// 再玩一次
onClickPlayAgain() {
    this.clearAllFood()
    StaticInstance.uiManager.gameStart(this.midConfig.level)
}

// 返回主菜单
backToStartMenu() {
    StaticInstance.gameManager.clearAllFood()
    StaticInstance.gameManager.hideBowl()
    this.showUI([UIType.StartMenu])
}
处理压缩后图片黑边问题(第23集)

黑边问题相信在压缩带透明图片时大家都遇到过,处理方案是,把图片勾选 premultiplyAlpha 选项,然后在精灵组件混合模式那里改成 ONE,如下图即可:

数据存储模块(第24集)

使用了一个 DataStorage 类,暴露存取静态方法,使用了 cc.sys 的内置方法:

static getUnLockLevel(): number {
    const value = cc.sys.localStorage.getItem('unLockLevel')
    if (!value) {
        // 默认取个 1
        console.warn('[DataStorage] getUnLockLevel is undefined, set 1')
        DataStorage.saveUnLockLevel(1)
        return 1
    }
    console.log('[DataStorage] getUnLockLevel is ' + value)
    return JSON.parse(value)
}

static saveUnLockLevel(level: number) {
    console.log(`[DataStorage] saveUnLockLevel ${level}`)
    cc.sys.localStorage.setItem('unLockLevel', JSON.stringify(level))
}
音频单例管理(第25集)

在 TypeScript 中书写单例有几种不同方法,这里使用的是私有化构造函数:

export class MusicManager {

    private static instance: MusicManager

    /** 构造函数私有化 */
    private constructor () { 
    }

    static getInstance (): MusicManager {
        if (!this.instance) {
            this.instance = new MusicManager()
        }
        return this.instance
    }
}

这样使用单例时,通过 MusicManager.getInstance() 来获取即可。在播放音频资源时,采取了放在 resources 文件下异步获取的方法,封装了一个获取方法,返回 promise,这样就可以在使用时用 await 关键字:

static loadMusic(url: MuiscResUrl): Promise<cc.AudioClip | undefined> {
    return new Promise((resolve, reject) => {
        cc.loader.loadRes(url, cc.AudioClip, (error, audioClip) => {
            if (error) {
                console.error('[Util] loadMusic error')
                resolve(undefined)
            }
            resolve(audioClip)
        })
    })
}

// 在音频单例类中使用举例
private async playBGM() {
    const audioClip = await Util.loadMusic(MuiscResUrl.Bgm)
    audioClip && cc.audioEngine.playMusic(audioClip, true)
}

结语

走过了两个月,从视频录制到剪辑与配音,实属不易,但还是坚持下来了。希望大家在看过视频后能有所收获,阔阔会继续学习分享知识的,哔哩哔哩求关注,然后 GitHub 希望能给点个 Star !

视频链接为:https://www.bilibili.com/video/BV1ck4y167mR

GitHub地址:https://github.com/KuoKuo666/NotLevelTheBowl

2020!我们一起进步!O(∩_∩)O~~

微信公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大家好我是阔阔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值