反弹的效果在很多游戏中都有体现,本文就从技术方面尝试实现一个简单的2d碰撞反弹效果
效果展示如下:
上述示例通过cocos creator游戏引擎实现,版本为 2.4.11,代码使用ts,只用到了引擎自带的碰撞组件,没有使用物理引擎
思考:
怎么实现上述效果呢?
这里我分成以下几个步骤:
1 搭建一个场景,编辑好碰撞相关的设置,实现鼠标点击生成一个子弹
2 设置子弹的移动方向,并且让子弹朝设置好的方向移动,并设置其旋转角度
3 当子弹碰撞到墙时子弹需要反弹,改变其运动方向,并且朝新方向继续移动
具体实现
1 搭建场景
编辑好碰撞相关的层,这里我分了两个层 墙壁(wall)和子弹(buttet), 注意这里的圆也是墙壁,
子弹用的矩形碰撞体,
墙分别做了一个矩形碰撞体的墙和圆形碰撞体的墙
简单搭建好场景后挂载一个脚本,实现监听触摸事件
场景部分代码如下:
start() {
cc.director.getCollisionManager().enabled = true;
this.node.on(cc.Node.EventType.TOUCH_START, function (event) {
})
this.node.on(cc.Node.EventType.TOUCH_MOVE, function (event) {
})
this.node.on(cc.Node.EventType.TOUCH_END, function (event) {
// 触摸结束时创建子弹
let touchPos = event.getLocation()
touchPos = this.node.convertToNodeSpaceAR(touchPos);
let srcPos = cc.v2()
let dir = touchPos.sub(srcPos)
this.actNode.getComponent(ReflectButtet).init(cc.v3(dir.x, dir.y, 0))
let item = cc.instantiate(this.bullet)
this.bullet.parent.addChild(item)
item.getComponent(ReflectButtet).init(cc.v3(dir.x, dir.y, 0))
item.getComponent(ReflectButtet).isMove = true
})
}
2 子弹相关
这里有一个需要注意的点,就是通过子弹反弹后的向量怎么正确的设置其旋转角度,如果是圆形子弹不需要旋转则不必考虑具体可以在子弹的类查看,里面有详细的说明,这里不再赘述,
3 当子弹碰撞到墙
子弹碰撞到墙时,这里让墙的颜色发生闪动变化,然后子弹需要反弹,改变其运动方向,关键点来了,怎么知道碰撞后需要反弹的方向呢?
这里也是要本文的重点,就是通过反射的原理计算出需要反射的方向,网上找的反射公式如下:
I 入射向量
N 法线向量
R = I - 2.0 * dot(N, I) * N;
typescript 代码实现如下:
/**
* 求一个向量的反射向量 3d
* @param inVec 入射向量
* @param N 法向量
* @returns 反射向量
*/
public static reflect(inVec: cc.Vec3, N: cc.Vec3) {
return inVec.sub((N.mul(2 * cc.Vec3.dot(inVec, N))))
}
这里不推导计算过程,大家感兴趣的可以自行推导一下,网上也有很多教程,这里主要只是应用.
公式有了,我们现在只要传入入射向量和法向量就能算出反射方向。我们计算入射向量很简单,就是子弹运动的方向,但是子弹和墙壁碰撞时墙壁的法向量怎么求呢?
这里我主要用到两种碰撞体,矩形碰撞和圆形碰撞
基于这两种碰撞体计算其法向量
矩形碰撞体
先说一下矩形碰撞的法向量计算,如上图所示这里我定义了墙本身有一个朝向,取朝右作为初始方向,那么它的法向量就变成了朝上(取逆时针旋转90度后的方向为其法向量,在cocos creator中默认逆时针为正方向) , 在上述效果中还有一些斜方向的墙,这里我是将墙旋转一定角度实现,那么将一个墙旋转后的法向量又是怎么算的呢?其实很简单,只要将其法向量也旋转相同角度就好了,代码如下:
// 计算法向量
private _caculateNormalVec() {
// 将一个角度转换成一个弧度
let radius = Util.getRadian(this.node.angle)
// 上面说了默认我们的法向量就是朝上,这里就是将法向量旋转当前节点角度后的向量
let n = cc.v2(0, 1).rotate(radius)
this.normal = cc.v3(n.x, n.y, 0).normalize()
}
这里我们就是计算出矩形碰撞的法向量,这里不限制子弹使用的碰撞类型,使用矩形/圆形/多边形都可以,因为子弹只需要知道什么时候开始碰撞就行.
到这一步一个简单的矩形墙碰撞反弹就实现了.
注意点:
细心的话会发现,矩形墙其实有四个面,两个方向,而这里只实现了一个方向,也就是两个面的情况,所以从墙的另外两侧会有穿透效果. 如下图所示:
有一个方法可以避免这种穿透,就是额外再用两个单一方向的墙组合成一个完整的墙
因为我们实现了墙的旋转,所以可以组合很多其他的形状,不过要处理好边缘.可以说是通过矩形的组合实现了多边形的碰撞,如下图所示::
如果要用单个碰撞体实现矩形四面墙的话(如果是多边形碰撞情况会更复杂),会有诸多限制,需要考虑碰撞体从哪个方向碰撞(这里又需要考虑子弹的碰撞体形状等等),这里不深入展开,继续下去就变成研究物理引擎相关的内容了,感兴趣的可以自行研究,本文的重点是反射,不深入去做这种碰撞相关的研究.
圆形碰撞体
圆形碰撞的法向量比较简单,就是圆心到碰撞点的向量,圆心的位置就是当前挂载了圆形碰撞体组件的节点位置,碰撞点我们可以直接取碰撞物体(也就是子弹)的位置.计算代码如下:
private _caculateNormalVec(pos: cc.Vec3) {
// pos为子弹的位置
this.normal = pos.sub(this.node.position).normalize()
}
至此我们就实现了子弹和墙壁碰撞反弹的全部功能
4 完整代码
工具类
export default class Util {
/**
* 弧度制转换为角度值
* @param radian 弧度制
* @returns {number}
*/
public static getAngle(radian: number): number {
return 180 * radian / Math.PI;
}
/**
* 角度值转换为弧度制
* @param angle
*/
public static getRadian(angle: number): number {
return angle / 180 * Math.PI;
}
/**
* 求一个向量的反射向量 3d
* @param inVec 入射向量
* @param N 法向量
* @returns 反射向量
*/
public static reflect(inVec: cc.Vec3, N: cc.Vec3) {
return inVec.sub((N.mul(2 * cc.Vec3.dot(inVec, N))))
}
/**
* 求一个向量的反射向量 2d
* @param inVec 入射向量
* @param N 法向量
* @returns 反射向量
*/
public static reflect_v2(inVec: cc.Vec2, N: cc.Vec2) {
return inVec.sub((N.mul(2 * cc.Vec2.dot(inVec, N))))
}
}
子弹类全部代码
import Util from "../util";
const { ccclass, property } = cc._decorator;
@ccclass
export default class ReflectButtet extends cc.Component {
@property({
tooltip: `需要按照方向旋转`
}) needRotation: boolean = false
curDir = cc.v3() // 当前运动方向
isMove: boolean = false
init(dir: cc.Vec3) {
let norDir = dir.normalize()
if (this.needRotation) {
this.setAngle(norDir)
}
this.curDir = norDir
}
// 设置子弹的旋转角度
setAngle(norDir: cc.Vec3) {
// 根据当前方向计算当前节点的旋转角度
let rad = 0
// 默认子弹的初始方向超右,则可以通过其和墙壁法向量进行反余弦算出其弧度
rad = Math.acos(cc.v3(1, 0, 0).dot(norDir))
// 弧度转换为角度
let angle = Util.getAngle(rad)
cc.log(angle)
if (norDir.y < 0) {
// 如果法线方向朝下,要将角度取反
angle = -angle
}
this.node.angle = angle
}
// 改变方向
changeDir(normal: cc.Vec3) {
// 根据子弹当前的方向,和墙壁的法向量算出反射向量
this.curDir = Util.reflect(this.curDir, normal)
this.curDir.normalizeSelf()
this.setAngle(this.curDir)
}
protected update(dt: number): void {
if (this.isMove) {
let addPos = this.node.position.add(this.curDir.mul(5))
this.node.setPosition(addPos)
}
}
}
矩形墙代码:
import Util from "../util";
import ReflectButtet from "./ReflectButtet";
const { ccclass } = cc._decorator;
@ccclass
export default class ReflectRectWall extends cc.Component {
normal = cc.v3() // 当前墙的法向量
protected onLoad(): void {
this._caculateNormalVec()
}
// 计算法向量
private _caculateNormalVec() {
// 将一个角度转换成一个弧度
let radius = Util.getRadian(this.node.angle)
// 上面说了默认我们的法向量就是朝上,这里就是将法向量旋转当前节点角度后的向量
let n = cc.v2(0, 1).rotate(radius)
this.normal = cc.v3(n.x, n.y, 0).normalize()
}
/**
* 当碰撞产生的时候调用
* @param {Collider} other 产生碰撞的另一个碰撞组件
* @param {Collider} self 产生碰撞的自身的碰撞组件
*/
onCollisionEnter(other, self) {
if (other.node.group == 'bullet') {
// 这里让墙的颜色闪一下
cc.tween(self.node)
.repeat(2,
cc.tween()
.to(0.1, { color: cc.Color.GREEN })
.to(0.1, { color: cc.Color.WHITE })
)
.start()
other.node.getComponent(ReflectButtet).changeDir(this.normal)
}
}
}
圆形墙代码:
import ReflectButtet from "./ReflectButtet";
const { ccclass } = cc._decorator;
@ccclass
export default class ReflectCircleWall extends cc.Component {
normal = cc.v3()
private _caculateNormalVec(pos: cc.Vec3) {
// pos为子弹的位置
this.normal = pos.sub(this.node.position).normalize()
}
/**
* 当碰撞产生的时候调用
* @param {Collider} other 产生碰撞的另一个碰撞组件
* @param {Collider} self 产生碰撞的自身的碰撞组件
*/
onCollisionEnter(other, self) {
if (other.node.group == 'bullet') {
// 这里让墙的颜色闪一下
cc.tween(self.node)
.repeat(2,
cc.tween()
.to(0.1, { color: cc.Color.GREEN })
.to(0.1, { color: cc.Color.WHITE })
)
.start()
this._caculateNormalVec(other.node.position)
other.node.getComponent(ReflectButtet).changeDir(this.normal)
}
}
}