Moving an object across the screen at a certain speed.(Sprite Kit)
I think you could revise your approach based on the calculation speed.
So, if you know your speed, you can calculate how many time a node takes to arrive to a point and change your speed to a reasonable value.
// #-#-#-#-#-#-#-#-#-#-#-#-#-#-#
//MARK: - Calculate action duration btw two points and speed
// #-#-#-#-#-#-#-#-#-#-#-#-#-#-#
func getDuration(pointA:CGPoint,pointB:CGPoint,speed:CGFloat)->NSTimeInterval {
let xDist = (pointB.x - pointA.x)
let yDist = (pointB.y - pointA.y)
let distance = sqrt((xDist * xDist) + (yDist * yDist));
let duration : NSTimeInterval = NSTimeInterval(distance/speed)
return duration
}
Suppose you have your node with a speed of 150.0
Your move will be:
let moveObject = SKAction.moveTo(endpoint, duration: getDuration(node.position,pointB:endpoint,speed:150.0))
With this approach you speed dont' change and you don't have this disagreeable slowness.
P.S. Don't forgot to change the uppercase to your properties: in swift is a bad attitude, use object instead of Object.
Moving Sprites Across Screen In Sprite Kit
You may want to use an SKAction assuming you want it to slide across the screen as opposed to simply moving from point a to point b. You could do something like this:
let move = SKAction.moveToX(self.frame.size.width, duration: 1.0)
redBall.runAction(move)
This creates an SKAction called move that moves a node to the right side of the screen and then runs that action on a node, in this case the redBall.
To move to the left, you just change the x value in the move action to this:
let move = SKAction.moveToX(0, duration: 1.0)
Moving objects random across the screen with a certain speed
What you are trying to solve here could be done in two ways. First is using physics, and second is without it of course. I have decided to go without physics because that is obviously how you are doing it.
In short, fish in these example move using SKAction
while there is no food around. When food is in range, fish are moved to their targets using update: method. When fish eat its found it continues to move around using SKAction
.
Also before everything, here are some useful extensions that I have borrowed here from Stackoverflow that you might find useful in the future:
import SpriteKit
import GameplayKit
//Extension borrowed from here : https://stackoverflow.com/a/40810305
extension ClosedRange where Bound : FloatingPoint {
public func random() -> Bound {
let range = self.upperBound - self.lowerBound
let randomValue = (Bound(arc4random_uniform(UINT32_MAX)) / Bound(UINT32_MAX)) * range + self.lowerBound
return randomValue
}
}
//Extension borrowed from here : https://stackoverflow.com/a/37760551
extension CGRect {
func randomPoint() -> CGPoint {
let origin = self.origin
return CGPoint(x:CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x,
y:CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y)
}
}
//Extension borrowed from here: https://stackoverflow.com/a/33292919
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
}
Now there is a Fish class like yours, which has few methods and a physics body which is used just to detect contact between food and a fish, but that's all from the physics. Here is the Collider struct just in case that you want to know how I have defined it:
struct Collider{
static let food : UInt32 = 0x1 << 0
static let fish : UInt32 = 0x1 << 1
static let wall : UInt32 = 0x1 << 2
}
Now back to the Fish class...I have put comments in the code, so I guess it is not needed to explain what those methods do. Here is the code:
class Fish:SKSpriteNode{
private let kMovingAroundKey = "movingAround"
private let kFishSpeed:CGFloat = 4.5
private var swimmingSpeed:CGFloat = 100.0
private let sensorRadius:CGFloat = 100.0
private weak var food:SKSpriteNode! = nil //the food node that this fish currently chase
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody?.affectedByGravity = false
physicsBody?.categoryBitMask = Collider.fish
physicsBody?.contactTestBitMask = Collider.food
physicsBody?.collisionBitMask = 0x0 //No collisions with fish, only contact detection
name = "fish"
let sensor = SKShapeNode(circleOfRadius: 100)
sensor.fillColor = .red
sensor.zPosition = -1
sensor.alpha = 0.1
addChild(sensor)
}
func getDistanceFromFood()->CGFloat? {
if let food = self.food {
return self.position.distance(point: food.position)
}
return nil
}
func lock(food:SKSpriteNode){
//We are chasing a food node at the moment
if let currentDistanceFromFood = self.getDistanceFromFood() {
if (currentDistanceFromFood > self.position.distance(point: food.position)){
//chase the closer food node
self.food = food
self.stopMovingAround()
}//else, continue chasing the last locked food node
//We are not chasing the food node at the moment
}else{
//go and chase then
if food.position.distance(point: self.position) <= self.sensorRadius {
self.food = food
self.stopMovingAround()
}
}
}
//Helper method. Not used currently. You can use this method to prevent chasing another (say closer) food while already chasing one
func isChasing(food:SKSpriteNode)->Bool{
if self.food != nil {
if self.food == food {
return true
}
}
return false
}
func stopMovingAround(){
if self.action(forKey: kMovingAroundKey) != nil{
removeAction(forKey: kMovingAroundKey)
}
}
//MARK: Chasing the food
//This method is called many times in a second
func chase(within rect:CGRect){
guard let food = self.food else {
if action(forKey: kMovingAroundKey) == nil {
self.moveAround(within: rect)
}
return
}
//Check if food is in the water
if rect.contains(food.frame.origin) {
//Take a detailed look in my Stackoverflow answer of how chasing works : https://stackoverflow.com/a/36235426
let dx = food.position.x - self.position.x
let dy = food.position.y - self.position.y
let angle = atan2(dy, dx)
let vx = cos(angle) * kFishSpeed
let vy = sin(angle) * kFishSpeed
position.x += vx
position.y += vy
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveAround(within rect:CGRect){
if scene != nil {
//Go randomly around the screen within view bounds
let point = rect.randomPoint()
//Formula: time = distance / speed
let duration = TimeInterval(point.distance(point: position) / self.swimmingSpeed)
let move = SKAction.move(to: point, duration: duration)
let block = SKAction.run {
[unowned self] in
self.moveAround(within: rect)
}
let loop = SKAction.sequence([move,block])
run(loop, withKey: kMovingAroundKey)
}
}
}
So basically, there are methods to move the fish around while it is not in a chase for a food. Also there is method which stop this (infinite) action (SKAction). The most important method is the chase(within rect:) method. That method is called in scene's update() method and defines how and when fish will (try to) chase the food.
Now the GameScene:
//MARK: GameScene
class GameScene: SKScene, SKPhysicsContactDelegate {
private var nodesForRemoval:[SKNode] = []
private var water = SKSpriteNode()
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector(dx: 0.0, dy: -0.5)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.categoryBitMask = Collider.wall
physicsBody?.contactTestBitMask = 0x0
physicsBody?.collisionBitMask = Collider.fish | Collider.food
self.backgroundColor = .white
//Water setup
water = SKSpriteNode(color: .blue, size: CGSize(width: frame.width, height: frame.height - 150))
water.position = CGPoint(x: 0, y: -75)
water.alpha = 0.3
addChild(water)
water.zPosition = 4
//Fish one
let fish = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
addChild(fish)
fish.position = CGPoint(x: frame.midX-50, y: frame.minY + 100)
fish.zPosition = 5
fish.moveAround(within: water.frame)
//Fish two
let fish2 = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
addChild(fish2)
fish2.position = CGPoint(x: frame.midX+50, y: frame.minY + 100)
fish2.zPosition = 5
fish2.moveAround(within: water.frame)
}
func feed(at position:CGPoint, with food:SKSpriteNode){
food.position = CGPoint(x: position.x, y: frame.size.height/2 - food.frame.size.height)
addChild(food)
}
//MARK: Food factory :)
func getFood()->SKSpriteNode{
let food = SKSpriteNode(color: .purple, size: CGSize(width: 10, height: 10))
food.physicsBody = SKPhysicsBody(rectangleOf: food.frame.size)
food.physicsBody?.affectedByGravity = true
food.physicsBody?.categoryBitMask = Collider.food
food.physicsBody?.contactTestBitMask = Collider.fish
food.physicsBody?.collisionBitMask = Collider.wall
food.physicsBody?.linearDamping = (0.1 ... 0.95).random()
food.name = "food"
return food
}
//MARK: Feeding
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let food = getFood()
feed(at: location, with: food)
}
}
//MARK: Eating
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
//Silliness like removing a node from a node tree before physics simulation is done will trigger this error
fatalError("Physics body without its node detected!")
}
let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch mask {
//Contact between fish and a food
case Collider.fish | Collider.food:
if let food = (contact.bodyA.categoryBitMask == Collider.food ? nodeA : nodeB) as? SKSpriteNode
{
self.nodesForRemoval.append(food)
}
default:
//some unknown contact occurred
break
}
}
//MARK: Removing unneeded nodes
override func didSimulatePhysics() {
for node in self.nodesForRemoval {
node.removeFromParent()
}
self.nodesForRemoval.removeAll()
}
//MARK: Chasing the food
override func update(_ currentTime: TimeInterval) {
self.enumerateChildNodes(withName: "fish") {
[unowned self] node, stop in
if let fish = node as? Fish {
self.enumerateChildNodes(withName: "food") {
node, stop in
fish.lock(food: node as! SKSpriteNode)
}
fish.chase(within: self.water.frame)
}
}
}
}
And that's it. Here we setup our nodes, resolve contact detections and tell which fish should chase which food node. I left the comments, so everything is there. I hope that methods are pretty much self explanatory, but of course you can ask me for details about how things works.
And here is a short video of how this works:
and the longer version because I can only upload two 2 megabytes: screen recording
Basically, fish don't chase the food node if it is not its defined range. Still, fish will chase the locked node until it eats it. But if there is some other food which is more close, the fish will chase for that food node. Of course this is not a must, and you can tweak it as you like (check isChasing:
) method.
How to speed up the movement of the object?
Your problem is not the speed, your problem is you are moving so fast on the screen, that your touch is not recognizing a node underneath it anymore because the node is physically not underneath it. How you handle this problem is on the touch begin event, you check for a node, and assign this node to a variable. Then on the touch move event, update the new variable. Finally on touch end, clear the variable. Note, you would need to handle this code for things like multi touch
var movingNode : SKNode?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.locationInNode(self)
let node = self.nodeAtPoint(touchLocation)
if (node.name == "circle") {
movingNode = node
let moveAction = SKAction.moveTo(touchLocation, duration: 0)
figureUser.runAction(moveAction)
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.locationInNode(self)
let moveAction = SKAction.moveTo(touchLocation, duration: 0)
//at this point I am lost, how does node even relate here,
//is figureUser suppose to be node?
figureUser.runAction(moveAction)
}
SpriteKit : Keep consistent sizes and speeds across devices
Don't do it
Changing the physics world in relation of the screen of the device is wrong.
The physics world should be absolutely agnostic about its graphics representation. And definitively it should have the same properties (size, mass, distance, ...) regardless of the screen.
I understand you don't want the scene to be smaller of the screen when the game runs on a iPad Pro instead of an iPhone 5 but you need to solve this problem in another way.
I suggest you to try another scaleMode
like aspectFill
(capitalized if you're on Xcode 7: AspectFill
). This way the scene is zoomed and all your sprites will appear bigger.
Another point of view
In the comments below @Knight0fDragon pointed out some scenarios where you might actually want to make some properties of the Physics World depending on the UI. I suggest the reader of this answer to take a look at the comments below for a point of view different from mine.
Sprite Kit - Move object to position of touch
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
SKNode *player = [self childNodeWithName:@"player"];
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
// Determine speed
int minDuration = 2.0;
int maxDuration = 4.0;
int rangeDuration = maxDuration - minDuration;
int actualDuration = (arc4random() % rangeDuration) + minDuration;
// Create the actions
SKAction * actionMove = [SKAction moveTo:CGPointMake(player.position.x, positionInScene.y); duration:actualDuration];
[player runAction:actionMove];
}
Move a node to finger using Swift + SpriteKit
You can save yourself a lot of trouble by using: myShip.physicsBody.applyImpluse(vector)
. It works by acting as if you gave myShip
a push in the direction vector
points. If you calculate vector
as the x distance from your last touch location to myShip
, then it'll accelerate, decelerate, change direction, etc. pretty close to the way you're describing because it'll be giving it little pushes in the right direction on each update
.
Basically you store the last touch location then, in your update
function, you calculate the CGVector
pointing from myShip
to lastTouch
and apply that as an impulse to your physics body.
Something like:
var lastTouch: CGPoint? = nil
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
lastTouch = touchLocation
}
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
lastTouch = touchLocation
}
// Be sure to clear lastTouch when touches end so that the impulses stop being applies
override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
lastTouch = nil
}
override func update(currentTime: CFTimeInterval) {
// Only add an impulse if there's a lastTouch stored
if let touch = lastTouch {
let impulseVector = CGVector(touch.x - myShip.position.x, 0)
// If myShip starts moving too fast or too slow, you can multiply impulseVector by a constant or clamp its range
myShip.physicsBody.applyImpluse(impulseVector)
}
}
You'll also probably want to play with the linearDamping
and angularDamping
values on myShip.physicsBody
. They'll help determine how fast myShip
accelerates and decelerates.
I maxed out the values at 1.0
in my app:
myShip.physicsBody.linearDamping = 1.0
myShip.physicsBody.angularDamping = 1.0
If myShip
doesn't stop fast enough for you, you can also try applying some breaking in your update
function:
override func update(currentTime: CFTimeInterval) {
// Only add an impulse if there's a lastTouch stored
if let touch = lastTouch {
let impulseVector = CGVector(touch.x - myShip.position.x, 0)
// If myShip starts moving too fast or too slow, you can multiply impulseVector by a constant or clamp its range
myShip.physicsBody.applyImpluse(impulseVector)
} else if !myShip.physicsBody.resting {
// Adjust the -0.5 constant accordingly
let impulseVector = CGVector(myShip.physicsBody.velocity.dx * -0.5, 0)
myShip.physicsBody.applyImpulse(impulseVector)
}
}
Related Topics
How to Filter Nsarray in Swift
What Does This Mean? Variable Declared Followed by a Block Without Assignment
Code Only Retrieving One Value from Data in Firebase
How Is a Global Variable Set to Private Understood in Swift
Swift 3 Errors with Additional Data
Checkboxes in Uitableview State Persistence
Decoding Dynamic JSON Structure in Swift 4
Generate Labels and Align in Middle
Swift Alamofire Return Value Is Empty
Replacing Multiple Different Occurences with Multiple Different Replacements - Swift4.2
Swift Why Isn't My Date Object That's (Equatable) Equal After Converting It to a String and Back
Swift Format Text Field When User Is Typing
Node.Physicsbody.Joints Downcasting Error
Retrieve an Image from Firebase to an Uiimage Swift5
Where to Implement Nsvaluetransformer for Core Data in Swift