开发flappy bird小游戏(附完整源码参考)

小游戏玩法介绍

  • 用户通过点击屏幕让小鸟往上飞,控制小鸟穿过管道,每穿过1组水管加1分,直到碰到管道游戏结束。

鸟节点

  • 制作一个鸟节点,给它添加碰撞体和刚体组件。

  • 碰撞体组件:可以选用CircleCollider2D组件,通过碰撞体可以方便监听鸟和管道是否产生碰撞。

  • 刚体组件:选用动力学类型,通过力的传递让鸟实现往上飞行的效果。

鸟运动

  • 鸟的飞行效果实现思路:让鸟只往上运动,而管道持续往左移动,从而实现鸟往右上飞行的效果。

  • 监听屏幕触摸事件,给鸟刚体传递一个向上的力。

// 监听屏幕触摸,让小鸟往上运动
input.on(Input.EventType.TOUCH_START, this.touchBegin, this)
touchBegin(event: EventTouch){
    if (this.gameStatus === GameStatus.end || this.gameStatus === GameStatus.init) return
    if (this.gameStatus === GameStatus.ready){
        this.startGame()
    }
    this.audioManager.playWing()
    // 通过力向上运动
    this.birdRd.applyForceToCenter(this.force, true)
    // 旋转动画
    this.beginTween = tween(this.birdNode).to(0, {angle: 0}).to(0.2, {angle: 40}).to(0.5, {angle: -20}).start()
}

管道生成

  • 管道的缺口是以屏幕高度尺寸得到一个随机值。

  • 管道分为上下两个管道,通过一个预制体管道实例两次,调整管道上下位置组成一个管道组,而下个管道组的位置是根据前一个管道组的位置进行偏移。

generatePipe(){
    const startIndex = this.pipeList.length
    for (let i=0;i<this.pipeNum;i++){
        const pipeItem:PipeItemInterface = {
            index: startIndex + i,
            recordScore: false,
            pipeTopNode: null,
            pipeBottomNode: null
        }
        const randomLimit = this.screenHeight * 0.65
        const randomY = randomRangeInt(0, randomLimit) - randomLimit / 2
        this.createPipe(pipeItem, randomY)
        this.pipeList.push(pipeItem)
    }
}

createPipe(pipeItem: PipeItemInterface, randomY: number){
    let previousPipeItem = null
    let previousPipePos: Vec3 = null
    if (pipeItem.index > 0){
        previousPipeItem = this.pipeList[pipeItem.index-1]
        previousPipePos = previousPipeItem.pipeTopNode.getWorldPosition()
    }else{
        previousPipeItem = pipeItem
        previousPipePos = new Vec3(this.pipeStartSpacing, 0, 0)
    }
    // 生成上管道
    pipeItem.pipeTopNode = instantiate(this.pipePrefab)
    pipeItem.pipeTopNode.getComponent(UITransform).setContentSize(this.pipeWidth, this.screenHeight)
    const pipeTopCollider: BoxCollider2D = pipeItem.pipeTopNode.getComponent(BoxCollider2D)
    pipeTopCollider.size = new Size(this.pipeWidth, this.screenHeight)
    pipeTopCollider.offset = new Vec2(0, this.screenHeight / 2)
    pipeItem.pipeTopNode.setWorldPosition(
        previousPipePos.x + this.pipeWidthSpacing,
        randomY + this.pipeHeightSpacing,
        0
    )
    // 生成下管道
    pipeItem.pipeBottomNode = instantiate(this.pipePrefab)
    pipeItem.pipeBottomNode.getComponent(UITransform).setContentSize(this.pipeWidth, this.screenHeight)
    const pipeBottomCollider: BoxCollider2D = pipeItem.pipeBottomNode.getComponent(BoxCollider2D)
    pipeBottomCollider.size = new Size(this.pipeWidth, this.screenHeight)
    pipeBottomCollider.offset = new Vec2(0, this.screenHeight / 2)
    pipeItem.pipeBottomNode.setWorldPosition(
        previousPipePos.x + this.pipeWidthSpacing,
        randomY - this.pipeHeightSpacing,
        0
    )
    pipeItem.pipeBottomNode.setScale(1, -1)
    this.pipesNode.addChild(pipeItem.pipeTopNode)
    this.pipesNode.addChild(pipeItem.pipeBottomNode)
}

管道移动

  • 先定义一个管道每秒移动速度,然后在帧回调函数中,让管道持续往左移动。

  • 每秒移动速度 * 前一帧间隔时间 = 要移动的像素值,让管道减去每帧要移动的像素值,达到往左持续移动效果。

  • 为了优化游戏,不能存在过多节点,要让管道出了屏幕范围时进行销毁。

update(deltaTime: number) {
    if (this.gameStatus === GameStatus.running){
        this.pipeList.forEach((pipeItem:PipeItemInterface)=>{
            // 管道移动
            pipeItem.pipeTopNode.getWorldPosition(this.tempVec3).subtract3f(this.pipeMoveSpeed * deltaTime, 0, 0)
            pipeItem.pipeTopNode.setWorldPosition(this.tempVec3)
            pipeItem.pipeBottomNode.getWorldPosition(this.tempVec3).subtract3f(this.pipeMoveSpeed * deltaTime, 0, 0)
            pipeItem.pipeBottomNode.setWorldPosition(this.tempVec3)
            // 销毁管道数据
            if (this.tempVec3.x < -this.pipeWidth){
                pipeItem.pipeTopNode.destroy()
                pipeItem.pipeBottomNode.destroy()
                this.pipeList.splice(this.pipeList.findIndex(item=>item===pipeItem), 1)
            }
        })
    }
}

得分

  • 在帧回调函数中,在管道往左移动后,判断鸟的x位置如果大于管道组x位置时,进行得分累计。

  • 并且可以根据得分逻辑,来生成新的管道组。

update(deltaTime: number) {
    if (this.gameStatus === GameStatus.running){
        this.pipeList.forEach((pipeItem:PipeItemInterface)=>{
            // 管道移动
            pipeItem.pipeTopNode.getWorldPosition(this.tempVec3).subtract3f(this.pipeMoveSpeed * deltaTime, 0, 0)
            pipeItem.pipeTopNode.setWorldPosition(this.tempVec3)
            pipeItem.pipeBottomNode.getWorldPosition(this.tempVec3).subtract3f(this.pipeMoveSpeed * deltaTime, 0, 0)
            pipeItem.pipeBottomNode.setWorldPosition(this.tempVec3)
            // 判断穿过管道记录成绩(每个管道只会执行一次)
            if (!pipeItem.recordScore && this.tempVec3.x <= this.birdX){
                pipeItem.recordScore = true
                this.score += 1
                this.audioManager.playScore()
                // 判断生成新的管道(只剩x组管道时)
                if (this.pipesNode.children.length / 2 <= 3){
                    this.generatePipe()
                }
            }
            // 销毁管道数据
            if (this.tempVec3.x < -this.pipeWidth){
                pipeItem.pipeTopNode.destroy()
                pipeItem.pipeBottomNode.destroy()
                this.pipeList.splice(this.pipeList.findIndex(item=>item===pipeItem), 1)
            }
        })
    }
}

游戏结束

  • 通过碰撞组件监听鸟和管道的碰撞,当鸟和管道碰撞时游戏结束。

  • 当鸟碰到管道时,让鸟顺时针旋转到-40,实现撞到墙后的效果,由于鸟是动力学会一直往下掉落,所以还需在x秒后改为静态类型让鸟停在屏幕外。

// 监听小鸟的碰撞
const birdCollider = this.birdNode.getComponent(Collider2D)
birdCollider.on(Contact2DType.BEGIN_CONTACT, this.onBirdColliderBegin, this)
onBirdColliderBegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){
    // 小鸟碰到管道、空气墙,游戏结束
    if (
        this.gameStatus === GameStatus.running && 
        (
            otherCollider.node.name === this.pipePrefab.name
        )
    ){
        this.gameOver()
    }
}

gameOver(){
    this.setGameStauts(GameStatus.end)
    this.audioManager.playDie()
    // 死亡动画
    this.beginTween.stop()
    tween(this.birdNode).to(0.1, {angle: -40}).start()
    // 不让鸟节点一直下落
    this.scheduleOnce(()=>{
        if (this.gameStatus === GameStatus.end){
            this.birdRd.type = ERigidBody2DType.Static
        }
    }, 3)
}

空气墙

  • 由于鸟是往上运动,而管道的高度是有限的,当疯狂点击屏幕可以让鸟绕过管道进行得分,因此可以在鸟的上下方加上空气墙,让鸟碰到空气墙时也游戏结束。

  • 创建空气墙节点,添加盒子碰撞组件,并在鸟的碰撞回调里判断碰撞东西是否是空气墙。

onBirdColliderBegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){
    // 小鸟碰到管道、空气墙,游戏结束
    if (
        this.gameStatus === GameStatus.running && 
        (
            otherCollider.node.name === this.pipePrefab.name || 
            otherCollider.node.name === this.topAirWallNode.name ||
            otherCollider.node.name === this.bottomAirWallNode.name
        )
    ){
        this.gameOver()
    }
}

背景缓缓移动

  • 为了让显示效果更好一些,可以让背景以极慢速度缓缓朝左边移动。

  • 需要创建两个相同的背景节点,让图片无缝排列在一起,在帧回调函数里让它俩持续往左移动,当第一个图片过了屏幕时,给它移动到第二个图片后面,再次整体持续移动,实现无缝背景图片移动。

update(deltaTime: number) {
    if (this.gameStatus === GameStatus.running){
        this.pipeList.forEach((pipeItem:PipeItemInterface)=>{
            // 管道移动
            pipeItem.pipeTopNode.getWorldPosition(this.tempVec3).subtract3f(this.pipeMoveSpeed * deltaTime, 0, 0)
            pipeItem.pipeTopNode.setWorldPosition(this.tempVec3)
            pipeItem.pipeBottomNode.getWorldPosition(this.tempVec3).subtract3f(this.pipeMoveSpeed * deltaTime, 0, 0)
            pipeItem.pipeBottomNode.setWorldPosition(this.tempVec3)
            // 判断穿过管道记录成绩(每个管道只会执行一次)
            if (!pipeItem.recordScore && this.tempVec3.x <= this.birdX){
                pipeItem.recordScore = true
                this.score += 1
                this.audioManager.playScore()
                // 判断生成新的管道(只剩x组管道时)
                if (this.pipesNode.children.length / 2 <= 3){
                    this.generatePipe()
                }
            }
            // 销毁管道数据
            if (this.tempVec3.x < -this.pipeWidth){
                pipeItem.pipeTopNode.destroy()
                pipeItem.pipeBottomNode.destroy()
                this.pipeList.splice(this.pipeList.findIndex(item=>item===pipeItem), 1)
            }
        })
        // 第一背景切换
        if (this.bgNode.children[0].getWorldPosition().x <= -this.bgWidth){
            this.bgNode.children[0].setWorldPosition(this.bgNode.children[this.bgNode.children.length-1].getWorldPosition().add3f(this.bgWidth, 0, 0))
            this.bgNode.insertChild(this.bgNode.children[0], this.bgNode.children.length)
        }
        // 背景持续往左移动
        for (let i=0;i<this.bgNode.children.length;i++){
            const node = this.bgNode.children[i]
            node.setWorldPosition(node.getWorldPosition().subtract3f(this.bgMoveSpeed * deltaTime, 0, 0))
        }
    }
}

获取完整小游戏源码

  • 以上内容是讲述了flappy bird小游戏核心逻辑实现,你如果是为了给别人玩、为了上架小游戏平台,那么就需要使用一个游戏引擎去开发小游戏。

  • Cocos商城 里,搜索 zezhou222 ,获取对应小游戏源码学习!

posted @ 2025-03-12 23:17  zezhou222  阅读(227)  评论(0)    收藏  举报