Rotate SkSpriteNode around internal point that is not .anchorPoint - swift

So I'm using this code to rotate an object either clockwise or anti-clockwise.
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first as UITouch!
let touchPosition = touch.locationInNode(self)
let newRotationDirection : rotationDirection = touchPosition.x < CGRectGetMidX(self.frame) ? .clockwise : .counterClockwise
if currentRotationDirection != newRotationDirection && currentRotationDirection != .none {
reverseRotation()
currentRotationDirection = newRotationDirection
}
else if (currentRotationDirection == .none) {
setupRotationWith(direction: newRotationDirection)
currentRotationDirection = newRotationDirection
}
}
func reverseRotation(){
let oldRotateAction = ship.actionForKey("rotate")
let newRotateAction = SKAction.reversedAction(oldRotateAction!)
ship.runAction(newRotateAction(), withKey: "rotate")
}
func stopRotation(){
ship.removeActionForKey("rotate")
}
func setupRotationWith(direction direction: rotationDirection){
let angle : CGFloat = (direction == .clockwise) ? CGFloat(M_PI) : -CGFloat(M_PI)
let rotate = SKAction.rotateByAngle(angle, duration: 2)
let repeatAction = SKAction.repeatActionForever(rotate)
ship.runAction(repeatAction, withKey: "rotate")
}
My question is, this creates clockwise or anticlockwise rotation around the SKSpriteNode's anchor point.
However I would like the object to rotate about another point inside of the SKSprite (something like 4/5's of the way up from the base of the sprite image). I assume I cannot use a set point defined through CGPointMake as I would like the sprite to independently move around the screen and this would not work?
Any suggestions anyone?

Have you tried setting the anchorPoint property of your sprite to the point around which you want the sprite to rotate?
ship.anchorPoint = CGPoint(x: 0, y: 4/5)
Is this what you were looking for?

Related

Changing the angle of a SKSpriteNode during a rotation

I am trying to make a drifting game. In order for the car to drift, the car (when turning) needs to be at an angle. I have tried rotation, however this conflicts with the code I already have for turning the car. Here is my code, any help?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.previousLocation(in: self)
let position = touch.location(in: self)
let node = self.nodes(at: location).first
if position.x < 0 {
let rotate = SKAction.repeatForever(SKAction.rotate(byAngle: CGFloat(M_PI), duration: 0.8))
car.run(rotate, withKey: "rotating")
} else {
let rotate = SKAction.repeatForever(SKAction.rotate(byAngle: CGFloat(-M_PI), duration: 0.8))
car.run(rotate, withKey: "rotating")
}
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
car.position = CGPoint(x:car.position.x + cos(car.zRotation) * 3.0,y:car.position.y + sin(car.zRotation) * 3.0)
}
}
This code currently turns the car without adding any angle, or "drifting" effect.
one approach is you could nest your car inside another SKNode as a container. apply your steering rotation to the car node, as you're doing it now. then apply the drift rotation to the container node. the result effect will be the sum of the two.
//embed car inside a SKNode container so you can apply different rotations to each
let car = SKShapeNode(ellipseOf: CGSize(width: 20, height: 40)) //change to how you draw your car
let drift_container = SKNode()
drift_container.addChild(car) //embed car inside the container
self.addChild(drift_container) //`self` here is a SKScene
//apply angle rotation to the container
func drift(byAngle angle:CGFloat) {
let rotate = SKAction.rotate(toAngle: angle, duration: 0.2)
drift_container.run(rotate)
}
then in update be sure to update drift_container.position instead of the car's position

SpriteKit - Touch moved, how to prevent node jump to touch location

I wrote code for touch moved like this:
"override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {return}
let location = touch.location(in: self)
player.position = location"
The problem is the node/player will jump to wherever I touch and moved in the scene.
How may I fix this? Thank you.
I am going to answer this in pseudo code for two reasons, I am here to help you not do it for you and as a beginner figuring out how to do this yourself could be greatly beneficial.
var ball: SKSpriteNode!
var minWidth: CGFloat = 0
var maxWidth: CGFloat = 0
var startPosX: CGFloat = 0
var startBallPosX: CGFloat = 0
func didMove() {
//did move func of the scene
//setup ball and min and max widths
ball = SKSpriteNode()
addChild(ball)
minWidth = scene.frame.minX
maxWidth = scene.frame.maxY
}
func touchesBegan() {
let location = touch.location(in: self)
startPosX = location.x
startBallPosX = ball.position.x
}
func touchesMoved() {
let location = touch.location(in: self)
var moveXValue = location.x - startPosX
if startBallPosX + moveXValue > maxWidth {
//if ball wants to go past right edge of screen keep it at the edge
ball.position.x = maxWidth
}
else if startBallPosX + moveXValue < minWidth {
//if ball wants to go past left edge of screen keep it at the edge
ball.position.x = minWidth
}
else {
ball.position.x = startBallPosX + moveXValue
}
}

enumerateChildNodes not finding child nodes in sprite kit - Swift 4

I am trying to create a SpriteKit game where a ball moves across the screen. When the ball leaves the screen I would like to remove it from the parent and switch to a different scene (GameOverScene).
I am using enumerateChildNodes however it doesn't as if that is working. I am not really sure what the problem is however I think it may have something to do with the parent/child relationship...
func createBall(forTrack track: Int) {
setupTracks()
player?.physicsBody?.linearDamping = 0
player = SKSpriteNode(imageNamed: "small")
player?.name = "BALL"
player?.size = CGSize(width: 100, height: 100)
ballValue = 1
randFloat = Float(arc4random()) / Float(UINT32_MAX)
if randFloat > 0.001 {
ballSpeed = randFloat / 50
}
else {
ballSpeed = randFloat / 50
}
let ballPosition = trackArray?[track].position
player?.position = CGPoint(x: (ballPosition?.x)!, y: (ballPosition?.y)!)
player?.position.y = (ballPosition?.y)!
if ballDirection == "right" {
player?.position.x = 0
moveRight()
}
else {
player?.position.x = (self.view?.frame.size.height)!
moveLeft(speed: ballSpeed)
}
self.addChild(player!)
self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in
if node.position.x < -100 || node.position.x > (self.size.width) + 100 {
print("balls Out")
node.removeFromParent()
let transition = SKTransition.fade(withDuration: 1)
self.gameScene = SKScene(fileNamed: "GameOverScene")
self.gameScene.scaleMode = .aspectFit
self.view?.presentScene(self.gameScene, transition: transition)
}
}
}
I call this function twice, first in override func didMove():
override func didMove(to view: SKView) {
createHUD()
createBall(forTrack: track)
}
And second in override func touchesBegan:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.previousLocation(in: self)
let node = self.nodes(at: location).first
if node?.name == "BALL" {
currentScore += ballValue
player?.removeFromParent()
createBall(forTrack: track)
}
else {
let transition = SKTransition.fade(withDuration: 1)
gameScene = SKScene(fileNamed: "GameOverScene")
gameScene.scaleMode = .aspectFit
self.view?.presentScene(gameScene, transition: transition)
}
}
}
update:
The line self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in works so it is not a child parent relationship issue. The if statement is not working.
reading your code I think it's not about a solution, but a different approach. These suggestions will make your life easier:
Start with a smaller code, avoid or comment out all unneeded (like the if randFloat)
Force unwrap player = SKSpriteNode(imageNamed: "small")! with the ! because you actually want to crash if the initialization fails, then you can get rid of all ?
in touchesBegan, better use for touch in touches { as it is more simple to manage
let location = touch.previousLocation(in: self) you probably mean let location = touch.location(in: self)
When the ball leaves the screen I would like to remove it from the parent so this is something happening at some time in the game. You would like to call your self.enumerateChildNodes(withName: "BALL") { into the update call, not every time you touch the screen
If this is not enough, feel free to post the least amount of code to make a playground and let me test it for you :]

How do I rotate a colour sprite by moving finger along y-axis in the BOTTOM HALF of the screen?

Code:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if currentGameType == .wobble{
main.zRotation = (location.y)/512
if location.y > maxTouchHeight { return }
}
}
}
So I am trying to rotate a paddle depending on where your finger is on the y-axis in a type of pong game. However, I have been unable to make it so that it only works in the bottom half of the screen i.e. the two extremities of rotation are at the very top and very bottom of the screen but I want the top extremity to be at the top of the bottom half of the screen. (main is a pong paddle).
Any help is appreciated :)
After followed discussion from comments and repeated answer updates, I have a solution for you. Please let me know if questions:
class GameScene: SKScene {
let paddle = SKSpriteNode()
let maximumRotation = CGFloat(45)
override func didMove(to view: SKView) {
paddle.color = .blue
paddle.size = CGSize(width: 200, height: 15)
let deg45 = CGFloat(0.785398)
let degNeg45 = -deg45
let constraint = [SKConstraint.zRotation(SKRange(lowerLimit: degNeg45, upperLimit: deg45))]
paddle.constraints = constraint
addChild(paddle)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: self) else { return }
guard let lastLocation = touches.first?.previousLocation(in: self) else { return }
guard location.y < self.frame.midY else { return }
// Our vertical plots:
let yVal = location.y
let lastY = lastLocation.y
// How much we moved in a certain direction this frame:
let deltaY = yVal - lastY
// This value represents 100% of a 45deg angle:
let oneHundredPercent = self.frame.height/2
assert(oneHundredPercent != 0)
// The % of 100%Val (45degrees) that we moved (in radians):
let absY = abs(deltaY)
let radToDegFactor = CGFloat(0.01745329252)
let multiplier = (absY / oneHundredPercent) * radToDegFactor
// I suggest a sensitivity of 2-4:
let sensitivity = CGFloat(3)
let amountToRotate = maximumRotation * (multiplier * sensitivity)
// Rotate the correct amount in the correct direction:
if deltaY > 0 {
// Rotate counter-clockwise:
paddle.run(.rotate(byAngle: amountToRotate, duration: 0))
} else {
// Rotate clockwise:
paddle.run(.rotate(byAngle: -amountToRotate, duration: 0))
}
}
}
Just open up a new project and try it out!
There may be an easier way to do this, but this is what popped in my head. It's a little verbose too so hopefully you can see each process step-by-step.
As far as doing something using the bottom half only, you can do something like this
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let fingerPosition = touch.location(in: self)
//this is the bottom
if fingerPosition.y < self.size.height/2 {
//just replace print with your own code
print("bottom half")
}
//this is the top half
if fingerPosition.y > self.size.width/2 {
//you can just leave this as it is or if ever you want to do something with the top part, just add your own code!
print("not bottom half")
}
}
}
Hopefully this helps.
put this inside of your touches functions:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if location.y > maxTouchHeight { return }
if currentGameType == .wobble{
main.zRotation = (location.y)/512
}
}

Drag Rotate a Node around a fixed point

I'm trying to create a spinable node similar to the "prize wheel" in this question. So far I have the flinging capability, adding angular impulses on a physics body using a UIPanGestureRecognizer that works really well. I can also stop the spinning using a touch.
Now I'm trying to allow fine adjustment of the wheel using a drag or swipe gesture so if the player isn't happy with what they end up with they can manually spin/drag/rotate it to their favoured rotation.
Currently I save the location of the touch in the touchesBegan and try to increment the zRotation of my node in the update loop.
The rotation doesn't follow my finger and is jerky. I'm not sure if I'm getting an accurate enough read on the finger movement or if the change position of the finger isn't being accurately translated into radians. I suspect detecting the touch and then dealing with it in the update isn't a great solution.
Here's my code.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent?) {
if let touch = touches.first as? UITouch {
var location = touch.locationInView(self.view)
location = self.convertPointFromView(location)
mostRecentTouchLocation = location
let node = nodeAtPoint(location)
if node.name == Optional("left") && node.physicsBody?.angularVelocity != 0
{
node.physicsBody = SKPhysicsBody(circleOfRadius:150)
node.physicsBody?.applyAngularImpulse(0)
node.physicsBody?.pinned = true
}
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if mostRecentTouchLocation != CGPointZero{
let node = nodeAtPoint(mostRecentTouchLocation)
if node.name == Optional("left")
{
var positionInScene:CGPoint = mostRecentTouchLocation
let deltaX:Float = Float(positionInScene.x) - Float(node.position.x)
let deltaY:Float = Float(positionInScene.y) - Float(node.position.y)
let angle:CGFloat = CGFloat(atan2f(deltaY, deltaX))
let maths:CGFloat = angle - (CGFloat(90) * (CGFloat(M_PI) / 180.0))
node.zRotation += maths
mostRecentTouchLocation = CGPointZero
}
}
}
I've spread some of the maths across multiple lines in the update to make debugging a bit easier.
I can add the PanGestureRecognizer code if needed but I'll try to keep it short for now.
EDIT
Here is my latest code based on GilderMan's recommendation. I think it's working better but the rotation is far from smooth. It's jumping in large increments and not following the finger well. Does this mean there's something wrong with my angle calculation?
override func didSimulatePhysics() {
if mostRecentTouchLocation != CGPointZero {
let node = nodeAtPoint(mostRecentTouchLocation)
if node.name == Optional("left")
{
var positionInScene:CGPoint = mostRecentTouchLocation
let deltaX:Float = Float(positionInScene.x) - Float(node.position.x)
let deltaY:Float = Float(positionInScene.y) - Float(node.position.y)
let angle:CGFloat = CGFloat(atan2f(deltaY, deltaX))
node.zRotation += angle
println(angle)
mostRecentTouchLocation = CGPointZero
}
}
}
The following code simulates a prize wheel that spins based on touch. As the user's finger moves, the wheel rotates proportionately to the speed of the finger. When the user swipes on the wheel, the wheel will spin proportionately to the velocity of the swipe. You can change the angularDamping property of the physics body to slow or increase the rate at which the wheel comes to a stop.
class GameScene: SKScene {
var startingAngle:CGFloat?
var startingTime:TimeInterval?
override func didMove(to view: SKView) {
let wheel = SKSpriteNode(imageNamed: "Spaceship")
wheel.name = "wheel"
wheel.setScale(0.5)
wheel.physicsBody = SKPhysicsBody(circleOfRadius: wheel.size.width/2)
// Change this property as needed (increase it to slow faster)
wheel.physicsBody!.angularDamping = 0.25
wheel.physicsBody?.pinned = true
wheel.physicsBody?.affectedByGravity = false
addChild(wheel)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in:self)
let node = atPoint(location)
if node.name == "wheel" {
let dx = location.x - node.position.x
let dy = location.y - node.position.y
// Store angle and current time
startingAngle = atan2(dy, dx)
startingTime = touch.timestamp
node.physicsBody?.angularVelocity = 0
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let location = touch.location(in:self)
let node = atPoint(location)
if node.name == "wheel" {
let dx = location.x - node.position.x
let dy = location.y - node.position.y
let angle = atan2(dy, dx)
// Calculate angular velocity; handle wrap at pi/-pi
var deltaAngle = angle - startingAngle!
if abs(deltaAngle) > CGFloat.pi {
if (deltaAngle > 0) {
deltaAngle = deltaAngle - CGFloat.pi * 2
}
else {
deltaAngle = deltaAngle + CGFloat.pi * 2
}
}
let dt = CGFloat(touch.timestamp - startingTime!)
let velocity = deltaAngle / dt
node.physicsBody?.angularVelocity = velocity
// Update angle and time
startingAngle = angle
startingTime = touch.timestamp
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startingAngle = nil
startingTime = nil
}
}
The zRotation of a SKNode is in radians. You can remove your conversion to degrees.
You may wish to do the angle adjustment in the didSimulatePhysics in order to compute the zRotation after physics have been applied. (This may not apply directly to this situation, but is good practice further down the line.)
I tried two set of coodes below.
Code Sample 1
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
let dx = location.x
let dy = location.y
startingAngle = atan2(dx, dy)
startingTime = touch.timestamp
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
if !isRotating {
let dx = location.x
let dy = location.y
let angle = atan2(dx, dy)
wheel.zRotation = -angle
//update angle and time
startingAngle = angle
startingTime = touch.timestamp
}
}
}
}
Code Sample 2
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
let dx = location.x
let dy = location.y
startingAngle = atan2(dx, dy)
startingTime = touch.timestamp
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
if !isRotating {
let dx = location.x
let dy = location.y
let angle = atan2(dx, dy)
let deltaAngle = angle - startingAngle!
wheel.zRotation -= deltaAngle
//update angle and time
startingAngle = angle
startingTime = touch.timestamp
}
}
}
}
The only difference between Code Sample 1 & 2 is the method of finding zRotation of the wheel.
Code Sample 1 uses wheel.zRotation = -angle. It ensures wheel follows the finger position exactly. But there is a bug that the wheel will jump position when you touch two distant locations in wheel then move your fingure. I am still unable to fix this bug.
Code Sample 2 uses wheel.zRotation -= deltaAngle to stack the change in angle of each finger movement. It supposes to generate the same result of code sample 1 but it does not exactly follow the finger movement.
I found out the reason by comparing the zPosition of Code Sample 1 and 2 with same finger movement. The deltaAngle in Code Sample 2 is slightly less (up to 8 decimal places)so the wheel does not exactly follow the fingure movement.