通过反射原理实现2d游戏的子弹反弹

反弹的效果在很多游戏中都有体现,本文就从技术方面尝试实现一个简单的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)
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值