I have two SKLabelNodes, one which displays the score and another which acts as a shadow behind the score. Every time my player passes through a goal I update the score labels to increment by 1 point by calling the updateScoreLabels() method below.
But for some reason no matter what zPosition I set for each of these SKLabelNodes, the two nodes are drawn in a random order and every time I call the updateScoreLabels() method their order flips randomly. It also doesn't seem to matter what I have .ignoresSiblingOrder set to or make a difference if I add one SKLabelNode as a child of the other. The nodes just ignore zPosition altogether. What am I doing wrong? Here is my code:
This method is called in didMoveToView():
func createScoreLabel() {
//Configure score label
scoreLabel = SKLabelNode(fontNamed: "8BIT WONDER Nominal")
scoreLabel.text = "O"
scoreLabel.fontSize = 32
scoreLabel.position = CGPoint(x: -UIScreen.main.bounds.size.width/2 - scoreLabel.frame.size.width/2, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60)
scoreLabel.zPosition = 999
//Configure shadow
scoreLabelShadow.attributedText = NSAttributedString(string: "O", attributes: scoreLabelShadowAttributes)
scoreLabelShadow.position = CGPoint(x: -UIScreen.main.bounds.size.width/2 - scoreLabel.frame.size.width/2, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60)
scoreLabelShadow.zPosition = -9999999999
//Add labels
cam.addChild(scoreLabel)
cam.addChild(scoreLabelShadow)
//Animate labels
scoreLabel.run(SKAction.move(to: CGPoint(x: -UIScreen.main.bounds.size.width/2 + scoreLabel.frame.size.width/2 + 30, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60), duration: 0.1))
scoreLabelShadow.run(SKAction.move(to: CGPoint(x: -UIScreen.main.bounds.size.width/2 + scoreLabel.frame.size.width/2 + 30, y: UIScreen.main.bounds.size.height/2 - scoreLabel.frame.size.height/2 - 60), duration: 0.1))
}
and then this is called when I increment the score labels. Whenever this is called the zPositions flip randomly:
func updateScoreLabel() {
let formattedScore = String(score).replacingOccurrences(of: "0", with: "O")
scoreLabel.text = String(formattedScore)
scoreLabelShadow.attributedText = NSAttributedString(string: formattedScore, attributes: scoreLabelShadowAttributes)
}
As I answered here: I usually create enums to establish zPositions, so I can simply manage the different layers:
enum ZPositions: Int {
case background
case foreground
case player
case otherNodes
}
“case background” is the lower position, it’s equal to the default position of 0;
“case foreground” = 1
“case player” = 2
“case other nodes” = 3
So, when you setup a new item, you can give it the zPosition simply this way:
button.zPosition = CGFloat(ZPosition.otherNodes.rawValue)
(button now has the highest zPosition) Hope it helps...
Try to reset zPositions each time you update labels see if that resolves.
func updateScoreLabel() {
let formattedScore = String(score).replacingOccurrences(of: "0", with: "O")
scoreLabel.text = String(formattedScore)
scoreLabelShadow.attributedText = NSAttributedString(string: formattedScore, attributes: scoreLabelShadowAttributes)
scoreLabel.zPosition = 999
scoreLabelShadow.zPosition = -9999999999
}
Also, having zPosition range of 999 to -9999999999 is not a good practice which could be the cause of the problem. I highly doubt, you have that many layers in your SKSCene.
Store and re-use your layers' zPositions in a Singleton instance and have a categorical system ie: zPosHUD:CGFloat = 10, zPosPlayer:CGFloat = 3, zPosBackground:CGFloat = -5 etc. This will help you be more organized and eventually less bugs will occur in your application.
Hope this helps.
Related
I apologize in advance if this question has already been asked/answered, I didn't know how to word the title, much less the search.
I'm making this iOS game where you have to tap to drop the player and hit the pillars below. I'm trying to implement "bad pillars"; pillars that if touched, will result in game over. Ideally, I'd like for the bad pillars to spawn in place of the good pillars, but they keep spawning on top of, or next to the them. Currently I have the good pillars, set up on a timer for an SKAction Array.
Disregard any comments throughout my code, they need updating
add pillar function:
func addPillar(){
let max = Double(UInt32.max)
let randomInterval = ((Double(arc4random()) / max)+0.4) * 0.5
// initiallize pillarArray as a shuffled array
pillarArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: pillarArray) as! [String]
// initiallize pillar node to first item in shuffled pillarArray
pillar = SKSpriteNode(imageNamed: pillarArray[0])
// set pillar position as a random integer between lowestValue , highestValue
let pillarPosition = GKRandomDistribution(lowestValue: Int(-self.frame.height / 2) , highestValue: Int((-self.frame.height / 2) + 40 ))
let position = CGFloat(pillarPosition.nextInt())
pillar.position = CGPoint(x: self.frame.width - 10, y: (position));
// set pillar physics body
pillar.physicsBody = SKPhysicsBody(rectangleOf: pillar.size)
pillar.physicsBody?.isDynamic = false;
pillar.physicsBody?.categoryBitMask = pillarBitMask
pillar.physicsBody?.collisionBitMask = playerBitMask
pillar.physicsBody?.contactTestBitMask = playerBitMask
//pillar.physicsBody?.restitution = 0.80
pillar.physicsBody?.friction = 10
// add a new pillar node to the scene
if(!isPaused){
self.addChild(pillar)
}
// an array of type SKAction
var animArray = [SKAction]()
// append a move sequence to the array for "time" seconds.
if(timerIsRunning && isPaused == true){
animArray.append(SKAction.stop())
}
animArray.append(SKAction.wait(forDuration: TimeInterval(Double(randomInterval))))
animArray.append(SKAction.move(to: CGPoint(x: -self.frame.size.width + 5, y: pillar.position.y), duration: 4))
// run the sequence
animArray.append(SKAction.removeFromParent())
pillar.run(SKAction.sequence(animArray))
timer to control when the pillars are spawned:
pausedTimer = Timer.scheduledTimer(timeInterval: TimeInterval(1), target: self, selector: #selector (pauseTimer), userInfo: nil, repeats: true)
As for the bad pillars I have it set up the same way as the regular pillars, i.e. a bad pillar function, array, and timer, except I've been trying to fiddle with the timing but I can't seem to get it to work the way I want to.
Is there any method in SKAction Array, or just in general, that can allow me to pick what gets spawned when without having to create another array/timer/function?
Let me know I need to supply additional code, or provide clarification. Thanks
I have created some particles animations with specific sprites which works fine if I use them in the function:
override init(size: CGSize)
I use the following lines:
let sheet_particles = Particles()
let particles_node = SKSpriteNode(texture: sheet_particles.particle000())
particles_node.name = kparticles
particles_node.position = CGPoint(x: 500, y: 500)
particles_node.zPosition = 5
background.addChild(particles_node)
particles_node.runAction(particlesAction)
To make them appear in my scene.
The problem I have is if I try to use them in other functions in my scene, I can not see them.
func panForTranslation(translation : CGPoint) {
let position = selectedNode.position
if selectedNode.name! == kpuzzleNodeName {
selectedNode.position = CGPoint(x: position.x + translation.x * 2, y: position.y + translation.y * 2)
switch selectedNode.name2 {
case "0":
if selectedNode.frame.intersects(NPuzzle13.frame) {
particles_node.position = selectedNode.position
particles_node.runAction(particlesAction)
NPuzzle13.hidden = false
selectedNode.removeFromParent()
}
I see no particles sprite when the condition "0" happens but I see correctly the NPuzzle13. When I check the position of the particles_node node, its position is equal with the node selectedNode. All that is OK, except for the visibility of the particles... What am I missing? Thanks.
About zPosition seems all correct. I dont see any anchorPoint in your code.
I think your switch-case is jumped (not fired, not executed) because you check switch selectedNode.name2 instead of switch selectedNode.name
I am creating a space shooter game. I have created an enemy node. What I want to do is have 4 of those nodes in each corner of the screen. In other words, I want to spawn multiple copies of the same node at the same time. When the game loads, I want there to be four nodes, that are all the same. How can I do this?
Thanks
Here's a section of code I use to generate a row of 4 space invaders. I reuse the same SKSpriteNode() variable as once the node has been added to the scene, the variable is no longer required:
for invaderPosition in 100.stride(to: 500, by: 120) {
invader = SKSpriteNode(texture: texturesA[0])
invader.position = CGPoint(x: CGFloat(invaderPosition), y: 200)
invader.xScale = 8
invader.yScale = 6
invader.color = SKColor.redColor()
invader.colorBlendFactor = 1.0
invader.name = "InvaderA"
invader.texture = texturesA[1]
invader.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(texturesA, timePerFrame: self.timePerMove)))
addChild(invader)
}
invader is defined as an SKSpriteNode property. texturesA is just an array of SKTextures; timePerMove is just a time interval after which the invader should move (in Update) and change shape(texture).
var invader = SKSpriteNode()
var texturesA:[SKTexture] = []
let timePerMove: CFTimeInterval = 1.0
So you could do something similar except the invader's positions would be the 4 corners rather than a row.
Just use the copy command,
let newNode = originalNode.copy() as! SKSpriteNode;
I am pretty new to drawing programmatically. I've managed to draw a line graph, complete with points, lines, auto-scaling axes, and axis labels. In other screens, I can change characteristics of the graph and when I return to the screen, I refresh it with setNeedsDisplay() in the viewWillAppear function of the containing viewController. The lines are redrawn perfectly when I do this.
The new data that is added in other screens may require rescaling the axes. The problem is that when the graph is redrawn, the number labels on the axes are just added to the graph, without removing the old ones, meaning that some labels may be overwritten, while some old ones just remain there next to the new ones.
I think I see why this happens, in that I am creating a label and adding a subview, but not removing it. I guess I figured that since the lines are erased and redrawn, the labels would be, too. How do I cleanly relabel my axes? Is there a better way to do this? My function for creating the labels is listed below. This function is called by drawRect()
func createXAxisLabels(interval: Float, numIntervals: Int) {
let xstart: CGFloat = marginLeft
let yval: CGFloat = marginTop + graphHeight + 10 // 10 pts below the x-axis
var xLabelVals : [Float] = [0]
var xLabelLocs : [CGFloat] = [] // gives the locations for each label
for i in 0...numIntervals {
xLabelLocs.append(xstart + CGFloat(i) * graphWidth/CGFloat(numIntervals))
xLabelVals.append(Float(i) * interval)
}
if interval >= 60.0 {
xUnits = "Minutes"
xUnitDivider = 60
}
for i in 0...numIntervals {
let label = UILabel(frame: CGRectMake(0, 0, 50.0, 16.0))
label.center = CGPoint(x: xLabelLocs[i], y: yval)
label.textAlignment = NSTextAlignment.Center
if interval < 1.0 {
label.text = "\(Float(i) * interval)"
} else {
label.text = "\(i * Int(interval/xUnitDivider))"
}
label.font = UIFont.systemFontOfSize(14)
label.textColor = graphStructureColor
self.addSubview(label)
}
}
drawRect should just draw the rectangle, not change the view hierarchy. It can be called repeatedly. Those other labels are other views and do their own drawing.
You have a couple of options
Don't use labels -- instead just draw the text onto the rect.
-or-
Add/remove the labels in the view controller.
EDIT (from the comments): The OP provided their solution
I just replaced my code inside the for loop with:
let str : NSString = "\(xLabelVals[i])"
let paraAttrib = NSMutableParagraphStyle()
paraAttrib.alignment = NSTextAlignment.Center
let attributes = [
NSFontAttributeName: UIFont.systemFontOfSize(14),
NSForegroundColorAttributeName: UIColor.whiteColor(),
NSParagraphStyleAttributeName: paraAttrib]
let xLoc : CGFloat = CGFloat(xLabelLocs[i] - 25)
let yLoc : CGFloat = yval - 8.0
str.drawInRect(CGRectMake(xLoc, yLoc, 50, 16), withAttributes: attributes)
I want to create a particle system on iOS using sprite kit where I define the colour of each individual particle. As far as I can tell this isn't possible with the existing SKEmitterNode.
It seems that best I can do is specify general behaviour. Is there any way I can specify the starting colour and position of each particle?
This can give you a basic idea what I was meant in my comments. But keep in mind that it is untested and I am not sure how it will behave if frame rate drops occur.
This example creates 5 particles per second, add them sequentially (in counterclockwise direction) along the perimeter of a given circle. Each particle will have different predefined color. You can play with Settings struct properties to change the particle spawning speed or to increase or decrease number of particles to emit.
Pretty much everything is commented, so I guess you will be fine:
Swift 2
import SpriteKit
struct Settings {
static var numberOfParticles = 30
static var particleBirthRate:CGFloat = 5 //Means 5 particles per second, 0.2 means one particle in 5 seconds etc.
}
class GameScene: SKScene {
var positions = [CGPoint]()
var colors = [SKColor]()
var emitterNode:SKEmitterNode?
var currentPosition = 0
override func didMoveToView(view: SKView) {
backgroundColor = .blackColor()
emitterNode = SKEmitterNode(fileNamed: "rain.sks")
if let emitter = emitterNode {
emitter.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame))
emitter.particleBirthRate = Settings.particleBirthRate
addChild(emitter)
let radius = 50.0
let center = CGPointZero
for var i = 0; i <= Settings.numberOfParticles; i++ {
//Randomize color
colors.append(SKColor(red: 0.78, green: CGFloat(i*8)/255.0, blue: 0.38, alpha: 1))
//Create some points on a perimeter of a given circle (radius = 40)
let angle = Double(i) * 2.0 * M_PI / Double(Settings.numberOfParticles)
let x = radius * cos(angle)
let y = radius * sin(angle)
let currentParticlePosition = CGPointMake(CGFloat(x) + center.x, CGFloat(y) + center.y)
positions.append(currentParticlePosition)
if i == 1 {
/*
Set start position for the first particle.
particlePosition is starting position for each particle in the emitter's coordinate space. Defaults to (0.0, 0,0).
*/
emitter.particlePosition = positions[0]
emitter.particleColor = colors[0]
self.currentPosition++
}
}
// Added just for debugging purposes to show positions for every particle.
for particlePosition in positions {
let sprite = SKSpriteNode(color: SKColor.orangeColor(), size: CGSize(width: 1, height: 1))
sprite.position = convertPoint(particlePosition, fromNode:emitter)
sprite.zPosition = 2
addChild(sprite)
}
let block = SKAction.runBlock({
// Prevent strong reference cycles.
[unowned self] in
if self.currentPosition < self.positions.count {
// Set color for the next particle
emitter.particleColor = self.colors[self.currentPosition]
// Set position for the next particle. Keep in mind that particlePosition is a point in the emitter's coordinate space.
emitter.particlePosition = self.positions[self.currentPosition++]
}else {
//Stop the action
self.removeActionForKey("emitting")
emitter.particleBirthRate = 0
}
})
// particleBirthRate is a rate at which new particles are generated, in particles per second. Defaults to 0.0.
let rate = NSTimeInterval(CGFloat(1.0) / Settings.particleBirthRate)
let sequence = SKAction.sequence([SKAction.waitForDuration(rate), block])
let repeatAction = SKAction.repeatActionForever(sequence)
runAction(repeatAction, withKey: "emitting")
}
}
}
Swift 3.1
import SpriteKit
struct Settings {
static var numberOfParticles = 30
static var particleBirthRate:CGFloat = 5 //Means 5 particles per second, 0.2 means one particle in 5 seconds etc.
}
class GameScene: SKScene {
var positions = [CGPoint]()
var colors = [SKColor]()
var emitterNode: SKEmitterNode?
var currentPosition = 0
override func didMove(to view: SKView) {
backgroundColor = SKColor.black
emitterNode = SKEmitterNode(fileNamed: "rain.sks")
if let emitter = emitterNode {
emitter.position = CGPoint(x: frame.midX, y: frame.midY)
emitter.particleBirthRate = Settings.particleBirthRate
addChild(emitter)
let radius = 50.0
let center = CGPoint.zero
for var i in 0...Settings.numberOfParticles {
//Randomize color
colors.append(SKColor(red: 0.78, green: CGFloat(i * 8) / 255.0, blue: 0.38, alpha: 1))
//Create some points on a perimeter of a given circle (radius = 40)
let angle = Double(i) * 2.0 * Double.pi / Double(Settings.numberOfParticles)
let x = radius * cos(angle)
let y = radius * sin(angle)
let currentParticlePosition = CGPoint.init(x: CGFloat(x) + center.x, y: CGFloat(y) + center.y)
positions.append(currentParticlePosition)
if i == 1 {
/*
Set start position for the first particle.
particlePosition is starting position for each particle in the emitter's coordinate space. Defaults to (0.0, 0,0).
*/
emitter.particlePosition = positions[0]
emitter.particleColor = colors[0]
self.currentPosition += 1
}
}
// Added just for debugging purposes to show positions for every particle.
for particlePosition in positions {
let sprite = SKSpriteNode(color: SKColor.orange, size: CGSize(width: 1, height: 1))
sprite.position = convert(particlePosition, from: emitter)
sprite.zPosition = 2
addChild(sprite)
}
let block = SKAction.run({
// Prevent strong reference cycles.
[unowned self] in
if self.currentPosition < self.positions.count {
// Set color for the next particle
emitter.particleColor = self.colors[self.currentPosition]
// Set position for the next particle. Keep in mind that particlePosition is a point in the emitter's coordinate space.
emitter.particlePosition = self.positions[self.currentPosition]
self.currentPosition += 1
} else {
//Stop the action
self.removeAction(forKey: "emitting")
emitter.particleBirthRate = 0
}
})
// particleBirthRate is a rate at which new particles are generated, in particles per second. Defaults to 0.0.
let rate = TimeInterval(CGFloat(1.0) / Settings.particleBirthRate)
let sequence = SKAction.sequence([SKAction.wait(forDuration: rate), block])
let repeatAction = SKAction.repeatForever(sequence)
run(repeatAction, withKey: "emitting")
}
}
}
Orange dots are added just for debugging purposes and you can remove that part if you like.
Personally I would say that you are overthinking this, but I might be wrong because there is no clear description of what you are trying to make and how to use it. Keep in mind that SpriteKit can render a bunch of sprites in a single draw call in very performant way. Same goes with SKEmitterNode if used sparingly. Also, don't underestimate SKEmitterNode... It is very configurable actually.
Here is the setup of Particle Emitter Editor:
Anyways, here is the final result:
Note that nodes count comes from an orange SKSpriteNodes used for debugging. If you remove them, you will see that there is only one node added to the scene (emitter node).
What you want is completely possible, probably even in real time. Unfortunately to do such a thing the way you describe with moving particles as being a particle for each pixel would be best done with a pixel shader. I don't know of a clean method that would allow you to draw on top of the scene with a pixel shader otherwise all you would need is a pixel shader that takes the pixels and moves them out from the center. I personally wouldn't try to do this unless I built the game with my own custom game engine in place of spritekit.
That being said I'm not sure a pixel per pixel diffusion is the best thing in most cases. Expecially if you have cartoony art. Many popular games will actually make sprites for fragments of the object they expect to shader. So like if it's an airplane you might have a sprite for the wings with perhaps even wires hanging out of this. Then when it is time to shatter the plane, remove it from the scene and replace the area with the pieces in the same shape of the plane... Sorta like a puzzle. This will likely take some tweaking. Then you can add skphysicsbodies to all of these pieces and have a force push them out in all directions. Also this doesn't mean that each pixel gets a node. I would suggest creatively breaking it into under 10 pieces.
And as whirlwind said you could all ways get things looking "like" it actually disintegrated by using an emitter node. Just make the spawn area bigger and try to emulate the color as much as possible. To make the ship dissappear you could do a fade perhaps? Or Mabye an explosion sprite over it? Often with real time special effects and physics, or with vfx it is more about making it look like reality then actually simulating reality. Sometimes you have to use trickery to get things to look good and run real-time.
If you want to see how this might look I would recommend looking at games like jetpac joyride.
Good luck!