基于cocos creator2.4.11 画一条物理线

基于cocos creator2.4.11 画一条物理线

在一些物理游戏中经常能看到画物理线的操作,这其实是一个很有意思的东西,能做出很多趣味性的玩法,这里我从技术的层面自己尝试实现一下这个有意思功能。

先看效果

在这里插入图片描述

使用了引擎自带的物理系统和画图组件 Graphics

思考与实现

画一条不带物理效果的线还是比较简单的,但是怎么让线拥有物理效果呢?

	刚开始想到的是记录所有的点,然后将点直接连接起来,
	基于这个思路然后就去引擎里面一顿查找,最后找到了一个这样的组件PhysicsChainCollider,
	捣鼓了一阵后发现不行,有些碰撞会不生效,而且这个不好实现线的宽度
	还是老老实实用多边形碰撞的方案解决,多边形的生成这里也绕了一下,
	刚开始是想着只生成一个多边形碰撞组件,但是因为线的交叉等等问题,发现实现不了,
	最后还是生成多个多边形碰撞组件才实现的

物理效果的线已经实现了,但是我们画笔画出来的线没有按照物理系统的规则动起来,怎么让其动起来呢?

这里我是将物理节点下面挂载一个画笔组件,记录好画线的位置,然后对其进行实时绘制。

具体步骤

1、画一根普通线,记录线的所有点。

2 、根据记录的点,生成多个多边形,根据生成的多边形创建多边形物理碰撞组件,这一步是关键,怎么生成多边形呢,我这里两个点构成一条线,往垂直线的两侧分别扩展出一个点来,四个点就能组成一个多边形了。如图所示,黑色为画的直线,这里生成了2个多边形。

这里我还优化了一条直线多个点的合并,最后测试效率还是挺高的。

在这里插入图片描述

3、新建一个带物理效果的节点,将上面生成的多边碰撞组件组和画笔组件都挂载到节点下,记录画线的点,在update里面实时绘制线条

注意点:要考虑物理世界的坐标系是和画线的坐标系,我这里是将物理世界按照世界坐标系对齐的,也就是左下角

在这里插入图片描述

哈哈,这样一根新鲜的物理线就出炉了,具体细节可以直接看代码

贴上全部代码:

PhysicLine 类:

const { ccclass } = cc._decorator;

@ccclass
export default class PhysicLine extends cc.Component {

    _graphicsPoints: cc.Vec2[] = []
    _physicPoints: cc.Vec2[] = []
    lineGraphics: cc.Graphics = null
    polygonPhysics: cc.PhysicsPolygonCollider[] = []

    init(graphicsPoints: cc.Vec2[], graphics: cc.Graphics, physicPoints: cc.Vec2[][]) {
        let node = new cc.Node()
        this._graphicsPoints = graphicsPoints
        this.lineGraphics = node.addComponent(cc.Graphics)
        this.lineGraphics.strokeColor = graphics.strokeColor
        this.lineGraphics.lineWidth = graphics.lineWidth
        this.lineGraphics.lineCap = graphics.lineCap
        this.lineGraphics.lineJoin = graphics.lineJoin
        this.lineGraphics.clear(true)
        this.node.addChild(node)

        for (let i = 0; i < physicPoints.length; ++i) {
            this._physicPoints = physicPoints[i]
            let pp = this.node.addComponent(cc.PhysicsPolygonCollider)
            pp.points = physicPoints[i]
            pp.apply()
            this.polygonPhysics.push(pp)
        }
    }

    drawPhycicsLine(points: cc.Vec2[], positon: cc.Vec3) {
        this.lineGraphics.clear(true)
        for (let i = 1; i < points.length; ++i) {
            this.lineGraphics.moveTo(positon.x + points[i - 1].x, positon.y + points[i - 1].y)
            this.lineGraphics.lineTo(positon.x + points[i].x, positon.y + points[i].y)
            this.lineGraphics.stroke()
        }
    }

    protected update(dt: number): void {
        if (this._graphicsPoints.length > 0 && this.polygonPhysics.length > 0 && this.lineGraphics) {
            // 根据节点坐标和之前记录的点 每帧重新绘制即可
            let pos = this.node.children[0].convertToNodeSpaceAR(this.node.position)
            this.drawPhycicsLine(this._graphicsPoints, pos)
        }
    }
}

场景类:

import PhysicLine from "./PhysicLine";

const { ccclass, property } = cc._decorator;

@ccclass
export default class DrawRigidBodyTest extends cc.Component {

    @property(cc.Node) handWritingNode: cc.Node = null // 画图节点
    @property(cc.Node) penNode: cc.Node = null // 笔
    @property(cc.Graphics) writingGraphics: cc.Graphics = null
    @property(cc.Node) tipsNode: cc.Node = null
    @property(cc.Node) rigidLayer: cc.Node = null

    isWriting: boolean = false
    isTouchStart: boolean = false
    _FirstTouchId: Number = null
    rects: { up: number, bottom: number, left: number, right: number }
    physicPoints: cc.Vec2[] = []
    graphicsPoints: cc.Vec2[] = []

    lineWidth: number = 20 // 线宽
    disLen: number = 20 // 两点画线的最小间距

    onLoad() {
        cc.director.getPhysicsManager().enabled = true;
        // cc.director.getPhysicsManager().debugDrawFlags = cc.PhysicsManager.DrawBits.e_aabbBit |
        //     cc.PhysicsManager.DrawBits.e_jointBit |
        //     cc.PhysicsManager.DrawBits.e_shapeBit
        var manager = cc.director.getPhysicsManager();
        // 开启物理步长的设置
        // manager.enabledAccumulator = true;
        // // 物理步长,默认 FIXED_TIME_STEP 是 1/60
        // cc.PhysicsManager.FIXED_TIME_STEP = 1 / 30;
        // // 每次更新物理系统处理速度的迭代次数,默认为 10
        // cc.PhysicsManager.VELOCITY_ITERATIONS = 8;
        // // 每次更新物理系统处理位置的迭代次数,默认为 10
        // cc.PhysicsManager.POSITION_ITERATIONS = 8;

        // cc.director.getPhysicsManager().gravity = cc.v2(-320, 0);
    }

    onDestroy() {
        cc.director.getPhysicsManager().enabled = false;
        cc.director.getPhysicsManager().debugDrawFlags = 0
    }

    /**
    * 添加触摸事件
    * @param button 
    * @param target 
    * @param callBack 
    */
    public addTouchEvent(node: cc.Node, target: any, callBack: any) {
        node.on(cc.Node.EventType.TOUCH_START, function (event) {
            callBack.call(target, event, node)
        })
        node.on(cc.Node.EventType.TOUCH_MOVE, function (event) {
            callBack.call(target, event, node)
        })
        node.on(cc.Node.EventType.TOUCH_END, function (event) {
            callBack.call(target, event, node)
        })
        node.on(cc.Node.EventType.TOUCH_CANCEL, function (event) {
            callBack.call(target, event, node)
        })
    }

    start() {
        this.addTouchEvent(this.handWritingNode, this, this.handleTouch);

        this.rects = {
            up: this.handWritingNode.height / 2,
            bottom: -this.handWritingNode.height / 2,
            left: -this.handWritingNode.width / 2,
            right: this.handWritingNode.width / 2
        }
        this.tipsNode.active = true
        this.refresh()
    }

    refresh() {
        this.physicPoints = []
        this.graphicsPoints = []
        this.writingGraphics.clear(true)
        this.writingGraphics.lineWidth = this.lineWidth
        this.penNode.setPosition(cc.v3(465, -350, 0))
        this.isWriting = false
        this.isTouchStart = false
        this.tipsNode.active = true
        this._FirstTouchId = null
    }

    /**
    * 触摸逻辑
    * @param event 
    */
    public handleTouch(event: cc.Event.EventTouch) {
        if (this._FirstTouchId === null) {
            this._FirstTouchId = event.getID()
        }
        // 不是第一个触碰点,不处理
        if (this._FirstTouchId != event.getID()) return

        let touchPos = event.getLocation()
        touchPos = this.handWritingNode.convertToNodeSpaceAR(touchPos);

        switch (event.type) {
            case cc.Node.EventType.TOUCH_START:
                if (!this.pointInRect(touchPos) || this.checkIsCollider(touchPos)) return // 未选中指定区域
                this.isTouchStart = true
                this.tipsNode.active = false
                if (!this.isWriting) {
                    this.penNode.setPosition(touchPos)
                    this.writingGraphics.moveTo(touchPos.x, touchPos.y)
                    this.physicPoints.push(touchPos)
                    this.graphicsPoints.push(touchPos)
                }
                break;
            case cc.Node.EventType.TOUCH_MOVE:
                this.handleTouchMove(event.getPreviousLocation(), touchPos)
                break;
            case cc.Node.EventType.TOUCH_END:
                this.onFinishedDraw()
                break;
            case cc.Node.EventType.TOUCH_CANCEL:
                this.onFinishedDraw()
                break;
        }

    }

    handleTouchMove(PreviousPos: cc.Vec2, touchPos: cc.Vec2) {
        if (this.isTouchStart && !this.isWriting) {
            if (!this.pointInRect(touchPos) || this.checkIsRayCollision(PreviousPos, touchPos)) {
                this.onFinishedDraw()
                return
            }

            this.penNode.setPosition(touchPos)
            let preTouchPos = this.physicPoints[this.physicPoints.length - 1]
            // 优化
            if (this.storePoint(touchPos)) {
                this.writingGraphics.moveTo(preTouchPos.x, preTouchPos.y)
                this.writingGraphics.lineTo(touchPos.x, touchPos.y)
                this.writingGraphics.stroke()

                this.graphicsPoints.push(touchPos)
            }
        }
    }

    onFinishedDraw() {
        if (this.isTouchStart && !this.isWriting) {
            // 只进一次
            this.writingGraphics.unscheduleAllCallbacks()
            if (this.physicPoints.length > 1) {
                this.drawPhysicLine(this.physicPoints)
            }

            this.physicPoints = []
            this.graphicsPoints = []
            this.isWriting = false
            this.isTouchStart = false
        }
    }

    drawPhysicLine(physicPoints: cc.Vec2[]) {
        let ppPoints: cc.Vec2[][] = []
        for (let i = 1; i < physicPoints.length; i++) {
            let dir = (physicPoints[i].sub(physicPoints[i - 1])) // 方向1
            let fDir = (dir.normalize().rotate(Math.PI * 90 / 360)).normalize() // 垂直方向向量
            let disV = fDir.mul(this.lineWidth / 2)
            let p1 = physicPoints[i - 1].add(disV)
            let p2 = physicPoints[i].add(disV)
            let p3 = physicPoints[i].add(disV.neg())
            let p4 = physicPoints[i - 1].add(disV.neg())

            let pps: cc.Vec2[] = [p1, p2, p3, p4]
            ppPoints.push(pps)
        }
        this.writingGraphics.clear(true)
        let node = new cc.Node()
        let com = node.addComponent(PhysicLine)
        let worldPos = this.writingGraphics.node.convertToWorldSpaceAR(cc.v3())
        node.setPosition(worldPos)
        com.init(this.graphicsPoints, this.writingGraphics, ppPoints)
        this.rigidLayer.addChild(node)
    }

    // 画线过程中是否碰到物理碰撞体
    checkIsCollider(touchPos: cc.Vec2) {
        let worldPos = this.node.convertToWorldSpaceAR(touchPos)
        let collider = cc.director.getPhysicsManager().testPoint(worldPos);
        if (collider) return true
    }

    checkIsRayCollision(p1: cc.Vec2, p2: cc.Vec2): boolean {
        let wp1 = p1//this.node.convertToWorldSpaceAR(p1)
        let wp2 = this.node.convertToWorldSpaceAR(p2)
        let results = cc.director.getPhysicsManager().rayCast(wp1, wp2, cc.RayCastType.Any);
        for (let i = 0; i < results.length; i++) {
            let result = results[i];
            let collider = result.collider;
            let point = result.point;
            let normal = result.normal;
            let fraction = result.fraction;
            return true
        }
        return false
    }

    // 判断点是否在限定范围内
    pointInRect(pos: cc.Vec2): boolean {
        let isIn = true
        if (pos.x > this.rects.right || pos.x < this.rects.left || pos.y > this.rects.up || pos.y < this.rects.bottom) {
            isIn = false
        }

        return isIn
    }

    // 合并一条线上的点
    storePoint(pos: cc.Vec2) {
        let len = this.physicPoints.length
        if (len > 0) {
            let endPos = this.physicPoints[len - 1]
            if (cc.Vec2.distance(pos, endPos) > this.disLen) {
                this.physicPoints.push(pos)
                if (len > 2) {
                    // 优化点数量
                    let line1 = this.physicPoints[len - 1].sub(this.physicPoints[len - 2])
                    let line2 = this.physicPoints[len - 2].sub(this.physicPoints[len - 3])
                    if (this.getLineIsSameDir(line1, line2)) {
                        // 剔除中间点,将两条线合并为一条线
                        this.physicPoints.splice(len - 2, 1)
                    }
                }

                return true
            }
            return false
        } else {
            this.physicPoints.push(pos)
            return true
        }
    }

    // 判断两条线是否可以合并为一条线
    getLineIsSameDir(line1: cc.Vec2, line2: cc.Vec2): boolean {
        line1.normalizeSelf()
        line2.normalizeSelf()
        let XGap = Math.abs(line1.x - line2.x)
        let yGap = Math.abs(line1.y - line2.y)
        let gap = 0.005 // 这个值越小,生成的点越多,碰撞越精细,但是效率越低
        // cc.log('>>>???', XGap, yGap, gap)
        if (line1.x == 0 && line2.x == 0) {
            // 竖直方向
            return true
        } else if (line1.y == 0 && line2.y == 0) {
            // 水平方向
            return true
        } else if ((line1.x > 0 && line1.y > 0) || (line1.x < 0 && line1.y < 0)) {
            // 第一三象限方向
            if (XGap < gap || yGap < gap) {
                return true
            }
        } else if ((line1.x > 0 && line1.y < 0) || (line1.x < 0 && line1.y > 0)) {
            // 第二四象限方向
            if (XGap < gap || yGap < gap) {
                return true
            }
        }
        return false
    }
}



  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值