How to create progress bar in sprite kit?

后端 未结 11 889
失恋的感觉
失恋的感觉 2020-12-28 17:56

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 pu

相关标签:
11条回答
  • 2020-12-28 18:21

    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.

    0 讨论(0)
  • 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.

    enter image description here

    0 讨论(0)
  • 2020-12-28 18:25

    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:

    0 讨论(0)
  • 2020-12-28 18:25

    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

    0 讨论(0)
  • 2020-12-28 18:29

    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.

    0 讨论(0)
  • 2020-12-28 18:32

    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.

    0 讨论(0)
提交回复
热议问题