基于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
}
}