How to create realistic spinning wheel in SpriteKit - swift

I am trying to create a spinning fortune wheel action via SKAction. I have a SKNode which used as wheel, this SKNode is a circle that divided to four quarters (each quarter in different color). also I set an SKAction (which is repeating for 10 counts) that spin the SKNode around fixed point (the node's center). The problem is that the action is running well but it stops suddenly and not slowing down - like a real wheel. I don't really have an idea how to set this animation, I mean to slow the spinning down before the action is stop.
Here is my code so far:
class GameScene: SKScene {
let colors = [SKColor.yellow, SKColor.red, SKColor.blue, SKColor.purple]
override func didMove(to view: SKView) {
createWheel()
let sq = CGRect(x: size.width/2, y: size.height/2, width: 300, height: 300)
let sqx = SKShapeNode(rect: sq)
sqx.lineWidth = 2
sqx.fillColor = .clear
sqx.setScale(1.0)
addChild(sqx)
}
func createWheel() {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: -200))
path.addArc(withCenter: CGPoint.zero,radius: 200,startAngle: CGFloat(0.0), endAngle: CGFloat(3.0 * Double.pi / 2),clockwise: false)
path.addLine(to: CGPoint(x: 200, y: 0))
let obstacle = obstacleByDuplicatingPath(path, clockwise: true)
obstacle.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(obstacle)
let rotateAction = SKAction.rotate(byAngle: CGFloat((3.0 * CGFloat(Double.pi / 2)) - 90), duration: 0.5)
//obstacle.run(SKAction.repeatForever(rotateAction))
obstacle.run(SKAction.repeat(rotateAction, count: 10))
}
func obstacleByDuplicatingPath(_ path: UIBezierPath, clockwise: Bool) -> SKNode {
let container = SKNode()
var rotationFactor = CGFloat(Double.pi / 2)
if !clockwise {
rotationFactor *= -1
}
for i in 0...3 {
let section = SKShapeNode(path: path.cgPath)
section.fillColor = colors[i]
section.strokeColor = colors[i]
section.zRotation = rotationFactor * CGFloat(i);
let origin = CGPoint(x: 0.0, y: 0.0)
switch i {
case 0:
section.position = CGPoint(x: (origin.x + 10), y: (origin.y - 10))
case 1:
section.position = CGPoint(x: (origin.x + 10), y: (origin.y + 10))
case 2:
section.position = CGPoint(x: (origin.x - 10), y: (origin.y + 10))
case 3:
section.position = CGPoint(x: (origin.x - 10), y: (origin.y - 10))
default:
print("bolbol")
}
container.addChild(section)
}
return container
}
}
edit:
I was thinking about it and I tried to do it via SKAction, I set another action but this time I set their duration to a long one. first it run a action of duration 0.5, then of 2 and at end of 4. I looks pretty good but still not smooth as I want it to be.
here is my code:
let rotateAction = SKAction.rotate(byAngle: CGFloat(2.0 * CGFloat(M_PI)), duration: 0.5)
let rotateAction2 = SKAction.rotate(byAngle: CGFloat(2.0 * CGFloat(M_PI)), duration: 2)
let rotateAction3 = SKAction.rotate(byAngle: CGFloat(2.0 * CGFloat(M_PI)), duration: 4)
let wait = SKAction.wait(forDuration: 5)
let g1 = SKAction.repeat(rotateAction, count: 10)
let group = SKAction.group([wait, g1, rotateAction2, rotateAction3])
what do you think? there is any way to do it better??
edit 2:
Continued to #Ali Beadle answer, I tried to do it via physics body, the problem now is the when I drag finger on the screen the SKShapeNode (shape) in continue to rotate and never stops. can you detect what is wrong?
class GameScene: SKScene {
var start: CGPoint?
var end:CGPoint?
var startTime: TimeInterval?
let shape = SKShapeNode.init(rectOf: CGSize(width: 150, height: 150))
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
let sceneBody = SKPhysicsBody.init(edgeLoopFrom: self.frame)
sceneBody.friction = 0
self.physicsBody = sceneBody
shape.fillColor = SKColor.red
shape.position = CGPoint(x: self.size.width/2, y: self.size.height/2)
shape.physicsBody = SKPhysicsBody.init(rectangleOf: CGSize(width: 50, height: 50))
shape.physicsBody?.affectedByGravity = false
shape.physicsBody?.isDynamic = true
addChild(shape)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {return}
self.start = touch.location(in: self)
self.startTime = touch.timestamp
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {return}
self.end = touch.location(in: self)
var dx = ((self.end?.x)! - (self.start?.x)!)
var dy = ((self.end?.y)! - (self.start?.y)!)
let magnitude:CGFloat = sqrt(dx*dx+dy*dy)
if(magnitude >= 25){
let dt:CGFloat = CGFloat(touch.timestamp - self.startTime!)
if dt > 0.1 {
let speed = magnitude / dt
dx = dx / magnitude
dy = dy / magnitude
print("dx: \(dx), dy: \(dy), speed: \(speed) ")
}
}
let touchPosition = touch.location(in: self)
if touchPosition.x < (self.frame.width / 2) {
self.shape.physicsBody?.angularVelocity = 10
self.shape.physicsBody?.applyAngularImpulse(-180)
} else {
self.shape.physicsBody?.angularVelocity = 10
self.shape.physicsBody?.applyAngularImpulse(180)
}
}}

I have created an open source prize spinning wheel in Spritekit that uses physics for realistic movement and flapper control. It also allows the user to drag the wheel to spin or generates a random spin by pushing the center of the wheel.
https://github.com/hsilived/SpinWheel

You can add realistic movement like this by using the built-in Physics simulation of SpriteKit. This will allow you to give your wheel a mass and friction and then use forces to rotate it. It will then slow down realistically.
In outline see Simulating Physics in the Apple Documentation:
To use physics in your game, you need to:
Attach physics bodies to nodes in the node tree and configure their physical properties. See SKPhysicsBody.
Define global characteristics of the scene’s physics simulation, such as gravity. See SKPhysicsWorld.
Where necessary to support your gameplay, set the velocity of physics bodies in the scene or apply forces or impulses to them. ...
The most appropriate method for your wheel is probably to make the wheel pinned to the scene and then rotate it with applyAngularImpulse.

Related

Display upperLimit SKRange

So here is my code:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var ship1 = [2,1]
var ship2 = [1,2]
let jonahSpriteNode = SKSpriteNode(imageNamed: "jonah_spaceship")
let georgeSpriteNode = SKSpriteNode(imageNamed: "george_spaceship")
override func didMove(to view: SKView) {
//var jonahFrames = [SKTexture]()
jonahSpriteNode.position = CGPoint(x: 30, y: frame.midY)
jonahSpriteNode.size = CGSize(width: 100.0, height: 100.0)
addChild(jonahSpriteNode)
georgeSpriteNode.position = CGPoint(x: 628, y: frame.midY)
georgeSpriteNode.size = CGSize(width: 100.0, height: 100.0)
addChild(georgeSpriteNode)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
var touchLocation = touch.location(in: self)
var angle1 = atan2(jonahSpriteNode.position.y - touchLocation.y , jonahSpriteNode.position.x - touchLocation.x)
var angle = angle1 - CGFloat(Double.pi / 1)
makeCircularRange(to: jonahSpriteNode)
let rotate = SKAction.rotate(toAngle: angle, duration: 1.0)
let move = SKAction.move(to: CGPoint(x: touchLocation.x, y: touchLocation.y), duration: 2.5)
let sequence = SKAction.sequence([rotate, move])
jonahSpriteNode.run(sequence)
}
}
func makeCircularRange(to node: SKNode) {
let range = SKRange(lowerLimit: 0, upperLimit: 400)
let constraint = SKConstraint.distance(range, to: .zero)
node.constraints = [constraint]
}
}
I wanted to display the SKRange by showing upperLimit in a certain color. I tried making nodes around the limit and then coloring the nodes but it just showed a bunch of errors. If you have any ideas please answer.
Something like this:
The sprite node will be in the center and the circle will show where it can move.
You can make a circular shape using range.upperLimit as a radius an add to scene.
func drawCircularRangeBy(range: SKRange) {
let radius = range.upperLimit
let node = SKShapeNode(circleOfRadius: radius)
node.strokeColor = .white
addChild(node)
}
Take a look of this example: https://github.com/Maetschl/SpriteKitExamples/blob/master/CircularRange/CircularRange/GameScene.swift
If you really need dots instead of a line please see this answer: Drawing dashed line in Sprite Kit using SKShapeNode

Moving SpriteNode Texture but not PhysicBody

Working on a game right now, I've faced a problem regarding management of SkSpriteNodes. I have a SpriteNode whose texture size is lower than physicsBody size assigned to it. It is possible to move, thanks to something similar to an SKAction, only the texture of a node and not its physicsBody?
To explain the problem in other terms, I will give you a graphic example:
As you can see, what I want to achieve is not to modify physicsBody proprieties(in order to not affect collision or having problems with continuos reassigning of PhysicsBody entities), but changing its texture length and adjusting its position. How can I achieve this programmatically?
A bit of code for context, which is just illustrative of the problem:
let node = SKSpriteNode(color: .red, size: CGSize(width: 8, height: 60)
node.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 60)
self.addChild(node)
//what I've tried is something like that
//It causes glitches in visualisation... and I need to move the object since resizing is towards the center.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
let move = SKAction.move(to: node.position - CGPoint(x:0, y:node.size.height*0.5), duration: 5)
let group = SKAction.group([resize, move])
node.run(group)
//And this is even worse if I add, in this specific example, another point fixed to the previous node
let node2 = SKSpriteNode(color: .blue, size: CGSize(width: 8, length: 8)
node2.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 8))
node2.position = CGPoint(x: 0, y: -node.height)
self.addChild(node2)
self.physicsWorld.add(SKPhysicsJointFixed.joint(withBodyA: node.physicsBody! , bodyB: node2.physicsBody!, anchor: CGPoint(x: 0, y: -node.size.height)))
I get your problem.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
This line will cause the physicsBody to scale the x and y axis uniformly. While your texture will just scale the y axis.
Its not so straight forward changing physicsBody shapes to match actions
One way to do it though would be to call a method from
override func didEvaluateActions()
Something like this:
var group1: SKAction? = nil
var group2: SKAction? = nil
var touchCnt = 0
var test = SKSpriteNode(texture: SKTexture(imageNamed: "circle"), color: .blue, size: CGSize(width: 100, height: 100))
func setActions() {
let newPosition = CGPoint(x: -200, y: 300)
let resize1 = SKAction.scaleY(to: 0.5, duration: 5)
let move1 = SKAction.move(to: newPosition, duration: 5)
group1 = SKAction.group([resize1, move1])
let resize2 = SKAction.scaleY(to: 1, duration: 5)
let move2 = SKAction.move(to: position, duration: 5)
group2 = SKAction.group([resize2, move2])
}
func newPhysics(node: SKSpriteNode) {
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size)
node.physicsBody?.affectedByGravity = false
node.physicsBody?.allowsRotation = false
node.physicsBody?.usesPreciseCollisionDetection = false
}
override func sceneDidLoad() {
physicsWorld.contactDelegate = self
test.position = CGPoint(x: 0, y: 300)
setActions()
newPhysics(node: test)
addChild(test)
}
override func didEvaluateActions() {
newPhysics(node: test)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if touchCnt == 0 {
if !test.hasActions() {
test.run(group1!)
touchCnt += 1
}
} else {
if !test.hasActions() {
test.run(group2!)
touchCnt -= 1
}
}
}
if you put the above code in your gameScene, taking care to replace any duplicated methods and replacing the test node texture. then when you tap the screen the sprite should animate as you want while keeping the physics body is resized at the same time. There are a few performance issues with this though. As it changes the physics body on each game loop iteration.

How to keep a 30˚ distance between two lines anchored at a point

I am trying to create two lines that are anchored at a certain point (sprite) and rotate to form a 30 degree angle between them. Below is an image what I want to achieve.
This is what I've done so far:
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class GameScene: SKScene, SKPhysicsContactDelegate {
var anchorSprite = SKSpriteNode(imageNamed: "anchorSprite")
var armLeft = SKSpriteNode(imageNamed: "lineSprite")
var armRight = SKSpriteNode(imageNamed: "lineSprite")
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx: 0, dy: -1.8)
self.physicsWorld.contactDelegate = self
var tealBg = SKSpriteNode(imageNamed: "tealBg")
tealBg.position = CGPoint(x: frame.midX, y: frame.midY)
tealBg.zPosition = 10
addChild(tealBg)
anchorSprite.position = CGPoint(x: frame.midX, y: frame.midY + frame.midY/2)
anchorSprite.zPosition = 20
anchorSprite.physicsBody = SKPhysicsBody(rectangleOf: anchorSprite.frame.size)
anchorSprite.physicsBody?.categoryBitMask = pinCategory
anchorSprite.physicsBody?.isDynamic = false
addChild(anchorSprite)
armRight.anchorPoint = CGPoint(x: 0.5, y: 1)
armRight.position = anchorSprite.position
armRight.zPosition = 20
armRight.physicsBody = SKPhysicsBody(rectangleOf: armRight.frame.size)
armRight.zRotation = CGFloat(Double(15).degreesToRadians)//CGFloat(Double.pi/6)
armRight.physicsBody!.isDynamic = true
addChild(armRight)
armLeft.anchorPoint = CGPoint(x: 0.5, y: 1)
armLeft.position = anchorSprite.position
armLeft.zPosition = 20
armLeft.physicsBody = SKPhysicsBody(rectangleOf: armRight.frame.size)
armLeft.zRotation = CGFloat(Double(-15).degreesToRadians)//CGFloat(-Double.pi/6)
armLeft.physicsBody!.isDynamic = true
addChild(armLeft)
//Pin joints
var pinAndRightArmJoint = SKPhysicsJointPin.joint(withBodyA: anchorSprite.physicsBody!, bodyB: armRight.physicsBody!, anchor: CGPoint(x: anchorSprite.position.x, y: self.armRight.frame.maxY))
self.physicsWorld.add(pinAndRightArmJoint)
var pinAndLeftArmJoint = SKPhysicsJointPin.joint(withBodyA: anchorSprite.physicsBody!, bodyB: armLeft.physicsBody!, anchor: CGPoint(x: anchorSprite.position.x, y: self.armLeft.frame.maxY))
self.physicsWorld.add(pinAndLeftArmJoint)
}
Below is an image from running the above code (they are close together).
How can I make sure the lines are always 30˚ apart and maintain 30˚ apart even when rotated?
To keep your two lines separated by exactly 30°, you can use an SKPhysicsJointFixed, which is just what it sounds like: it pins two physicsBodies together in a fixed position. Since you already have them positioned the way you want, just add this code where you have the other SKPhysicsJoints to hold them that way:
let fixArms = SKPhysicsJointFixed.joint(withBodyA: armLeft.physicsBody!, bodyB: armRight.physicsBody!, anchor: CGPoint.zero)
self.physicsWorld.add(fixArms)
Result:
If you make the line nodes children of the anchor sprite (instead of the scene), rotating the anchor sprite node will rotate all the lines along with it without doing anything special with physics. You just need to mind the anchor points so that they align properly (i.e. line's anchor at its extremity rather than center)

Sprite-kit Main Character Picker

I am using swift 2 and sprite-kit. I was wondering how I would make a choose character scene for my player to choose a character to play in the game, I have the scene set up, but I don't know how to integrate multiple characters, that can be chosen as the one to play? Any references or sample code would be appreciated.
in GameScene() i have
//plane = SKSpriteNode(imageNamed: "plane")
plane.size = CGSize(width: 80, height: 80)
plane.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 4)
plane.physicsBody = SKPhysicsBody(circleOfRadius: plane.frame.height / 2)
//plane.physicsBody = SKPhysicsBody(texture: plane.texture!, size: plane.size)
plane.physicsBody?.affectedByGravity = true
plane.physicsBody?.dynamic = true
plane.zPosition = 2
self.addChild(plane)'
in settings() i have
func mainScene(){
mainSceenButton = SKSpriteNode(imageNamed: "mainMenu")
mainSceenButton.size = CGSizeMake(200, 100)
mainSceenButton.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2 + self.frame.height / 3)
self.addChild(mainSceenButton)
plane = SKSpriteNode(imageNamed: "plane")
plane.size = CGSizeMake(50, 50)
plane.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2 + 100 )
self.addChild(plane)
plane1 = SKSpriteNode(imageNamed: "plane1")
plane1.size = CGSizeMake(50, 50)
plane1.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2 )
self.addChild(plane1)
plane2 = SKSpriteNode(imageNamed: "plane2")
plane2.size = CGSizeMake(50, 50)
plane2.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2 - 100 )
self.addChild(plane2)
}
override func didMoveToView(view: SKView) {
mainScene()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let mainMenuScene = StartScene(fileNamed: "StartScene")
for touch in touches{
let location = touch.locationInNode(self)
if mainSceenButton.containsPoint(location){
self.scene?.view?.presentScene(mainMenuScene!, transition: SKTransition.fadeWithDuration(1.0))
}
if plane1.containsPoint(location){
GameScene().plane = SKSpriteNode(imageNamed: "plane1")
} else {
GameScene().plane = SKSpriteNode(imageNamed: "plane")
}
}
}'
Really too broad of a question, but here's how I have seen this done in the past (and done myself)
Create a subclass of SKSpriteNode to make your custom player class.
When initializing change the texture of the node depending on what they select.
You could try using NSUserDefaults to save the users selection in Settings touchesBegan.
let defaults = NSUserDefaults.standardUserDefaults()
if plane.containsPoint(location) {
defaults.setObject("plane", forKey: "userPlane")
} else if plane1.containsPoint(location) {
defaults.setObject("plane1", forKey: "userPlane")
} else if plane2.containsPoint(location) {
defaults.setObject("plane2", forKey: "userPlane")
}
Then in GameScene, retrieve the selection when creating the plane.
let defaults = NSUserDefaults.standardUserDefaults()
if let userPlane = defaults.stringForKey("userPlane") {
plane = SKSpriteNode(imageNamed: userPlane)
}
In your code you are trying to set the plane on a new instance of GameScene, but that selection will be lost as soon as you exit the settings scene.

How to move sprite with a joystick within a certain limit?

I want to make it so that the player can move with a joystick but cant go out of the circle. I made the joystick but I don't know how to do the other stuff. There is an example in the image below and also my code. Hope someone can help me, thanks.
class GameScene: SKScene {
var circuloPrincipal = SKSpriteNode(imageNamed: "circulo")
var circuloFondo = SKSpriteNode(imageNamed: "circuloFondo")
let base = SKSpriteNode(imageNamed: "circuloFondo")
let ball = SKSpriteNode(imageNamed: "circulo")
var stickActive:Bool = false
override func didMoveToView(view: SKView) {
/* Setup your scene here */
base.size = CGSize(width: 100, height: 100)
base.alpha = 0.3
base.zPosition = 2.0
base.position = CGPoint(x: frame.width / 2, y: frame.height / 2 - 310)
self.addChild(base)
ball.size = CGSize(width: 50, height: 50)
ball.color = circuloPrincipal.color
//ball.alpha = 0
ball.zPosition = 3.0
ball.position = base.position
self.addChild(ball)
circuloPrincipal.size = CGSize(width: 35, height: 35)
circuloPrincipal.position = CGPoint(x: frame.width / 2, y: frame.height / 2)
self.addChild(circuloPrincipal)
circuloPrincipal.color = UIColor(red: 0.75, green: 0.75, blue: 0.75, alpha: 1.0)
circuloPrincipal.colorBlendFactor = 1.0
circuloPrincipal.zPosition = 3.0
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
if (CGRectContainsPoint(ball.frame, location)) {
stickActive = true
}else {
stickActive = false
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
if (stickActive == true) {
let v = CGVector(dx: location.x - base.position.x, dy: location.y - base.position.y)
let angle = atan2(v.dy, v.dx)
let deg = angle * CGFloat(180 / M_PI)
print(deg + 180)
let lenght:CGFloat = base.frame.size.height / 2 - 20
let xDist: CGFloat = sin(angle - 1.57079633) * lenght
let yDist: CGFloat = cos(angle - 1.57079633) * lenght
ball.position = CGPointMake(base.position.x - xDist, base.position.y + yDist)
if (CGRectContainsPoint(base.frame, location)) {
ball.position = location
}else {
ball.position = CGPointMake(base.position.x - xDist, base.position.y + yDist)
}
} // termina stickActive
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (stickActive == true) {
let move: SKAction = SKAction.moveTo(base.position, duration: 0.2)
move.timingMode = .EaseOut
ball.runAction(move)
}
}
Look into SKConstraint - you can set a constraint that a node cannot move further than a specified distance from another node. You'd set this 'other' node to the centre of the area that your player is limited to. Sprite-Kit will automatically move your node back should it go to far. As this is being in the game loop at 60fps before the node is redrawn after it's moved, you won't get any weird 'jumping' effects.
https://developer.apple.com/reference/spritekit/skconstraint
That documentation is a bit lacking compared to some of the other SK documentation. Here's and example that make a green rectangle 'follow' a yellow triangle within a specified distance:
let distanceRange = SKRange(lowerLimit: 200, upperLimit: 400)
let distanceConstraint = SKConstraint.distance(distanceRange, toNode: yellowTriangle)
greenRect.constraints = [distanceConstraint]
The constraints property of an SKNode (greenRect) is an array, so if you also want an orientation constraint (to keep greenRect facing the yellow triangle, for example), you would code something like this:
let orientRange = SKRange(lowerLimit: 0.0, upperLimit: 0.0)
let orientConstraint = SKConstraint.orientToNode(yellowTriangle, offset: orientRange)
greenRect.constraints = [orientatConstraint, distanceConstraint]
For your particular example, you might want to set the constraint as:
let distanceRange = SKRange(lowerLimit: 0, upperLimit: limitcircle.size/2)
let distanceConstraint = SKConstraint.distance(distanceRange, toPoint: limitCircle.position)
player.constraints = [distanceConstraint]
This assumes that the circle from which the player cannot move is an SKNode called limitcircle and it's anchor point is set to (0.5, 0.5) i,e, it's center. The above code would constrain the player to a distance of between 0 and 1/2 the width of the circle (i.e. it's radius) from a point at the center of the circle.