关于ios:如何在sprite kit中创建进度条?

How to create progress bar in sprite kit?

我想在Sprite Kit中创建自己的进度条。
我想我需要图像 - 一个完全空的进度条和填充的进度条。
我有那些图像,我把它们放在空的一个上面,它们是常规的SKSPriteNodes现在我无法弄清楚如何在我需要的地方剪切我填充的图像?

如何在特定点剪切SKSpriteNode图像? 也许质地?


我建议调查SKCropNode。有关SKCropNode如何工作的视觉帮助,请在Apple编程指南中查找。我已多次阅读整个文档,这是一个特别好的阅读。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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

在你的场景中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import"CustomProgressBar.h"

// ...

CustomProgressBar * progressBar = [CustomProgressBar new];
[self addChild:progressBar];

// ...

[progressBar setProgress:0.3];

// ...

[progressBar setProgress:0.7];

注意:此代码不会移动蒙版(因此精灵将在任何一侧被裁剪)但我相信你明白了。


很简单:你需要一个帧图像(可选)和一个"条形"图像。条形图像是单一的纯色,高达您需要的宽度和1或2像素宽。作为栏的SKShapeNode也可以。

只需更改SKSpriteNode的大小属性即可制作条形图和动画。例如,要使条形表示介于0和100之间的进度,只需执行以下操作:

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

每当progressValue更改时更新大小。

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

1
sprite.anchorPoint = CGPointMake(0.0, 0.5);

就这些。在它周围画一个框架精灵,使它看起来更好。


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


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

1
2
3
4
5
6
7
8
9
- (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设置为新值将导致健康栏(作为孩子添加到主场景,比如说)正确裁剪。


这是我在swift中的ProgressBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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")
}

}

//如何使用 :

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

要么

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

并将此progressBar添加到任何SKNode

1
self.addChild(progressBar)

//就这样。


斯威夫特4

(我的回答3 - >旧的SpriteBar项目完全转换为swift)

要创建基于SKTextureAtlas的进度条,您可以使用由Henry Everett编写的名为SpriteBar的Objective C项目。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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()
    }
}

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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仓库


我这样做了,它完美无缺。

首先,我宣布了一个SKSpriteNode:

1
2
3
4
5
6
7
8
9
10
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)和笔触颜色:

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

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

1
2
3
4
5
6
7
8
9
    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");
    }

简单,干净,并不复杂。


没有"切割"图像/纹理。

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

enter image description here


斯威夫特4:

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

要根据纹理和颜色制作复杂的进度条,您可以将一个简单的SKNode子类化。关于这种情况,SpriteKit目前(swift v4.1.2)没有直接剪切SKTexture的方法。我们需要使用另一个名为texture(from:crop:)的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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:

enter image description here

coverTxt.png:

enter image description here

码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)

输出:

有颜色:

enter image description here

带纹理:

enter image description here


斯威夫特4:

(我的回答1 - >快速简单的进度条)

要根据颜色创建一个简单的进度条,您可以在不使用SKCropNode的情况下继承简单的SKNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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)
        }
    }
}

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

输出:

enter image description here