拯救狗狗系列之物理画线

这几年出了一系列益智类物理画线玩法的爆款产品,比如拯救狗狗,救救小鸡等,本文基于Cocos Creator 3.8.1来实现物理画线。

原理

  • 通过Graphics绘画组件画线
  • 给绘制的线添加RigidBody2D组件,用于物理计算
  • 将绘制的线分成多个线段,每个线段添加一个PolygonCollider2D组件,用于碰撞检测

每个线段包含四个点p1, p2, p3, p4,通过start点、end点和线的宽度lineWidth算出p1, p2, p3, p4的坐标,这个四个点就是每个PolygonCollider2D的多边形顶点数组points

求四个点的坐标

  • 求出start点到end点的方向向量: d = (end - start).normalize();
  • 求出垂直于方向向量d,并且指向p1 - p2线段的方向向量d1
  • 求出垂直于方向向量d,并且指向p3 - p4线段的方向向量d2

d1,d2可以通过2D的旋转矩阵 乘以 方向向量d得出。

2D中的旋转的矩阵是:

将旋转矩阵 R 与向量 d 相乘来得到旋转后的向量 ′d′:

进行矩阵乘法运算,我们得到:

d1d的夹角是-90度,因此 d1 = (dy, -dx)

d2d的夹角是90度,因此 d2 = (-dy, dx)

求出d1d2方向向量后,可以得出4点的坐标

js 复制代码
 halfWidth = lineWidth / 2;
 p1 = start + d1 * halfWidth;
 p2 = end + d1 * halfWidth;
 p3 = end + d2 * halfWidth;
 p4 = start + d2 * halfWidth;

执行

1, 在Cocos Creator创建一个空的2D项目

2,创建一个场景,将角色(这里不是狗头哦, 没有狗头资源)添加到场景中,给角色添加上RigidBody2DBoxCollider2D组件,并设置碰撞范围,将RigidBody2D的类型设置成Dynamic,这样就可以收到重力的影响而下落

3,创建Game.ts,代码如下

Typescript 复制代码
import { _decorator, Component, EventTouch, find, Node, macro, Graphics, v2, Vec2, UITransform, v3, Color, RigidBody2D, PolygonCollider2D, PhysicsSystem2D } from 'cc';
const { ccclass, property } = _decorator;

const __tempV2 = v2()
const __tempV3 = v3()

@ccclass('Game')
export class Game extends Component {

    /** 角色 */
    @property(Node)
    role: Node = null;

    /** 结算节点 */
    @property(Node)
    settle: Node = null;

    private _curGraphics: Graphics;

    start() {
        // 禁用多点触摸
        macro.ENABLE_MULTI_TOUCH = false;
        this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this)
        this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this)
        this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this)
        this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this)

        // 先禁用角色的物理组件
        this.role.getComponent(RigidBody2D).enabled = false;
    }

    /** 获取目标节点的相对位置 */
    private getRelativePos(pos: Vec2) {
        __tempV3.set(pos.x, pos.y, 0)
        this._curGraphics.node.getComponent(UITransform).convertToNodeSpaceAR(__tempV3, __tempV3)
        pos.set(__tempV3.x, __tempV3.y)
        return pos;
    }
    
    private onTouchStart(event: EventTouch) {
        event.getUILocation(__tempV2)
        // 重置花线节点列表
        this._pathPointList.length = 0;

        // 创建Graphics节点
        const graphicsNode = new Node()
        graphicsNode.layer = this.node.layer;
        this.node.addChild(graphicsNode);

        // 添加Graphics绘图组件
        this._curGraphics = graphicsNode.addComponent(Graphics)
        this._curGraphics.strokeColor = Color.BLACK;
        this._curGraphics.lineWidth = 10;   

        // 设置路径起点
        const { x, y } = this.getRelativePos(__tempV2)
        this._curGraphics.moveTo(x, y)

        // 添加到路径点列表
        this._pathPointList.push(v2( x, y ))
    }

    /** 上一个点的斜率 */
    private _preSlope: number = 0
    /** 路径点列表 */
    private _pathPointList: Vec2[] = []

    private onTouchMove(event: EventTouch) {
        event.getUILocation(__tempV2)

        // 获取当前触摸点的相对位置
        const { x, y } = this.getRelativePos(__tempV2)
        // 获取上一个点的位置
        const { x: preX, y: preY } = this._pathPointList[this._pathPointList.length - 1];
        // 计算两点之间的距离
        const diffX = x - preX;
        const diffY = y - preY;
        // 两点之间的距离的平方
        const dis = diffX * diffX + diffY * diffY;
        const lineWidth = this._curGraphics.lineWidth;
        // 两点之间的距离大于线宽的平方,才添加到路径点列表
        if (dis >= lineWidth * lineWidth) {
            const d = 0.001
            const curSlope = Math.abs(diffX) < d ? (Number.MAX_SAFE_INTEGER * Math.sign(diffX) * Math.sign(diffY)) : (diffY / diffX)
            if (this._pathPointList.length > 1) {
                const diffK = curSlope - this._preSlope;
                // 斜率相同去掉前一个点
                if (Math.abs(diffK) < d) {
                    this._pathPointList.pop()
                }
            }
            // 添加到路径点列表
            this._pathPointList.push(v2( x, y ))
            // 绘制路径
            this._curGraphics.lineTo(x, y)
            this._curGraphics.stroke();

            // 保存上一个点的斜率
            this._preSlope = curSlope;
        }
    }

    private onTouchEnd(evt: EventTouch) {
        // 绘制结束,添加刚体组件
        // 两个点以上才添加刚体组件
        if (this._pathPointList.length > 1) {
            this._curGraphics.addComponent(RigidBody2D);
            // 添加多边形碰撞组件
            for (let index = 0; index < this._pathPointList.length - 1; index++) {
                const start = this._pathPointList[index];
                const end = this._pathPointList[index + 1];
                const polyCollider = this._curGraphics.addComponent(PolygonCollider2D);
                // 计算两点的方向向量
                const directVector = v2(end.x - start.x, end.y - start.y).normalize();
                const widhtHalf = this._curGraphics.lineWidth / 2;
                // 计算多边形的四个点
                const p1 = v2(directVector.y, -directVector.x).multiplyScalar(widhtHalf).add2f(start.x, start.y)
                const p2 = v2(-directVector.y, directVector.x).multiplyScalar(widhtHalf).add2f(start.x, start.y)
                const p3 = v2(directVector.y, -directVector.x).multiplyScalar(widhtHalf).add2f(end.x, end.y)
                const p4 = v2(-directVector.y, directVector.x).multiplyScalar(widhtHalf).add2f(end.x, end.y)
                // 设置多边形碰撞组件的定点列表
                polyCollider.points = [p1, p2, p4, p3];
                // 让修改生效
                polyCollider.apply();
            }
        } else {
            this._curGraphics.node.destroy();
        }
        // 重置
        this._curGraphics = null;
        // 启用角色的物理组件
        this.role.getComponent(RigidBody2D).enabled = true;
    }

    protected update(dt: number): void {
        // role的位置掉到屏幕外,显示结算节点
        if (this.role.position.y < -this.node.getComponent(UITransform).height / 2) {
            this.settle.active = true;
        }
    }
}

直接将Game.ts挂在Canvas节点上,预览游戏:

方块间画一个夹角线段:

角色上方添加一个方块,给方块添加RigidBody2DBoxCollider2D组件:

画一个不规则图形:

底部添加长方块:

相关推荐
LcGero11 天前
TypeScript 快速上手:泛型与工具类型
typescript·cocos creator·游戏开发
LcGero11 天前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机
LcGero12 天前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲
LcGero13 天前
Cocos Creator平台适配层框架设计
cocos creator·平台·框架设计
LcGero14 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
LcGero15 天前
TypeScript 快速上手:前言
typescript·cocos creator·游戏开发
Setsuna_F_Seiei15 天前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发
CodeCaptain3 个月前
cocoscreator 2.4.x 场景运行时的JS生命周期浅析
cocos creator·开发经验
CodeCaptain3 个月前
CocosCreator 3.8.x [.gitignore]文件内容,仅供参考
经验分享·cocos creator
VaJoy5 个月前
Cocos Creator Shader 入门 (21) —— 高斯模糊的高性能实现
前端·cocos creator