SpriteKit Lots of Nodes from Same Image: Load Separately or Duplicate? - swift

I'm programming a game in SpriteKit and am coding a section where the "level" is loaded in from a text file, placing a wall node in every spot marked by an "x" in the text file. However, if I know that there are going to be a lot of nodes, and they're all being loaded from the same "wall.png" file, is it more efficient to load the image once and then duplicate the object each time needed, or just load the image each time?
for line in lines {
for letter in line {
if letter == "x" {
let wall = SKSpriteNode(imageNamed: "wall")
self.addChild(wall)
} else { ... }
}
}
VS
let wall = SKSpriteNode(imageNamed: "wall")
for line in lines {
for letter in line {
if letter == "x" {
self.addChild(wall.copy())
} else { ... }
}
}
self in this scope is a class holding the level that extends SKNode, so I'm adding walls to self and then adding that SKNode to the scene.

To answer your question without resorting to 3rd party support
Go with the 2nd option (the copy option)
This will use the same texture across multiple sprites, where as the 1st choice creates a new texture every iteration.

The approach to developing your project remember me a TileMap. Pay attention because you can earn a lot of time instead to load each elements, you can prepare your levels and you have more fun.
There are thousand of tutorials that can help you to build a TileMap with Sprite-kit and Swift. Many of them use this GitHub library called also JSTileMap here
In these tutorials you can learn to how to:
Create a map with Tiled
Add the map to the game
Scroll the map to follow the player
Use object layers.
It's very simple , for example you can load a tmx map:
let tiledMap = JSTileMap("mapFileName.tmx") //map name
if t = tileMap {
self.addChild(tiledMap)
}

Related

Using if let... don't understand where data is coming from

I am following the Stanford CS193 SwiftUI iOS Development Course on YouTube and I am having a very difficult time comprehending how a certain piece of the code is working.
import Foundation
struct MemoryGame<CardContent> where CardContent: Equatable {
private(set) var cards: Array<Card>
**private var indexOfTheOneAndOnlyFaceUpCard: Int?**
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{
**if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard** {
**if cards[chosenIndex].content == cards[potentialMatchIndex].content** {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
cards[chosenIndex].isFaceUp.toggle()
}
print("\(cards)")
}
I have put asterisks around the lines I'm focusing on. I do not understand how cards[chosenIndex].content is being compared to cards[potentialMatchIndex].content, which stems from me not understanding how the if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard line is working. I believe I have a general understanding of optionals and how this code is running but I truly don't understand where the data/value is coming from for indexOfTheOneAndOnlyFaceUpCard.
Here is the video for reference. He begins talking about these lines of code at 1:12:40. Thank you for any help in advance!
In the memory game, a turn proceeds in two stages.
First, one card is turned face up.
Then, the user chooses another card. At that point, one of two things can happen: the new card is a match with the face up card or it isn't. But either way, that turn is over and all cards are now face down.
Thus, when the user chooses a card, if there is a face up card, we are in the middle of the turn, and we arrange things so there is no face up card; the turn is over.
But if there is no face up card, we are at the start of the turn, and we just turn the chosen card face up and wait for the second part of the turn.
So as far as the variable that tracks the face up card is concerned, the logic works like this:
var faceupCard : Card? = nil
if let card = faceupCard {
faceupCard = nil
} else {
faceupCard = chosenCard
}
So that's very clear, when pared down in that way: if the card is face up, make it not be face up; if it isn't face up, make it be face up.
The reason you are confused, in my opinion, is that this is a really bad way to write the code. The face-up-ness of a card is being used as a signal for whether we are in the middle of the turn or at the start of the turn. That's terrible programming. The way to signal what stage we are at is to have an enum whose job is to say what stage we are at! When you are programming you should say what you mean, not use a value in two different ways as is being done here (both track what card is face up and signal what stage of the turn we're on).
Another reason you might be confused is that everything has to be done in terms of indexes, because Card is a value type (struct). We cannot cycle through Cards, or point directly to the face up Card, or even ask a Card whether it is face up; we have to cycle thru indexes in our array of Cards, and track the index of the face up Card.

SKScene Pausing briefly When SKLabelNode's text is changed

I am programming a small game using SpriteKit
I added a SKLabelNode to my SKScene with the initial text of just "0".
When I try and update the text of this SKLabel using:
func updateScoreLabel() {
scoreNumber++
scoreLabel.text = String(scoreNumber)
}
There is a short pause of the entire SKScene between when it gets called and then when it is updated.
However this only happens the first time it is called so If I am running this a second time as in updating the scoreLabel any subsequent time the Scene Pausing then the pause does not occur.
what is triggering the method call is... CC is an enum of physicsBody categoryBitMasks typed to Int
func isCollisionBetween(nodeTypeOne: CC, nodeTypeTwo: CC, contact: SKPhysicsContact) -> Bool {
let isAnodeTypeOne = contact.nodeA.physicsBody!.categoryBitMask == nodeTypeOne.rawValue
let isAnodeTypeTwo = contact.nodeA.physicsBody!.categoryBitMask == nodeTypeTwo.rawValue
let isBnodeTypeOne = contact.nodeB.physicsBody!.categoryBitMask == nodeTypeOne.rawValue
let isBnodeTypeTwo = contact.nodeB.physicsBody!.categoryBitMask == nodeTypeTwo.rawValue
if (isAnodeTypeOne && isBnodeTypeTwo) || (isAnodeTypeTwo && isBnodeTypeOne) {
return true
} else {
return false
}
}
then if
if isCollisionBetween(CC.TypeA, nodeTypeTwo: CC.TypeB, contact: contact) {
updateScoreLabel()
}
Can someone please point out the problem. The score updating does not pause the scene when the same collision is detected and a println statement is used to output the score so I think it is specific to changing the text of the SKLabelNode
You should check for two things which for sure can cause the lag:
for typos in a font name
that you are not loading entire font family, ie Arial instead of just Arial-BoldMT or Arial-ItalicMT. You want to be specific, because loading entire font family can make a delay when using certain fonts. List of iOS fonts could be found here.
If you need to list available fonts(and see real font names), you can use something like this:
for familyName in UIFont.familyNames() as [String] {
println("\(familyName)")
for fontName in UIFont.fontNamesForFamilyName(familyName) as [String] {
println("\tFont: \(fontName)")
}
}
When initializing label for the first time (say at the moment when collision occurs) the delay may happen if you are using custom fonts which are not available in iOS.
In that case try to "preload" a font before using the label. So before actual gameplay, you should instantiate SKLabelNode and set its text property to some value. You have to set a text property, because by doing that the font will be preloaded and ready for use. Otherwise it will be loaded at the time you set label's text property.
EDIT:
Sorry, I just noticed that you are said that you are initializing already a label with initial text. So the just ignore part of my answer related to that and look for typos and the part about loading specific font.
Hope this will take you somewhere. Good luck!

How do run an SKAction on a group of SKSpriteNodes with the same name?

I'm making a game in Xcode 6 using spriteKit and swift. I have a plane on scene, and to make it look like its moving, I'm create clouds off the scene to the left of the screen. I then us an SKAction to move the cloud to the right side of the screen. This works great. You then click to jump off the plane, and the plane moves up off the scene. I then have it start making the clouds on the bottom of the scene, then they move up off the top of the scene, but the problem is, the already existing clouds still have to move to the right side of the screen. My question is, how do I make it so all of the existing clouds stop their action that moves them to the right, then begin to move up exactly where they are? How do I access the group of existing clouds all at the same time once they have been created? I also want the clouds to slow down after you have jumped when you tap the screen to open your parachute, but this should be able to be done by ending the SKAction that moves the clouds, then using another SKAction on them that moves them up slower, but I don't know how to access a group of SKSpriteNodes.
Here is the code that I have to make the clouds:
//This is how the cloud is first declared at the top of the .swift file
var cloud = SKSpriteNode()
//This is the function that runs every certain interval through an NSTimer
func createCloud()
{
cloud = SKSpriteNode(imageNamed: "cloud")
cloud.xScale = 1.25
cloud.yScale = cloud.xScale/2
cloud.zPosition = -1
self.addChild(cloud)
if personDidJump == false
{
let moveRight = SKAction.moveToX(CGRectGetWidth(self.frame) + CGRectGetWidth(cloud.frame), duration: cloudSpeed)
var apple = CGRectGetWidth(self.frame)
var randomNumber:CGFloat = CGFloat(arc4random_uniform(UInt32(apple)))
cloud.position = CGPointMake(-CGRectGetWidth(cloud.frame), randomNumber)
cloud.runAction(moveRight)
}
if personDidJump == true
{
let moveUp = SKAction.moveToY(CGRectGetHeight(self.frame) + CGRectGetWidth(cloud.frame), duration: cloudSpeed)
var apple = CGRectGetWidth(self.frame)
var randomNumber:CGFloat = CGFloat(arc4random_uniform(UInt32(apple)))
cloud.position = CGPointMake(randomNumber, -CGRectGetHeight(cloud.frame))
cloud.runAction(moveUp)
}
}
Also, should I be worried about deleting the clouds when they move off the scene? Or can I just leave them there because you can't see them, and from what I've seen, they don't lag you.
Any help would be greatly appreciated. Thank you.
-Callum-
Put your clouds in an array. When your player jumps (or whenever you need to move them) run through the array and run your action on each cloud.
stackoverflow.com/a/24213529/3402095
^ That is where I found my answere ^
When you make a node give it a name:
myNode.name = "A Node Name"
self.addChild(myNode)
If there are a lot of these nodes, later you can change properties or preform SKAcions on these nodes or do whatever you want to do by using enumerateChildNodesWithName:
self.enumerateChildNodesWithName("A Node Name")
{
myNode, stop in
myNode.runAction(SKAction.moveToY(CGRectGetHeight(self.Frame), duration: 1)
}
I hope this is useful to whoever may need it.

Adding and transitioning animations in SceneKit

I looked at Banana game from WWDC which is written in Objective-C was trying to convert the code to Swift for importing animation and transitioning between them but I am having problems running the animations in swift from DAE files.
I have exporter DAE files in both AutoDesk format from 3dsMax and in openCollada format. The Autodesk format the animation is for each bone so I am unable to call an animation by name so I just import the scene and do the following for the animation to start as soon as the file loads.
scene = SCNScene(named: "monster.scnassets/monsterScene.DAE")
scene2 = SCNScene(named:"monster.scnassets/monster.DAE")
var heroNode = SCNNode()
heroNode = scene.rootNode.childNodeWithName("heroNode", recursively: false)!
var nodeArray = scene2.rootNode.childNodes
for childNode in nodeArray {
heroNode.addChildNode(childNode as SCNNode)
}
Although the animation plays as soon as the scene starts I done know how to store the animation.
If I export the collada file using openCollada. I can use the following to run the get and run the animation as there is just one animation for the whole object instead of each bone in case of AutoDesk collada format. By this way I can store the animation also using CAAnimation.
var anim = scene2.rootNode.animationForKey("monster-1")
childNode.addAnimation(anim, forKey: "monster-1")
But then the character runs at an angle and also runs back and forth instead of running at the same spot.
Also the lighting is better using openCollada. I just would like to use openCollada instead of autodesk collada export. Right now I am using openCollada format for exporting the scene and autodesk for exporting character.
How do store animations in SceneKit/Swift and transition between them? Thanks.
If you don't change the default options when loading a scene from a file, all animations in the scene immediately and automatically attach to their target nodes and play. (See the Animation Import Options in the SceneKit API reference.)
If you want to load the animations from a scene file and hold on to them for attaching to nodes (that is, playing) later, you're better off loading them with the SCNSceneSource class. In addition, you can (but don't have to) store your base model in one file and animations in other files.
Just look at this Bananas animation loading code. Just look at it.*
// In AAPLGameLevel.m:
SCNNode *monkeyNode = [AAPLGameSimulation loadNodeWithName:nil fromSceneNamed:#"art.scnassets/characters/monkey/monkey_skinned.dae"];
AAPLMonkeyCharacter *monkey = [[AAPLMonkeyCharacter alloc] initWithNode:monkeyNode];
[monkey createAnimations];
// In AAPLSkinnedCharacter.m (parent class of AAPLMonkeyCharacter):
+ (CAAnimation *)loadAnimationNamed:(NSString *)animationName fromSceneNamed:(NSString *)sceneName
{
NSURL *url = [[NSBundle mainBundle] URLForResource:sceneName withExtension:#"dae"];
SCNSceneSource *sceneSource = [SCNSceneSource sceneSourceWithURL:url options:nil ];
CAAnimation *animation = [sceneSource entryWithIdentifier:animationName withClass:[CAAnimation class]];
//...
}
// In AAPLMonkeyCharacter.m:
- (void)update:(NSTimeInterval)deltaTime
{
// bunch of stuff to decide whether/when to play animation, then...
[self.mainSkeleton addAnimation:[self cachedAnimationForKey:#"monkey_get_coconut-1"] forKey:nil];
//...
}
What's going on here:
There's a custom class managing the animated character. It owns a SCNNode containing the character model, as well as a bunch of CAAnimations for all of the things the model can do (idle/jump/throw/etc).
That class is initialized by passing the character node loaded from one DAE file. (AAPLGameSimulation loadNodeWithName:fromSceneNamed: is a convenience wrapper around loading a SCNScene from a file and grabbing a named node out of it.) That DAE file contains only the character model, with no animations.
Then, AAPLMonkeyCharacter loads (and stores references to) the animations it needs from the separate DAE files containing each animation. This is where SCNSceneSource comes in — it lets you grab animations out of the file without playing them.
When it's time to play, the monkey class calls addAnimation:forKey: to run the animation on its main node.
Translating to Swift and applying to your problem — where you seem to have all animations in the same file — I'd do something like this (vague outline of a hypothetical class):
class Monster {
let node: SCNNode
let attackAnimation: CAAnimation
init() {
let url = NSBundle.mainBundle().URLForResource(/* dae file */)
let sceneSource = SCNSceneSource(URL: url, options: [
SCNSceneSourceAnimationImportPolicyKey : SCNSceneSourceAnimationImportPolicyDoNotPlay
])
node = sceneSource.entryWithIdentifier("monster", withClass: SCNNode.self)
attackAnimation = sceneSource.entryWithIdentifier("monsterIdle", withClass: CAAnimation.self)
}
func playAttackAnimation() {
node.addAnimation(attackAnimation, forKey: "attack")
}
}
The key bits:
SCNSceneSourceAnimationImportPolicyDoNotPlay makes sure that nodes loaded from the scene source don't start with animations attached/playing.
You have to load the animations separately with entryWithIdentifier:withClass:. Be sure to configure them how you like (repeating, fade duration etc) before attaching to the nodes.
2022 code fragment.
In terms of the swift sample in #rickster 's answer.
It seems the code is now more like:
let p = Bundle.main.url(forResource: "File Name", withExtension: "dae")!
source = SCNSceneSource(url: p, options: nil)!
let geom = source.entryWithIdentifier("geometry316",
withClass: SCNGeometry.self)!
as SCNGeometry
yourDragonNode = SCNNode(geometry: geom)
yourAnime = source.entryWithIdentifier("unnamed_animation__0",
withClass: CAAnimation.self)!
In terms of how to get the "mystery strings":
"geometry316" and "unnamed_animation__0" in the example.
notice: https://stackoverflow.com/a/75088130/294884
and: https://stackoverflow.com/a/56787980/294884

SpriteKit - How do I check if a certain set of coordinates are inside an SKShapeNode?

In my game, I'm trying to determine what points to dole out depending on where an arrow hits a target. I've got the physics and collisions worked out and I've decided to draw several nested circular SKShapeNodes to represent the different rings of the target.
I'm just having issues working out the logic involved in checking if the contact point coordinates are in one of the circle nodes...
Is it even possible?
The easiest solution specific to Sprite Kit is to use the SKPhysicsWorld method bodyAtPoint:, assuming all of the SKShapeNode also have an appropriate SKPhysicsBody.
For example:
SKPhysicsBody* body = [self.scene.physicsWorld bodyAtPoint:CGPointMake(100, 200)];
if (body != nil)
{
// your cat content here ...
}
If there could be overlapping bodies at the same point you can enumerate them with enumerateBodiesAtPoint:usingBlock:
You can also compare the SKShapeNode's path with your CGPoint.
SKShapeNode node; // let there be your node
CGPoint point; // let there be your point
if (CGPathContainsPoint(node.path, NULL, point, NO)) {
// yepp, that point is inside of that shape
}