创建通过“转弯圈”将玩家从当前位置移动到新位置的路径

时间:2015-07-16 03:56:01

标签: ios swift math sprite-kit

我一直在捣乱我的大脑几天试图想出一种方法,使用Swift和SpriteKit将玩家从当前位置移动到新位置。听起来相对容易。

现在,我知道我可以使用CGPathSKAction来沿着路径移动玩家,但我需要知道的是如何创建玩家移动的路径。

我需要玩家移动预定的半径,因为它在移动时首先转向新点,让我演示......

Turn Circle

因此,红色圆圈是玩家及其当前方向,大圆圈是转弯半径,红色十字架是玩家想要移动的可能点(显然你在任何时间点都只有一个) ,但这个想法证明了一个可能的点和另一个点之间的运动差异)

此外,玩家可以向左或向右移动,这取决于哪条路径最短到目标点。

我尝试了什么(对不起,这个名单有点简短)......

基本上,我知道玩家当前的位置/方向;我知道转弯圈的半径,我知道我要移动的点。我需要计算玩家最初需要通过哪个弧来定位自己到新点(在CGPathAddLineToPoint上加到弧的末端应该是微不足道的)

除了花费大量时间阅读文档,谷歌搜索,阅读博客文章和教程之外,我还尝试从开始角度到给定迭代级别(例如+/- 0.5度)循环一系列角度计算圆上当前点与下一个点之间的角度,并将其与当前点与目标点的角度进行比较,基本上选择差值最小的角度/Δ...

Delta angles

因此,两个红色圆圈代表圆圈上的两个点,蓝色线条代表它们之间的角度,绿色线条代表从第一个点到目标点的角度。

我们只是说,虽然这可能有用,但我对这个想法感到震惊,并希望有可能提出更好/更快的解决方案。

我不确定CGPathAddArcToPoint之类的东西是否会有所帮助,因为它会从我的玩家当前位置到目标点创建一个弧线,而不是允许玩家通过转弯圈。

一旦玩家离开转弯圈,如果直线移动或不移动(即它们可以略微弯曲到目标点),我不会特别讨价还价,但我现在专注于尝试计算所需的让玩家开始的弧度。

对不起,我的数学很差,所以,请你好,

代码“当前”看起来像(完全混乱)

func pointTowards(point thePoint: CGPoint) {

    // Need to calculate the direction of the turn

    //let angle = atan2(thePoint.y - self.position.y, thePoint.x - self.position.x) - CGFloat(180.0.toRadians());
    let angle = angleBetween(startPoint: self.position, endPoint: thePoint) - CGFloat(180.0.toRadians())

    if (self.zRotation < 0) {
    //  self.zRotation
    //  self.zRotation = self.zRotation + M_PI * 2;
    }

    let rotateTo: SKAction = SKAction.rotateToAngle(angle, duration: 1, shortestUnitArc: true)
    rotateTo.timingMode = SKActionTimingMode.EaseInEaseOut

    self.runAction(rotateTo)

    let offset = CGPoint(x: rotorBlur.position.x, y: rotorBlur.position.y + (rotorBlur.size.width / 2))
    let radius = rotorBlur.size.width / 2.0
    var points: [AnglesAndPoints] = self.pointsOnCircleOf(
        radius: radius,
        offset: offset);

    let centerPoint = CGPoint(x: offset.x + radius, y: offset.y + radius)

    var minAngle = CGFloat.max
    var minDelta = CGFloat.max
    for var p: Int = 1; p < points.count; p++ {
        let p1 = points[p - 1].point
        let p2 = points[p].point

        let point = angleBetween(startPoint: p1, endPoint: p2) - CGFloat(180.0.toRadians())
        let target = angleBetween(startPoint: p1, endPoint: thePoint) - CGFloat(180.0.toRadians())

        let delta = target - point
        if delta < minDelta {
            minDelta = delta
            minAngle = points[p - 1].angle
        }

    }

    println("projected: \(minAngle); delta = \(minDelta)")

    if let pathNode = pathNode {
        pathNode.removeFromParent()
    }

    //points = self.pointsOnCircleOf(
    //  radius: rotorBlur.size.width / 2.0,
    //  offset: CGPoint(x: 0, y: rotorBlur.size.width / 2));

    let path = CGPathCreateMutable()
    CGPathAddArc(
        path,
        nil,
        0,
        rotorBlur.size.width / 2,
        rotorBlur.size.width / 2,
        CGFloat(-180.0.toRadians()),
        minAngle,
        true)
    pathNode = SKShapeNode()
    pathNode?.path = path
    pathNode?.lineWidth = 1.0
    pathNode?.strokeColor = .lightGrayColor()
    addChild(pathNode!)

}

func pointsOnCircleOf(radius r : CGFloat, offset os: CGPoint) -> [AnglesAndPoints] {

    var points: [AnglesAndPoints] = []

    let numPoints = 360.0 * 2.0
    let delta = 360.0 / numPoints
    for var degrees: Double = 0; degrees < numPoints; degrees += delta {

        var point: CGPoint = pointOnCircle(angle: CGFloat(degrees.toRadians()), radius: r)
        point = CGPoint(x: point.x + os.x, y: point.y + os.y)
        points.append(AnglesAndPoints(angle: CGFloat(degrees.toRadians()), point: point))

    }

    return points

}

func pointOnCircle(angle radians:CGFloat, radius theRadius:CGFloat) -> CGPoint {

    return CGPointMake((cos(radians) * theRadius),
        (sin(radians) * theRadius));

}

func angleBetween(startPoint p1: CGPoint, endPoint p2: CGPoint) -> CGFloat {

    return atan2(p2.y - p1.y, p2.x - p1.x) //- CGFloat(180.0.toRadians());

}

基本上,我开始使用给定的偏移量预先计算给定半径的圆上的点,这是非常可怕的,如果我现在有时间,将重新使用它以便动态创建点(或者我可以将这些值缓存一些并简单地翻译它们),但正如我所说,这是一个非常可怕的想法,我真的想找到一种不同的方式并放弃这种方法

我很确定当前的代码没有考虑到玩家当前的方向,它应该提供一个开始角度和方向(计数器/顺时针方向)来迭代,但我已经达到了这一点我在尝试用它修复任何问题之前,我想先看看它们是否只是一个更好的解决方案

1 个答案:

答案 0 :(得分:4)

有趣的是,我的游戏中的动作几乎和你描述的完全一样,只是当它在右侧而不是总是顺时针方向时,而在左侧则是反时钟,它会选择更接近的路径。

所以我抓住了一些代码并对其进行了修改以适合您的描述。当目标点位于玩家左侧时,它将向左移动,否则它将向右移动。您还可以设置节点的速度,以及&#34;轨道的半径和位置。&#34;

但我的实现不使用SKActions和路径移动。一切都是动态地实时完成的,这允许与移动物体的碰撞和更大的运动控制。但是,如果你绝对需要使用SKActions的路径让我知道,我会尝试提出一个解决方案。基本上它归结为找到切线点的弧(代码已经达到了一定程度)。

物理计算来自我的两个回答者herehere

实现的方式是首先确定最终目标点,以及使用辅助圆找到切点的最佳切点的角距离。然后使用向心运动,节点沿着路径移动到切点,然后切换到线性运动以完成移动到最终目的地。

以下是GameScene的代码:

import SpriteKit

enum MotionState { case None, Linear, Centripetal }

class GameScene: SKScene {
    var node: SKShapeNode!
    var circle: SKShapeNode!
    var angularDistance: CGFloat = 0
    var maxAngularDistance: CGFloat = 0
    let dt: CGFloat = 1.0/60.0 //Delta Time
    var centripetalPoint = CGPoint() //Point to orbit.
    let centripetalRadius: CGFloat = 60 //Radius of orbit.
    var motionState: MotionState = .None
    var invert: CGFloat = 1
    var travelPoint: CGPoint = CGPoint() //The point to travel to.
    let travelSpeed:CGFloat = 200 //The speed at which to travel.

    override func didMoveToView(view: SKView) {
        physicsWorld.gravity = CGVector(dx: 0, dy: 0)
        circle = SKShapeNode(circleOfRadius: centripetalRadius)
        circle.strokeColor = SKColor.redColor()
        circle.hidden = true
        self.addChild(circle)
    }
    func moveToPoint(point: CGPoint) {
        travelPoint = point
        motionState = .Centripetal

        //Assume clockwise when point is to the right. Else counter-clockwise
        if point.x > node.position.x {
            invert = -1
            //Assume orbit point is always one x radius right from node's position.
            centripetalPoint = CGPoint(x: node.position.x + centripetalRadius, y: node.position.y)
            angularDistance = CGFloat(M_PI)
        } else {
            invert = 1
            //Assume orbit point is always one x radius left from node's position.
            centripetalPoint = CGPoint(x: node.position.x - centripetalRadius, y: node.position.y)
            angularDistance = 0
        }
    }

    final func calculateCentripetalVelocity() {
        let normal = CGVector(dx:centripetalPoint.x + CGFloat(cos(self.angularDistance))*centripetalRadius,dy:centripetalPoint.y + CGFloat(sin(self.angularDistance))*centripetalRadius);
        let period = (CGFloat(M_PI)*2.0)*centripetalRadius/(travelSpeed*invert)
        self.angularDistance += (CGFloat(M_PI)*2.0)/period*dt;
        if (self.angularDistance>CGFloat(M_PI)*2)
        {
            self.angularDistance = 0
        }
        if (self.angularDistance < 0) {
            self.angularDistance = CGFloat(M_PI)*2
        }
        node.physicsBody!.velocity = CGVector(dx:(normal.dx-node.position.x)/dt ,dy:(normal.dy-node.position.y)/dt)

        //Here we check if we are at the tangent angle. Assume 4 degree threshold for error.
        if abs(maxAngularDistance-angularDistance) < CGFloat(4*M_PI/180) {
            motionState = .Linear
        }
    }

    final func calculateLinearVelocity() {
        let disp = CGVector(dx: travelPoint.x-node.position.x, dy: travelPoint.y-node.position.y)
        let angle = atan2(disp.dy, disp.dx)
        node.physicsBody!.velocity = CGVector(dx: cos(angle)*travelSpeed, dy: sin(angle)*travelSpeed)

        //Here we check if we are at the travel point. Assume 15 point threshold for error.
        if sqrt(disp.dx*disp.dx+disp.dy*disp.dy) < 15 {
            //We made it to the final position! Code that happens after reaching the point should go here.
            motionState = .None
            println("Node finished moving to point!")
        }
    }

    override func update(currentTime: NSTimeInterval) {
        if motionState == .Centripetal {
            calculateCentripetalVelocity()
        } else if motionState == .Linear {
            calculateLinearVelocity()
        }
    }

    func calculateMaxAngularDistanceOfBestTangent() {
        let disp = CGVector(dx: centripetalPoint.x - travelPoint.x, dy: centripetalPoint.y - travelPoint.y)
        let specialCirclePos = CGPoint(x: (travelPoint.x+centripetalPoint.x)/2.0, y: (travelPoint.y+centripetalPoint.y)/2.0)
        let specialCircleRadius = sqrt(disp.dx*disp.dx+disp.dy*disp.dy)/2.0
        let tangentPair = getPairPointsFromCircleOnCircle(centripetalPoint, radiusA: centripetalRadius, pointB: specialCirclePos, radiusB: specialCircleRadius)
       let tangentAngle1 = (atan2(tangentPair.0.y - centripetalPoint.y,tangentPair.0.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
       let tangentAngle2 = (atan2(tangentPair.1.y - centripetalPoint.y,tangentPair.1.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
        if invert == -1 {
            maxAngularDistance = tangentAngle2
        } else {
            maxAngularDistance = tangentAngle1
        }
    }

    //Not mine, modified algorithm from https://stackoverflow.com/q/3349125/2158465
    func getPairPointsFromCircleOnCircle(pointA: CGPoint, radiusA: CGFloat, pointB: CGPoint, radiusB: CGFloat) -> (CGPoint,CGPoint) {
        let dX = (pointA.x - pointB.x)*(pointA.x - pointB.x)
        let dY = (pointA.y - pointB.y)*(pointA.y - pointB.y)
        let d = sqrt(dX+dY)
        let a = (radiusA*radiusA - radiusB*radiusB + d*d)/(2.0*d);
        let h = sqrt(radiusA*radiusA - a*a);
        let pointCSub = CGPoint(x:pointB.x-pointA.x,y:pointB.y-pointA.y)
        let pointCScale = CGPoint(x: pointCSub.x*(a/d), y: pointCSub.y*(a/d))
        let pointC = CGPoint(x: pointCScale.x+pointA.x, y: pointCScale.y+pointA.y)
        let x3 = pointC.x + h*(pointB.y - pointA.y)/d;
        let y3 = pointC.y - h*(pointB.x - pointA.x)/d;
        let x4 = pointC.x - h*(pointB.y - pointA.y)/d;
        let y4 = pointC.y + h*(pointB.x - pointA.x)/d;
        return (CGPoint(x:x3, y:y3), CGPoint(x:x4, y:y4));

    }

    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        let touchPos = (touches.first! as! UITouch).locationInNode(self)
        node = SKShapeNode(circleOfRadius: 10)
        node.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
        node.physicsBody = SKPhysicsBody(circleOfRadius: 10)
        self.addChild(node)

        moveToPoint(touchPos)
        calculateMaxAngularDistanceOfBestTangent()  //Expensive!

        circle.hidden = false
        circle.position = centripetalPoint
    }
}


enter image description here

请注意,您看到的圆圈是我添加到场景中的另一个节点,以使动画更加清晰可见;你可以轻松地删除它。调试时,您可能还发现在切点处添加节点很有用。 calculateMaxAngularDistanceOfBestTangent函数内的tangentPair元组包含两个切点。

另外请注意,找到切点/角度是很昂贵的,但只有在每次需要移动到新点时才会发生。但是,如果你的游戏需要不断地移动到一个新的点,那么在许多节点上重复使用这个算法可能代价很高(在假设这个之前总是要进行配置)。检查何时从向心运动移动到线性运动的另一种方法是检查速度矢量是否接近结束位置,如下所示。这不太准确,但允许您完全删除calculateMaxAngularDistanceOfBestTangent函数。

let velAngle = atan2(node.physicsBody!.velocity.dy,node.physicsBody!.velocity.dx)
let disp = CGVector(dx: travelPoint.x-node.position.x, dy: travelPoint.y-node.position.y)
let dispAngle = atan2(disp.dy,disp.dx)

//Here we check if we are at the tangent angle. Assume 4 degree threshold for error.
if velAngle != 0 && abs(velAngle - dispAngle) < CGFloat(4*M_PI/180) {
    motionState = .Linear
}


最后让我知道你是否需要使用带有SKActions的路径,不管我想我会更新最后一部分来说明这是如何完成的(除非有人打败我!而且正如我之前提到的,我发布的代码在某种程度上做到了这一点。我现在没有时间,但希望我很快就有机会!我希望这个答案中提到的东西可以帮到你。祝你的游戏好运。

更新,包括SKActions

下面的代码显示获得相同的确切效果,除非这次使用SKActions将CGPath设置为切线角度,然后设置为最终目标点。它更简单,因为不再需要手动计算向心和线性运动,但是因为它是动画,您将失去上述解决方案提供的动态实时运动控制。

class GameScene: SKScene {

    var centripetalPoint = CGPoint() //Point to orbit.
    let centripetalRadius: CGFloat = 60 //Radius of orbit.
    var travelPoint: CGPoint = CGPoint() //The point to travel to.
    var travelDuration: NSTimeInterval = 1.0 //The duration of action.
    var node: SKShapeNode!
    var circle: SKShapeNode!

    override func didMoveToView(view: SKView) {
        physicsWorld.gravity = CGVector(dx: 0, dy: 0)
        circle = SKShapeNode(circleOfRadius: centripetalRadius)
        circle.strokeColor = SKColor.redColor()
        circle.hidden = true
        self.addChild(circle)
    }
    //Not mine, modified algorithm from https://stackoverflow.com/q/3349125/2158465
    func getPairPointsFromCircleOnCircle(pointA: CGPoint, radiusA: CGFloat, pointB: CGPoint, radiusB: CGFloat) -> (CGPoint,CGPoint) {
        let dX = (pointA.x - pointB.x)*(pointA.x - pointB.x)
        let dY = (pointA.y - pointB.y)*(pointA.y - pointB.y)
        let d = sqrt(dX+dY)
        let a = (radiusA*radiusA - radiusB*radiusB + d*d)/(2.0*d);
        let h = sqrt(radiusA*radiusA - a*a);
        let pointCSub = CGPoint(x:pointB.x-pointA.x,y:pointB.y-pointA.y)
        let pointCScale = CGPoint(x: pointCSub.x*(a/d), y: pointCSub.y*(a/d))
        let pointC = CGPoint(x: pointCScale.x+pointA.x, y: pointCScale.y+pointA.y)
        let x3 = pointC.x + h*(pointB.y - pointA.y)/d;
        let y3 = pointC.y - h*(pointB.x - pointA.x)/d;
        let x4 = pointC.x - h*(pointB.y - pointA.y)/d;
        let y4 = pointC.y + h*(pointB.x - pointA.x)/d;
        return (CGPoint(x:x3, y:y3), CGPoint(x:x4, y:y4));
    }

    func moveToPoint(point: CGPoint) {
        travelPoint = point

        //Assume clockwise when point is to the right. Else counter-clockwise
        if point.x > node.position.x {
            centripetalPoint = CGPoint(x: node.position.x + centripetalRadius, y: node.position.y)
        } else {
            centripetalPoint = CGPoint(x: node.position.x - centripetalRadius, y: node.position.y)
        }

        let disp = CGVector(dx: centripetalPoint.x - travelPoint.x, dy: centripetalPoint.y - travelPoint.y)
        let specialCirclePos = CGPoint(x: (travelPoint.x+centripetalPoint.x)/2.0, y: (travelPoint.y+centripetalPoint.y)/2.0)
        let specialCircleRadius = sqrt(disp.dx*disp.dx+disp.dy*disp.dy)/2.0
        let tangentPair = getPairPointsFromCircleOnCircle(centripetalPoint, radiusA: centripetalRadius, pointB: specialCirclePos, radiusB: specialCircleRadius)
        let tangentAngle1 = (atan2(tangentPair.0.y - centripetalPoint.y,tangentPair.0.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
        let tangentAngle2 = (atan2(tangentPair.1.y - centripetalPoint.y,tangentPair.1.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)

        let path = CGPathCreateMutable()
        CGPathMoveToPoint(path, nil, node.position.x, node.position.y)
        if travelPoint.x > node.position.x  {
            CGPathAddArc(path, nil, node.position.x+centripetalRadius, node.position.y, centripetalRadius, CGFloat(M_PI), tangentAngle2, true)
        } else {
            CGPathAddArc(path, nil, node.position.x-centripetalRadius, node.position.y, centripetalRadius, 0, tangentAngle1, false)
        }
        CGPathAddLineToPoint(path, nil, travelPoint.x, travelPoint.y)

        let action = SKAction.followPath(path, asOffset: false, orientToPath: false, duration: travelDuration)
        node.runAction(action)
    }

    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        let touchPos = (touches.first! as! UITouch).locationInNode(self)
        node = SKShapeNode(circleOfRadius: 10)
        node.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
        self.addChild(node)

        moveToPoint(touchPos)

        circle.hidden = false
        circle.position = centripetalPoint
    }
}