前言
21年年底找了一个大厂远程的工作,从北京回了老家,本来挺爽,没想到今年6月份集体被裁,闲着无事,学习一下用Cocos creator做小游戏。之前和媳妇谈恋爱的时候,经常玩一个投篮游戏,两个人比着谁玩的分高,挺有意思,就照猫画虎模仿着做了一个,媳妇挺喜欢玩,玩了最高记录300多分,我最高只能到200多。。。下面分享一下做这个投篮小游戏的心路历程和技术实战。
游戏引擎
目前,Cocos、Egret、Laya 已经完成了自身引擎及其工具对小游戏的适配和支持,对应的官方文档已经对接入小游戏开发做了介绍。
- Cocos:docs.cocos.com/creator/man...
- Egret:docs.egret.com/engine/docs...
- LayaBox:ldc.layabox.com/doc/?nav=zh...
Egret了解的不多,主要在Cocos Creator 3.x和LayaBox中进行选择,两个都是比较成熟的游戏引擎,都支持Typescript编写代码,支持跨平台编译,一份代码可以编译为H5,小游戏,移动端。但是Cocos的编辑器更为出色,游戏开发方式简捷明了,概念清楚,用起来很顺手,Layaair就略显杂乱。且Cocos社区更加成熟,遇到问题基本可以在社区找到解决方案,也有很多开发者开发了开箱即用的游戏框架。
Cocos 开发流程图
游戏简介
玩家可以通过在屏幕滑动,观察引线的方向及大小,以此改变投篮的方向及大小,通过自身对路径的评判,进行投篮。您需要在投篮中同时评估外界环境的干扰,比如不同风力,以及不同风向,都会对您要施加在篮球上的力和大小有影响,如果投中非空心球,分数加 1 分,如果投中空心球,时间将延长 3 秒,分数加 3 分,持续投中空心球>=3 次,则每次投篮时间延长 5 秒,同时分数加 5分。
初始化场景
场景是Cocos的基本概念,场景可以简单理解为一个游戏画面,是这个游戏画面中各种资源的集合。我们开发游戏,就是搭建场景的过程。场景的创建过程可以参考Cocos文档。
我们新建一个背景,然后在背景中添加这个游戏的三个要素:
- 背景
- 地面
- 篮球
- 篮筐
- 球员
这三个本质上来说就是图片资源,Cocos Creator允许我们在场景中随机添加资源,资源的大小,位置,各种属性都可以随意编辑。这样一个基本静态场景我们就完成了。
让篮球动起来
那如何让篮球运动起来呢?这就要说到物理系统了,物理系统是一种模拟现实世界物理行为的计算系统。它用于模拟物体之间的运动、碰撞、重力、摩擦等物理效果,以使游戏中的物体和角色表现得更加真实和自然。
物理系统通常由以下几个组件组成:
- 刚体(Rigid Body):刚体是物理系统中的基本单元,它具有质量、位置和速度等属性,并且可以受到力的作用而产生运动。
- 碰撞检测(Collision Detection):碰撞检测用于检测物体之间的碰撞,并判断它们是否发生了碰撞。它可以通过各种算法和数据结构来实现,如包围盒、网格、凸包等。
- 碰撞响应(Collision Response):碰撞响应用于处理物体之间的碰撞,包括计算碰撞力、碰撞反应和碰撞后的运动变化等。
- 重力(Gravity):重力是指物体受到的地球引力或其他引力的影响,使物体沿着下降的路径运动。
- 摩擦(Friction):摩擦是指物体之间的表面接触时产生的力,它影响物体在表面上的滑动和停止。
- 关节(Joint):关节用于连接两个或多个刚体,以模拟物体之间的约束和关系,如旋转关节、固定关节等。
物理系统提供了一套高效的算法和数据结构,以便开发者可以快速实现和模拟物理效果。Cocos的2D物理系统分为内嵌物理系统和Box2D。内嵌物理系统可以实现简单的物理效果,如果功能不复杂可以使用,这样包体积更小。Box2D功能更为强大。这里我们选择使用Box2D。
刚体
首先我们要让球成为一个刚体,这样它就具备了一些基本的物理元素,质量、速度,并且可以收到力的作用产生运动。 刚体通常分为3种类型:Static 、Dynamic 、Kinematic 。Animated 是Cocos中独有的,主要和动画编辑结合使用,我们可以先不关心。Cocos 2D物理系统。
刚体类型 | 说明 |
---|---|
Static | 静态刚体,零质量,零速度,即不会受到重力或速度影响,但是可以设置他的位置来进行移动。该类型通常用于制作场景 |
Dynamic | 动态刚体,有质量,可以设置速度,会受到重力影响。 唯一可以通过 applyForce 和 applyTorque 等方法改版受力的刚体类型 |
Kinematic | 运动刚体,零质量,可以设置速度,不会受到重力的影响,但是可以设置速度来进行移动 |
Animated | 动画刚体,在上面已经提到过,从 Kinematic 衍生的类型,主要用于刚体与动画编辑结合使用 |
球需要通过球员投篮而运动,我们需要在球员投篮的时候对它施加力的影响,从而使其运动。所以Dynamic 符合我们的要求。我们为球添加RigidBody2D组件,并设置类型为Dynamic ,这样篮球就成了一个刚体。然后我们设置合理的重力参数。这里需要注意的是我们应该取消Awake On Load, 否则篮球初始化的时候,会因为重力原因掉下去。
碰撞体
刚体让一个物体具有物理属性,而碰撞体则负责刚体与刚体直接的碰撞检测以及碰撞之后产生的效果。我们知道篮球在运动过程要与篮筐和地面产生碰撞。所以我们需要同时为篮球、篮筐、地面加上碰撞体,让它们产生碰撞效果。注意一个物体需要先加刚体,再加碰撞体才能生效,因为非刚体的物体是无法碰撞的。
目前引擎支持三种不同的碰撞组件: 盒碰撞组件(BoxCollider2D) 、圆形碰撞组件(CircleCollider2D) 和 多边形碰撞组件(PolygonCollider2D) 。在 属性检查器 上点击 添加组件 按钮,输入碰撞组件的名称即可添加。
为篮球加入圆形碰撞组件(CircleCollider2D)
为篮筐加入盒碰撞组件(BoxCollider2D) , 可以看到我们需要先给篮筐加一个RigidBody2D 组件使其成为刚体,且类型为Static,不受重力影响。然后我们加了两个碰撞体,且把碰撞体的大小和位置调整到篮筐的两边,这样只有篮球碰到篮筐边缘的时候才会碰撞。
同样的,我们为地面添加多边形碰撞组件(PolygonCollider2D),使其形状符合草地的起伏。
好了,现在我们的篮球,篮筐,地面已经具有物理特效,且可以相互碰撞了。那下面我们就让篮球动起来。
力的作用
移动一个刚体有两种方式:
- 可以施加一个力或者冲量到这个物体上。力会随着时间慢慢修改物体的速度,而冲量会立即修改物体的速度。
- 直接修改物体的位置,只是这看起来不像真实的物理,你应该尽量去使用力或者冲量来移动刚体,这会减少可能带来的奇怪问题。
typescript
// 施加一个力到刚体上指定的点,这个点是世界坐标系下的一个点
rigidbody.applyForce(force, point);
// 或者直接施加力到刚体的质心上
rigidbody.applyForceToCenter(force);
// 施加一个冲量到刚体上指定的点,这个点是世界坐标系下的一个点
rigidbody.applyLinearImpulse(impulse, point);
为了便于后续的计算,我们这里选择使用冲量,根据手指在屏幕的位置,产生不同方向和大小的冲量,手指释放的时候,将这个冲量施加到篮球上。
typescript
// 根据手指位置获得冲量大小
getImpulsFromAxis(axis: Vec2) {
return new Vec2(axis.x / 4 - 40, axis.y / 3 + 140)
}
onTouchEnd(event) {
const vec2 = event.getLocation()
const basketballRigidBody = this.curBasketball.getComponent(RigidBody2D)
basketballRigidBody.applyLinearImpulseToCenter(this.getImpulsFromAxis(vec2), true);
}
现在我们已经可以让篮球运动起来了。那如何实现瞄准线呢?
瞄准线
瞄准线的制作需要一些基础的物理和数学知识。我们需要综合分析篮球运动过程中受到的力,算出水平和垂直方向移动速度,模拟出运动轨迹。计算过程如下:
- 根据冲量大小和刚体质量得出物体初始速度, 速度 = 冲量 * 质量,这里Cocos有个单位换算,PHYSICS_2D_PTM_RATIO,具体可查看这里。
- 由于杆体受重力影响,所以垂直方向有重力加速度,据此我们计算出随时间变化,篮球的y坐标。
targetY = vt + 1/2at*t
, 其中v
是初始速度,a
为加速度,t
为运动时间。 - 刚体水平方向速度不变。
targetX = vt
- 瞄准线不能画全,那样就太简单了,所以我们只画一少半,这可以通过刚体运动到最高点,也就是垂直方向速度为0的运动时间来限制
const toTopTime = -velocity.y / (PhysicsSystem2D.instance.gravity.y * basketballRigidBody.gravityScale)
- 每隔固定的距离画一个轨迹中的点,从而得到瞄准线。这里注意如果我们每隔相同的时间去画点,得到的瞄准线不是均匀的,因为垂直方向的速度一直在变。
js
drawMoveLine(vec2: Vec2) {
const basketballRigidBody = this.curBasketball.getComponent(RigidBody2D)
const mass = basketballRigidBody.getMass()
const implus = this.getImpulsFromAxis(vec2)
const velocity = implus.multiplyScalar(PHYSICS_2D_PTM_RATIO / mass)
this.graphicLine.clear()
let lastTargetX = 0
let lastTargetY = 0
const toTopTime = -velocity.y / (PhysicsSystem2D.instance.gravity.y * basketballRigidBody.gravityScale)
const lineTime = toTopTime - 0.09
for (let time = 0; time < lineTime; time += 0.001) {
const targetX = velocity.x * time;
const targetY = velocity.y * time + 0.5 * PhysicsSystem2D.instance.gravity.y * basketballRigidBody.gravityScale * time * time;
const distance = Math.sqrt((targetX - lastTargetX) * (targetX - lastTargetX) + (targetY - lastTargetY) * (targetY - lastTargetY))
if (distance > 40) {
lastTargetX = targetX
lastTargetY = targetY
this.graphicLine.circle(targetX, targetY, 6);
}
}
const color = (this.isGameCountDownMode() && this.cleanShotTimes >= 3) ? '#FC5E05' : '#ffffff'
this.graphicLine.fillColor = new Color(color)
this.graphicLine.fill()
}
好了,现在我们可以进行瞄准投篮了。
判断是否进球
判断进行的标准就是篮球是否通过篮筐。我们可以在篮筐往下的位置创建一个碰撞体Space
,然后在碰撞回调中检测篮球是否通过该碰撞体,如果通过,且方向是从上到下的,则表示投中。
下面是篮球的碰撞检测回调
js
onBeginContact(selfCollider: CircleCollider2D, otherCollider: CircleCollider2D, contact: IPhysics2DContact | null) {
// 是否碰到篮筐,用于判断是否是空心球
if (otherCollider.node.name === 'Iron') {
AudioMgr.inst.playOneShot('Sound/iron', 0.5)
this.hasCollisionEdge = true
} else if (otherCollider.node.name === 'Space') {
contact.disabled = true
const worldManifold = contact.getWorldManifold();
const normal = worldManifold.normal;
// 篮球和Space碰撞,方向向下,且之前不是从底部上来的,则表示进球
if (normal.y > 0 && !this.fromDown) {
this.node.dispatchEvent(new MyEvent(EVENT_TYPE.GOT_BALL, true, {
ball: this.node,
hasCollisionEdge: this.hasCollisionEdge
}))
} else {
this.fromDown = true
}
}
}
结语
这篇文章主要描述了实现篮球游戏的基本要素,给新手朋友提供一个指引,有很多细节没有详细阐述。从被裁到现在已经2个多月了,现在开始找工作,如今站在人生的十字路口,祝自己也祝大家好运!