小游戏玩法介绍
- 用户通过点击屏幕让小鸟往上飞,控制小鸟穿过管道,每穿过1组水管加1分,直到碰到管道游戏结束。
鸟节点
-
制作一个鸟节点,给它添加碰撞体和刚体组件。
-
碰撞体组件:可以选用CircleCollider2D组件,通过碰撞体可以方便监听鸟和管道是否产生碰撞。
-
刚体组件:选用动力学类型,通过力的传递让鸟实现往上飞行的效果。
鸟运动
-
鸟的飞行效果实现思路:让鸟只往上运动,而管道持续往左移动,从而实现鸟往右上飞行的效果。
-
监听屏幕触摸事件,给鸟刚体传递一个向上的力。
// 监听屏幕触摸,让小鸟往上运动
input.on(Input.EventType.TOUCH_START, this.touchBegin, this)
touchBegin(event: EventTouch){if (this.gameStatus === GameStatus.end || this.gameStatus === GameStatus.init) returnif (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.lengthfor (let i=0;i<this.pipeNum;i++){const pipeItem:PipeItemInterface = {index: startIndex + i,recordScore: false,pipeTopNode: null,pipeBottomNode: null}const randomLimit = this.screenHeight * 0.65const randomY = randomRangeInt(0, randomLimit) - randomLimit / 2this.createPipe(pipeItem, randomY)this.pipeList.push(pipeItem)}
}createPipe(pipeItem: PipeItemInterface, randomY: number){let previousPipeItem = nulllet previousPipePos: Vec3 = nullif (pipeItem.index > 0){previousPipeItem = this.pipeList[pipeItem.index-1]previousPipePos = previousPipeItem.pipeTopNode.getWorldPosition()}else{previousPipeItem = pipeItempreviousPipePos = 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 = truethis.score += 1this.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 = truethis.score += 1this.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 ,获取对应小游戏源码学习!