How to create progress bar in sprite kit? - ios

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)
}
}

Related

how to 3d rotate arkit text (Swift4)

My code below takes text and places 3d text in argument reality. The problem is that I cannot rotate the text. I can just move it around in a 2d direction. I would like to be move and rotate the text. I dont know how to do this. I have not seen any online tutorials on this problem. Currently this is a swift 4 problem.
import UIKit
import SceneKit
import ARKit
class TextNode: SCNNode{
var textGeometry: SCNText!
init(text: String, depth: CGFloat = 1, font: String = "Helvatica", textSize: CGFloat = 3, colour: UIColor) {
super.init()
textGeometry = SCNText(string: text , extrusionDepth: depth)
textGeometry.font = UIFont(name: font, size: textSize)
textGeometry.flatness = 0
textGeometry.firstMaterial?.diffuse.contents = colour
self.geometry = textGeometry
self.scale = SCNVector3(0.01, 0.01 , 0.01)
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
extension ViewController: UITextFieldDelegate{
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let textEntered = textField.text,
let textRange = Range(range, in: textEntered) {
let updatedText = textEntered.replacingCharacters(in: textRange, with: string)
print("User Has Entered \(updatedText)")
DispatchQueue.main.async {
self.textNode.textGeometry.string = updatedText
}
}
return true
}
}
class ViewController: UIViewController {
/////
var name: String?
#IBOutlet var theBar: UITextField!
#IBOutlet var augmentedRealityView: ARSCNView!
//2. Create Our ARWorld Tracking Configuration
let configuration = ARWorldTrackingConfiguration()
//3. Create Our Session
let augmentedRealitySession = ARSession()
//4. Create A Variable To Store The Current Nodes Rotation Around It's Y-Axis
var currentAngleY: Float = 0.0
var isRotating = false
var currentNode: SCNNode?
/////
var textNode: TextNode!
fileprivate func judo() {
augmentedRealityView.scene.rootNode.addChildNode(textNode)
textNode.position = SCNVector3(0, 0, -1.5)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
augmentedRealityView.session.pause()
}
#IBAction func changeTextColour(){
let snapShot = self.augmentedRealityView.snapshot()
UIImageWriteToSavedPhotosAlbum(snapShot, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}
#objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
print("Error Saving ARKit Scene \(error)")
} else {
print("ARKit Scene Successfully Saved")
}}
#objc func scaleCurrentNode(_ gesture: UIPinchGestureRecognizer) {
if !isRotating, let selectedNode = currentNode{
if gesture.state == .changed {
let pinchScaleX: CGFloat = gesture.scale * CGFloat((selectedNode.scale.x))
let pinchScaleY: CGFloat = gesture.scale * CGFloat((selectedNode.scale.y))
let pinchScaleZ: CGFloat = gesture.scale * CGFloat((selectedNode.scale.z))
selectedNode.scale = SCNVector3Make(Float(pinchScaleX), Float(pinchScaleY), Float(pinchScaleZ))
gesture.scale = 1
}
if gesture.state == .ended {}
}
}
#objc func rotateNode(_ gesture: UIRotationGestureRecognizer){
if let selectedNode = currentNode{
//1. Get The Current Rotation From The Gesture
let rotation = Float(gesture.rotation)
//2. If The Gesture State Has Changed Set The Nodes EulerAngles.y
if gesture.state == .changed{
isRotating = true
selectedNode.eulerAngles.y = currentAngleY + rotation
}
//3. If The Gesture Has Ended Store The Last Angle Of The CurrentNode
if(gesture.state == .ended) {
currentAngleY = selectedNode.eulerAngles.y
isRotating = false
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
augmentedRealityView.automaticallyUpdatesLighting = true
theBar.delegate = self as? UITextFieldDelegate
textNode = TextNode(text: theBar.text!, colour: .white)
augmentedRealityView.scene.rootNode.addChildNode(textNode)
textNode.position = SCNVector3(0, 0, -1.5)
augmentedRealityView.automaticallyUpdatesLighting = true
//1. Run The ARSession
augmentedRealityView.session = augmentedRealitySession
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
//2. Add A UIPinchGestureRecognizer So We Can Scale Our TextNode
let scaleGesture = UIPinchGestureRecognizer(target: self, action: #selector(scaleCurrentNode(_:)))
self.view.addGestureRecognizer(scaleGesture)
//3. Add A Tap Gesture Recogizer So We Can Place Our TextNode
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(placeOrAssignNode(_:)))
self.view.addGestureRecognizer(tapGesture)
//4. Add A Rotation Gesture Recogizer So We Can Rotate Our TextNode
let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotateNode(_:)))
self.view.addGestureRecognizer(rotateGesture)
}
#objc func placeOrAssignNode(_ gesture: UITapGestureRecognizer){
//1. Get The Current Location Of The Tap
let currentTouchLocation = gesture.location(in: self.augmentedRealityView)
//2. If We Hit An SCNNode Set It As The Current Node So We Can Interact With It
if let nodeHitTest = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node{
currentNode = nodeHitTest
return
}
//3. Do An ARHitTest For Features Points So We Can Place An SCNNode
if let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, types: .featurePoint).first {
//4. Get The World Transform
let hitTestPosition = hitTest.worldTransform.columns.3
//5. Add The TestNode At The Desired Position
createTextFromPosition(SCNVector3(hitTestPosition.x, hitTestPosition.y, hitTestPosition.z))
return
}
}
func createTextFromPosition(_ position: SCNVector3){
let textNode = SCNNode()
//1. Create The Text Geometry With String & Depth Parameters
let textGeometry = SCNText(string: theBar.text! , extrusionDepth: 1)
//2. Set The Font With Our Set Font & Size
textGeometry.font = UIFont(name: "Helvatica", size: 1)
//3. Set The Flatness To Zero (This Makes The Text Look Smoother)
textGeometry.flatness = 0
//4. Set The Colour Of The Text
textGeometry.firstMaterial?.diffuse.contents = UIColor.white
//5. Set The Text's Material
textNode.geometry = textGeometry
//6. Set The Pivot At The Center
let min = textNode.boundingBox.min
let max = textNode.boundingBox.max
textNode.pivot = SCNMatrix4MakeTranslation(
min.x + (max.x - min.x)/2,
min.y + (max.y - min.y)/2,
min.z + (max.z - min.z)/2
)
//7. Scale The Text So We Can Actually See It!
textNode.scale = SCNVector3(0.005, 0.005 , 0.005)
//8. Add It To The Hierachy & Position It
self.augmentedRealityView.scene.rootNode.addChildNode(textNode)
textNode.position = position
//9. Set It As The Current Node
currentNode = textNode
}
func changeColour(_ value: Int){
if value == 0{
textNode.textGeometry.firstMaterial?.diffuse.contents = UIColor.lightGray
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let name = UserDefaults.standard.value(forKey: "name") as? String{
theBar.text = name
}
theBar.delegate = self as? UITextFieldDelegate
if name == String(1) {
textNode = TextNode(text: theBar.text!, colour: .red)
augmentedRealityView.scene.rootNode.addChildNode(textNode)
textNode.position = SCNVector3(0, 0, -1.5)
}
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
augmentedRealityView.session.run(configuration)
}
}
//////////////////////////////////////
A SCNNode object has a rotation property you can utilize. I don't see where you're setting it in your code.
https://developer.apple.com/documentation/scenekit/scnnode/1408034-rotation
// Sample code: Rotates node to the `heading`
public func nodeRotation(heading: Double) -> SCNVector4 {
return SCNVector4(0, 1, 0, Double.pi - ((heading * (Double.pi/180)) - Double.pi))
}
textNode.rotation = nodeRotation(heading: 45.0)

Trying to update text (specifically health of a boss in a game) and getting "exc_bad_instruction (code=exc_i386_invop subcode=0x0)"

Relevant code:
import SpriteKit
class GameScene: SKScene {
//1
let warrior = SKSpriteNode(imageNamed: "warr")
var healthBoss = SKLabelNode(text: "Boss HP: 25")
override func didMove(to view: SKView){
gameScene = self
//2
setupLayers()
spawnWarr(at: warriorposition)
spawnBoss(at: bossposition)
setupLabels()
backgroundColor = SKColor.blue
}
func setupLayers() {
objectsLayer = SKNode()
objectsLayer.name = "Objects Layer"
addChild(objectsLayer)
}
func spawnWarr(at: CGPoint) {
let newWarr = warriorclass()
newWarr.position = at
self.addChild(newWarr)
}
func spawnBoss(at: CGPoint) {
let newBoss = bossclass()
newBoss.position = at
self.addChild(newBoss)
}
func setupLabels() {
healthBoss.position = CGPoint(x: size.width * 0.5, y: size.height * 0.1)
healthBoss.fontColor = whiteColor
healthBoss.fontName = "Copperplate"
healthBoss.text = "Boss HP: \(spawnedBoss.health)"
\ Specifically here using (spawnedBoss.health) is where I get "exc_bad_instruction (code=exc_i386_invop subcode=0x0)"
healthBoss.fontSize = 22
healthBoss.zPosition = layers.uistuff
objectsLayer.addChild(healthBoss)
}
bossclass.swift file -
import UIKit
import SpriteKit
class bossclass: character, pTargetable {
var health = 25
var maxhealth = 25
override init() {
super.init()
scorePoints = 5
let texture = texturesBoss
let xSize = texture.size().width*bossscale
let ySize = texture.size().height*bossscale
let size = CGSize(width: xSize, height: ySize)
self.name = "boss"
let top = SKSpriteNode (texture: texture, size: size)
top.zPosition = layers.raiders
top.color = SKColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
top.colorBlendFactor = 1.0
self.addChild(top)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func takeDamage(damage: Int) {
health -= damage
print("You lost \(damage) hit points")
if health <= 0 {
die()
print("You are dead now")
}
}
}
global.swift file includes:
var spawnedBoss: bossclass!
and protocol:
protocol pTargetable {
var health: Int { get set }
func takeDamage(damage: Int)
}
So the issue from what I can see is there is a nil. I thought maybe I was referencing the boss' health before I spawned the boss, but looking at the code it doesn't seem that way. Maybe there is an easier solution to having a boss health bar that updates with damage?
Any help would be appreciated. Thank you.
When you wrote:
var spawnedBoss: bossclass!
You promised Swift that you would never try to use spawnedBoss if it was nil and that it shouldn't force you to check.
Then you wrote:
healthBoss.text = "Boss HP: \(spawnedBoss.health)"
And at this point spawnedBoss is probably nil.
So, I would change:
var spawnedBoss: bossclass!
to
var spawnedBoss: bossclass?
And then add in all of the proper checking for nil to clear the compiler errors you are going to get. When you clear them all (not with !, but with if let or ??), then you can be sure that you are using spawnedBoss only when it's not nil.

iOS animation using spritekit

I'm trying to create an animation on my iOS application using spritekit.
so far the code that i have done for this is
import UIKit
import SpriteKit
import GameplayKit
class TygaScene: SKScene {
var tygaFrames: [SKTexture]? // array of images
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(size: CGSize) { // init function then set size
super.init(size: size)
self.backgroundColor = UIColor.white // setting colour to background
var frames:[SKTexture] = [] // create place holder for texture to initialise
let tygaAtlas = SKTextureAtlas(named: "tyga")
for index in 1...5 { // adding all images from folder
let textureName = "tyga_\(index)" // naming the first bit of the file
let texture = tygaAtlas.textureNamed(textureName)
frames.append(texture)
}
self.tygaFrames = frames
}
func moveTyga() {
let texture = self.tygaFrames![0]
let tyga = SKSpriteNode(texture: texture)
tyga.size = CGSize(width: 280, height: 280)
let randometygaYPositionGenerator = GKRandomDistribution(lowestValue: 50, highestValue: Int(self.frame.size.height))
let yPosition = CGFloat(randometygaYPositionGenerator.nextInt())
let rightToLeft = arc4random() % 2 == 0
let xPosition = rightToLeft ? self.frame.size.width + tyga.size.width / 2 : -tyga.size.width / 2
tyga.position = CGPoint(x: xPosition, y: yPosition)
if rightToLeft {
tyga.xScale = -1
}
self.addChild(tyga)
tyga.run(SKAction.repeatForever(SKAction.animate(with: self.tygaFrames!, timePerFrame: 0.05, resize: false, restore: true)))
var distanceToCover = self.frame.size.width + tyga.size.width
if rightToLeft {
distanceToCover += -1
}
let time = TimeInterval(abs(distanceToCover/140))
let moveAction = SKAction.run {
tyga.removeAllActions ()
tyga.removeFromParent()
}
let allActions = SKAction.sequence([moveAction, removeAction])
tyga.run(allActions)
}
}
The part that is giving an error is the line:
let allActions = SKAction.sequence([moveAction, removeAction])
tyga.run(allActions)
}
could you please advise me on how i could overcome this problem?
Thanks.

SpriteKit - Adding enemy causes frame drop

My game is a side scroller using SpriteKit. The player stays at the same x-position and the obstacles are moving towards him. But whenever I add an enemy the frame rate drops for a short moment. I tried to change many things, like removing the enemy's dynamics or animation. But nothing works.
Here's some code:
func addZombie() {
let zombie = Zombie()
zombie.position = CGPoint(x: screenWidth + zombie.size.width * 0.5, y: screenHeight * 0.5)
zombies.append(zombie)
addChild(zombie)
}
The above method creates the enemies. It's called by using:
func generateZombies() {
let wait = SKAction.waitForDuration(2.5)
let run = SKAction.runBlock {
self.addZombie()
}
runAction(SKAction.repeatActionForever(SKAction.sequence([wait, run])))
}
I also tried NSTimer, but nothing works. Please help me.
Zombie():
class Zombie: SKSpriteNode {
var textureArrayWalking = [SKTexture]()
init() {
let mySize = CGSize(width: Player().size.width, height: Player().size.height * (4/3))
super.init(texture: nil, color: UIColor.greenColor(), size: mySize)
orderTextureAtlases()
texture = textureArrayWalking[0]
zPosition = 1
physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: (2/3) * self.size.width, height: self.size.height))
physicsBody?.restitution = 0.0
physicsBody?.categoryBitMask = enemyCategory
physicsBody?.collisionBitMask = groundCategory
physicsBody?.contactTestBitMask = playerCategory | bulletCategory | groundCategory
physicsBody!.allowsRotation = false;
walk()
}
func walk() {
runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(textureArrayWalking, timePerFrame: 0.07)), withKey: walkingActionKeyZombie)
}
func orderTextureAtlases() {
let textureAtlasWalking = SKTextureAtlas(named: "ZombieWalking")
for index in 0..<textureAtlasWalking.textureNames.count {
let name = "ZombieWalking_JeromeWalk_\(index)"
let tex = SKTexture(imageNamed: name)
tex.filteringMode = .Nearest
textureArrayWalking.append(tex)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

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
}
}

Resources