Simulate Universal Gravitation for Two Sprite Kit Nodes

Simulate universal gravitation for two Sprite Kit nodes

You can loop through all nodes and calculate the impulse to all other nodes using the appropriate ratios of the universal gravitation equation. I just wrote a quick example showing how this is done. You can make your own custom "mass" factor, however I'm simply using Sprite Kit's. I also added a strength factor to amplify the impulse. I'm also assuming fixed time step of 1/60 seconds.

class GameScene: SKScene {
var nodes: [SKShapeNode] = []
let dt: CGFloat = 1.0/60.0 //Delta time.
let radiusLowerBound: CGFloat = 1.0 //Minimum radius between nodes check.
let strength: CGFloat = 10000 //Make gravity less weak and more fun!
override func didMoveToView(view: SKView) {
self.physicsWorld.gravity = CGVector()
for i in 1 ... 50 { //Create 50 random nodes.
let rndRadius = 15 + CGFloat(arc4random_uniform(20))
let rndPosition = CGPoint(x: CGFloat(arc4random_uniform(UInt32(self.size.width))), y: CGFloat(arc4random_uniform(UInt32(self.size.height))))
let node = SKShapeNode(circleOfRadius: rndRadius)
node.position = rndPosition
node.physicsBody = SKPhysicsBody(circleOfRadius: rndRadius)
self.addChild(node)
nodes.append(node)
}
}
override func update(currentTime: NSTimeInterval) {
for node1 in nodes {
for node2 in nodes {
let m1 = node1.physicsBody!.mass*strength
let m2 = node2.physicsBody!.mass*strength
let disp = CGVector(dx: node2.position.x-node1.position.x, dy: node2.position.y-node1.position.y)
let radius = sqrt(disp.dx*disp.dx+disp.dy*disp.dy)
if radius < radiusLowerBound { //Radius lower-bound.
continue
}
let force = (m1*m2)/(radius*radius);
let normal = CGVector(dx: disp.dx/radius, dy: disp.dy/radius)
let impulse = CGVector(dx: normal.dx*force*dt, dy: normal.dy*force*dt)

node1.physicsBody!.velocity = CGVector(dx: node1.physicsBody!.velocity.dx + impulse.dx, dy: node1.physicsBody!.velocity.dy + impulse.dy)
}
}
}
}

enter image description here

Instead of performing the calculation manually you could also add field nodes to your physics bodies to simulate the effect. Although be warned, field nodes are broken in certain versions of Sprite Kit.

Custom gravity for single node in SpriteKit

Use an SKFieldNode with fieldBitMask and categoryBitMask set so that it only affects the desired nodes.

If you want to have reduced gravity for some nodes, add a linearGravityField to affect those nodes and set its strength to reduce the normal gravity to the degree desired. Or you can set the added field to the desired value directly and have the special nodes' affectedByGravity set to false to turn off the normal gravity for those nodes.

If you want some falling nodes to reach a constant speed, maybe include a dragField as well and set its strength to balance the gravity acceleration when it reaches the desired terminal velocity.

https://developer.apple.com/documentation/spritekit/skfieldnode

Adding a gravitational field about SpriteNode

SpriteKit has SKFieldNode for creating gravity and magnets that apply to bodies. Normally it deflects charged bodies instead of attracting them like ferromagnetism in the real world.

If you want a field that attracts things you'll need a radialGravityField() method around your hero. To attract specific things like your coins you would use categoryBitMask on the hero field and fieldBitMask on the coin sprites you want to attract.

With the electricField() method you could also have your hero attract different bodies with stronger or weaker forces. Or even attract and repel different bodies at the same time. You can use the charge property of physics bodies.

Code examples:

        var field:SKFieldNode?

switch( name )
{
case .Electric:
var electric = SKFieldNode.electricField()
electric.strength = 100.0
bestBodyMass = 0.5
impulseMultiplier = 400
field = electric
case .Magnetic:
var magnetic = SKFieldNode.magneticField()
magnetic.strength = 1.0
bestBodyMass = 0.5
impulseMultiplier = 400
field = magnetic
}

The Apple documentation for SKFieldNode is awesome.
https://developer.apple.com/reference/spritekit/skfieldnode

Here's are two cool YT videos showing the effect.

https://www.youtube.com/watch?v=-mjRPgP0oAE

https://www.youtube.com/watch?v=JGk3agy-c50

Simulate zero gravity style player movement

Basically we need a scene with zero gravity and a player where the touches cause force type physics actions. This is instead of moveBy type digital actions that simple move a character on the screen by such and such.

I went ahead and tested the code to try and get you something similar to what you describe. I altered some of your code a tad... to get it to work with my own set-up, as you didn't provide your GameViewController code so ask if you have any questions.

I've provided the code at the end with comments that say // IMPORTANT CODE with a # beside.

Here's details on why you use each piece of "IMPORTANT CODE

  1. We need physics to accomplish what you describe so first ensure the player class will have a physics body. The body will be dynamic and affected by gravity (Zero Gravity), however you may want to fiddle with the gravity slightly for gameplay sake.

    let body:SKPhysicsBody = SKPhysicsBody(texture: texture, alphaThreshold: 0, size: texture.size() )

    self.physicsBody = body
    self.physicsBody?.allowsRotation = false

    self.physicsBody?.dynamic = true
    self.physicsBody?.affectedByGravity = true
  2. Since you want zero gravity we need to change our physics worlds gravity in our scene

      scene?.physicsWorld.gravity = CGVectorMake(0, 0)
  3. Next we change your movePlayerBy() to work with forces instead of simple digital movement. We do this with SKAction.applyForce.

This gives you a set-up based on force that's correlated with the swipe.
However, you may want a constant velocity no matter how hard the swipe. You can do that by normalizing the vector.. See here for somehow who asked that question and how it may apply here
(http://www.scriptscoop2.com/t/adc37b4f2ea8/swift-giving-a-physicsbody-a-constant-force.html)

     func movePlayerBy(dxVectorValue: CGFloat, dyVectorValue: CGFloat, duration: NSTimeInterval)->(){

print("move player")
let moveActionVector = CGVectorMake(dxVectorValue, dyVectorValue)
let movePlayerAction = SKAction.applyForce(moveActionVector, duration: 1/duration)
self.runAction(movePlayerAction)
}

  1. If you want the player to decelerate , we must add a function to set the player's velocity to 0. I've made it so this happens 0.5 seconds after the function is initially called.. otherwise the "floating through gravity" effect isn't really noticed as the movement would end with touchesEnded().

You can experiment with other ways to de-accelerate like a negative force of what was used initially, before the pause action in the sequence below.

There's many other ways to make it more of a true deceleration ... like a second sequence that subtracts -1 from velocity at a set time interval until it hits 0, before we hard code velocity to 0.
But, that's up to you from a gameplay standpoint.

So this should be enough to give you an idea.

    func stopMoving() {
let delayTime: NSTimeInterval = 0.5 // 0.5 second pause

let stopAction: SKAction = SKAction.runBlock{
self.physicsBody?.velocity = CGVectorMake(0, 0)
}

let pause: SKAction = SKAction.waitForDuration(delayTime)

let stopSequence: SKAction = SKAction.sequence([pause,stopAction])

self.runAction(stopSequence)

}

  1. We alter touchesEnded() to call stopMoving() .. But, try it without this to see it without that "deceleration".

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    player.removeAllActions()

    player.stopMoving()
    }

Other Notes.

Currently the bounds only catch the player on the left and right with the code I created... I'm not sure if that will happen in your set-up. But, as that's another question to figure out, I didn't further look into it.

Here's my code I used ... I'm providing it since I made a few other minor alterations for the sake of testing. I wouldn't worry about anything other than where I place the new important pieces of code.

GameScene.Swift

import SpriteKit

// Global

/*
Level_1 set up and control
*/

class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */

}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */

}

override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}

class Level_1: GameScene {
// Instance variables
var lastUpdateTime:NSTimeInterval = 0
var dt:NSTimeInterval = 0
var player = Player() // Sub classed SKSpriteNode for all player related stuff

var currentTouchPosition: CGPoint = CGPointZero
var beginningTouchPosition:CGPoint = CGPointZero
var currentPlayerPosition: CGPoint = CGPointZero

var playableRectArea:CGRect = CGRectZero

override func didMoveToView(view: SKView) {
/* Setup your scene here */
// IMPORTANT CODE 2 //

scene?.physicsWorld.gravity = CGVectorMake(0, 0)

// Constant - Max aspect ratio supported
let maxAspectRatio:CGFloat = 16.0/9.0

// Calculate playable height
let playableHeight = size.width / maxAspectRatio

// Determine margin on top and bottom by subtracting playable height
// from scene height and then divide by 2
let playableMargin = (size.height-playableHeight)/2.0

// Calculate the actual playable area rectangle
playableRectArea = CGRect(x: 0, y: playableMargin,
width: size.width,
height: playableHeight)

currentTouchPosition = CGPointZero
beginningTouchPosition = CGPointZero

let background = SKSpriteNode(imageNamed: "Level1_Background")
background.position = CGPoint(x: size.width/2, y: size.height/2)
background.zPosition = -1

self.addChild(background)

// CHANGED TO Put my own texture visible on the screen

currentPlayerPosition = CGPoint(x: size.width/2, y: size.height/2)

player.position = currentPlayerPosition

self.addChild(player)

}

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
currentTouchPosition = touch.locationInNode(self)
}

let dxVectorValue = (-1) * (beginningTouchPosition.x - currentTouchPosition.x)
let dyVectorValue = (-1) * (beginningTouchPosition.y - currentTouchPosition.y)

player.movePlayerBy(dxVectorValue, dyVectorValue: dyVectorValue, duration: dt)

}

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
player.removeAllActions()

// IMPORTANT CODE 5 //
player.stopMoving()
}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
print("touch")
for touch: AnyObject in touches {
beginningTouchPosition = touch.locationInNode(self)
currentTouchPosition = beginningTouchPosition
}

}

override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
currentPlayerPosition = player.position

if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
}else{
dt = 0
}
lastUpdateTime = currentTime

player.boundsCheckPlayer(playableRectArea)
}
}

GameViewController.swift

import UIKit
import SpriteKit

class GameViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true

/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true

/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill

skView.presentScene(scene)
}
}

override func shouldAutorotate() -> Bool {
return true
}

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}

override func prefersStatusBarHidden() -> Bool {
return true
}
}

Player.swift

import Foundation
import SpriteKit

struct PhysicsCategory {
static let None : UInt32 = 0
static let All : UInt32 = UInt32.max
static let Player : UInt32 = 0b1 // 1
static let Enemy : UInt32 = 0b10 // 2
}

class Player: SKSpriteNode{

init(){

// Initialize the player object
let texture = SKTexture(imageNamed: "Player1")

super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())

self.xScale = 2
self.yScale = 2
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.zPosition = 1

// Player physics

// IMPORTANT CODE 1 //

let body:SKPhysicsBody = SKPhysicsBody(texture: texture, alphaThreshold: 0, size: texture.size() )

self.physicsBody = body
self.physicsBody?.allowsRotation = false

self.physicsBody?.dynamic = true
self.physicsBody?.affectedByGravity = true

self.physicsBody?.categoryBitMask = PhysicsCategory.Player
}

required init?(coder aDecoder: NSCoder) {

super.init(coder: aDecoder)
}

// Check if the player sprite is within the playable area bounds
func boundsCheckPlayer(playableArea: CGRect){
let bottomLeft = CGPoint(x: 0, y: CGRectGetMinY(playableArea))
let topRight = CGPoint(x: playableArea.size.width, y: CGRectGetMaxY(playableArea))

if(self.position.x <= bottomLeft.x){
self.position.x = bottomLeft.x
// velocity.x = -velocity.x
}

if(self.position.x >= topRight.x){
self.position.x = topRight.x
// velocity.x = -velocity.x
}

if(self.position.y <= bottomLeft.y){
self.position.y = bottomLeft.y
// velocity.y = -velocity.y
}

if(self.position.y >= topRight.y){
self.position.y = topRight.y
// velocity.y = -velocity.y
}
}

/*
Move the player in a certain direction by a specific amount
*/

// IMPORTANT CODE 3 //

func movePlayerBy(dxVectorValue: CGFloat, dyVectorValue: CGFloat, duration: NSTimeInterval)->(){

print("move player")
let moveActionVector = CGVectorMake(dxVectorValue, dyVectorValue)
let movePlayerAction = SKAction.applyForce(moveActionVector, duration: 1/duration)
self.runAction(movePlayerAction)
}

// IMPORTANT CODE 4 //

func stopMoving() {
let delayTime: NSTimeInterval = 0.5 // 0.5 second pause

let stopAction: SKAction = SKAction.runBlock{
self.physicsBody?.velocity = CGVectorMake(0, 0)
}

let pause: SKAction = SKAction.waitForDuration(delayTime)

let stopSequence: SKAction = SKAction.sequence([pause,stopAction])

self.runAction(stopSequence)

}

}

Spritekit Simulating fluid currents

This can be handled in your -update method. You can check whether a certain node is located within a certain region and apply a force on it, as if it were being affected by a current.

EDIT: I have posted a way you can differentiate the force based on the center of the current.

-(void)update:(CFTimeInterval)currentTime
{

for (SKNode *node in self.children)
{
if (node.position.y > 300 && node.position.y < 500) //Let's say the current is between these values, modify to your situation.
{
float diff = ABS (node.position.y - 400);//Difference from center of current.
CGVector force = CGVectorMake(2*(100 - diff), 0);//Variable force
[node.physicsBody applyForce:force];
}
}
}


Related Topics



Leave a reply



Submit