Getting the filename of an SKSpriteNode in Swift 3
It is stored in the description, so here is a nice little extension I made to rip it out.
extension SKTexture
{
var name : String
{
return self.description.slice(start: "'",to: "'")!
}
}
extension String {
func slice(start: String, to: String) -> String?
{
return (range(of: start)?.upperBound).flatMap
{
sInd in
(range(of: to, range: sInd..<endIndex)?.lowerBound).map
{
eInd in
substring(with:sInd..<eInd)
}
}
}
}
usage:
print(sprite.texture!.name)
SKTexture get image name
Hmm my strategy would be this:
On your ninja class have a property for your textures. Keep them around
@property (nonatomic, strong) NSArray * hitTextures;
Then store the hit textures (note this is a lazy loading getter method)
-(NSArray *)hitTextures{
if (_hitTextures == nil){
SKTexture *hit1 = [SKTexture textureWithImageNamed:@"Ninja_hit_1"];
SKTexture *hit2 = [SKTexture textureWithImageNamed:@"Ninja_hit_2"];
SKTexture *hit3 = [SKTexture textureWithImageNamed:@"Ninja_hit_3"];
SKTexture *hit4 = [SKTexture textureWithImageNamed:@"Ninja_hit_4"];
_hitTextures = @[hit1, hit2, hit3, hit4];
}
return _hitTextures;
}
Note that we don't need to make the SKTextureAtlas
object explicitly:
When loading the texture data, Sprite Kit searches the app bundle for an image file with the specified filename. If a matching image file cannot be found, Sprite Kit searches for the texture in any texture atlases stored in the app bundle. If the specified image does not exist anywhere in the bundle, Sprite Kit creates a placeholder texture image.
Use this texture array to fill in your SKAction
SKAction *hitAnimation = [SKAction animateWithTextures:self.hitTextures
timePerFrame:0.1];
This allows you do change your -isHit
method like so:
- (BOOL)isHit:(SKTexture *)texture
{
NSUInteger index = [self.hitTextures indexOfObject:texture];
if (index == 1 ||
index == 2)
{
return YES;
}
return NO;
}
This way you don't rely on an implementation detail of the -description
method that is subject to change.
Folder of images: create and fill SKShapeNodes, 1 of each
Too many requests , so I try to help you with some code:
Create a folder:
func createDir(folderName:String) {
let fileManager = FileManager.default
if let directory = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "APP_GROUP_IDENTIFIER") {
let newDirectory = directory.appendingPathComponent(folderName)
try! fileManager.createDirectory(at: newDirectory, withIntermediateDirectories: false, attributes: nil)
}
}
Now you have your folder so you can decide to save a file or to copy one file from documents to this new directory. I make an example for the last one.
Copy a file:
let fileManager = FileManager.default
do {
try fileManager.copyItem(atPath: "hero.png", toPath: "subfolder/hero.swift")
}
catch let error as NSError {
print("Something went wrong: \(error)")
}
Now you want to know how to extract all files from a directory.
Extract all files from a directory:
func extractAllFile(atPath path: String, withExtension fileExtension:String) -> [String] {
let pathURL = NSURL(fileURLWithPath: path, isDirectory: true)
var allFiles: [String] = []
let fileManager = FileManager.default
if let enumerator = fileManager.enumerator(atPath: path) {
for file in enumerator {
if #available(iOS 9.0, *) {
if let path = NSURL(fileURLWithPath: file as! String, relativeTo: pathURL as URL).path
, path.hasSuffix(".\(fileExtension)"){
allFiles.append(path)
}
} else {
// Fallback on earlier versions
}
}
}
return allFiles
}
Usage:
let folderPath = Bundle.main.path(forResource: "Images", ofType: nil)
let allPngFiles = extractAllFile(atPath: folderPath!, withExtension: "png") // returns file path of all the png files inside the folder
After that, you can create an array of SKShapeNode
based from allPngFiles
using fillTexture
with a SKTexture created from each image and give to each node the name
of the file used (so you know at the end also how many nodes you have created from array.count
).
Setting a name to an SKSpriteNode?
There may be a better way to do this, but this is the first thing I thought of - make the playerTexture variable an array of tuples containing the texture and the name you want to give it:
var playerTexture = [(SKTexture, String)]()
playerTexture.append((SKTexture(imageNamed: “red”), "red"))
playerTexture.append((SKTexture(imageNamed: “blue”), "blue"))
playerTexture.append((SKTexture(imageNamed: “green”), "green"))
let rand = Int(arc4random_uniform(UInt32(playerTexture.count)))
let chosenColor = playerTexture[rand].0 as SKTexture
let colorName = playerTexture[rand].1
let playerSkin = chosenColor
player = SKSpriteNode(texture: playerSkin)
player.size = CGSize(width: 50, height: 50)
player.position = location
self.addChild(player)
player.name = colorName
Animation atlas and dynamic physicsBody in Swift 3
Solution for isDynamic = false
.
*Update to 2017 below
After days of test maded with my answer, Knight0fDragon answer's and some other ideas came from other SO answers (Confused and Whirlwind suggestions..) I've seen that there is a new problem : physicsBody
can't propagate their properties to other bodies
adequately and correctly. In other words copy all properties from a body to another body it's not enough. That's because Apple restrict the access to some methods and properties of the physicsBody
original class.
It may happen that when you launch a physicsBody.applyImpulse
propagating adequately the velocity
, the gravity isn't yet respected correctly. That's really orrible to see..and obviusly that's wrong.
So the main goal is: do not change the physicBody
recreating it.In other words DON'T RECREATE IT!
I thought that, instead of creating sprite children, you could create a ghost sprites that do the work instead of the main sprite, and the main sprite takes advantage of the ghost changes but ONLY the main sprite have a physicsBody.
This seems to work!
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
private var ghostKnight:SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightTexture : SKTexture!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector.zero
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%@%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
// Prepare the ghost
ghostKnight = SKSpriteNode(texture:textures.first)
addChild(ghostKnight)
ghostKnight.alpha = 0.2
ghostKnight.position = CGPoint(x:self.frame.midX,y:100)
lastKnightTexture = ghostKnight.texture
// Prepare my sprite
knight = SKSpriteNode(texture:textures.first,size:CGSize(width:1,height:1))
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
let ghostAnimation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false))
ghostKnight.run(ghostAnimation,withKey:"ghostAnimation")
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: false, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
if ghostKnight.action(forKey: "ghostAnimation") != nil {
if ghostKnight.texture != lastKnightTexture {
setPhysics()
lastKnightTexture = ghostKnight.texture
}
}
}
func setPhysics() {
if let _ = knight.physicsBody{
knight.xScale = ghostKnight.frame.size.width
knight.yScale = ghostKnight.frame.size.height
} else {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.frame.size)
knight.physicsBody?.isDynamic = true
knight.physicsBody?.allowsRotation = false
knight.physicsBody?.affectedByGravity = true
}
}
}
Output:
Obviusly you can hide with alpha
set to 0.0 and re-positioning the ghost as you wish to make it disappear.
Update 2017:
After hours of testing I've try to improve the code, finally I managed to remove the ghost sprite but, to work well, one condition is very important: you should not use SKAction.animate
with resize
in true. This because this method resize the sprites and don't respect the scale (I really don't understand why, hope to some future Apple improvements..). This is the best I've obtain for now:
- NO CHILDS
- NO OTHER GHOST SPRITES
- NO EXTENSION
- NO ANIMATE METHOD RECREATED
Code:
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightSize: CGSize!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector.zero
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%@%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
// Prepare my sprite
knight = SKSpriteNode(texture:textures.first,size:CGSize(width:1,height:1))
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
lastKnightSize = knight.texture?.size()
setPhysics()
}
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: false, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
lastKnightSize = knight.texture?.size()
knight.xScale = lastKnightSize.width
knight.yScale = lastKnightSize.height
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.frame.size)
knight.physicsBody?.isDynamic = true
knight.physicsBody?.allowsRotation = false
knight.physicsBody?.affectedByGravity = true
}
}
Important detail:
About isDynamic = true
that's not possible simply because , during the frequently changes of size, Apple reset also frequently the knight physicsBody but don't apply the inherit of the latest physicsBody
properties to the new resetted physicsBody
, this is a real shame, you can test it in update printing the knight.physicsBody?.velocity
(is always zero but should change due to gravity...). This is probably the reason why Apple recommended to don't scale sprites during physics. To my point of view is a Sprite-kit limitation.
Horizontally mirror a SKSpriteNode texture
You can use this code to flip among x-axis:
spriteNode.xScale = spriteNode.xScale * -1;
but be careful you can lose some of physicsbody's property, I highly suggest u to use xScale in this way:
spriteNodeBody = [SKNode node];
spriteNodeBody.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:spriteNode.size];
spriteNodeBody.physicsBody.categoryBitMask = CNPhysicsCategoryPlayer;
spriteNodeBody.physicsBody.collisionBitMask = CNPhysicsCategoryBall;
[spriteNodeBody addChild:spriteNode];
[self addChild:spriteNodeBody];
And now you can safely use:
spriteNode.xScale = spriteNode.xScale * -1;
Related Topics
Swift: Protocol VS. Struct VS. Class
Exc_Bad_Access Using Ibinspectable
Code Coverage Result Is Not Accurate to Real Coverage in Xcode 7
Saving Audio After Effect in iOS
Errortype' Is Not Convertible to 'Nserror'
Why am I Getting Com.Facebook.Sdk.Login Error 308
Swift Core Data Sync with Web Server
Most Efficient/Realtime Way to Get Pixel Values from iOS Camera Feed in Swift
Multiple Iboutlets in Same Line of Same Type in Swift
Finding It Difficult to Pass Data to Separate Viewcontroller
Swift 3 Nscache Generic Parameter 'Keytype' Could Not Be Inferred
How to Get the Nondecoded Attributes from a Decoder Container in Swift 4
3D Touch Quick Actions Not Working at All
How to Set Kerning (Spacing Between Characters) on Uinavigationbar Title - Swift or Objective-C
Swift Universal Framework Depending on Pod
Issue with Observing Wkwebview Url Changes via JavaScript Events