How to create progress bar in sprite kit?

泪湿孤枕 提交于 2019-12-18 11:36:11

问题


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?


回答1:


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.




回答2:


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.




回答3:


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.




回答4:


I built a small library to deal with this exact scenario! Here is SpriteBar: https://github.com/henryeverett/SpriteBar




回答5:


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.




回答6:


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.




回答7:


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.




回答8:


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




回答9:


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:




回答10:


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:



来源:https://stackoverflow.com/questions/23563079/how-to-create-progress-bar-in-sprite-kit

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!