sprite kit: how to vary skaction duration based on distance? - swift

Ok, so I have an array of Cgpoints that I move a sprite node to as I iterate through the array. The sprites distance from the next point varies with each point
func moveGuy() {
let action = SKAction.move(to: points[i], duration: 2)
action.timingMode = .easeInEaseOut
guy.run(action)
}
//UPDATE
override func update(_ currentTime: CFTimeInterval) {
if(mouseIsDown)
{
moveGuy()
}
}
My problem is with a static duration, depending on where the sprite is he moves at a slower/faster speed. No matter the distance sprite must travel I need to have constant speed.
How can I vary an SKAction speed based on sprite's distance from end point?

That would require using the distance formula. Now to maintain a constant speed, you need to determine points per second (pps) you want to move.
Basically the formula becomes distance/pps
So let's say we want to travel (0,0) to (0,300) (This is 300 points so it should take us 2 seconds)
pseudo code:
let duration = sqrt((0 - 0) * (0 - 0) + (0 - 300) * (0 - 300)) / (150)
duration = sqrt(90000) / (150)
duration = 300 / 150
duration = 2
You would then pop it into your move action:
let action = SKAction.move(to: points[i], duration: duration)
Your Swift code should look something like this:
let pps = 150 //define points per second
func moveGuy() {
var actions = [SKAction]()
var previousPoint = guy.position // let's make sprite the starting point
for point in points
{
//calculate the duration using distance / pps
//pow seems to be slow and some people have created ** operator in response, but will not show that here
let duration = sqrt((point.x - previousPoint.x) * (point.x - previousPoint.x) +
(point.y - previousPoint.y) * (point.y - previousPoint.y)) / pps
//assign the action to the duration
let action = SKAction.move(to: point, duration: duration)
action.timingMode = .easeInEaseOut
actions.append(action)
previousPoint = point
}
guy.run(actions)
}
//Since the entire path is done in moveGuy now, we no longer want to be doing it on update, so instead we do it during the mouse click event
//I do not know OSX, so I do not know how mouse down really works, this may need to be fixed
//Also I am not sure what is suppose to happen if you click mouse twice,
//You will have to handle this because it will run 2 actions at once if not done
override func mouseDown(event:NSEvent)
{
moveGuy()
}
Now you will notice your sprite running up and slowing down at every iteration, you may not want that, so for your timing mode, you may want to do this instead
//Use easeInEaseOut on our first move if we are only moving to 1 point
if points.count == 1
{
action.timingMode = .easeInEaseOut
}
//Use easeIn on our first move unless we are only moving to 1 point
else if previousPoint == sprite.position
{
action.timingMode = .easeIn
}
//Use easeOut on our last move unless we are only moving to 1 point
else if point == point.last
{
action.timingMode = .easeOut
}
//Do nothing
else
{
action.timingMode = .linear
}

You will have to create a new SKAction with a duration calculated from the distance to move and the speed (in points/s) of the node to be moved.

Related

how to plot forward/backward tilt on y axis?

I have a line (UIView with width = screen width and height = 2), I need to move this line up and down the screen with forward tilt and backward tilt. I know I need to use Gyroscope, how can I achieve this using MotionManager()?
var motionManager = CMMotionManager()
private func getGyroUpdates() {
if motionManager.isDeviceMotionAvailable == true {
motionManager.deviceMotionUpdateInterval = 0.1
let queue = OperationQueue()
motionManager.startDeviceMotionUpdates(to: queue, withHandler: { [weak self] motion, error in
// Get the attitude of the device
guard let motion = motion else { return }
let pitch = Double(round(motion.attitude.pitch.rad2deg()))
let length = sqrt(motion.gravity.x * motion.gravity.x + motion.gravity.y * motion.gravity.y + motion.gravity.z * motion.gravity.z)
// how to i get the value to be plotted in Y? do i use gravity? or pitch ?
DispatchQueue.main.async {
// frontBackMovement is the line view here
self?.frontBackMovement.transform = CGAffineTransform(translationX: 0, y: "what should be the value here??")
}
})
print("Device motion started")
}else {
print("Device motion unavailable")
}
}
line needs to move in the frame which is a uiview, I need to get value to put in the place of Y to put it in CGAffineTransform. so basically how do I map value I get from Motion object to plot it in Y. I tried the radian value I get from attitude.pitch but how do I convert that into Y? If I use gravity value how do I use it?
Thank you very much for your help in advance.
Depending on the effect you are trying to achieve you would want to get the pitch angle from motion.attitude.pitch. Then you need to calculate the y offset based on how far you want the line to move relative to the pitch angle.
Let's say you want the line to move 100 points up or down as the device is tilted between -90º and 90º.
You had the angle:
let pitch = Double(round(motion.attitude.pitch.rad2deg()))
So now calculate the distance:
let maxDistance = 100.0
let currentDistance = pitch / 90.0 * maxDistance
where maxDistance is the furthest you want the line to move. You probably need some extra checks to ensure pitch is kept between -90 and 90.
A much simpler approach than using CMMotionManager is to use UIInterpolatingMotionEffect. First setup your line to be in the center of its parent view. Then use the following code:
let maxDistance: Float = 100 // how far do you want the line to move
let eff = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
eff.maximumRelativeValue = maxDistance
eff.minimumRelativeValue = -maxDistance
lineView.addMotionEffect(eff)
where maxDistance is the max distance you want the line to move as the device is tilted. In your case this sounds like it should be half the height of the line's parent view.

How can I change the touch offset of a sprite in SpriteKit?

When I touch the player mallet (Air Hockey), I want to make it so the mallet moves slightly above the touch. This way the mallet will be more visible in the game. I have found some solutions but am having a hard time implementing properly in my function.
Here is a sample of my touchesMoved() function:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
bottomTouchIsActive = true
var releventTouch:UITouch!
//convert set to known type
let touchSet = touches
//get array of touches so we can loop through them
let orderedTouches = Array(touchSet)
for touch in orderedTouches
{
//if we've not yet found a relevent touch
if releventTouch == nil
{
//look for a touch that is in the activeArea (Avoid touches by opponent)
if activeArea.contains(touch.location(in: parent!))
{
isUserInteractionEnabled = true
releventTouch = touch
}
else
{
releventTouch = nil
}
}
}
if (releventTouch != nil)
{
//get touch position and relocate player
let location = releventTouch!.location(in: parent!)
position = location
//find old location and use pythagoras to determine length between both points
let oldLocation = releventTouch!.previousLocation(in: parent!)
let xOffset = location.x - oldLocation.x
let yOffset = location.y - oldLocation.y
let vectorLength = sqrt(xOffset * xOffset + yOffset * yOffset)
//get eleapsed and use to calculate speed6A
if lastTouchTimeStamp != nil
{
let seconds = releventTouch.timestamp - lastTouchTimeStamp!
let velocity = 0.01 * Double(vectorLength) / seconds
//to calculate the vector, the velcity needs to be converted to a CGFloat
let velocityCGFloat = CGFloat(velocity)
//calculate the impulse
let directionVector = CGVector(dx: velocityCGFloat * xOffset / vectorLength, dy: velocityCGFloat * yOffset / vectorLength)
//pass the vector to the scene (so it can apply an impulse to the puck)
delegate?.bottomForce(directionVector, fromBottomPlayer: self)
delegate?.bottomTouchIsActive(bottomTouchIsActive, fromBottomPlayer: self)
}
//update latest touch time for next calculation
lastTouchTimeStamp = releventTouch.timestamp
}
}
Is the position var, which you take from the touch location, used to set the position of the mallet? If it is, then if you want the mallet above the touch, why not do something like position.y += 50 immediately after position = location to move it up by 50 points?
Alternatively, you might find it more logical to set the mallet's anchorPoint property (https://developer.apple.com/documentation/spritekit/skspritenode/1519877-anchorpoint and https://developer.apple.com/documentation/spritekit/skspritenode/using_the_anchor_point_to_move_a_sprite) to be somewhere other than the default poisition (the centre of the sprite) e.g. the point that corresponds to the part of the handle of the mallet where one would normally hold it.

for loop not executing code sequencially (swift + spritekit)

Here's my code:
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let origin = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
let delay = SKAction.waitForDuration(2.0)
for (var i = 0; i < 6; i++) {
//generate a random point on a circle centered at origin with radius of 500
let point = randomPointOnCircle(500, center: origin)
let chanceToSpawn = arc4random_uniform(100)
switch chanceToSpawn {
case 0...60:
self.runAction(delay){
let smallBall = SKSpriteNode(imageNamed: "smallBall”)
self.addChild(smallBall)
smallBall.xScale = 0.05
smallBall.yScale = 0.05
smallBall.position = point
let finalDest = CGPointMake(origin.x, origin.y)
let moveAction = SKAction.moveTo(finalDest, duration: 5.0)
smallBall.runAction(SKAction.sequence([SKAction.waitForDuration(0.1), moveAction, SKAction.runBlock({
smallBall.removeFromParent()
})]))
delay
}
break
case 61...80:
//same as case 0…60, smallBall changed to “mediumBall”
break
case 81...99:
//same as case 0…60, smallBall changed to “largeBall”
break
default:
break
} // ends switch statement
}//ends for loop
}//end didMoveToView
Basically I have balls that spawn on a random point on a circle and move towards the center. But the problem I am having is that no matter how I rearrange things (or rewrite things, i.e., trying different loops or defining functions for spawning the ball and the movement of the ball and simply calling them from inside the switch statement) all of the balls spawn at the same time. I am trying to make it so they spawn one after the other (meaning a ball spawns, the thread waits a few seconds, then spawns another ball.), and I can't seem to figure out why each ball is spawning at the same time as all the other balls.
I'm fairly new to Swift/Spritekit programming and I feel like I'm overlooking some small detail and this is really bugging me. Any help appreciated!
For loops are executed faster than you think! Your for loop is probably executed instantly.
"Why is that? I have already called self.runAction(delay) to delay the for loop!" you asked. Well, the delay action doesn't actually delay the for loop, unfortunately. It just delays the other actions you run on self.
To fix this, try delaying different amounts, dependind on the for loop counter:
self.runAction(SKAction.waitForDuration(i * 2 + 2)) { ... }
This way, balls will appear every two seconds.
try this, may be due to same time u are adding the actions for each balls,
smallBall.runAction(SKAction.sequence([SKAction.waitForDuration(0.1 + (i * 0.35/2)), moveAction, SKAction.runBlock({
smallBall.removeFromParent()
})])) // delay added (0.1 + (increment the daly))
try this
Your problem is you are calling runAction on each iteration of your for loop. This means that all your actions will run concurrently.
What you want to do is create an array of actions. then after the for loop, run a sequence of actions.
(Note: I took the liberty of cleaning up your code)
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let origin = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
let delay = SKAction.waitForDuration(2.0)
var sequence = [SKAction]()
for (var i = 0; i < 6; i++) {
//generate a random point on a circle centered at origin with radius of 500
let point = randomPointOnCircle(500, center: origin)
let chanceToSpawn = arc4random_uniform(100)
var ballname = "smallBall”
switch chanceToSpawn {
case 61...80:
//same as case 0…60, smallBall changed to “mediumBall”
ballname = "mediumBall”
case 81...99:
//same as case 0…60, smallBall changed to “largeBall”
ballname = "largeBall”
default: ()
} // ends switch statement
sequence.append(delay)
sequence.append(
SKAction.run()
{
[unowned self] in
let ball = SKSpriteNode(imageNamed: ballName)
self.addChild(ball)
ball.xScale = 0.05
ball.yScale = 0.05
ball.position = point
let finalDest = CGPointMake(origin.x, origin.y)
let moveAction = SKAction.moveTo(finalDest, duration: 5.0)
let briefWait = SKAction.waitForDuration(0.1)
let remove = SKAction.removeFromParent()
let moveSeq = SKAction.sequence([briefWait ,moveAction,remove])
ball.runAction(moveSeq)
}
)
}//ends for loop
self.run(SKAction.sequence(sequence))
}//end didMoveToView

How to flip a SKPriteNode upside down while being generated automatically

I have a SKSpriteNode generated at every 1 second on a floor. Sometime it's over and sometime under it. The SpriteNode is an image and I want it to be flipped upside down when its under the floor. I tried
SKAction.scaleYto(scale, duration: 0.1
But it doesn't work!
Here is the code
func startGeneratingWallsEvery(seconds: NSTimeInterval) {
generationTimer = NSTimer.scheduledTimerWithTimeInterval(seconds, target: self, selector: "generateWall", userInfo: nil, repeats: true)
}
func stopGenerating() {
generationTimer?.invalidate()
}
func generateWall(){
var scale: CGFloat
let wall = ADWall()
let rand = arc4random_uniform(2)
if rand == 0 {
scale = -1.0
} else {
scale = 1.0
}
//let translate1 = SKAction.moveByX(0, y: scale*(wall.size.height), duration: 0.1)
let flip = SKAction.scaleYTo(scale, duration: 0.1)
wall.position.x = size.width/2 + wall.size.width/2
wall.position.y = scale * (kADGroundHeight/2 + wall.size.height/2)
walls.append(wall)
addChild(wall)
runAction(flip)
}
Help me please!!
I can think of a couple of different strategies for causing the sprit to be generated upside down automatically when they are created:
The first strategy is to have two different images at hand, one which is vertically flipped, and one which is not. When the sprite is created, if it spawns below the floor, simply retexture the SKSpriteNode with:
wall.texture = SKTexture(imageNamed:"flipped_image")
You can use that same method to retexture the sprite again should it ever appear above the floor.
Rotate the image. If you want to keep the texturing simple and but a single image for the wall object, another method you could try is to rotate the image by 180 degress, like so:
if belowFloor == true {
zRotation = CGFloat(M_PI)
}
HTH!

how to set physics properties for a circle so it follows given path

The movement of a physics body circle is too erratic for what I want to achieve. I would like to restrict it so it follows a certain path touching specific points (or a range of points) as shown in the image below. How can I set the physics properties to traverse a similar path?
how to set physics properties for a circle so it follows given path
So essentially you are looking to move a node to a particular point using real-time motion. I have an answer here showing how to do this, however given the number of up votes this question has received, I will provide a more detailed answer.
What the answer I linked to doesn't provide is traversing a path of points. So below I have provided a solution showing how this can be done below. It simply just moves to each point in the path, and each time the node reaches a point, we increment the index to move to the next point. I also added a few variables for travel speed, rate (to make the motion more smooth or static) and whether or not the node should repeat the path. You could further expand upon this solution to better meet the needs of your game. I would definitely consider subclassing a node and building this behavior into it so you can re-use this motion for multiple nodes.
One final note, you may notice the calculation for the impulse varies between my solution below and the answer I linked to above. This is because I am avoiding using angle calculation because they are very expensive. Instead I am calculating a normal so that the calculation is more computationally efficient.
One final note, my answer here explains the use of the rate factor to smooth the motion and leave room for motion distortions.
import SpriteKit
class GameScene: SKScene {
var node: SKShapeNode! //The node.
let path: [CGPoint] = [CGPoint(x: 100, y: 100),CGPoint(x: 100, y: 300),CGPoint(x: 300, y: 300),CGPoint(x: 300, y: 100)] //The path of points to travel.
let repeats: Bool = true //Whether to repeat the path.
var pathIndex = 0 //The index of the current point to travel.
let pointRadius: CGFloat = 10 //How close the node must be to reach the destination point.
let travelSpeed: CGFloat = 200 //Speed the node will travel at.
let rate: CGFloat = 0.5 //Motion smoothing.
override func didMoveToView(view: SKView) {
node = SKShapeNode(circleOfRadius: 10)
node.physicsBody = SKPhysicsBody(circleOfRadius: 10)
node.physicsBody!.affectedByGravity = false
self.addChild(node)
}
final func didReachPoint() {
//We reached a point!
pathIndex++
if pathIndex >= path.count && repeats {
pathIndex = 0
}
}
override func update(currentTime: NSTimeInterval) {
if pathIndex >= 0 && pathIndex < path.count {
let destination = path[pathIndex]
let displacement = CGVector(dx: destination.x-node.position.x, dy: destination.y-node.position.y)
let radius = sqrt(displacement.dx*displacement.dx+displacement.dy*displacement.dy)
let normal = CGVector(dx: displacement.dx/radius, dy: displacement.dy/radius)
let impulse = CGVector(dx: normal.dx*travelSpeed, dy: normal.dy*travelSpeed)
let relativeVelocity = CGVector(dx:impulse.dx-node.physicsBody!.velocity.dx, dy:impulse.dy-node.physicsBody!.velocity.dy);
node.physicsBody!.velocity=CGVectorMake(node.physicsBody!.velocity.dx+relativeVelocity.dx*rate, node.physicsBody!.velocity.dy+relativeVelocity.dy*rate);
if radius < pointRadius {
didReachPoint()
}
}
}
}
I did this pretty quickly so I apologize if there is a mistake. I don't have time now but I will add a gif showing the solution later.
A note about collisions
To fix the erratic movement during a collision, after the 2 bodies collide set the "rate" property to 0 or preferably a very low number to reduce the travel velocity impulse which will give you more room for motion distortion. Then at some point in the future (maybe some time after the collision occurs or preferably when the body is moving slow again) set the rate back to its initial value. If you really want a nice effect, you can actually ramp up the rate value over time from 0 to the initial value to give yourself a smooth and gradual acceleration.
Quick implementation using followPath:duration:
override func didMoveToView(view: SKView) {
self.backgroundColor = UIColor.blackColor()
let xWidth: CGFloat = CGRectGetMaxX(self.frame)
let yHeight: CGFloat = CGRectGetMaxY(self.frame)
let ball = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake(50.0, 50.0))
let offset : CGFloat = ball.size.width / 2
// Moving path
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, offset, yHeight / 2)
CGPathAddLineToPoint(path, nil, xWidth * 2 / 3, yHeight - offset)
CGPathAddLineToPoint(path, nil, xWidth - offset, yHeight * 2 / 3)
CGPathAddLineToPoint(path, nil, xWidth / 2, offset)
CGPathAddLineToPoint(path, nil, offset, yHeight / 2)
// Movement
let moveByPath = SKAction.followPath(path, asOffset: false, orientToPath: false, duration: 4.0)
let moveForever = SKAction.repeatActionForever(moveByPath)
ball.runAction(moveForever)
self.addChild(ball)
}
In GameViewController.swift, I changed the default GameScene.unarchiveFromFile method to let scene = GameScene(size: view.bounds.size) for creating the scene's size.
Preview: