【问题标题】:How to create progress bar in sprite kit?如何在精灵套件中创建进度条?
【发布时间】:2014-06-27 02:28:31
【问题描述】:

我想在 Sprite Kit 中创建自己的进度条。
我想我需要图像 - 一个完全为空的进度条和一个填充的进度条。
我有这些图像,我把填充的一张放在空白的上面,它们是普通的SKSPriteNodes 现在我不知道如何在我需要的地方剪切我的填充图像?

如何在某个点剪切 SKSpriteNode 图像?也许是纹理?

【问题讨论】:

  • 你应该能够通过调用+(SKTexture *)textureWithRect:inTexture:得到节点纹理的一个子集(假设是整个颜色条)

标签: ios sprite-kit progress-bar


【解决方案1】:

一个使用两个精灵节点的简单类

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

}

【讨论】:

    【解决方案2】:

    斯威夫特 4

    (我的答案 3 -> 旧 SpriteBar 项目完全翻译为 swift)

    要制作基于SKTextureAtlas 的进度条,您可以使用Henry Everett 开发的名为SpriteBar 的Objective C 项目。

    我已经分叉并完全翻译了这个项目,这是源代码:

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

    用法:

    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)
    

    要了解更多详细信息,您可以找到我的 GitHUB repo here

    【讨论】:

      【解决方案3】:

      斯威夫特 4:

      (我的答案1 -> 制作一个快速简单的进度条)

      要根据颜色制作简单进度条,您可以使用SKCropNode 子类化简单的SKNode 不使用:

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

      用法:

      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)
      

      输出:

      【讨论】:

        【解决方案4】:

        斯威夫特 4:

        (我的答案 2 -> 使用纹理制作复杂的进度条)

        要根据纹理和颜色制作复杂的进度条,您可以将简单的SKNode 子类化。关于这种情况,SpriteKit 目前 (swift v4.1.2) 没有直接剪切SKTexture 的方法。我们需要使用另一种方法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)
                    }
                }
            }
        }
        

        用法:

        一些纹理只是为了做一个例子:

        baseTxt.jpeg:

        coverTxt.png:

        代码:

        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)
        

        输出:

        有颜色:

        带有纹理:

        【讨论】:

          【解决方案5】:

          这是我的ProgressBar 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")
          }
          

          }

          //如何使用:

          let progressBar = IMProgressBar(emptyImageName: "emptyImage",filledImageName: "filledImage")
          

          或

          let progressBar = IMProgressBar(emptyImageName: nil,filledImageName: "filledImage")
          

          并将此 progressBar 添加到任何 SKNode :

          self.addChild(progressBar)
          

          //仅此而已。

          【讨论】:

            【解决方案6】:

            我是这样做的,而且效果很好。

            首先,我声明了一个 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];
            

            然后我为条形添加了一个“框架”,带有一个 SKShapeNode,没有填充颜色 (clearcolour) 和描边颜色:

            //The following was so useful
            SKShapeNode *edges = [SKShapeNode shapeNodeWithRect:baseBar.frame];
            edges.fillColor = [UIColor clearColor];
            edges.strokeColor = [UIColor blackColor];
            [self addChild:edges];
            

            当我想减少健康时,我做了以下事情:

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

            简单,干净,一点也不复杂。

            【讨论】:

              【解决方案7】:

              我建议您查看SKCropNode。有关SKCropNode 工作原理的视觉帮助,请在Apple Programming Guide 中查找。整篇文档我已经通读了好几遍,读起来特别好。

              SKCropNode 基本上是一个添加到场景中的 SKNode,但它的子节点可以被蒙版裁剪。此掩码在 SKCropNode 的 maskNode 属性中设置。这样,您只需要一张纹理图像。我会将 SKCropNode 子类化以实现移动或调整蒙版大小的功能,因此您可以轻松更新其外观。

              @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
              

              在你的场景中:

              #import "CustomProgressBar.h"
              
              // ...
              
              CustomProgressBar * progressBar = [CustomProgressBar new];
              [self addChild:progressBar];
              
              // ...
              
              [progressBar setProgress:0.3];
              
              // ...
              
              [progressBar setProgress:0.7];
              

              注意:此代码不会移动遮罩(因此精灵会在任一侧被裁剪),但我相信您明白了。

              【讨论】:

              • SKCropNode 没有 mask 属性,所以我假设这应该是 maskNode?​​span>
              • 看准了!我已经编辑了答案以反映您的改进。
              • 您好,很抱歉问这个问题,但这段代码确实显示了带有progressBarImage 图像的精灵,但spriteNodeWithColor:[SKColor whiteColor] 没有做任何事情,这是为什么呢?我在ios9开发。
              • 如何使maskNode 与图像节点sprite 的大小相同?这样我们就不需要手动指定(300,20)了。
              • @FranklinYu 扎实的想法——在这个例子中,(300,20) 是神奇的数字。您可以在此处阅读有关幻数的信息:stackoverflow.com/questions/47882/… :)
              【解决方案8】:

              我构建了一个小型库来处理这个确切的场景!这里是 SpriteBar:https://github.com/henryeverett/SpriteBar

              【讨论】:

                【解决方案9】:

                假设 HealthBarNode 是 SKSpriteNode 的子类,其公共属性 health 在 0.0 和 1.0 之间变化,其父属性 texture 是从宽度为 _textureWidth 的整个彩条图像生成的(私有属性),你可以这样做:

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

                将health 设置为新值将导致健康栏(例如,作为子场景添加到主场景中)被正确裁剪。

                【讨论】:

                • 另一种方法是使用SKCropNode
                • 我会添加 SKCropNode 作为另一种选择并发送它。 :)
                • 我使用这种方法得到了垃圾来代替纹理。我尝试更改不同的值,但相同,原始纹理是黄色的,我得到一些蓝色和灰色的废话。
                • @Dvole 我能告诉你什么,它对我有用。我猜你做错了什么。
                【解决方案10】:

                没有“切割”图像/纹理。

                Cocos 提供的替代方案是制作几个纹理并根据健康状况将它们交换到您的节点中。我做了一个游戏,健康栏每 10 点改变纹理(范围是 0-100)。不过经过一些尝试和错误,我最终还是按照 Cocos 的建议做了。

                【讨论】:

                • 有是一种剪切图像的方法,请参阅我的回答参考SKCropNode。
                【解决方案11】:

                很简单:您需要一个框架图像(可选)和一个“条形”图像。条形图像是单一的纯色,与您需要的一样高,宽度为 1 或 2 像素。一个 SKShapeNode 作为 bar 也可以。

                只需更改 SKSpriteNode 的 size 属性即可制作条形图和动画。例如,要让条形代表 0 到 100 之间的进度,只需执行以下操作:

                sprite.size = CGSizeMake(progressValue, sprite.size.height);
                

                每当progressValue 改变时更新大小。

                您会注意到图像的宽度会向左和向右增加,以使其仅向右拉伸,将锚点更改为左对齐图像:

                sprite.anchorPoint = CGPointMake(0.0, 0.5);
                

                就是这样。在它周围画一个框架精灵,让它看起来更漂亮。

                【讨论】:

                • 是的,如果我有纯色和方形条,这很好,但如果我有复杂条怎么办?只是想剪掉它? Sprite Kit中有没有什么方法可以剪切或操作纹理?
                猜你喜欢
                • 1970-01-01
                • 2013-10-23
                • 2015-12-14
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-10-14
                • 1970-01-01
                相关资源
                最近更新 更多