ios - 创建通过 "turn circle"将玩家从当前位置移动到新位置的路径

标签 ios swift math sprite-kit

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

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

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

Turn Circle

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

此外,玩家可以根据到达目标点的最短路径向左或向右移动。

我尝试了什么(抱歉列表有点短)...

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

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

Delta angles

所以,两个红圈代表圆上的两点,蓝线代表它们之间的夹角,绿线代表第一个点到目标点的夹角。

这么说吧,虽然这可能行得通,但我对这个想法有点害怕,希望有可能提出更好/更快的解决方案。

我不确定像 CGPathAddArcToPoint 这样的东西是否有帮助,因为它会创建从我的玩家当前位置到目标点的弧线,而不是让玩家移动通过一个转弯圈。

一旦玩家离开转弯圈,我就不会特别在意是否沿直线移动(即他们可能会稍微弯曲到目标点),但我目前专注于尝试计算所需的arc 需要玩家开始。

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

“当前”的代码看起来像(一团糟)

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());

}

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

我很确定当前代码没有考虑到玩家的当前方向,它应该提供迭代的起始角度和方向(逆时针/顺时针),但我已经到了要点在尝试解决更多问题之前,我想看看他们是否只是一个更好的解决方案

最佳答案

有趣的是,实际上我的游戏中的运动几乎与您描述的完全一样,只是它不会总是在右侧时顺时针方向移动,在左侧时总是逆时针方向移动,而是选择较近的路径。

所以我抓取了一些代码并对其进行了适当的修改以符合您的描述。当目标点在玩家的左边时它会向左移动,否则它会向右移动。您还可以设置节点的速度,以及“轨道”的半径和位置。

然而,我的实现不使用 SKActions 和移动路径。一切都是实时动态完成的,这允许与移动物体发生碰撞和更好的运动控制。但是,如果您绝对需要将路径与 SKActions 一起使用,请告诉我,我会尝试提出解决方案。本质上,它归结为找到切点的弧(代码已经在一定程度上做了)。

物理计算来 self 的两位答主here , 和 here .

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

下面是游戏场景的代码:

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

请注意,您看到的圆圈是我添加到场景中的另一个节点,目的是使 Action 更加明显;您可以轻松地将其删除。调试时,您可能还会发现在切点处添加节点很有用。 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
    }
}

关于ios - 创建通过 "turn circle"将玩家从当前位置移动到新位置的路径,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31445122/

相关文章:

ios - 动态单元格高度的自动布局约束

swift - 具有多个键和关联值的可编码枚举

ios - 检查 Realm 创建函数返回新的或更新的对象

java - 我应该如何在Java中抛出除以零异常而不实际除以零?

javascript - 每次单击按钮时,都会生成一个范围为 1 - 100 的唯一随机数 - JavaScript

python - python 中的数学错误计算 log(det(AA^T)+1)

iOS:搜索框的语音转文本

iOS/OS X - 最后一行文本的合理对齐

ios - 当我的自定义键盘显示在屏幕上时会调用哪个方法?

ios - println(currentWeatherDictionary) 函数不工作