UISnapBehavior strange jiggle - swift - ios

I am trying to use UISnapBehavior to create a tweening back animation.
I am trying to make a square draggable, then when the square is released, it tweens back to its original position. Each time it tweens back, it seems to have a strange trajectory and rather than easing back at the correct angleit seems to ease back in a sort of strange semi circle before reaching its destination.
Thank you in advance.
my code is as follows:
var animator:UIDynamicAnimator? = nil;
var ball : Ball!;
var ball2 : Ball!;
var snap: UISnapBehavior!
var gravity : UIGravityBehavior!;
var speed : Double! = 0;
var lastTouchPoint : CGPoint!;
var attach : UIAttachmentBehavior! = nil;
var itemBehavior : UIDynamicItemBehavior!;
var snapping : UISnapBehavior! = nil;
var currentBall : Ball! = nil;
override init(frame: CGRect) {
super.init(frame: frame);
println( "-- initiated view --" );
self.backgroundColor = UIColor.orangeColor();
ball = Ball( frame: CGRect(x: 200, y: 200, width: 50, height: 50 ) );
self.addSubview(ball);
ball2 = Ball( frame: CGRect(x: 100, y: 100, width: 50, height: 50 ) );
self.addSubview(ball2);
setup()
self.addGestureRecognizer( UIPanGestureRecognizer(target: self, action: "panHandler:") );
self.attach = UIAttachmentBehavior(item: self, attachedToAnchor: CGPoint(x: 0, y: 0) );
}
func setup() {
animator = UIDynamicAnimator(referenceView:self);
var collision: UICollisionBehavior!
collision = UICollisionBehavior(items: [ball, ball2])
collision.translatesReferenceBoundsIntoBoundary = false;
collision.collisionDelegate = self;
animator?.addBehavior(collision)
var dynamicItem : UIDynamicItemBehavior = UIDynamicItemBehavior(items: [ball, ball2] );
dynamicItem.allowsRotation = false;
self.animator!.addBehavior(dynamicItem);
}
func panHandler( recognizr : UIPanGestureRecognizer ) {
switch( recognizr.state ) {
case UIGestureRecognizerState.Began:
let location = recognizr.locationInView(self);
let view : AnyObject? = self.hitTest(location, withEvent: nil);
if( view is Ball ) {
currentBall = view as! Ball;
if( self.attach != nil ){ self.animator!.removeBehavior(attach); self.attach = nil; }
self.attach = UIAttachmentBehavior(item: currentBall, attachedToAnchor: location );
self.animator!.addBehavior(self.attach);
self.attach.damping = 1;
self.attach.length = 1;
let dynamicItem : UIDynamicItemBehavior = UIDynamicItemBehavior(items: [currentBall] );
dynamicItem.allowsRotation = false;
self.animator!.addBehavior(dynamicItem);
}
break;
case UIGestureRecognizerState.Changed :
if( currentBall != nil ) {
let location = recognizr.locationInView(self);
self.attach.anchorPoint = location;
}
break;
default :
println("--- snap ---");
if( self.currentBall == nil ) {return;}
self.animator!.removeBehavior(attach);
self.attach = nil;
var snap : UISnapBehavior = UISnapBehavior(item: self.currentBall, snapToPoint: self.currentBall.startingPoint );
snap.damping = 1;
self.animator!.addBehavior(snap);
break;
}
}

It's 2018 now and I'm seeing pretty much the same movement. I think it's a bug in Apple's implementation and you and I both should file a bug report since it's the only way this will get fixed.
In my experience, this jiggle in a semi-circle movement toward the end appears more pronounced when damping is higher than 1.0.
In my example below, my gesture recognizer strictly fixes the x-axis value, so theoretically there should not be any horizontal movement.
allowsRotation is set to false.

Related

How can I dynamically track SCNNodes in a scene for removal?

Obligatory: first time writing an app, relevant code is below. My code runs mostly as I want it to, but does not achieve my goal of dynamic object tracking.
I'm using Swift and Scenekit to build a simple puzzle game, similar to a 3d-version of candy crush.
I have a class Cube that extends SCNNode. On initialization, this class will randomly draw a 5x5 cube of SCNBoxes with each box being red, green, or blue (all 6 sides of a box are 1 color).
The goal of the game is to get the highest score by removing "chains" of SCNBoxes of like-colors. When a chain is removed, cubes should recognize gravity and drop to fill in the voids created by the removed-chain. This is where I need to dynamically track position. As the cubes fall into the gaps, their neighbors change.
My approach: build a struct CubeDetails that has properties var color: String and var location: SCNVector3. Next, build a dictionary masterCubeDict = [SCNNode: CubeDetails] that has all of the cubes of 1 color (the color is provided by a hittestresult).
Every time a user taps a cube, grab its color, refresh the masterCubeDict, and then use math on the SCNVector3 position to determine which cubes are neighbors.
I think my algorithm for finding 'cube neighbors' using math on the scnvector3 is where I'm off. There must be a better way for scenekit nodes to identity/find each other, right?
Also -- I would like the physics of the cubes to let them fall and have no bounce/sliding at all. They should only move straight up/down. Collisions should never happen. I thought I implemented that properly through friction, restituion, and mass of the cubes but I'm not getting the outcome I want.
class Cube
import SceneKit
class Cube : SCNNode {
let cubeWidth:Float = 0.95
let spaceBetweenCubes:Float = 0.05
var cubecolor:UIColor = UIColor.black
var masterCubeDict: [SCNNode: CubeDetails] = [:]
struct CubeDetails {
var color:String
var position:SCNVector3
}
override init() {
super.init()
let cubeOffsetDistance = self.cubeOffsetDistance()
var cubeColorString: String = ""
var xPos:Float = -cubeOffsetDistance
var yPos:Float = -cubeOffsetDistance
var zPos:Float = -cubeOffsetDistance
let xFloor:Float = -1.5
let yFloor:Float = -1.5
let zFloor:Float = -1.5
let floorGeo = SCNBox(width: 20, height: 0, length: 20, chamferRadius: 0)
let floor = SCNNode(geometry: floorGeo)
floor.position = SCNVector3(x: xFloor, y: yFloor, z: zFloor)
floor.name = "floor"
floor.opacity = 0.0
floor.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil)
floor.physicsBody?.collisionBitMask = 1
floor.physicsBody?.friction = 1.0
self.addChildNode(floor)
for _ in 0..<5 {
for _ in 0..<5 {
for _ in 0..<5 {
let cubeGeometry = SCNBox(width: CGFloat(cubeWidth), height: CGFloat(cubeWidth), length: CGFloat(cubeWidth), chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = randomColor()
//unwrap material (type any) and cast to uicolor for switch
if let unwrapColor: UIColor = material.diffuse.contents as? UIColor {
switch unwrapColor {
case UIColor.red:
cubeColorString = "red"
case UIColor.green:
cubeColorString = "green"
case UIColor.blue:
cubeColorString = "blue"
default:
cubeColorString = "black"
}
} else { print("Error unwrapping color") }
cubeGeometry.materials = [material, material, material, material, material, material]
let cube = SCNNode(geometry: cubeGeometry)
cube.name = cubeColorString
cube.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
cube.physicsBody?.restitution = 0.0
cube.physicsBody?.isAffectedByGravity = true
cube.physicsBody?.mass = 25.0
cube.physicsBody?.friction = 1.0
cube.physicsBody?.collisionBitMask = 1
cube.position = SCNVector3(x: xPos, y: yPos, z: zPos)
let details = CubeDetails(color: cubeColorString, position: cube.position)
//add cube details to the master dict
masterCubeDict[cube] = details
//print(masterCubeDict)
xPos += cubeWidth + spaceBetweenCubes
self.addChildNode(cube)
}
xPos = -cubeOffsetDistance
yPos += cubeWidth + spaceBetweenCubes
}
xPos = -cubeOffsetDistance
yPos = -cubeOffsetDistance
zPos += cubeWidth + spaceBetweenCubes
}
}
private func cubeOffsetDistance()->Float {
return (cubeWidth + spaceBetweenCubes) / 2
}
private func randomColor() -> UIColor{
var tmpColor: UIColor
let num = Int.random(in:0...2)
switch num {
case 0:
tmpColor = UIColor.red
case 1:
tmpColor = UIColor.blue
case 2:
tmpColor = UIColor.green
default:
tmpColor = UIColor.black
}
return tmpColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
GameViewController
import UIKit
import QuartzCore
import SceneKit
var myMasterCubeDict: [SCNNode: Cube.CubeDetails] = [:]
class GameViewController: UIViewController {
let gameCube = Cube()
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
// let scene = SCNScene(named: "art.scnassets/ship.scn")!
let scene = SCNScene()
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 2, y: 0, z: 20)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// init cube
myMasterCubeDict = gameCube.masterCubeDict
scene.rootNode.addChildNode(gameCube)
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
#objc
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let scnView = self.view as! SCNView
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result = hitResults[0]
//get dict of same-color node
var dictOfSameColor = findAndReturnChain(boi: result.node)
// print(dictOfSameColor)
var finalNodes: [SCNNode] = [result.node]
var resFlag = 1
repeat {
var xSame: Bool = false
var ySame: Bool = false
var zSame: Bool = false
resFlag = 0
for node in finalNodes {
// var nodeX = node.position.x
for (key, value) in dictOfSameColor {
if(abs(node.position.x - value.position.x) < 0.7) {
xSame = true
}
if(abs(node.position.y - value.position.y) < 0.7) {
ySame = true
}
if(abs(node.position.z - value.position.z) < 0.7) {
zSame = true
}
//print("X-val: \(xDif) \nY-val: \(yDif) \nZ-val: \(zDif) \nColor: \(key.name) \n\n\n\n")
if (xSame && ySame ) {
if !(zSame) {
if (abs((node.position.z-value.position.z)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
if (xSame && zSame) {
if !(ySame) {
if (abs((node.position.y-value.position.y)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
if (ySame && zSame) {
if !(xSame) {
if (abs((node.position.x-value.position.x)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
xSame = false
ySame = false
zSame = false
}
}
//print(finalNodes)
} while resFlag == 1
//print(finalNodes)
for node in finalNodes {
if node.name != "floor" {
node.removeFromParentNode()
}
}
//IMPLEMENT: Reset dicts to current state of the cube
myMasterCubeDict = updateMasterCubeDict(cube: gameCube)
dictOfSameColor.removeAll()
}
}
func findAndReturnChain(boi: SCNNode) -> [SCNNode:Cube.CubeDetails] {
var ret: [SCNNode:Cube.CubeDetails] = [:]
//find cubes with the same color
for (key, value) in myMasterCubeDict {
if value.color == boi.name {
ret[key] = value
}
}
return ret
}
func updateMasterCubeDict(cube: Cube) -> [SCNNode:Cube.CubeDetails] {
myMasterCubeDict.removeAll()
var newNode: SCNNode = SCNNode()
var newDetails = Cube.CubeDetails(color: "", position: SCNVector3Zero)
cube.enumerateChildNodes { (cube, stop) in
newNode = cube
if let newName = cube.name {
newDetails.color = newName
}
newDetails.position = cube.position
myMasterCubeDict[newNode] = newDetails
}
return myMasterCubeDict
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
}
I did a game somewhat like this. You could probably get the math to work, but the way I did it was to map out each node and have an array containing its adjacent nodes. Doing it this way, I'm sure that when I remove a node and loop through its adjacent[array] nodes, then I got the right ones.
I don't subclass SCNNodes - some do, but I create the class I want that contains info about my node - I add the node to Scenekit there, that's separates the actual node from other work I may want to do with the class. Some nodes have a lot of detail that I may want to manage separately (multiple particle systems, movements, etc). Then I just keep my classes of nodes in an array and each class has direct access to it's own node.
Sorry - I don't know about the bounce, there are a lot of choices with the physics engine.

Glitchy UIScrollView Infinite Scroll

Im trying to implement an infinitely paging UIScrollView based on the Advanced ScrollView Techniques from WWDC 2011. The problem that Im facing is that as I scroll the screen keeps on Jumping backwards instead of advancing forward in the array. Is there any way to create this effect. Below is the code I have implemented thus far.
import Foundation
import UIKit
class CustomScrollView:UIScrollView{
var label1:CustomLabel!
var label2:CustomLabel!
var label3:CustomLabel!
var labels:[UILabel]!
var visibleLabels:[UILabel]!
var recycledPages:Set<CustomLabel>!
var visiblePages:Set<CustomLabel>!
override init(frame: CGRect) {
super.init(frame: frame)
indicatorStyle = .white
recycledPages = Set<CustomLabel>()
visiblePages = Set<CustomLabel>()
var firstScreenPostion:CGRect = CGRect(origin: CGPoint(x: 0 * bounds.width, y: 0), size: bounds.size)
var secondeScreenPosition:CGRect = CGRect(origin: CGPoint(x: 1 * bounds.width, y: 0), size: bounds.size)
var thirdScreenPosition:CGRect = CGRect(origin: CGPoint(x: 2 * bounds.width, y: 0), size: bounds.size)
label1 = CustomLabel(frame: firstScreenPostion)
label1.backgroundColor = .red
label1.text = "1"
label2 = CustomLabel(frame: secondeScreenPosition)
label2.backgroundColor = .green
label2.text = "2"
label3 = CustomLabel(frame: thirdScreenPosition)
label3.backgroundColor = .blue
label3.text = "3"
visibleLabels = [label1,label2,label3]
labels = [label1,label2,label3]
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func recenterIfNecessary(){
let currentOffset = contentOffset
let contentWidth = contentSize.width
let centerOffset = (contentWidth - bounds.width)/2
let distanceFromCenter = abs(currentOffset.x - centerOffset)
if distanceFromCenter > contentWidth/4{
self.contentOffset = CGPoint(x: centerOffset, y: 0)
for label in visibleLabels{
var center = label.center
center.x += centerOffset - currentOffset.x
label.center = center
}
}
}
override func layoutSubviews() {
recenterIfNecessary()
let visibleBounds = bounds
let minimumVisibleX = bounds.minX
let maximumVisbleX = bounds.maxX
tilePages(minimumVisibleX: minimumVisibleX, toMaxX: maximumVisbleX)
// tileLabelsFromMinX(minimumVisibleX: minimumVisibleX, toMaxX: maximumVisbleX)
}
func insertLabel()->UILabel{
let recycledLabels = visibleLabels.filter { (label) -> Bool in
return label.superview == nil
}
let label = recycledLabels.last ?? UILabel(frame: bounds)
self.addSubview(label)
return label
}
func placeNewLabelOnRight(rightEdge: CGFloat)->CGFloat{
let label = self.insertLabel()
visibleLabels.append(label) // add rightmost label at the end of the array
label.frame.origin.x = rightEdge;
label.frame.origin.y = 0
label.text = labels.last?.text
return label.frame.maxX
}
func placeNewLabelOnLeft(leftEdge:CGFloat)->CGFloat{
let label = self.insertLabel()
self.visibleLabels.insert(label, at: 0) // add leftmost label at the beginning of the array
label.frame.origin.x = leftEdge - frame.size.width;
label.frame.origin.y = bounds.size.height - frame.size.height;
label.text = labels[0].text
return label.frame.minX
}
//function used in video
// func tileLabelsFromMinX(minimumVisibleX:CGFloat, toMaxX maximumVisibleX:CGFloat){
// // the upcoming tiling logic depends on there already being at least one label in the visibleLabels array, so
// // to kick off the tiling we need to make sure there's at least one label
// if (self.visibleLabels.count == 0)
// {
// self.placeNewLabelOnRight(rightEdge: minimumVisibleX);
// }
// print("visible labels.count: \(visibleLabels.count)")
//
// // add labels that are missing on right side
// // UILabel *lastLabel = [self.visibleLabels lastObject];
// var lastLabel = visibleLabels.last!
// var rightEdge = lastLabel.frame.maxX
// while (rightEdge < maximumVisibleX){
// rightEdge = self.placeNewLabelOnRight(rightEdge: rightEdge)
// }
//
// // add labels that are missing on left side
// var firstLabel = self.visibleLabels[0]
// var leftEdge = firstLabel.frame.minX
// while (leftEdge > minimumVisibleX){
// leftEdge = self.placeNewLabelOnLeft(leftEdge:leftEdge)
// }
//
// // remove labels that have fallen off right edge
// // lastLabel = [self.visibleLabels lastObject];
//
// while (lastLabel.frame.origin.x > maximumVisibleX){
// lastLabel.removeFromSuperview()
// self.visibleLabels.removeLast()
// lastLabel = self.visibleLabels.last!
// }
//
// // remove labels that have fallen off left edge
// firstLabel = self.visibleLabels[0];
// while (firstLabel.frame.maxX < minimumVisibleX){
// firstLabel.removeFromSuperview()
// self.visibleLabels.removeFirst()
// firstLabel = self.visibleLabels[0];
// }
// }
func tilePages(minimumVisibleX:CGFloat, toMaxX maximumVisibleX:CGFloat){
let visibleBounds = bounds
var firstNeededPageIndex:Int = Int(floorf(Float(minimumVisibleX/visibleBounds.width)))
var lastNeededPageIndex:Int = Int(floorf(Float((maximumVisibleX - 1)/visibleBounds.width)))
firstNeededPageIndex = max(firstNeededPageIndex, 0)
lastNeededPageIndex = min(lastNeededPageIndex, labels.count - 1)
//Recycle no-longer needed pages
for page in visiblePages{
if page.index < Int(firstNeededPageIndex) || page.index > Int(lastNeededPageIndex){
recycledPages.insert(page)
page.removeFromSuperview()
}
}
visiblePages.subtract(recycledPages)
//add missing pages
for i in firstNeededPageIndex...lastNeededPageIndex{
if !isDisplaying(pageForIndex: i){
let page:CustomLabel = dequeueRecycledPage() ?? CustomLabel()
print("index i: \(i)")
self.configurePage(page: page, forIndex: i)
self.addSubview(page)
visiblePages.insert(page)
}
}
}
func isDisplaying(pageForIndex index:Int)->Bool{
for page in visiblePages{
if page.index == index{
return true
}
}
return false
}
func configurePage(page:CustomLabel,forIndex index:Int){
page.index = index
page.text = "current index: \(index)"
let width = bounds.width
let newX:CGFloat = CGFloat(index) * width
page.backgroundColor = labels[index].backgroundColor
page.frame = CGRect(origin: CGPoint(x: newX, y: 0), size: bounds.size)
}
func dequeueRecycledPage()->CustomLabel?{
let page = recycledPages.first
if let page = page{
recycledPages.remove(page)
return page
}
return nil
}
}

Score label in top left Corner for all devices

I’m new to programming and I recently did a tutorial that I found online to make a endless frogger game. The tutorial person didn’t show me how to do the score label and I have tried endlessly to find a video tutorial that will show me how to position a score label in the top left corner. I have tried using this code:
label.horizontalAlignmentMode = .Left
label.position = CGPoint(x:0.0, y:self.size.height)
but I have had no luck, It does not show up in the top left corner. So i tried another way to get it positioned in the corner and I played around with the values and ended up with this
scoreLabel.position = CGPointMake(frame.size.width / -2.231, frame.size.height / 2.29)
which positions it perfectly in the corner of the screen on the simulator, but not for all the devices. I have a seperate swift file for my score label with is here:
import Foundation
import SpriteKit
import UIKit
class Score: SKLabelNode {
var number = 0
init (num: Int) {
super.init()
fontColor = UIColor.whiteColor()
fontName = "Helvetica"
fontSize = 150.0
number = num
text = "\(num)"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addOneToScore() {
number++
text = "\(number)"
}
}
I have a world node that has a player inside it and the camera position is focused on the player. I have provided my game Scene class for you to have a look at. All I want is to be able to position a score label in the top left or top right corner of the screen for all devices.
import SpriteKit
enum BodyType:UInt32 {
case player = 1
case roadObject = 2
case waterObject = 4
case water = 8
case road = 16
}
enum LevelType:UInt32 {
case road, water
}
class GameScene: SKScene, SKPhysicsContactDelegate {
//recongises swipe and tap gestures
let TapUpRec = UITapGestureRecognizer()
let swipeRightRec = UISwipeGestureRecognizer()
let swipeLeftRec = UISwipeGestureRecognizer()
let swipeDownRec = UISwipeGestureRecognizer()
//defines the attributes for level units.
var levelUnitCounter:CGFloat = 1 // Not sure, i think it starts the frog further up.
var levelUnitWidth:CGFloat = 0 //will be screenwidth
var levelUnitHeight:CGFloat = 50 // changes the height of the level units
var initialUnits:Int = 10 // tells how many level units will be spawned as you climb.
//defines the world node for the scene & defines the player image in a constant.
var screenWidth:CGFloat = 0
var screenHeight:CGFloat = 0
let worldNode:SKNode = SKNode()
let thePlayer:Player = Player(imageNamed: "Frog")
var increment:CGFloat = 0
// variables with boolean values to check if the player is on certain types of levels or objects.
var onLilyPad:Bool = false
var onWater:Bool = false
var onRoad:Bool = false
//same variable that checks if the player is dead.
var isDead:Bool = false
//variable that links with the swift file called object and (maybe passes it through :( )
var waterObject:Object?
//creates a constant that will be the starting point of the player
let startingPosition:CGPoint = CGPointMake(0, 0)
var direction:CGFloat = 1
// var nodeToMove:Object?
// var moveInProgress:Bool = false
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// Defines the view as the target defines the direction and calls the function (in red)
swipeRightRec.addTarget(self, action:"swipedRight")
swipeRightRec.direction = .Right
self.view!.addGestureRecognizer(swipeRightRec)
swipeLeftRec.addTarget(self, action: "swipedLeft")
swipeLeftRec.direction = .Left
self.view!.addGestureRecognizer(swipeLeftRec)
TapUpRec.addTarget(self, action: "tapUp")
self.view!.addGestureRecognizer(TapUpRec)
swipeDownRec.addTarget(self, action: "swipedDown")
swipeDownRec.direction = .Down
self.view!.addGestureRecognizer(swipeDownRec)
//makes the background colour black and defines the screenWidth variable as sk view boundry
self.backgroundColor = SKColor.greenColor()
screenWidth = self.view!.bounds.width
screenHeight = self.view!.bounds.height
//makes the world able to have objects that can collide (I Think)
physicsWorld.contactDelegate = self
//physicsWorld.gravity = CGVector(dx:0.3, dy:0.0)
//creates the world node point to be in the middle of the screen
self.anchorPoint = CGPointMake(0.5, 0.5)
addChild(worldNode)
let scoreLabel = Score(num: 0)
scoreLabel.horizontalAlignmentMode = .Left
scoreLabel.position = CGPoint(x:0.0, y:self.size.height)
addChild(scoreLabel)
//scoreLabel.position = CGPointMake(frame.size.width / -2.231, frame.size.height / 2.29)
// let highscoreLabel = Score(num: 0)
//adds a child node to the world node (so the player is playing within the world node)
worldNode.addChild(thePlayer)
thePlayer.position = startingPosition
thePlayer.zPosition = 500 // zPosition is the order the player will be displayed.
addLevelUnits() //runs the func that addds levelUnits.
}
func addScoreLabels(){
}
// this function along with the next three run the swipe and tap gesture actions.
func swipedRight(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(amountToMove, y: 0, duration: 0.1)
thePlayer.runAction(move) // links the action with the players
}
func swipedLeft(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(-amountToMove, y: 0, duration: 0.1)
thePlayer.runAction(move)
}
func swipedDown(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(0, y: -amountToMove, duration: 0.1)
thePlayer.runAction(move)
// clearNodes()
}
func tapUp(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(0, y: amountToMove, duration: 0.1)
thePlayer.runAction(move)
clearNodes()
}
func resetLevel(){
//searches the world node for child nodes that have the name "levelUnit"
worldNode.enumerateChildNodesWithName("levelUnit") {
node, stop in
//removes all the child nodes from the parent.
node.removeFromParent()
}
levelUnitCounter = 1
addLevelUnits()
}
func addLevelUnits() {
for (var i = 0; i < initialUnits; i++ ) {
createLevelUnit()
}
}
func createLevelUnit() {
if (direction == 1) {
direction = -1
} else {
direction = 1
}
print(direction )
let levelUnit:LevelUnit = LevelUnit()
worldNode.addChild(levelUnit)
levelUnit.zPosition = -1
levelUnit.levelUnitWidth = screenWidth
levelUnit.levelUnitHeight = levelUnitHeight
levelUnit.direction = direction
levelUnit.setUpLevel()
levelUnit.position = CGPointMake( 0 , levelUnitCounter * levelUnitHeight) // counts the level unit and multiplies it so t goes above the last level unit.
levelUnitCounter++ //constantly makes the level units appear.
}
func clearNodes(){
var nodeCount:Int = 0
worldNode.enumerateChildNodesWithName("levelUnit") {
node, stop in
let nodeLocation:CGPoint = self.convertPoint(node.position, fromNode: self.worldNode) //converts cordinates of level units with the world node.
if ( nodeLocation.x < -(self.screenWidth / 2) - self.levelUnitWidth ) { // checks to see if the node is off the screen.
node.removeFromParent()
print("levelUnit was removed", terminator: "")
} else {
nodeCount++
}
}
print( "levelUnits in the scene is \(nodeCount)")
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
worldNode.enumerateChildNodesWithName("levelUnit"){
node, stop in
let levelUnit:LevelUnit = node as! LevelUnit //cast as an actual LevelUnit class
levelUnit.enumerateChildNodesWithName("obstacle"){
node, stop in
let obstacle:Object = node as! Object //cast as an actual Object class
obstacle.update()
let obstacleLocation:CGPoint = self.convertPoint(obstacle.position, fromNode: levelUnit)
let buffer:CGFloat = 150
if (obstacleLocation.x < -(self.screenWidth / 2) - buffer) { //changes the speed of object when it reaches middle of the screen.
levelUnit.changeSpeed()
obstacle.position = CGPointMake(obstacle.position.x + (self.screenWidth + (buffer * 2)) , obstacle.position.y)
} else if (obstacleLocation.x > (self.screenWidth / 2) + buffer ) { //changes the speed of the object again.
levelUnit.changeSpeed()
obstacle.position = CGPointMake(obstacle.position.x - (self.screenWidth + (buffer * 2)) , obstacle.position.y)
}
}
}
// creates new level units if always centering horizontally
let nextTier:CGFloat = (levelUnitCounter * levelUnitHeight) - (CGFloat(initialUnits) * levelUnitHeight)
if (thePlayer.position.y > nextTier) {
createLevelUnit()
}
//deal with the players location....
let playerLocation:CGPoint = self.convertPoint(thePlayer.position, fromNode: worldNode)
var repositionPlayer:Bool = false
if ( playerLocation.x < -(screenWidth / 2)) {
repositionPlayer = true
} else if ( playerLocation.x > (screenWidth / 2)) {
repositionPlayer = true
} else if ( playerLocation.y < 0) {
repositionPlayer = true
} else if ( playerLocation.y > screenHeight) {
repositionPlayer = true
}
if (repositionPlayer == true) {
/* great code for reference later */
killPlayer()
thePlayer.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
}
}
// this function centers the world node on teh player.
override func didSimulatePhysics() {
self.centerOnNode(thePlayer)
if (onLilyPad == true) {
thePlayer.position = CGPointMake(thePlayer.position.x + waterObject!.xAmount , thePlayer.position.y)
}
}
//centers the camera on the node world.
func centerOnNode(node:SKNode) {
let cameraPositionInScene:CGPoint = self.convertPoint(node.position, fromNode: worldNode)
worldNode.position = CGPoint(x: worldNode.position.x , y:worldNode.position.y - cameraPositionInScene.y )
}
func didBeginContact(contact: SKPhysicsContact) {
// Defines the contact between objects.
/// lily pad
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.waterObject.rawValue ) {
waterObject = contact.bodyB.node!.parent as? Object
onLilyPad = true
self.removeActionForKey("checkOnLilyPad")
let waterObjectLocation:CGPoint = self.convertPointToView(waterObject!.position)
thePlayer.position = self.convertPointFromView(waterObjectLocation)
} else if (contact.bodyA.categoryBitMask == BodyType.waterObject.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
waterObject = contact.bodyA.node!.parent as? Object
onLilyPad = true
self.removeActionForKey("checkOnLilyPad")
let waterObjectLocation:CGPoint = self.convertPointToView(waterObject!.position)
thePlayer.position = self.convertPointFromView(waterObjectLocation)
}
//// check on water
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.water.rawValue ) {
onRoad = false
onWater = true
waitAndThenCheckOnLilyPad()
} else if (contact.bodyA.categoryBitMask == BodyType.water.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
onRoad = false
onWater = true
waitAndThenCheckOnLilyPad()
}
//// cars
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.roadObject.rawValue ) {
killPlayer()
} else if (contact.bodyA.categoryBitMask == BodyType.roadObject.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
killPlayer()
}
//// check on road
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.road.rawValue ) {
onRoad = true
onWater = false
onLilyPad = false
} else if (contact.bodyA.categoryBitMask == BodyType.road.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
onRoad = true
onWater = false
onLilyPad = false
}
}
func waitAndThenCheckOnLilyPad() {
let wait:SKAction = SKAction.waitForDuration(0.1)
let check:SKAction = SKAction.runBlock(checkIfOnLilyPad)
let seq:SKAction = SKAction.sequence([wait, check])
self.runAction(seq, withKey:"checkOnLilyPad")
}
func checkIfOnLilyPad() {
if ( onRoad == false) {
if ( onWater == true && onLilyPad == false) {
killPlayer()
} else if ( onWater == true && onLilyPad == true) {
print("safely on water")
// maybe play sound here
}
}
}
func didEndContact(contact: SKPhysicsContact) {
let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch (contactMask) {
case BodyType.waterObject.rawValue | BodyType.player.rawValue:
onLilyPad = false
waterObject = nil
onWater = true
waitAndThenCheckOnLilyPad()
default:
return
}
}
func killPlayer() {
if ( isDead == false) {
isDead = true
let fadeOut:SKAction = SKAction.fadeAlphaTo(0, duration: 0.2)
let move:SKAction = SKAction.moveTo(startingPosition, duration: 0.2)
let block:SKAction = SKAction.runBlock(revivePlayer)
let seq:SKAction = SKAction.sequence([fadeOut, move, block])
thePlayer.runAction(seq)
}
}
func revivePlayer() {
isDead = false
onRoad = false
onWater = false
onLilyPad = false
let fadeOut:SKAction = SKAction.fadeAlphaTo(0, duration: 0.2)
let block:SKAction = SKAction.runBlock(resetLevel)
let fadeIn:SKAction = SKAction.fadeAlphaTo(1, duration: 0.2)
let seq:SKAction = SKAction.sequence([fadeOut, block, fadeIn])
worldNode.runAction(seq)
let wait:SKAction = SKAction.waitForDuration(1)
let fadeIn2:SKAction = SKAction.fadeAlphaTo(1, duration: 0.2)
let seq2:SKAction = SKAction.sequence([wait , fadeIn2])
thePlayer.runAction(seq2)
}
}
If other solutions don't work, you could try something like this:
import UIKit
var screenSize = UIScreen.mainscreen().bounds
var screenWidth = screenSize.width
var screenHeight = screenSize.height
This will get the dimensions of the screen and store them in the screenHeight and screenWidth variables.
Then when you call scoreLabel.position you could say
scoreLabel.position = CGPoint(x: screenWidth / 10, y: screenHeight / 15)
Or something like that but experiment with the math until it is in the correct position.
If this doesn't work you may also need to declare the size or scoreLabel. Let me know if it works.
Swift 4 and Xcode 9 code:
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
self.menuButton.position = CGPoint(x: screenWidth / 10, y: screenHeight / 15)
I think you are on the right track with:
let scoreLabel = Score(num: 0)
scoreLabel.horizontalAlignmentMode = .Left
scoreLabel.position = CGPoint(x:0.0, y:self.size.height)
addChild(scoreLabel)
But I think you also need to add
scoreLabel.verticalAlignmentMode = .Top
as the default is.Baseline which would cause it draw most off the screen.

How to combine two SKTextures into one

I am trying to create a background fora game and have two background images. I can use two different SKSpriteNode(), SKTexture, SKAction.sequence..etc and some math to stitch them together but it comes out kind of choppy and also I cant seem to get the math just right. They are supposed to be able to be combined to make a long changing background but it always offsets.
Can someone tell me how to combine both textures into one so I dont have to break my head with the math. Or if someone spots some errors in my math can you please point them out? both background have the same width of 1200 pixels
override func didMoveToView(view: SKView) {
/* Setup your scene here */
var bgTexture = SKTexture(imageNamed: "bg_2_midground_1.png")
var bgTexture2 = SKTexture(imageNamed: "bg_2_midground_2.png")
var totalBG = bgTexture.size().width + bgTexture2.size().width
var moveBG1 = SKAction.moveByX(-bgTexture.size().width, y: 0, duration: 12)
//shift bg back to be able to repeat itself
var replaceBG1 = SKAction.moveByX(bgTexture.size().width, y: 0, duration: 0.0) //move immediately back to replace self
var moveBG1Forever = SKAction.repeatActionForever(SKAction.sequence([moveBG1,replaceBG1]))
var moveBG2 = SKAction.moveByX(-bgTexture2.size().width, y: 0, duration: 12)
//shift bg back to be able to repeat itself
var replaceBG2 = SKAction.moveByX(bgTexture2.size().width, y: 0, duration: 0.0) //move immediately back to replace self
var moveBG2Forever = SKAction.repeatActionForever(SKAction.sequence([moveBG2,replaceBG2]))
for var i:CGFloat = 0; i<2; i++ {
var bg = SKSpriteNode(texture: bgTexture)
var bg2 = SKSpriteNode(texture: bgTexture2)
bg.position = CGPointMake(bgTexture.size().width/2.0 + bgTexture.size().width * i + bgTexture2.size().width * i, CGRectGetMidY(self.frame))
bg2.position = CGPointMake(bgTexture.size().width/2.0 + bgTexture2.size().width + bgTexture2.size().width * 2 * i, CGRectGetMidY(self.frame))
bg.runAction(moveBG1Forever)
bg2.runAction(moveBG2Forever)
self.addChild(bg)
self.addChild(bg2)
println(bg.position)
println(bgTexture.size().width)
println(bg2.position)
println(bgTexture2.size().width)
}
}
Instead of using SKActions, you can use the update function to move all background nodes together, moving each node to the end as soon as each node goes out of the screen.
To make things more manageable, we can create a custom Background SKNode.
class BackgroundNode : SKNode
{
override init() {
super.init()
var bgTexture1 = SKTexture(imageNamed: "b1.jpg")
var bgTexture2 = SKTexture(imageNamed: "b2.png")
let totalBG = bgTexture1.size().width + bgTexture2.size().width
for index in 0..<2
{
var bg1 = SKSpriteNode(texture: bgTexture1)
var bg2 = SKSpriteNode(texture: bgTexture2)
bg1.anchorPoint = CGPointMake(0, 0)
bg2.anchorPoint = CGPointMake(0, 0)
let i = CGFloat(index)
bg1.position = CGPointMake(i * bgTexture1.size().width + i * bgTexture2.size().width, 0)
bg2.position = CGPointMake((i+1) * bgTexture1.size().width + i * bgTexture2.size().width, 0)
self.addChild(bg1)
self.addChild(bg2)
lastNode = bg2
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var speedOfBackground : CGFloat = 300.0
var previousTime : CFTimeInterval = -1
var lastNode : SKSpriteNode!
func update(currentTime: CFTimeInterval) {
if previousTime != -1
{
var outOfBoundsSprite : SKSpriteNode? = nil
let deltaX = speedOfBackground * CGFloat(currentTime - previousTime)
for sknode in self.children
{
if let sprite = sknode as? SKSpriteNode
{
sprite.position = CGPointMake(sprite.position.x - deltaX, sprite.position.y)
if (sprite.position.x < -sprite.size.width)
{
outOfBoundsSprite = sprite
}
}
}
if (outOfBoundsSprite != nil)
{
outOfBoundsSprite?.position.x = lastNode.position.x + lastNode.size.width
lastNode = outOfBoundsSprite!
}
}
previousTime = currentTime
}
}
You can vary the speedOfBackground property to vary the speed of movement.
It can be used in the SKScene like this
class GameScene: SKScene ,SKPhysicsContactDelegate {
var background : BackgroundNode!
override func didMoveToView(view: SKView) {
background = BackgroundNode()
background.speedOfBackground = 500.0
self.addChild(background)
}
override func update(currentTime: CFTimeInterval) {
background.update(currentTime)
}
}
#rakeshbs - Your answer was perfect, I did decide to expand it a bit. This will allow anyone to throw a Atlas in their project and be able to set up a scrolling background of any number of images. HOWEVER, watch out for loading to many assets at once, if you do that you need to remove them from the game or bad things might happen. =P
import SpriteKit
class BackgroundNode: SKNode {
override init() {
super.init()
// create containers for all the Textures and Nodes
var backgroundTextures = [SKTexture]()
var backgroundSpriteNodes = [SKSpriteNode]()
// load the TextureAtlas for the Background
let backgroundAtlas : SKTextureAtlas = SKTextureAtlas(named: "BackgroundImages")
let imagesCount:Int = backgroundAtlas.textureNames.count
// fetch all of the image resources and store them as both textures and spriteNodes
for var i = 1; i <= imagesCount; i++ {
let imageTextureName = "background_\(i)"
let imageTexture = backgroundAtlas.textureNamed(imageTextureName)
let spriteNode = SKSpriteNode(texture: imageTexture)
spriteNode.anchorPoint = CGPointMake(0, 0)
backgroundTextures.append(imageTexture)
backgroundSpriteNodes.append(spriteNode)
}
// for each image figure out what the x value of the location should be
for var i = 0; i < imagesCount; i++ {
var addtionalWidth:CGFloat = 0
// add the x value of all the images before this image to come up with the latest x
for var j = 0; j < i; j++ {
addtionalWidth += backgroundTextures[j].size().width
}
// assign each SKNode a position
backgroundSpriteNodes[i].position = CGPointMake(0 + addtionalWidth, 0)
// add the Node to the scene
self.addChild(backgroundSpriteNodes[i])
}
// keep track of where you are to reset scrolls images
lastNode = backgroundSpriteNodes[imagesCount - 1]
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var speedOfBackground : CGFloat = 300.0
var previousTime : CFTimeInterval = -1
var lastNode : SKSpriteNode!
func update(currentTime: CFTimeInterval) {
if previousTime != -1 {
var outOfBoundsSprite : SKSpriteNode? = nil
let deltaX = speedOfBackground * CGFloat(currentTime - previousTime)
for sknode in self.children {
if let sprite = sknode as? SKSpriteNode {
sprite.position = CGPointMake(sprite.position.x - deltaX, sprite.position.y)
if (sprite.position.x < -sprite.size.width) {
outOfBoundsSprite = sprite
}
}
}
if (outOfBoundsSprite != nil) {
outOfBoundsSprite?.position.x = lastNode.position.x + lastNode.size.width
lastNode = outOfBoundsSprite!
}
}
previousTime = currentTime
}
}

How to create progress bar in sprite kit?

I want to create my own progress bar in Sprite Kit.
I figured I will need to images - one fully empty progress bar and filled progress bar.
I have those images, I put filled one on top of empty one, they are regular SKSPriteNodes now I can't figure out how do I cut my filled image where I need?
How do I cut SKSpriteNode image at certain point? Maybe texture?
I would recommend looking into SKCropNode. For a visual aid how SKCropNode works, look it up in the Apple Programming Guide. I have read through the entire document multiple times and it is a particularly good read.
SKCropNode is basically an SKNode which you add to your scene, but its children can be cropped by a mask. This mask is set in the maskNode property of the SKCropNode. In this way, you only need one texture image. I would subclass SKCropNode to implement functionality to move or resize the mask, so you can easily update its appearance.
#interface CustomProgressBar : SKCropNode
/// Set to a value between 0.0 and 1.0.
- (void) setProgress:(CGFloat) progress;
#end
#implementation CustomProgressBar
- (id)init {
if (self = [super init]) {
self.maskNode = [SKSpriteNode spriteNodeWithColor:[SKColor whiteColor] size:CGSizeMake(300,20)];
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:#"progressBarImage"];
[self addChild:sprite];
}
return self;
}
- (void) setProgress:(CGFloat) progress {
self.maskNode.xScale = progress;
}
#end
In your scene:
#import "CustomProgressBar.h"
// ...
CustomProgressBar * progressBar = [CustomProgressBar new];
[self addChild:progressBar];
// ...
[progressBar setProgress:0.3];
// ...
[progressBar setProgress:0.7];
Note: this code doesn't move the mask (so the sprite will be cropped on either side) but I'm sure you get the idea.
Quite simply: you need a frame image (optional) and a "bar" image. The bar image out to be a single, solid color and as high as you need it and 1 or 2 pixels wide. A SKShapeNode as bar will do as well.
Just making the bar and animating is simply a matter of changing the SKSpriteNode's size property. For example to make the bar represent progress between 0 and 100 just do:
sprite.size = CGSizeMake(progressValue, sprite.size.height);
Update the size whenever progressValue changes.
You'll notice the image will increase in width to both left and right, to make it stretch only to the right change the anchorPoint to left-align the image:
sprite.anchorPoint = CGPointMake(0.0, 0.5);
That is all. Draw a frame sprite around it to make it look nicer.
that is my ProgressBar in swift :
import Foundation
import SpriteKit
class IMProgressBar : SKNode{
var emptySprite : SKSpriteNode? = nil
var progressBar : SKCropNode
init(emptyImageName: String!,filledImageName : String)
{
progressBar = SKCropNode()
super.init()
let filledImage = SKSpriteNode(imageNamed: filledImageName)
progressBar.addChild(filledImage)
progressBar.maskNode = SKSpriteNode(color: UIColor.whiteColor(),
size: CGSize(width: filledImage.size.width * 2, height: filledImage.size.height * 2))
progressBar.maskNode?.position = CGPoint(x: -filledImage.size.width / 2,y: -filledImage.size.height / 2)
progressBar.zPosition = 0.1
self.addChild(progressBar)
if emptyImageName != nil{
emptySprite = SKSpriteNode.init(imageNamed: emptyImageName)
self.addChild(emptySprite!)
}
}
func setXProgress(xProgress : CGFloat){
var value = xProgress
if xProgress < 0{
value = 0
}
if xProgress > 1 {
value = 1
}
progressBar.maskNode?.xScale = value
}
func setYProgress(yProgress : CGFloat){
var value = yProgress
if yProgress < 0{
value = 0
}
if yProgress > 1 {
value = 1
}
progressBar.maskNode?.yScale = value
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//How to use :
let progressBar = IMProgressBar(emptyImageName: "emptyImage",filledImageName: "filledImage")
or
let progressBar = IMProgressBar(emptyImageName: nil,filledImageName: "filledImage")
and add this progressBar to any SKNode :
self.addChild(progressBar)
//That's all.
Assuming HealthBarNode is a subclass of SKSpriteNode with a public property health that varies between 0.0 and 1.0 and whose parental property texture is generated from the entire color bar image of width _textureWidth (a private property), you could do something like this:
- (void)setHealth:(CGFloat)fraction
{
self.health = MIN(MAX(0.0, fraction), 1.0); // clamp health between 0.0 and 1.0
SKTexture *textureFrac = [SKTexture textureWithRect:CGRectMake(0, 0, fraction, 1.0) inTexture:self.texture];
// check docs to understand why you can pass in self.texture as the last parameter every time
self.size = CGSizeMake(fraction * _textureWidth, self.size.height);
self.texture = textureFrac;
}
Setting the health to a new value will cause the health bar (added as a child to the main scene, say) to get cropped properly.
I built a small library to deal with this exact scenario! Here is SpriteBar: https://github.com/henryeverett/SpriteBar
There is no "cutting" an image/texture.
An alternative to what Cocos offered is to make a couple of textures and interchange them into your node depending on health. I did a game where the health bar changed texture every 10 points (range was 0-100). After some trial and error though, I just ended up doing what Cocos already suggested.
I did it like this, and it works perfectly.
So first, I declared a SKSpriteNode:
baseBar = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:CGSizeMake(CGRectGetMidX(self.frame)-40, self.frame.size.height/10)];
//The following will make the health bar to reduce from right to left
//Change it to (1,0.5) if you want to have it the other way
//But you'd have to play with the positioning as well
[baseBar setAnchorPoint:CGPointMake(0, 0.5)];
CGFloat goodWidth, goodHeight;
goodHeight =self.frame.size.height-(baseBar.frame.size.height*2/3);
goodWidth =self.frame.size.width-(10 +baseBar.frame.size.width);
[baseBar setPosition:CGPointMake(goodWidth, goodHeight)];
[self addChild:baseBar];
I then added a 'Frame' for the bar, with an SKShapeNode, without fill colour (clearcolour), and a stroke colour:
//The following was so useful
SKShapeNode *edges = [SKShapeNode shapeNodeWithRect:baseBar.frame];
edges.fillColor = [UIColor clearColor];
edges.strokeColor = [UIColor blackColor];
[self addChild:edges];
When I wanted to reduce the health, I did the following:
if (playerHealthRatio>0) {
playerHealthRatio -= 1;
CGFloat ratio = playerHealthRatio / OriginalPlayerHealth;
CGFloat newWidth =baseBar.frame.size.width*ratio;
NSLog(#"Ratio: %f newwidth: %f",ratio,newWidth);
[baseBar runAction:[SKAction resizeToWidth:newWidth duration:0.5]];
}else{
// NSLog(#"Game over");
}
Simple, clean and not complicated at all.
Swift 4:
( my answer 1 -> make a rapid and simple progress bar)
To make a simple progress bar based to colors you can subclass a simple SKNode without using SKCropNode:
class SKProgressBar: SKNode {
var baseSprite: SKSpriteNode!
var coverSprite: SKSpriteNode!
override init() {
super.init()
}
convenience init(baseColor: SKColor, coverColor: SKColor, size: CGSize ) {
self.init()
self.baseSprite = SKSpriteNode(color: baseColor, size: size)
self.coverSprite = SKSpriteNode(color: coverColor, size: size)
self.addChild(baseSprite)
self.addChild(coverSprite)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setProgress(_ value:CGFloat) {
print("Set progress bar to: \(value)")
guard 0.0 ... 1.0 ~= value else { return }
let originalSize = self.baseSprite.size
var calculateFraction:CGFloat = 0.0
self.coverSprite.position = self.baseSprite.position
if value == 0.0 {
calculateFraction = originalSize.width
} else if 0.01..<1.0 ~= value {
calculateFraction = originalSize.width - (originalSize.width * value)
}
self.coverSprite.size = CGSize(width: originalSize.width-calculateFraction, height: originalSize.height)
if value>0.0 && value<1.0 {
self.coverSprite.position = CGPoint(x:(self.coverSprite.position.x-calculateFraction)/2,y:self.coverSprite.position.y)
}
}
}
Usage:
self.energyProgressBar = SKProgressBar.init(baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
addChild(self.energyProgressBar)
// other code to see progress changing..
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5])
self.run(sequence)
Output:
Swift 4
( my answer 3 -> old SpriteBar project fully translated to swift)
To make a progress bar based to SKTextureAtlas you can use the Objective C project called SpriteBar maded by Henry Everett.
I've forked and fully translated this project, this is the source:
class SpriteBar: SKSpriteNode {
var textureReference = ""
var atlas: SKTextureAtlas!
var availableTextureAddresses = Array<Int>()
var timer = Timer()
var timerInterval = TimeInterval()
var currentTime = TimeInterval()
var timerTarget: AnyObject!
var timerSelector: Selector!
init() {
let defaultAtlas = SKTextureAtlas(named: "sb_default")
let firstTxt = defaultAtlas.textureNames[0].replacingOccurrences(of: "#2x", with: "")
let texture = defaultAtlas.textureNamed(firstTxt)
super.init(texture: texture, color: .clear, size: texture.size())
self.atlas = defaultAtlas
commonInit()
}
convenience init(textureAtlas: SKTextureAtlas?) {
self.init()
self.atlas = textureAtlas
commonInit()
}
func commonInit() {
self.textureReference = "progress"
resetProgress()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func closestAvailableToPercent(_ percent:Int)->Int {
var closest = 0
for thisPerc in self.availableTextureAddresses {
if labs(Int(thisPerc) - percent) < labs(closest - percent) {
closest = Int(thisPerc)
}
}
return closest
}
func percentFromTextureName(_ string:String) -> Int? {
let clippedString = string.replacingOccurrences(of: "#2x", with: "")
let pattern = "(?<=\(textureReference)_)([0-9]+)(?=.png)"
let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let matches = regex?.matches(in: clippedString, options: [], range: NSRange(location: 0, length: clippedString.count))
// If the matches don't equal 1, you have done something wrong.
if matches?.count != 1 {
NSException(name: NSExceptionName(rawValue: String("SpriteBar: Incorrect texture naming.")), reason: "Textures should follow naming convention: \(textureReference)_#.png. Failed texture name: \(string)", userInfo: nil).raise()
}
for match: NSTextCheckingResult? in matches ?? [NSTextCheckingResult?]() {
let matchRange = match?.range(at: 1)
let range = Range(matchRange!, in: clippedString)!
return Int(clippedString[range.lowerBound..<range.upperBound])
}
return nil
}
func resetProgress() {
self.texture = self.atlas.textureNamed("\(self.textureReference)_\(closestAvailableToPercent(0)).png")
self.availableTextureAddresses = []
for name in self.atlas.textureNames {
self.availableTextureAddresses.append(self.percentFromTextureName(name)!)
}
self.invalidateTimer()
self.currentTime = 0
}
func setProgress(_ progress:CGFloat) {
// Set texure
let percent: CGFloat = CGFloat(lrint(Double(progress * 100)))
let name = "\(textureReference)_\(self.closestAvailableToPercent(Int(percent))).png"
self.texture = self.atlas.textureNamed(name)
// If we have reached 100%, invalidate the timer and perform selector on passed in object.
if fabsf(Float(progress)) >= fabsf(1.0) {
if timerTarget != nil && timerTarget.responds(to: timerSelector) {
typealias MyTimerFunc = #convention(c) (AnyObject, Selector) -> Void
let imp: IMP = timerTarget.method(for: timerSelector)
let newImplementation = unsafeBitCast(imp, to: MyTimerFunc.self)
newImplementation(self.timerTarget, self.timerSelector)
}
timer.invalidate()
}
}
func setProgressWithValue(_ progress:CGFloat, ofTotal maxValue:CGFloat) {
self.setProgress(progress/maxValue)
}
func numberOfFrames(inAnimation animationName: String) -> Int {
// Get the number of frames in the animation.
let allAnimationNames = atlas.textureNames
let nameFilter = NSPredicate(format: "SELF CONTAINS[cd] %#", animationName)
return ((allAnimationNames as NSArray).filtered(using: nameFilter)).count
}
func startBarProgress(withTimer seconds: TimeInterval, target: Any?, selector: Selector) {
resetProgress()
timerTarget = target as AnyObject
timerSelector = selector
// Split the progress time between animation frames
timerInterval = seconds / TimeInterval((numberOfFrames(inAnimation: textureReference) - 1))
timer = Timer.scheduledTimer(timeInterval: timerInterval, target: self, selector: #selector(self.timerTick(_:)), userInfo: seconds, repeats: true)
}
#objc func timerTick(_ timer: Timer) {
// Increment timer interval counter
currentTime += timerInterval
// Make sure we don't exceed the total time
if currentTime <= timer.userInfo as! Double {
setProgressWithValue(CGFloat(currentTime), ofTotal: timer.userInfo as! CGFloat)
}
}
func invalidateTimer() {
timer.invalidate()
}
}
Usage:
let progressBarAtlas = SKTextureAtlas.init(named: "sb_default")
self.energyProgressBar = SpriteBar(textureAtlas: progressBarAtlas)
self.addChild(self.energyProgressBar)
self.energyProgressBar.size = CGSize(width:350, height:150)
self.energyProgressBar.position = CGPoint(x:self.frame.width/2, y:self.frame.height/2)
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let action6 = SKAction.run {
self.energyProgressBar.startBarProgress(withTimer: 10, target: self, selector: #selector(self.timeOver))
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5,wait,action6])
self.run(sequence)
To have more details you can find my GitHUB repo here
Swift 4:
( my answer 2 -> make a complex progress bar using textures)
To make a complex progress bar based to texture and colors you can subclass a simple SKNode. About this case, SpriteKit for now (swift v4.1.2) doesn't have a method to directly cutting a SKTexture. We need to use another method called texture(from:crop:)
class SKProgressImageBar: SKNode {
var baseSprite: SKSpriteNode!
var coverSprite: SKSpriteNode!
var originalCoverSprite: SKSpriteNode!
override init() {
super.init()
}
convenience init(baseImageName:String="", coverImageName:String="", baseColor: SKColor, coverColor: SKColor, size: CGSize ) {
self.init()
self.baseSprite = baseImageName.isEmpty ? SKSpriteNode(color: baseColor, size: size) : SKSpriteNode(texture: SKTexture(imageNamed:baseImageName), size: size)
self.coverSprite = coverImageName.isEmpty ? SKSpriteNode(color: coverColor, size: size) : SKSpriteNode(texture: SKTexture(imageNamed:coverImageName), size: size)
self.originalCoverSprite = self.coverSprite.copy() as! SKSpriteNode
self.addChild(baseSprite)
self.addChild(coverSprite)
self.coverSprite.zPosition = 2.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setProgress(_ value:CGFloat) {
print("Set progress bar to: \(value)")
guard 0.0 ... 1.0 ~= value else { return }
self.coverSprite.texture = self.originalCoverSprite.texture
let originalSize = self.baseSprite.size
var calculateFraction:CGFloat = 0.0
self.coverSprite.position = self.baseSprite.position
if value == 1.0 {
calculateFraction = originalSize.width
} else if 0.01..<1.0 ~= value {
calculateFraction = originalSize.width * value
}
let coverRect = CGRect(origin: self.baseSprite.frame.origin, size: CGSize(width:calculateFraction,height:self.baseSprite.size.height))
if let parent = self.parent, parent is SKScene, let parentView = (parent as! SKScene).view {
if let texture = parentView.texture(from: self.originalCoverSprite, crop: coverRect) {
let sprite = SKSpriteNode(texture:texture)
self.coverSprite.texture = sprite.texture
self.coverSprite.size = sprite.size
}
if value == 0.0 {
self.coverSprite.texture = SKTexture()
self.coverSprite.size = CGSize.zero
}
if value>0.0 && value<1.0 {
let calculateFractionForPosition = originalSize.width - (originalSize.width * value)
self.coverSprite.position = CGPoint(x:(self.coverSprite.position.x-calculateFractionForPosition)/2,y:self.coverSprite.position.y)
}
}
}
}
Usage:
some texture just to make an example:
baseTxt.jpeg:
coverTxt.png:
Code:
self.energyProgressBar = SKProgressImageBar.init(baseImageName:"baseTxt.jpeg", coverImageName: "coverTxt.png", baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
//self.energyProgressBar = SKProgressImageBar.init(baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
self.addChild(self.energyProgressBar)
self.energyProgressBar.position = CGPoint(x:self.frame.width/2, y:self.frame.height/2)
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5])
self.run(sequence)
Output:
with colors:
with textures:
A simple class using two sprite nodes
class PBProgressBar: SKNode {
private var baseNode : SKSpriteNode!
private var progressNode : SKSpriteNode!
private var basePosition: CGPoint!
var progress: CGFloat!
init(progress: CGFloat = 0.45, position: CGPoint = CGPoint.zero) {
super.init()
self.progress = progress
self.basePosition = position
configureProgress()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureProgress() {
baseNode = SKSpriteNode(color: .white, size: CGSize(width: 10, height: 100))
baseNode.anchorPoint = CGPoint.zero
let heightFraction = baseNode.size.height * progress
baseNode.position = basePosition
progressNode = SKSpriteNode(color: .blue, size: CGSize(width: 10, height: heightFraction))
progressNode.anchorPoint = CGPoint.zero
baseNode.addChild(progressNode)
self.addChild(baseNode)
}
}

Resources