I created a SKSPriteNode without texture but color
let tileNode = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 80.0, height: 120.0))
tileNode.position = CGPointMake(50, 50)
tileNode.name = "rectangle"
addChild(tileNode)
But the node doesn't display on the screen.
However, I can detect it with touch collision
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
let location: CGPoint = (touches.anyObject() as UITouch).locationInNode(tilesLayer)
let rect: SKSpriteNode = tilesLayer.childNodeWithName("rectangle") as SKSpriteNode
if rect.containsPoint(location) {
println("TOUCHED") //It works
}
}
EDIT : The SKSpriteNode color is only hidden if there is a background SKSpriteNode texture. Complete code : https://gist.github.com/BabyAzerty/9dca752d9faa7b768bf0
I believe you created your project using the Game project template in Xcode, correct? Your issue is this line in GameViewController.m (roughly line 41).
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
If you set that to NO, then it will render as you expect. I build my game projects from empty projects, so I never had that property set. Just search your code for ignoresSiblingOrder
Of course if you want that rendering optimization, then you can always use zPosition.
I noticed that the difference between creating a SKSpriteNode with texture and a SKSpriteNode with color is that the texture version doesn't need to have its z-index modified. It will display over the previous addChild(someNodes) automatically.
They both have a default z-index of 0 but for some reasons SKSpriteNode with color is hidden behind your previous addChild(someNodes).
This line should do the trick :
tileNode.zPosition = 1 //Or higher
However, I decided to create an enum that controls the different z-layers (just like the one in Unity Engine) :
enum ZLayer: CGFloat {
case Undefined = 0
case Background
case Foreground
case GUI
}
And use it like this :
tileNode.zPosition = ZLayer.Background.rawValue
The advantage of this enum is that if you need to add a new layer between background and foreground layers, you don't need to bother with the values at all, just an entry in the enum between the 2 cases.
Related
For a game I'm creating, an SKSpriteNode gradually makes its way down the user's screen. There's another SKSpriteNode (position is static) near the bottom of the screen, leaving only a little bit of space for the original SKSpriteNode to fit in. I need to detect when the first SKSpriteNode is COMPLETELY below the second SKSpriteNode, which I'm having a bit of trouble with. Here's the code I'm currently using:
if (pos.y > barPosY) //pos.y = 1st SKSpriteNode, barPosY = 2nd SKSpriteNode
{
touchedTooEarly = true
}
For some reason, when the first SKSpriteNode goes just a little bit over the 2nd SKSpriteNode (not all the way), it still detects it as being completely over. Is there a coordinate space issue I'm missing?
The logic
A sprite a covers a sprite b if
b.frame is inside a.frame
b.zPosition is below a.zPosition
The extension
Now let's build an extension
extension SKSpriteNode {
func isCoveredBy(otherSprite: SKSpriteNode) -> Bool {
let otherFrame = CGRect(
origin: convertPoint(otherSprite.position, fromNode: otherSprite),
size: otherSprite.frame.size
)
return zPosition < otherSprite.zPosition && CGRectContainsRect(frame, otherFrame)
}
}
Problem #1: transparency
This mechanism totally ignores transparency.
Problem #2: same sprite
If you compare 2 sprites of the same type the function CGRectContainsRect will return true only when they are exactly aligned. Althoug you can solve this problem creating a smaller rectangle when you compare the sprites.
I have a game made of little SKNodes moving around. They can not overlap, so I use physics bodies, so that they move around each other. However, in the case that one sknode is animated to follow another, it pushes the sknode ahead. Setting the collision bitmask to 0 makes them overlap, so that is not an option. But otherwise, they push each other way beyond my desired speed. I need a way to get rid of the 'pushing' without overlapping using skphysics bodies. Is there a property I can set to fix this?
Notes: I use skanimations to move my nodes around. If you need any pertinent code, tell me... I don't know where to start. I am using Swift 3.0, but I will accept answers with 2.2/2.3 syntax.
EDIT: The real solution was to change the node's velocity instead of animating movement with SK Actions.
Change the restitution on your physics body of the moving object to 0 when a contact happens, and set the weight of the object you do not want to move really high
Have you tried setting the dynamic property to false on the body being pushed?
After you have set contactTestBitMask you could set this:
myHero.physicsBody!.collisionBitMask = 0
to prevent collisions but permit contacts.
Updating position of dynamic physics body with SKAction doesn't work well. It's just like saying - Hey, no matter what physics world rules say just move my node like I want.
First solution - to make one node follow another with some particular distance use SKConstraint.
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let nodeA = SKSpriteNode(color: SKColor.greenColor(), size: CGSize(width: 50, height: 50))
nodeA.name = "nodeA"
addChild(nodeA)
let nodeB = SKSpriteNode(color: SKColor.redColor(), size: nodeA.size)
nodeB.name = "nodeB"
addChild(nodeB)
let followConstraint = SKConstraint.distance(SKRange.init(lowerLimit: nodeB.size.width * 1.5, upperLimit: nodeB.size.width * 1.5), toNode: nodeA)
nodeB.constraints = [followConstraint]
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch = touches.first else {
return
}
childNodeWithName("nodeA")!.runAction(SKAction.moveTo(touch.locationInNode(self), duration: 2))
}
}
If you still want physics bodies and SKActions...
Set "dynamic" property of every physics body to false.
Check in didBeginContact (SKPhysicsContactDelegate protocol) if they are overlapping. If it's true than remove all the actions that are changing their positions. Tip - make the physics body little bigger than it's node.
I'm currently working on a SpriteKit project and need to create a comet with a fading tail that animates across the screen. I am having serious issues with SpriteKit in this regards.
Attempt 1. It:
Draws a CGPath and creates an SKShapeNode from the path
Creates a square SKShapeNode with gradient
Creates an SKCropNode and assigns its maskNode as line, and adds square as a child
Animates the square across the screen, while being clipped by the line/SKCropNode
func makeCometInPosition(from: CGPoint, to: CGPoint, color: UIColor, timeInterval: NSTimeInterval) {
... (...s are (definitely) irrelevant lines of code)
let path = CGPathCreateMutable()
...
let line = SKShapeNode(path:path)
line.lineWidth = 1.0
line.glowWidth = 1.0
var squareFrame = line.frame
...
let square = SKShapeNode(rect: squareFrame)
//Custom SKTexture Extension. I've tried adding a normal image and the leak happens either way. The extension is not the problem
square.fillTexture = SKTexture(color1: UIColor.clearColor(), color2: color, from: from, to: to, frame: line.frame)
square.fillColor = color
square.strokeColor = UIColor.clearColor()
square.zPosition = 1.0
let maskNode = SKCropNode()
maskNode.zPosition = 1.0
maskNode.maskNode = line
maskNode.addChild(square)
//self is an SKScene, background is an SKSpriteNode
self.background?.addChild(maskNode)
let lineSequence = SKAction.sequence([SKAction.waitForDuration(timeInterval), SKAction.removeFromParent()])
let squareSequence = SKAction.sequence([SKAction.waitForDuration(1), SKAction.moveBy(CoreGraphics.CGVectorMake(deltaX * 2, deltaY * 2), duration: timeInterval), SKAction.removeFromParent()])
square.runAction(SKAction.repeatActionForever(squareSequence))
maskNode.runAction(lineSequence)
line.runAction(lineSequence)
}
This works, as shown below.
The problem is that after 20-40 other nodes come on the screen, weird things happen. Some of the nodes on the screen disappear, some stay. Also, the fps and node count (toggled in the SKView and never changed)
self.showsFPS = true
self.showsNodeCount = true
disappear from the screen. This makes me assume it's a bug with SpriteKit. SKShapeNode has been known to cause issues.
Attempt 2. I tried changing square from an SKShapeNode to an SKSpriteNode (Adding and removing lines related to the two as necessary)
let tex = SKTexture(color1: UIColor.clearColor(), color2: color, from: from, to: to, frame: line.frame)
let square = SKSpriteNode(texture: tex)
the rest of the code is basically identical. This produces a similar effect with no bugs performance/memory wise. However, something odd happens with SKCropNode and it looks like this
It has no antialiasing, and the line is thicker. I have tried changing anti-aliasing, glow width, and line width. There is a minimum width that can not change for some reason, and setting the glow width larger does this
. According to other stackoverflow questions maskNodes are either 1 or 0 in alpha. This is confusing since the SKShapeNode can have different line/glow widths.
Attempt 3. After some research, I discovered I might be able to use the clipping effect and preserve line width/glow using an SKEffectNode instead of SKCropNode.
//Not the exact code to what I tried, but very similar
let maskNode = SKEffectNode()
maskNode.filter = customLinearImageFilter
maskNode.addChild(line)
This produced the (literally) exact same effect as attempt 1. It created the same lines and animation, but the same bugs with other nodes/fps/nodeCount occured. So it seems to be a bug with SKEffectNode, and not SKShapeNode.
I do not know how to bypass the bugs with attempt 1/3 or 2.
Does anybody know if there is something I am doing wrong, if there is a bypass around this, or a different solution altogether for my problem?
Edit: I considered emitters, but there could potentially be hundreds of comets/other nodes coming in within a few seconds and didn't think they would be feasible performance-wise. I have not used SpriteKit before this project so correct me if I am wrong.
This looks like a problem for a custom shader attached to the comet path. If you are not familiar with OpenGL Shading Language (GLSL) in SpriteKit it lets you jump right into the GPU fragment shader specifically to control the drawing behavior of the nodes it is attached to via SKShader.
Conveniently the SKShapeNode has a strokeShader property for hooking up an SKShader to draw the path. When connected to this property the shader gets passed the length of the path and the point on the path currently being drawn in addition to the color value at that point.*
controlFadePath.fsh
void main() {
//uniforms and varyings
vec4 inColor = v_color_mix;
float length = u_path_length;
float distance = v_path_distance;
float start = u_start;
float end = u_end;
float mult;
mult = smoothstep(end,start,distance/length);
if(distance/length > start) {discard;}
gl_FragColor = vec4(inColor.r, inColor.g, inColor.b, inColor.a) * mult;
}
To control the fade along the path pass a start and end point into the custom shader using two SKUniform objects named u_start and u_end These get added to the custom shader during initialization of a custom SKShapeNode class CometPathShape and animated via a custom Action.
class CometPathShape:SKShapeNode
class CometPathShape:SKShapeNode {
//custom shader for fading
let pathShader:SKShader
let fadeStartU = SKUniform(name: "u_start",float:0.0)
let fadeEndU = SKUniform(name: "u_end",float: 0.0)
let fadeAction:SKAction
override init() {
pathShader = SKShader(fileNamed: "controlFadePath.fsh")
let fadeDuration:NSTimeInterval = 1.52
fadeAction = SKAction.customActionWithDuration(fadeDuration, actionBlock:
{ (node:SKNode, time:CGFloat)->Void in
let D = CGFloat(fadeDuration)
let t = time/D
var Ps:CGFloat = 0.0
var Pe:CGFloat = 0.0
Ps = 0.25 + (t*1.55)
Pe = (t*1.5)-0.25
let comet:CometPathShape = node as! CometPathShape
comet.fadeRange(Ps,to: Pe) })
super.init()
path = makeComet...(...) //custom method that creates path for comet shape
strokeShader = pathShader
pathShader.addUniform(fadeStartU)
pathShader.addUniform(fadeEndU)
hidden = true
//set up for path shape, eg. strokeColor, strokeWidth...
...
}
func fadeRange(from:CGFloat, to:CGFloat) {
fadeStartU.floatValue = Float(from)
fadeEndU.floatValue = Float(to)
}
func launch() {
hidden = false
runAction(fadeAction, completion: { ()->Void in self.hidden = true;})
}
...
The SKScene initializes the CometPathShape objects, caches and adds them to the scene. During update: the scene simply calls .launch() on the chosen CometPathShapes.
class GameScene:SKScene
...
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.name = "theScene"
...
//create a big bunch of paths with custom shaders
print("making cache of path shape nodes")
for i in 0...shapeCount {
let shape = CometPathShape()
let ext = String(i)
shape.name = "comet_".stringByAppendingString(ext)
comets.append(shape)
shape.position.y = CGFloat(i * 3)
print(shape.name)
self.addChild(shape)
}
override func update(currentTime: CFTimeInterval) {
//pull from cache and launch comets, skip busy ones
for _ in 1...launchCount {
let shape = self.comets[Int(arc4random_uniform(UInt32(shapeCount)))]
if shape.hasActions() { continue }
shape.launch()
}
}
This cuts the number of SKNodes per comet from 3 to 1 simplifying your code and the runtime environment and it opens the door for much more complex effects via the shader. The only drawback I can see is having to learn some GLSL.**
*not always correctly in the device simulator. Simulator not passing distance and length values to custom shader.
**that and some idiosyncrasies in CGPath glsl behavior. Path construction is affecting the way the fade performs. Looks like v_path_distance is not blending smoothly across curve segments. Still, with care constructing the curve this should work.
I'm trying to recreate something for a iOS app (Swift) which I already made in HTML5 (using map area-coords).
I want to show a human body diagram that interacts with user clicks/touches. The body exists of let's say 20 different parts, and the user can select one or more body-parts. For every body-part there is a selected-state image that should appear when a part is selected. Clicking on a selected part will deselect it. After selecting one or more parts, the user can continue and will get some information about these parts in a next viewcontroller. I am attaching a simplified image to explain my goal.
Can somebody explain what the best way is to achieve this goal? Is there a comparable technique that can be used in Swift to create such an interactive image?
Thanks!
There is no built-in mechanism for detecting taps in irregular shapes. The standard UIView tap detection uses frame rectangles. (Likewise with CALayers, as suggested by sketchyTech in his comment above.)
I would suggest drawing your body regions as bezier paths. You could create a custom subclass of UIView (BodyView) that would manage an array of BodyRegion objects each of which include a bezier path.
The UIBezierPath class includes a method containsPoint that lets you tell if a point is inside the path. Your BodyView could us that to decide which path object was tapped.
Bezier paths would handle both drawing your body regions and figuring out which part was tapped.
A very simple way to achieve this, would be to place invisible buttons over the areas, then hook every one up to an IBAction and put anything you want to happen inside.
#IBAction func shinTapped(sender: UIButton) {
tappedCounter = 0
if tappedCounter == 0 {
// set property to keep track
shinSelected = true
// TODO: set button image to selected state
// increment counter
tappedCounter++
}else{
// set property to keep track
shinSelected = false
// TODO: set button image to nil
// reset counter
tappedCounter = 0
}
This might get a little tricky to layout, so that every button sits in the right spot, but if you work with size classes it is totally doable.
I am sure there is a more elegant way to do it, but this is one way.
Fixed!
First I added sublayers to the UIImageView like this
var path = UIBezierPath()
path.moveToPoint(CGPointMake(20, 30))
path.addLineToPoint(CGPointMake(40, 30))
// add as many coordinates you need...
path.closePath()
var layer = CAShapeLayer()
layer.path = path.CGPath
layer.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.5).CGColor
layer.hidden = true
bodyImage.layer.addSublayer(layer)
Than I overrided the touchesbegan function in order to show and hide when the shapes are tapped.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first! as? UITouch {
// get tapped position
let position = touch.locationInView(self.bodyImage)
// loop all sublayers
for layer in self.bodyImage.layer.sublayers! as! [CAShapeLayer] {
// check if tapped position is inside shape-path
if CGPathContainsPoint(layer.path, nil, position, false) {
if (layer.hidden) {
layer.hidden = false
}
else {
layer.hidden = true
}
}
}
}
}
I'm trying to create a full-screen pixelation effect on SKScene. I've learned that there should be two options to do this:
Using a custom SKShader using GLES 2.0.
Using Core Image filters.
I've tried to add a custom SKShader that should modify the whole screen by pixelating it. I'm not sure that if it's possible, but documentation from SKScene (which is a subclass of SKEffectNode) suggests it:
An SKEffectNode object renders its children into a buffer and
optionally applies a Core Image filter to this rendered output.
It's possible to assign a SKShader to the SKScene, as in GameScene : SKScene:
override func didMoveToView(view: SKView) {
let shader = SKShader(fileNamed: "pixelation.fsh")
self.shader = shader
self.shouldEnableEffects = true
}
... but it seems that the rendered buffer is not passed as the u_texture to the GLES:
void main()
{
vec2 coord = v_tex_coord;
coord.x = floor(coord.x * 10.0) / 10.0;
coord.y = floor(coord.y * 10.0) / 10.0;
vec4 texture = texture2D(u_texture, coord);
gl_FragColor = texture;
}
... so the previous shader doesn't work.
If I assign that shader to a texture-based SKSpriteNode, it works.
So is it possible to modify the whole frame buffer (and for example pixelate it) as a post-processing measure after all the nodes have been rendered?
Edit: I found a way to do the pixelation using Core Image filters in OS X (How do you add a CIPixellate Core Image Filter to a Sprite Kit scene?), but copying that implementation doesn't yield any results on iOS. According to the documents CIPixellate should be Available in OS X v10.4 and later and in iOS 6.0 and later..
I managed to make it work using Core Image filter CIPixellate. I used is as a filter to SKEffectNode to produce the pixelation effect. Couple of things to note:
SKScene is a subclass of SKEffectNode, but applying the filter to SKScene doesn't work. It'll mess up the background and doesn't do any pixellation.
You need to create a SKEffectNode and add the nodes to be pixelated under that.
Here's the solution based on the code generated when you choose a Game type project with Swift:
import SpriteKit
class GameScene: SKScene {
var effectNode : SKEffectNode = SKEffectNode.node()
override func didMoveToView(view: SKView) {
let filter = CIFilter(name: "CIPixellate")
filter.setDefaults()
filter.setValue(5.0, forKey: "inputScale")
self.effectNode.filter = filter
self.effectNode.shouldEnableEffects = true
self.addChild(effectNode)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let sprite = SKSpriteNode(imageNamed:"Spaceship")
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.position = location
let action = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)
sprite.runAction(SKAction.repeatActionForever(action))
self.effectNode.addChild(sprite)
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
In order to get your .shader running on SKScene, you need to set shouldEnableEffects to true on the scene (same thing goes for SKEffectNode).
While technically, that "works" (the shader is applied), there's a bug in the rendering of the scene afterwards that gets slightly resized.
So using CoreImage filters is, so far, the best way to go.
I actually had to do the exact same thing for a recent project as a way to transition between levels, and ended up doing a work around for it. Basically, I took a screenshot of the screen in the code then when I loaded the next level, I called a previously saved screenshot of how the level should look when it was loaded. I added the previous level screenshot as an SKSpriteNode and then ran the shader a number of times until it was incredibly pixelated. Then I did the same to the screenshot for that level and replaced the two and then I un-pixelated the second screenshot so it looked like as soon as the level was beaten everything pixelated itself then un-pixelated itself to reveal a new level.
UIGraphicsBeginImageContextWithOptions(UIScreen.mainScreen().bounds.size, false, 0);
self.view!.drawViewHierarchyInRect(view!.bounds, afterScreenUpdates: true)
let image:UIImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//UIGraphicsEndImageContext()
protoImage = SKSpriteNode(texture: SKTexture(CGImage: image.CGImage!))
protoImage.size = CGSizeMake(self.frame.size.width, self.frame.size.height)
node.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2)
protoImage.zPosition = 9000
Second Scene
let shader: SKShader = SKShader(fileNamed: "RWTGradient2.fsh")
let ratioX: Float = divisor/Float(protoImage.frame.size.width)
let ratioY: Float = divisor/Float(protoImage.frame.size.height)
shader.uniforms = [
SKUniform(name: "ratioX", float: ratioX),
SKUniform(name: "ratioY", float: ratioY),
]
protoImage.shader = shader;