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)
}
}
}
}
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
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 = trueSince you want zero gravity we need to change our physics worlds gravity in our scene
scene?.physicsWorld.gravity = CGVectorMake(0, 0)
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)
}
- 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)
}
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
Disable Wkwebview for Opening Links to Redirect to Apps Installed on My Iphone
Forcing iPhone Microphone as Audio Input
Mfmailcomposeviewcontroller Crashes Because of Global Appearance Properties on iOS6
Arc4Random_Uniform Not Available in Xcode 7.0 Beta (7A176X) on Osx 10.10.4
Alamofire Fire Variable Type Has No Subscript Members
How to Use Nsdateformatter to Venezuela
Xcode 10B5 - Duplicate Symbol Linker Error, Can't Compile with Crashlytics
How to Add Particle Effects to an iOS App That Is Not a Game Using iOS 7 Spritekit Particle
Posting Photos to Facebook Fan Page with iOS Sdk
How to Get Memory Usage in Swift
Passing Variables Between View Controllers
Swift iOS9 New Contacts Framework - How to Retrieve Only Cncontact That Has a Valid Email Address
Replace a Particular Color Inside an Image with Another Color
Fix Cordova Geolocation Ask for Location Message