这几年出了一系列益智类物理画线玩法的爆款产品,比如拯救狗狗,救救小鸡等,本文基于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
′:
进行矩阵乘法运算,我们得到:
d1
和d
的夹角是-90度,因此 d1 = (dy, -dx)
。
d2
和d
的夹角是90度,因此 d2 = (-dy, dx)
。
求出d1
和d2
方向向量后,可以得出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,创建一个场景,将角色(这里不是狗头哦, 没有狗头资源)添加到场景中,给角色添加上RigidBody2D
和BoxCollider2D
组件,并设置碰撞范围,将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
节点上,预览游戏:
方块间画一个夹角线段:
角色上方添加一个方块,给方块添加RigidBody2D
和BoxCollider2D
组件:
画一个不规则图形:
底部添加长方块: