You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中实现SpriteKit节点沿路径自由移动(替代SKAction follow)

Great question! Instead of letting SKAction.follow move the node automatically, you want players to drag the node along a predefined path (like a straight line or Bézier ellipse) while keeping it constrained to that path. Here's a step-by-step implementation to make this work in SpriteKit:

How to Drag a Node Along a Predefined Path in SpriteKit

Core Idea

Instead of relying on automatic path following, we’ll handle touch events directly. For every touch movement, we calculate the closest point on the predefined path to the user’s touch location, then move the draggable node to that point. This ensures the node stays strictly on the path while following the user’s drag.

Step 1: Set Up Your Scene, Node, and Path

First, define your draggable node and path as properties in your SKScene, then initialize them:

import SpriteKit
import UIKit

class GameScene: SKScene {
    // Properties
    var draggableNode: SKSpriteNode!
    var path: CGPath!
    var isDragging = false
    
    override func didMove(to view: SKView) {
        // Create a draggable node (red square for example)
        draggableNode = SKSpriteNode(color: .red, size: CGSize(width: 50, height: 50))
        // Start the node on the path (adjust this to your path's starting point)
        draggableNode.position = CGPoint(x: 100, y: 200)
        addChild(draggableNode)
        
        // Example 1: Create an ellipse path (using UIBezierPath)
        path = UIBezierPath(ovalIn: CGRect(x: 100, y: 100, width: 300, height: 200)).cgPath
        
        // Example 2: Create a straight line path (uncomment to use)
        // let straightPath = CGMutablePath()
        // straightPath.move(to: CGPoint(x: 100, y: 200))
        // straightPath.addLine(to: CGPoint(x: 400, y: 200))
        // path = straightPath
        
        // Optional: Draw the path to visualize it (blue line)
        let pathNode = SKShapeNode(path: path)
        pathNode.strokeColor = .blue
        pathNode.lineWidth = 2
        addChild(pathNode)
    }
}

Step 2: Handle Touch Events

Add touch handlers to detect when the user starts dragging, moves their finger, and stops dragging:

// MARK: - Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let touchLocation = touch.location(in: self)
    
    // Only start dragging if the user taps the draggable node
    if draggableNode.contains(touchLocation) {
        isDragging = true
    }
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard isDragging, let touch = touches.first else { return }
    let touchLocation = touch.location(in: self)
    
    // Find the closest point on the path to the touch location
    let closestPathPoint = closestPointOnPath(touchLocation, path: path)
    
    // Move the node to this constrained point
    draggableNode.position = closestPathPoint
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    isDragging = false
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    isDragging = false
}

Step 3: Calculate the Closest Point on the Path

This is the critical part: we need functions to compute the closest point on line segments and cubic Bézier curves (since ellipses and complex paths use Bézier curves). Add these helper functions and extensions to your scene:

// MARK: - Path Helper Functions
func closestPointOnPath(_ touchPoint: CGPoint, path: CGPath) -> CGPoint {
    let bezierPath = UIBezierPath(cgPath: path)
    var closestPoint = CGPoint.zero
    var minDistance = CGFloat.infinity
    let tolerance: CGFloat = 0.1 // Adjust for precision vs performance
    
    var prevPoint = CGPoint.zero
    var isFirstPoint = true
    
    // Iterate through every element in the path
    bezierPath.forEach { element in
        switch element {
        case .moveTo(let point):
            prevPoint = point
            isFirstPoint = true
        case .lineTo(let point):
            let lineClosest = closestPointOnLineSegment(touchPoint: touchPoint, start: prevPoint, end: point)
            let distance = lineClosest.distance(to: touchPoint)
            if distance < minDistance {
                minDistance = distance
                closestPoint = lineClosest
            }
            prevPoint = point
            isFirstPoint = false
        case .curveTo(let control1, let control2, let end):
            // Handle cubic Bézier curves (used for ellipses, circles, etc.)
            let curveClosest = closestPointOnCubicBezier(
                touchPoint: touchPoint,
                start: prevPoint,
                control1: control1,
                control2: control2,
                end: end,
                tolerance: tolerance
            )
            let distance = curveClosest.distance(to: touchPoint)
            if distance < minDistance {
                minDistance = distance
                closestPoint = curveClosest
            }
            prevPoint = end
            isFirstPoint = false
        case .close:
            // Connect the last point back to the path's start
            let lineClosest = closestPointOnLineSegment(touchPoint: touchPoint, start: prevPoint, end: bezierPath.currentPoint)
            let distance = lineClosest.distance(to: touchPoint)
            if distance < minDistance {
                minDistance = distance
                closestPoint = lineClosest
            }
        }
    }
    
    return closestPoint
}

// Find closest point on a straight line segment
func closestPointOnLineSegment(touchPoint: CGPoint, start: CGPoint, end: CGPoint) -> CGPoint {
    let lineVector = end - start
    let lengthSquared = lineVector.x * lineVector.x + lineVector.y * lineVector.y
    guard lengthSquared > 0 else { return start } // Avoid division by zero
    
    // Calculate the projection of the touch point onto the line
    let t = max(0, min(1, (touchPoint - start).dot(lineVector) / lengthSquared))
    return start + lineVector * t
}

// Find closest point on a cubic Bézier curve (recursive subdivision method)
func closestPointOnCubicBezier(touchPoint: CGPoint, start: CGPoint, control1: CGPoint, control2: CGPoint, end: CGPoint, tolerance: CGFloat) -> CGPoint {
    // Subdivide the curve until it's short enough to treat as a line
    let midStart = (start + control1) / 2
    let midControl1 = (control1 + control2) / 2
    let midControl2 = (control2 + end) / 2
    let midMid = (midStart + midControl1) / 2
    let midMid2 = (midControl1 + midControl2) / 2
    let midEnd = (midMid + midMid2) / 2
    
    let distanceToLine = distanceFromPointToLine(touchPoint, lineStart: start, lineEnd: end)
    if distanceToLine < tolerance {
        return closestPointOnLineSegment(touchPoint: touchPoint, start: start, end: end)
    }
    
    // Recursively check left and right halves of the curve
    let leftClosest = closestPointOnCubicBezier(
        touchPoint: touchPoint,
        start: start,
        control1: midStart,
        control2: midMid,
        end: midEnd,
        tolerance: tolerance
    )
    let rightClosest = closestPointOnCubicBezier(
        touchPoint: touchPoint,
        start: midEnd,
        control1: midMid2,
        control2: midControl2,
        end: end,
        tolerance: tolerance
    )
    
    // Return the closer of the two points
    return leftClosest.distance(to: touchPoint) < rightClosest.distance(to: touchPoint) ? leftClosest : rightClosest
}

// Calculate distance from a point to an infinite line
func distanceFromPointToLine(_ point: CGPoint, lineStart: CGPoint, lineEnd: CGPoint) -> CGFloat {
    let lineVector = lineEnd - lineStart
    let pointVector = point - lineStart
    let crossProduct = lineVector.x * pointVector.y - lineVector.y * pointVector.x
    return abs(crossProduct) / lineVector.length()
}

// MARK: - CGPoint Extensions
extension CGPoint {
    static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
        CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
    }
    
    static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
        CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
    }
    
    static func *(lhs: CGPoint, scalar: CGFloat) -> CGPoint {
        CGPoint(x: lhs.x * scalar, y: lhs.y * scalar)
    }
    
    static func /(lhs: CGPoint, scalar: CGFloat) -> CGPoint {
        CGPoint(x: lhs.x / scalar, y: lhs.y / scalar)
    }
    
    func dot(_ other: CGPoint) -> CGFloat {
        x * other.x + y * other.y
    }
    
    func length() -> CGFloat {
        sqrt(x * x + y * y)
    }
    
    func distance(to other: CGPoint) -> CGFloat {
        (self - other).length()
    }
}

How It Works

  • Touch Detection: We only enable dragging when the user taps directly on the draggable node, preventing unintended movement.
  • Path Constraining: For every touch move, we calculate the closest point on your predefined path to the user’s finger. This ensures the node never strays off the path, whether it’s a straight line, ellipse, or complex Bézier curve.
  • Precision Adjustment: The tolerance value in the Bézier curve function lets you balance precision and performance—lower values are more precise but use more processing power.

内容的提问来源于stack exchange,提问作者willy wijaya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 11:13:25