寻找弹跳的SKSpriteNode将要走的路径

10

当一个SKSpriteNode在屏幕上弹跳时,我希望显示两行内容:一行是节点前进的方向,另一行是节点将要与其他精灵节点弹跳的方向。

基本上,我想做这样的事情:

enter image description here

使用球的速度可以轻松创建第一条线,如果我知道该线将在哪里与另一个SKSpriteNode碰撞,则第二条线也很容易。

对于第一条线,我正在创建一个SKShapeNode并将其路径设置为与球的速度相同的线路,但我不确定在球反弹到最近的障碍物后球将采取的路径。

简而言之——如何找到运动物体与另一个物体碰撞的点,然后找到它与物体碰撞后移动的方向?如果我知道它将与哪个对象碰撞,这不是问题,但除了进行一些极其低效的蛮力计算外,我不知道球将与哪个对象碰撞以及在哪里碰撞。

我正在寻找像这样的东西:

let line: SKSpriteNode // this is the first line
let scene: SKScene // the scene that contains all of the objects

// this doesn't exist, but I'm looking for something that would do this
let collisionPoint: CGPoint = line.firstCollision(in: scene)

针对我的需求,精灵没有任何环境因素(如重力、空气阻力等),它以恒定的速度移动,在反弹时不会失去速度,但如果能够计算出任何环境下精灵第n次反弹的方法将非常有用。


你需要进行命中测试来确定你将要击中什么。你可以追踪你的线,或者对你的线和每个节点的框架进行线交叉。 - Knight0fDragon
球反弹后的方向取决于许多因素,包括它反弹的精灵的形状/旋转/属性、球的旋转速率、是否开启重力等。 - 0x141E
@0x141E在这里就出现了问题——在反弹发生之前计算反弹的一种简单(或至少是计算上简单)的方法。 - Jojodmo
1个回答

11
这是我们要构建的结果:
  • enter image description here

光线投射

我们使用的技术称为光线投射 (Ray Casting),你可以在我已经写过的类似答案中找到更多细节。
基本上,我们使用SpriteKit物理引擎来确定一个直线并获取该直线与物理体的第一个交点。

代码

当然,你需要:
  • 在SpriteKit基础上创建一个空Xcode项目
  • 添加一个圆形精灵(ball)
  • 给球添加物理体并赋予一些速度
以下是你所需要的全部代码:

扩展功能

将这两个扩展添加到你的项目中:
extension CGVector {

    init(angle: CGFloat) {
        self.init(dx: cos(angle), dy: sin(angle))
    }

    func normalized() -> CGVector {
        let len = length()
        return len>0 ? self / len : CGVector.zero
    }

    func length() -> CGFloat {
        return sqrt(dx*dx + dy*dy)
    }

    static func / (vector: CGVector, scalar: CGFloat) -> CGVector {
        return CGVector(dx: vector.dx / scalar, dy: vector.dy / scalar)
    }

    func bounced(withNormal normal: CGVector) -> CGVector {
        let dotProduct = self.normalized() * normal.normalized()
        let dx = self.dx - 2 * (dotProduct) * normal.dx
        let dy = self.dy - 2 * (dotProduct) * normal.dy
        return CGVector(dx: dx, dy: dy)
    }

    init(from:CGPoint, to:CGPoint) {
        self = CGVector(dx: to.x - from.x, dy: to.y - from.y)
    }

    static func * (left: CGVector, right: CGVector) -> CGFloat {
        return (left.dx * right.dx) + (left.dy * right.dy)
    }

}

extension CGPoint {

    func length() -> CGFloat {
        return sqrt(x*x + y*y)
    }

    func distanceTo(_ point: CGPoint) -> CGFloat {
        return (self - point).length()
    }

    static func -(left: CGPoint, right: CGPoint) -> CGPoint {
        return CGPoint(x: left.x - right.x, y: left.y - right.y)
    }

}

场景

现在用以下代码替换您在Scene.swift中的所有代码

class GameScene: SKScene {

    lazy var ball: SKSpriteNode = {
        return childNode(withName: "ball") as! SKSpriteNode
    }()

    override func didMove(to view: SKView) {
        self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
    }

    override func update(_ currentTime: TimeInterval) {

        self.enumerateChildNodes(withName: "line") { (node, _) in
            node.removeFromParent()
        }

        guard let collision = rayCast(start: ball.position, direction: ball.physicsBody!.velocity.normalized()) else { return }

        let line = CGVector(from: ball.position, to: collision.destination)
        drawVector(point: ball.position, vector: line, color: .green)

        var nextVector = line.normalized().bounced(withNormal: collision.normal.normalized()).normalized()
        nextVector.dx *= 200
        nextVector.dy *= 200
        drawVector(point: collision.destination, vector: nextVector, color: .yellow)
    }

    private func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)? {

        let endVector = CGVector(
            dx: start.x + direction.normalized().dx * 4000,
            dy: start.y + direction.normalized().dy * 4000
        )

        let endPoint = CGPoint(x: endVector.dx, y: endVector.dy)

        var closestPoint: CGPoint?
        var normal: CGVector?

        physicsWorld.enumerateBodies(alongRayStart: start, end: endPoint) {
            (physicsBody:SKPhysicsBody,
            point:CGPoint,
            normalVector:CGVector,
            stop:UnsafeMutablePointer<ObjCBool>) in

            guard start.distanceTo(point) > 1 else {
                return
            }

            guard let newClosestPoint = closestPoint else {
                closestPoint = point
                normal = normalVector
                return
            }

            guard start.distanceTo(point) < start.distanceTo(newClosestPoint) else {
                return
            }

            normal = normalVector
        }
        guard let p = closestPoint, let n = normal else { return nil }
        return (p, n)
    }

    private func drawVector(point: CGPoint, vector: CGVector, color: SKColor) {

        let start = point
        let destX = (start.x + vector.dx)
        let destY = (start.y + vector.dy)
        let to = CGPoint(x: destX, y: destY)

        let path = CGMutablePath()
        path.move(to: start)
        path.addLine(to: to)
        path.closeSubpath()
        let line = SKShapeNode(path: path)
        line.strokeColor = color
        line.lineWidth = 6
        line.name = "line"
        addChild(line)
    }
}

它是如何工作的?

这种方法

func drawVector(point: CGPoint, vector: CGVector, color: SKColor)

该功能仅仅从给定的起始点开始绘制一个向量,并使用指定的颜色

这个

func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)?

使用上面讨论的射线投射技术来查找交点和相关的法向量

最后在

func update(_ currentTime: TimeInterval)

我们要做的是:
  • 删除上一帧绘制的线条
  • 使用射线检测获取碰撞点
  • 绘制球和碰撞点之间的线条
  • 计算球反弹的方向
  • 使用前一步得到的方向值,从碰撞点开始绘制一条线
就这些。

他还想检查与其他节点的碰撞。 - Sam
1
如果可能的话,您可以更改顶部的GIF。目前,它似乎没有检查节点碰撞(在GIF中),并且您也没有说明。只是想为其他人澄清一下。 - Sam
谢谢您的回复 - 这肯定会帮助到我。如果您可以编辑您的答案,包括如何处理所有(或至少大多数)情况(重力、摩擦等),我将授予您答案的奖励。 - Jojodmo
@Jojodmo 请注意,我已更新了我的代码。你说的“适用于所有场景”是什么意思?不确定你需要什么。 - Luca Angeletti
@LucaAngeletti 目前的代码没有考虑到弹跳时速度的损失,也没有考虑到球是否会因为重力或其他因素从当前位置加速到弹跳位置。 - Jojodmo
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接