A solution for the n-body problem on a collection of nodes. This library computes the necessary forces to produce a force directed graph in a SpriteKit scene. Only the repulsive/attractive forces amongst the charges on each node are simulated. SKPhysicsJointSpring
should be used to add spring forces for links.
ForceDirectedScene is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'ForceDirectedScene'
Implement the ForceBody
protocol on your data model. We ship this protocol so that you're no required to provide a list of SKNode
s with attached SKPhysicsBody
s
protocol ForceBody {
var position: CGPoint { get }
var charge: CGFloat { get }
func applyForce(force: CGVector)
}
Example: say your nodes are instances of a class MyNode
, which stores an SKNode
in a property, skNode
, to render in your SpriteKit scene. Then you could implement the protocol as:
extension MyNode: ForceBody {
public var position: CGPoint {
get {
return self.skNode.position
}
}
public var charge: CGFloat {
get {
if let physics = self.skNode.physicsBody {
return physics.charge
}
return 0.0
}
}
public func applyForce(force: CGVector) {
self.skNode.physicsBody?.applyForce(force)
}
}
You're free to set up your scene using all the tools SpriteKit has to offer. If you share the charge property of the physicsBody through the protocol to ForceDirectedGraph, your nodes can simultaneously be affected by electric fields and their mutual replusion or attraction. Also, the barnes-hut algorithm uses a quad tree to speed up simulation, which requires the bounds of your forced directed graph to be explicitly defined, so that we recommend setting constraints to confine the movement of your nodes to whatever bounds you define.
We also suggest setting a strong linearDamping
property.
for node in mynodes {
node.skNode = SKShapeNode( ... )
node.skNode.positition = CGPoint( ... )
node.skNode.physicsBody = SKPhysicsBody(circleOfRadius: 10.0)
node.skNode.physicsBody?.isDynamic = true
node.physicsBody?.charge = 5.5
node.physicsBody?.linearDamping = 1.3
node.skNode.constraints = [
SKConstraint.positionX(SKRange(lowerLimit: 0, upperLimit: self.view.bounds.width)),
SKConstraint.positionY(SKRange(lowerLimit: 0, upperLimit: self.view.bounds.height))
]
scene.addChild(node.skNode)
}
You can model the links in your force directed graph with spring joints. For example:
for link in links {
let src = link.sourceSKNode
let dest = link.destinationSKNode
let spring = SKPhysicsJointSpring.joint(withBodyA: src.physicsBody!, bodyB: dest.physicsBody!, anchorA: src.position, anchorB: dest.position)
spring.damping = 10.0
spring.frequency = 0.25
scene.physicsWorld.add(spring)
}
(Note that you probably also want to create an SKShapeNode
for each link to render a line between src and dest)
The ForceDirectedGraph
constructor has two required arguments, (1) the bounds of graph's display area and (2) an array of objects conforming to the ForceBody
protocol
fdGraph = ForceDirectedGraph(bounds: self.view.bounds, nodes: mynodes)
Then, call the graph's update method in the SKSceneDelegate
's update method:
func update(_ currentTime: TimeInterval, for scene: SKScene) {
fdGraph.update()
}
It's also probable that you'll want to update the position of your links' SKShapeNode
s. We suggest doing that in the didApplyConstraints
method:
func didApplyConstraints(for scene: SKScene) {
var path: UIBezierPath
for link in links {
let src = link.sourceSKNode
let dest = link.destinationSKNode
let lineNode = link.skNode
path = UIBezierPath()
path.move(to: src.position)
path.addLine(to: dest.position)
lineNode.path = path.cgPath
}
}
Property | Description |
---|---|
position | a getter that must return the node's current position in the scene |
charge | a getter that must return the charge of the current node. This does not have to be the same charge of SKPhysicsBody. It can be postive or negative. Similar charges repel one another. Dissimilar charges attract. |
applyForce | a function that must ultimately pass the supplied force to the applyForce method of the SKPhysicsBody for each node |
Public Property | Description |
---|---|
theta | Threshold value for Barnes-Hut algorithm. Low values improve simulation accuracy at higher computational cost. High values speed up simulation. |
maxDistance | The maximum distance at which two nodes can have an affect upon one another. Nodes farther apart than this distance will artificially no longer influence the force applied to each other. |
minDistance | The minimum distance required between two nodes for each to affect the other. Nodes closer than this distance will stop having an affect upon each other. |
center? | a point around which all nodes should attempt to cluster. When not nil, causes the application of an additional constant force to each node, directing the node to the defined center. If either of the x or y coordinates of center are set such that CGFloat.isFinite is false, the force will not be applied along that cardinal direction, e.g. a center of CGPoint(x: 50.0, y: CGFloat.infinity) will cause the nodes to cluster around the vertical line at x = 50.0. |
centeringStrength | defines the strength of the force applied to each node while moving it to its clustering point/line |
bounds | a CGRect that defines the maximum boundaries of the force directed graph scene. Undefined behavior will result if your scene pushes the nodes beyond these bounds. |
update() | method that must be called periodically to update the simuation, usually in an SKSceneDelegate method. |
init() | accepts initializers for each of these properties. See below. |
Each of these can be passed to the constructor as well. The default values defined in the signature are:
public init(bounds: CGRect,
nodes: Array<ForceBody>,
theta: CGFloat = 0.5,
min: CGFloat = 0.0,
max: CGFloat = CGFloat.infinity,
center: CGPoint? = nil,
centeringStrength: CGFloat = 0.0002)
Dylan Knight, [email protected]
ForceDirectedScene is available under the MIT license. See the LICENSE file for more info.