Best practise for managing UI in SpriteKit - sprite-kit

In SpriteKit, I'm using SKNodes, SKShapeNodes and SKLabelNodes for all my UI. It works well for the small game I've made, however, one small interface such as a 'Game Over' prompt can consist of well over 100 lines of code, and that's merely declaring and defining node properties. All these prompts and other UI are declared in their own separate functions and are called when needed throughout the Game Scene, but I can't shake the feeling that there is a better practise to managing UI. Perhaps a class where I can call the 'getGameOverPromptUI' or 'getScoreLabel' functions? Would I limit such a class to only the Game Scene? or include UI for the Menu Scene?
Edit:
To clarify, a Game Over prompt consists of a faded background covering the scene, a rectangle as the prompt box behind the text, labels for the game over title, the current score, the highest score, a button for playing again, a button for going back to the menu etc.
The reason for the code being so large is every component is pretty much created like this
let gameOverNode = SKNode()
gameOverNode.position = CGPoint(x: 0, y: screenSize.height / 16)
addChild(gameOverNode)
// Faded Background
let fadedBackgroundNode = SKShapeNode(rectOf: screenSize)
fadedBackgroundNode.fillColor = UIColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 0.6)
fadedBackgroundNode.zPosition = 1
fadedBackgroundNode.position = CGPoint(x: -gameOverNode.position.x, y: -gameOverNode.position.y)
gameOverNode.addChild(fadedBackgroundNode)
// Prompt Background
let gameOverGradientNode = SKShapeNode(rectOf: gameOverGradientNodeSize, cornerRadius: gameOverGradientCornerRadius)
gameOverGradientNode.fillColor = .white
gameOverGradientNode.strokeColor = .clear
let gameOverCropNode = SKCropNode()
gameOverCropNode.maskNode = gameOverGradientNode
gameOverCropNode.addChild(helper.getBackgroundNode2(nodeSize: gameOverGradientNode.frame.size))
gameOverCropNode.zPosition = 2
gameOverNode.addChild(gameOverCropNode)
// Title Label
let gameOverLabel = SKLabelNode(fontNamed: "")
gameOverLabel.name = "gameOverLabel"
gameOverLabel.text = "Game Over"
gameOverLabel.position = CGPoint(x: 0, y: gameOverGradientNode.frame.height / 4 + gameOverGradientNode.frame.height / 8)
gameOverLabel.fontSize = 160
gameOverLabel.fontColor = .white
gameOverLabel.zPosition = 3
gameOverLabel.numberOfLines = 2
gameOverLabel.horizontalAlignmentMode = .center
gameOverNode.addChild(gameOverLabel)

Related

Issue with invisible Shadow Plane in SceneKit / ARKit

Since a while I am looking around to find answers to my Issue, but cannot find anything that really helps, or explains what is happening. I also over and over checked everything I found on SO, but couldn't find the answer.
The Issue is happening continuously when displaying Objects in the AR World. iEx I place an object to a Plane on the floor, which is my invisible Shadow Plane. Then it depends all on the viewing angle from the device. To clarify, I added to images, which has just a slightly different viewing angle. Have a look what is happening to the shadows:
I would like to have a good shadow all the time and not such artefacts as you can see.
Note: I already played around using the shadowSampleCount, the Bias, and all the other options, that should help to get a proper, low rendering cost shadow.
Here are is the extract of the relevant code for Lighting and Plane, Material, etc
For the SCNLight:
class func directionalLight() -> SCNLight {
let light = SCNLight()
light.type = .directional
light.castsShadow = true
light.color = UIColor.white
light.shadowMode = .deferred
light.shadowSampleCount = 8
light.shadowRadius = 1
// light.automaticallyAdjustsShadowProjection = false
light.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
light.categoryBitMask = -1
return light
}
and how I add it:
func setupLights() {
lightDirectionNode.light = Lighting.directionalLight()
// lightDirectionNode.eulerAngles = SCNVector3(-66.degreesToRadians, 0, 0)
lightDirectionNode.eulerAngles = SCNVector3(0, 90.degreesToRadians, 45.degreesToRadians)
sceneView.scene.rootNode.addChildNode(lightDirectionNode)
}
For the SCNPlane:
class func shadowPlane() -> SCNNode {
let objectShape = SCNPlane(width: 200, height: 200)
objectShape.heightSegmentCount = 2
objectShape.widthSegmentCount = 2
objectShape.cornerRadius = 100
objectShape.cornerSegmentCount = 16
let objectNode = SCNNode(geometry: objectShape)
objectNode.geometry?.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
objectNode.geometry?.firstMaterial?.colorBufferWriteMask = SCNColorMask(rawValue: 0)
objectNode.physicsBody = Physics.floorPhysicsBody(shape: objectShape)
objectNode.name = "floor"
objectNode.renderingOrder = -10 // renderingOrder // 0
return objectNode
}
and how I add it:
func setupShadowPlane() {
let shadowPlane = NodeFactory.shadowPlane()
// Set the Node's properties
shadowPlane.position = SCNVector3(x: (focusSquare.lastPosition?.x)!, y: (focusSquare.lastPosition?.y)!, z: (focusSquare.lastPosition?.z)!)
shadowPlane.eulerAngles = SCNVector3(-90.degreesToRadians, 0.0, 0.0)
sceneView.scene.rootNode.addChildNode(shadowPlane)
}
What am I doing wrong? Can anyone help?
There are 3 more instance properties to take into consideration:
var shadowRadius: CGFloat { get set }
var shadowCascadeCount: Int { get set }
var shadowCascadeSplittingFactor: CGFloat { get set }
If you don't setup these ones they definitely cause rendering artifacts.
let lightNode = SCNNode()
lightNode.light = SCNLight()
// POSITION OF DIRECTIONAL LIGHT ISN'T IMPORTANT.
// ONLY DIRECTION IS CRUCIAL FOR DIRECTIONAL LIGHTS.
lightNode.rotation = SCNVector4(x: 0, y: 0, z: 0, w: 1)
lightNode.light!.type = .directional
lightNode.light!.castsShadow = true
lightNode.light?.shadowMode = .deferred
/* THREE INSTANCE PROPERTIES TO SETUP */
lightNode.light?.shadowRadius = 3.25
lightNode.light?.shadowCascadeCount = 3
lightNode.light?.shadowCascadeSplittingFactor = 0.09
lightNode.light?.shadowColor = UIColor(white: 0, alpha: 0.75)
scene.rootNode.addChildNode(lightNode)
And one more thing – when Auto Adjust is off:
Light, like a camera, has near and far clipping planes for setup.
lightNode.light?.zNear = 0
lightNode.light?.zFar = 1000000 // Far Clipping Plane is important
Hope this helps.

Swift: Make translucent overlapping lines of the same color not change color when intersecting

Currently I have drawn 2 lines on the screen which are the same color but both have an alpha value less than 1. When these lines intersect, the intersection is a different color than the rest of the lines. There was a previous post addressing the same issue: swift drawing translucent lines, how to make overlapping parts not getting darker? However, this post wasn't sufficiently answered. I draw the lines currently like this:
var points = [CGPoint]()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 0.5)
FirstLine.lineWidth = 30
addChild(FirstLine)
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 0.5)
SecondLine.lineWidth = 30
addChild(SecondLine)
Here is what it looks like:
I understand why this occurs, but is there any way to make the intersection look the same so it looks much better?
Edit: I have decided to implement #Confused's answer. However, the problem now is that the texture always centres to the middle of the screen. This is an example:
The red cross is in its correct position as it is connecting the specified points together. However, once I make the red cross a texture, it always centres to the middle of the screen (the green cross is the red cross as a texture). Can I use any code that could re-position the texture to its correct position. Note: this code can't just work for this example, I need one that works all the time regardless of the red cross' position.
FINAL EDIT FOR PEOPLE WITH THE SAME PROBLEM
First, setup everything like this:
var points = [CGPoint]()
let crossParent = SKNode()
addChild(crossParent)
Please note THAT YOU MUST create a parent SKNode for the texture otherwise everything on the screen will become the texture and not just the node that you want. Then, add that parent node to the scene.
After, create the lines that you want (in this case the green cross):
//The first line of the green cross
points = [CGPoint(x: -300, y: 300), CGPoint(x: -100, y: 100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1.0)
FirstLine.lineWidth = 30
crossParent.addChild(FirstLine)
Remember to NOT add the first line that you create to the scene, but rather to the parent SKNode that you made at the beginning. Also, set the alpha value to 1.0 for every line that you draw. Then add your other lines:
//The second line of the green cross
points = [CGPoint(x: -100, y: 300), CGPoint(x: -300, y: 100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1.0)
SecondLine.lineWidth = 30
FirstLine.addChild(SecondLine)
Note that you MUST add that line to the first line like this and not to the scene. If you are adding more than one line after adding the first line, then add it to the first line like I have done here too for each consecutive
line that you add.
Now, create the texture like this:
let tex = view?.texture(from: FirstLine)
let cross = SKSpriteNode(texture: tex, color: .clear, size: (tex?.size())!)
cross.alpha = 0.5
addChild(cross)
After doing this, whatever you call cross will be your texture, and you can change the alpha value like I have done here to anything you like and the image will not have varying colors. Remember to then add that texture to the scene.
Finally, you may notice that the texture is not in the same position as where you originally put the points. You can put it back into the same position like this:
cross.position = CGPoint(x: (FirstLine.frame.midX), y: (FirstLine.frame.midY))
Hope this helps :) Thank you to #Confused for the texture part of the program :D
This is a technique. It does not solve your problem, nor directly answer your question. Instead it offers a way to have the desired result, but without the flexibility and inherent nature of lines you actually want.
You can create textures from anything you draw with any number of nodes, with any number of techniques.
You do this (easiest way) by attaching all the drawing elements to a single node, within an SKView space that you have access to, and then render the "parent" node of your drawn objects to a texture.
How does this help?
I'm glad you asked:
You can draw everything at an opacity level of 100%, and render it to a texture, then take that texture of your drawings, put it where you like, and reduce its opacity to any percentage you like, and get an even result. No bright spots where things overlay each other.
Here's code that does all the above:
var points = [CGPoint]()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
FirstLine.lineWidth = 30
// ^^ Note the FirstLine is not added to the Scene
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
SecondLine.lineWidth = 30
FirstLine.addChild(SecondLine)
// ^^ Note SecondLine being added to FirstLine, and that they both have alpha of 1
// Now the magic: use the view of the SKScene to render FirstLine and its child (SecondLine)
// They are rendered into a texture, named, imaginatively, "tex"
let tex = view.texture(from: FirstLine)
let cross = SKSpriteNode(texture: tex, color: .clear, size: (tex?.size())!)
cross.alpha = 0.5
// ^^ The alpha of the above sprite is set to your original desire of 0.5
// And then added to the scene, with the desired result.
addChild(cross)
and here's the result:
NO! I think is, unfortunately, the answer.
There are the following blend modes, none of which provide the result you're looking for:
case alpha // Blends the source and destination colors by multiplying the source alpha value.
case add // Blends the source and destination colors by adding them up.
case subtract // Blends the source and destination colors by subtracting the source from the destination.
case multiply // Blends the source and destination colors by multiplying them.
case multiplyX2 // Blends the source and destination colors by multiplying them and doubling the result.
case screen // Blends the source and destination colors by multiplying one minus the source with the destination and adding the source.
case replace // Replaces the destination with the source (ignores alpha).
The only option that might work is a custom shader that figures out if it's overlapping itself, somehow. But I have NO CLUE where to even start making something like that, or if it's even possible.
Make the stroke color fully opaque.
Add your path nodes to an SKEffectNode.
Set the alpha of the effect node to the desired value.
Put the effect node into your main scene.
While doing this you can also put all lines in one shape node:
let path = CGMutablePath()
path.move( to: CGPoint(x: -100, y: 100))
path.addLine(to: CGPoint(x: 100, y: -100))
path.move( to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: -100, y: -100))
let shape = SKShapeNode(path: path)
shape.strokeColor = UIColor(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
shape.lineWidth = 30
let effect = SKEffectNode()
effect.addChild(shape)
effect.alpha = 0.5
addChild(effect)
The problem is you are doing the alpha blend at the color level, do the alpha blending at the node level
var points = [CGPoint]()
var cross = SKEffectNode()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
FirstLine.lineWidth = 30
cross.addChild(FirstLine)
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
SecondLine.lineWidth = 30
cross.addChild(SecondLine)
cross.alpha = 0.5
addChild(cross)

Blend Mode between NSWindow and CAShapeLayer

I'm trying to make a semi-transparent window (white), and have a view that sits over the top of it to make it appear clear again.
Essentially, it looks like:
And what I want it to look like is:
My window is defined like:
window!.alphaValue = 0;
// Set level to screensaver level
window!.level = 1000
window!.backgroundColor = NSColor.white;
window!.animator().alphaValue = 0.5;
A white window with 50% opacity. The view on top contains a single CAShapeLayer. This layer is defined like:
shapeLayer = CAShapeLayer();
shapeLayer!.lineWidth = 1.0;
shapeLayer!.strokeColor = NSColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1).cgColor;
shapeLayer!.fillColor = NSColor(red: 0, green: 0, blue: 0, alpha: 0.5).cgColor;
This window is movable by the user, so I need some kind of dynamic way to clear the contents of the main window and show through to whatever is underneath (the desktop, another application, etc)

Placing UILabel in the center of screen

I have a UILabel which I want to be displayed at the center of screen regardless of which iPhone it is played on!
But I can't get it right! It's always somewhere else but not in the middle.
what am i doing wrong?
Here is the picture and code of what is happening!
class end: SKScene {
var label = UILabel()
override func didMoveToView(view: SKView) {
scene?.backgroundColor = UIColor(red: CGFloat(59.0/255.0), green: CGFloat(89.0/255.0), blue: CGFloat(152.0/255.0), alpha: CGFloat(1.0))
let w = UIScreen.mainScreen().bounds.width
let h = UIScreen.mainScreen().bounds.height
label = UILabel(frame: CGRect(x: w / 2, y: h / 2, width: 120, height: 30))
label.text = "REPLAY"
label.center = CGPoint(x: w / 2, y: 2)
label.textColor = UIColor.whiteColor()
self.view?.addSubview(label)
I think your label actually is centered.
If I add a red background color like so:
label.backgroundColor = UIColor.redColor()
and change the vertical center to be the center of the screen, that is, changes this line in your example:
label.center = CGPoint(x: w / 2, y: 2)
to this:
label.center = CGPoint(x: w / 2, y: h / 2)
I end up with this:
So, the label actually is centered. The text however, is not centered in the label, which makes it seems as if the text is not centered when you have no background color.
If I change the textAlignment to .Center and remove the background color again I end up with this result:
The final code example looks like this:
override func didMoveToView(view: SKView) {
scene?.backgroundColor = UIColor(red: CGFloat(59.0/255.0), green: CGFloat(89.0/255.0), blue: CGFloat(152.0/255.0), alpha: CGFloat(1.0))
let w = UIScreen.mainScreen().bounds.width
let h = UIScreen.mainScreen().bounds.height
label = UILabel(frame: CGRect(x: w / 2, y: h / 2, width: 120, height: 30))
label.text = "REPLAY"
label.center = CGPoint(x: w / 2, y: h / 2)
label.textAlignment = .Center
label.textColor = UIColor.whiteColor()
self.view?.addSubview(label)
}
And when all that is said, then maybe you should look into the SKLabelNode as #whirlwind suggests in the comment, that way your label becomes an integrated part of the SKScene and not something that is bolted to the outer UIView (if that makes any sense to you).
Hope this helps you.
Best option is going for autolayout. You can set it vertically and horizontally aligned to view. Regardless of display or orientation it works.
You can use either interface builder or you can add autolayoutconstraints
The easiest way would be to use:
label.center = view.center

SpriteKit: How to spawn object in exact place?

I'm in the middle of making a simple game in XCode 7, using sprite kit. I currently have a square in the middle of the scene and I have set up swipe gestures which make smaller squares "shoot" out of the left, right, top of bottom of the square, depending on the way of the swipe gesture. They go in a straight direction and I would like other random "Enemy" squares to come in the opposite way, again in the same line of path as the squares that are shot out so they can collide... Any ideas on how to do this?
Here's the basic idea, same for the right, up and down:
func swipedLeft(){
print("SwipedLeft")
let SmallSquare1 = SKSpriteNode(imageNamed: "Square")
SmallSquare1.position = MainSquare.position
SmallSquare1.size = CGSize(width: 30, height: 30)
SmallSquare1.physicsBody = SKPhysicsBody(circleOfRadius: SmallSquare1.size.width / 2)
SmallSquare1.physicsBody?.affectedByGravity = false
self.addChild(SmallSquare1)
let moveLeft = SKAction.moveByX(-frame.size.width, y: 0, duration: 2)
SmallSquare1.runAction(moveLeft)
Here's the Enemy function I'm stuck on...
func Enemies(){
let Enemy = SKSpriteNode(imageNamed: "Square")
Enemy.size = CGSize(width: 20, height: 20)
Enemy.color = UIColor(red: 0.9, green: 0.1, blue: 0.1, alpha: 1.0)
Enemy.colorBlendFactor = 1.0
}
For the one on the left:
Enemy.position = CGPoint(x: (any X position), y: MainSquare.position.y)
And do the same for the right side.
And for above and below just do the MainSquare.position.x as the x position