I'm playing around with some of the new SpriteKit tools and I've run into a frustrating problem.
As you know, iOS 8 introduced the SKFieldNode class which enables the user to create custom "force fields" which affect other SKNodes. I have gotten great results using the springField and the radialGravityFields, however I have yet to figure out how to use the magneticField and electricField types.
Let me explain.
The following code produces absolutely no effects on node4, the SKSpriteNode which I would like to be affected by the SKFieldNode.
SKSpriteNode *node4 = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImageNamed:#"Red"] size:CGSizeMake(25.0, 25.0)];
node4.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:12.5];
node4.position = CGPointMake(300.0, 250.0);
node4.physicsBody.dynamic = YES;
node4.physicsBody.charge = 30;
node4.physicsBody.linearDamping = 3;
node4.physicsBody.affectedByGravity = NO;
node4.physicsBody.collisionBitMask = 0;
node4.physicsBody.mass = 1;
SKFieldNode *centerNode = [SKFieldNode magneticField];
centerNode.position = CGPointMake(150.0, 200.0);
centerNode.strength = 100000000000;
centerNode.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:25];
centerNode.physicsBody.charge = -30;
centerNode.physicsBody.dynamic = NO;
centerNode.falloff = 0;
The node is drawn on screen, yet stays completely static. As you can see, this happens even with a falloff of 0 and a strength of a 10000000000.
Changing the line:
SKFieldNode *centerNode = [SKFieldNode magneticField];
Into:
SKFieldNode *centerNode = [SKFieldNode springField];
Causes the centerNode to act as a springField, and sends the node4 all over the place.
Using an electricField, rather than a magneticField, yields no results either.
Before someone asks, yes, I am adding both nodes to the scene, I just didn't include those lines.
Can anyone see where I'm going wrong?
There's not enough code here for me to test it fully, but I see two likely problems up front:
If node4 isn't moving to start with, a magnetic field has no effect on it. SpriteKit's magnetic field models the second half of the Lorentz force equation (F = qv ⨯ B), where the force on a particle relates to its charge and velocity.
You're setting node4's linear damping to greater than the maximum. The linearDamping property takes a value between 0 and 1, where 1.0 fully arrest's a body's motion. Values greater than 1 are probably clamped to 1.0, so your body shouldn't be moving even if hit with tremendous force.
Probably unrelated: you don't need to add a charged physics body to the field node unless you want that node to be affected by other electric/magnetic fields.
I have the same problem only when I create nodes by writing code. If you work with SpriteKit Editor, it works well!
You can add a new sks file and then unarchive it to SKScene by this code:
extension SKScene {
class func unarchiveFromFile(file : NSString) -> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as SKScene
scene.size = GameKitHelper.sharedGameKitHelper().getRootViewController().view.frame.size
archiver.finishDecoding()
return scene
} else {
return nil
}
}
}
Open the sks file and drag sprites on the scene, it really works well.
Related
The goal is to animate particles coming from a SKEmitterNode, but the following code doesn't work. The particles don't change texture. They only show the first texture -- or more specifically the original image used in the Xcode Particle Editor -- during the lifetime.
The particle lifetime is longer than the frame duration so this isn't the issue.
// Create animation textures
let animationAtlas = SKTextureAtlas(named: atlasFilename)
var animationFrames = [SKTexture]()
// Set number of animation frames
let numImages = animationAtlas.textureNames.count
// Load texture array
for i in 0..<numImages {
let textureName = "\(texturePrefix)\(i)"
animationFrames.append(animationAtlas.textureNamed(textureName))
}
// Create emitter node w/ animation on each particle
let emitterNode = SKEmitterNode(fileNamed: EmitterFilename)!
let animation = SKAction.animate(with: animationFrames, timePerFrame: 0.05)
emitterNode.particleAction = animation
// Define fade out sequence
let fadeOut = SKAction.fadeOut(withDuration: sparkleFadeOutDur)
let remove = SKAction.removeFromParent()
let sequence = SKAction.sequence([fadeOut, remove])
// Add emitter node to scene
addChild(emitterNode)
emitterNode.run(sequence)
While your code looks flawless and this may potentially be a bug of the particle engine, it's worth assuming that your animation runs out of frames before the particles become large or visible enough so that you see the animation perform. If that's the case, after the animation is finished the texture may revert back to the original texture that you're seeing.
To check if that's the issue, you may want to wrap your animate action into a repeatForever action:
repeatAction = SKAction.repeatForever(animation)
and then change the particleAction assignment as follows:
emitterNode.particleAction = repeatAction
It's worth noting that Apple's documentation seems contradictory. At this page (https://developer.apple.com/reference/spritekit/skaction/1417828-animate) we see: "This action can only be executed by an SKSpriteNode object."
Particles are clearly not accessible sprite nodes as this page (https://developer.apple.com/reference/spritekit/skemitternode) states, but at the same time they should behave like sprites:
"For the purpose of using actions on particles, you can treat the particle as if it were a sprite. This means you can perform other interesting tricks, such as animating the particle’s textures."
In my original build, my sprites were static, so to make the collision physics as accurate as I could rather than use rectangleOfSize for SKPhysicsBody passing in the node, I used this tool to define a path SpriteKit's SKPhysicsBody with polygon helper tool.
However now I'm animating the sprites so that they move back and forth during gameplay (obviously my physics path remains static given the above) so my physicsbody no longer matches what the player sees on screen.
The helper tool seemed like a bit of hack that Apple would eventually fix in the API, has there been anything recent in SpriteKit that would help me out here so that I can pass in the node and define a precise physicsbody rather than the hard-coded approach? If not, any other alternatives?
Not sure how performant this is but you can pre-generate the physics bodies for each texture then animate the sprite along with its physicsBody using a custom action.
Here is an example:
func testAnimation() {
var frameTextures = [SKTexture]()
var physicsBodies = [SKPhysicsBody]()
for index in 1...8 {
// The animation has 8 frames
let texture = SKTexture(imageNamed: "guy\(index)")
frameTextures.append(texture)
let physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
physicsBody.affectedByGravity = false
physicsBodies.append(physicsBody)
}
let sprite = SKSpriteNode(texture: frameTextures[0])
let framesPerSecond = 16.0
let animation = SKAction.customAction(withDuration: 1.0, actionBlock: { node, time in
let index = Int((framesPerSecond * Double(time))) % frameTextures.count
if let spriteNode = node as? SKSpriteNode {
spriteNode.texture = frameTextures[index]
spriteNode.physicsBody = physicsBodies[index]
}
})
sprite.run(SKAction.repeatForever(animation));
sprite.position = CGPoint(x: 0, y: 0)
addChild(sprite)
}
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.
Situation: I have two or more ships on my iOS screen. Both have different attributes like name, size, hitpoints and score points. They are displayed as SKSpriteNodes and each one has added a physicsBody.
At the moment those extra attributes are variables of an extended SKSpriteNode class.
import SpriteKit
class ship: SKSpriteNode {
var hitpoints: Int = nil?
var score: Int = nil?
func createPhysicsBody(){
self.physicsBody = SKPhysicsBody(circleOfRadius: self.size.width / 2)
self.physicsBody?.dynamic = true
...
}
}
In this 'game' you can shoot at those ships and as soon as a bullet hits a ship, you get points. 'Hits a ship' is detected by collision.
func didBeginContact(contact: SKPhysicsContact){
switch(contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask){
case shipCategory + bulletCategory:
contactShipBullet(contact.bodyA, bodyB: contact.bodyB)
break;
default:
break;
}
}
Problem: Collision detection just returns a physicsBody and I do not know how to get my extended SKSpriteNode class just by this physicsBody.
Thoughts: Is it a correct way to extend SKSpriteNode to get my objects like a ship to life? When I add a ship to my screen it looks like:
var ship = Ship(ship(hitpoints: 1, score: 100), position: <CGPosition>)
self.addChild(ship)
Or is this just a wrong approach and there is a much better way to find out which object with stats so and so is hit by a bullet thru collision detection?
This question is similar to my other question - I just want ask this in broader sense.
The SKPhysicsBody has a property node which is the SKNode associated to the body. You just need to perform a conditional cast to your Ship class.
if let ship = contact.bodyA.node as? Ship {
// here you have your ship object of type Ship
print("Score of this ship is: \(ship.score)!!!")
}
Please note that the Ship node could be the one associated with bodyB so.
if let ship = contact.bodyA.node as? Ship {
// here you have your ship...
} else if let ship = contact.bodyB.node as? Ship {
// here you have your ship...
}
Hope this helps.
I'm trying to figure out how to optimize my game. I have a lot of turrets etc shooting out physics bodies. The game is playable but can dip into 50 or 45 fps. I've figured out that it is related to adding objects to my scene on the fly.
for example. when my turret is shooting it runs this code
func shootBlaster(){
let blaster = Projectile(color: SKColor.cyanColor(), size: CGSize(width: convertNum(3), height: convertNum(3)))
blaster.name = "blaster"
blaster.physicsBody = SKPhysicsBody(rectangleOfSize: blaster.size)
blaster.physicsBody!.categoryBitMask = CategoryEnemyProjectile
blaster.physicsBody!.contactTestBitMask = CategoryShip | CategoryShipShield
blaster.physicsBody!.collisionBitMask = 0
let fireAction = SKAction.runBlock({
let angle = self.turret.base.zRotation + self.zRotation
let velocity = CGPoint(angle: angle) * convertNum(420)
let vector = CGVectorMake(velocity.x, velocity.y)
let blas = blaster.copy() as Projectile
blas.wpnDmg = 10
blas.position = self.gameScene.gameLayer.convertPoint(self.turret.barrelPos, fromNode: self.turret.base)
self.gameScene.gameLayer.addChild(blas)
blas.physicsBody!.velocity = vector
blas.zRotation = blas.physicsBody!.velocity.angle
blas.runAction(SKAction.removeFromParentAfterDelay(1))
})
let recoilAction = SKAction.moveByX(-5, y: 0, duration: 0.08)
let reverseRecoil = recoilAction.reversedAction()
self.turret.barrel.runAction(SKAction.repeatAction(
SKAction.sequence([
recoilAction,
fireAction,
reverseRecoil
])
,count: self.blasterNum))
}
you can see that I'm adding a "blaster" inside of each of the fireAction blocks. I've noticed games with better performance are rarely adding children at runtime. It seems best to have everything pre-loaded. It makes sense that loading resources during runtime would put a strain on things, but what is the alternative? Do I need to add everything to the scene ahead of time and somehow hide and show it?
With any kind of shooting game where you have numerous projectiles you want to utilize "pooling". Create a pool of projectiles and reuse them as opposed to creating new ones when you need them.