I have created an SKSpriteNode subclass and have attempted to instantiate it in an SKScene and set some SKSpriteNode properties with other properties that I have stored in the subclass on instantiation. Here is the class definition.
class WalkingMonster: SKSpriteNode {
var rangeOfMovement: CGFloat
var originalPosition: CGFloat
var platformNumber: Int
var imageName = "walkingAlien"
var sizes = [CGSize(width: CGFloat(30.0), height: CGFloat(30.0)), CGSize(width: CGFloat(30.0), height: CGFloat(15.0)), CGSize(width: CGFloat(30.0), height: CGFloat(7.0))]
var monsterName: String
var textureName: String
init(texture: SKTexture, color: UIColor, size: CGSize, rangeOfMovement: CGFloat, originalPosition: CGFloat, platformNumber: Int, name: String, textureName: String) {
self.rangeOfMovement = rangeOfMovement
self.originalPosition = originalPosition
self.platformNumber = platformNumber
self.monsterName = name
self.textureName = textureName
super.init(texture: texture, color: color, size: size)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the instantiation of the class:
var walkingMonsters = [WalkingMonster(texture: SKTexture(), color: UIColor(), size: CGSize(), rangeOfMovement: CGFloat(25.0), originalPosition: CGFloat(750.0), platformNumber: -1, name: "walkingMonster1", textureName: "walkingAlien1")]
This is how I am setting its properties in the SKScene within didMoveToView():
for monster in walkingMonsters {
let texture = SKTexture(imageNamed: monster.textureName)
monster.texture = texture
monster.size = monster.sizes[0]
monster.position.x = monster.originalPosition
//If it is supposed to sit on the ground
if monster.platformNumber < 0 {
monster.position.y = self.groundHeight + super.groundBlockSize.height / 2 + monster.size.height / 2
}
monster.physicsBody?.categoryBitMask = physicsBitMasks.walkingEnemy
monster.physicsBody?.contactTestBitMask = physicsBitMasks.ground | physicsBitMasks.player | physicsBitMasks.aerialBlock
monster.physicsBody?.contactTestBitMask = physicsBitMasks.ground | physicsBitMasks.player | physicsBitMasks.aerialBlock
monster.physicsBody?.dynamic = true
monster.physicsBody?.affectedByGravity = true
self.addChild(monster)
}
}
At runtime there is a monster that spawns, but he (or at least his texture) appears to be off the ground a little. When the player (who has categoryBitMask of .player and whose contact and collision bit masks include .walkingEnemy) walks into him, nothing happens to the movement of the main character and the walkingMonster (walkingMonsters[0]) does not move either. I am not sure if there is some small thing I am missing syntactically or if there is a larger blocker.
My other method of doing this, which I have already implemented successfully, was to store those member variables of what should stylistically be a subclass of SKSpriteNode in corresponding parameter vectors, then set them after using the SKSpriteNode convenience initializer SKSpriteNode(imageNamed: "whatever"). I think subclassing is the way to go however.
The issue was that I was not initializing the physicsbody for each monster in the loop.
for monster in walkingMonsters {
let body = SKPhysicsBody(rectangleOfSize: monster.sizes[0])
monster.physicsBody = body
//rest of setting comes here
}
Related
I would like to create a node of the subclass Unit from GameScene.swift. The code can be executed, but you can't see a node.
I use the following code:
GameScene.swift
func setFirstUnit() {
let myUnit = Unit(pHealthpoints: 10, pDamage: 5, pMovement: 1)
myUnit.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
self.addChild(myUnit)
}
Unit.swift
class Unit: SKNode{
var healthPoints: Int
var damage: Int
var movement: Int
var texture: SKTexture
let knightTexture = "KnightBlueV2"
init(pHealthpoints: Int, pDamage: Int, pMovement: Int) {
healthPoints = pHealthpoints
damage = pDamage
movement = pMovement
texture = SKTexture(imageNamed: knightTexture)
let unit = SKSpriteNode(texture: texture)
unit.zPosition = 4
unit.setScale(1)
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I would be very grateful for any answer.
You are inheriting from SKNode, which has no properties to display anything on the screen. That's why nothing shows up on the screen. If you want to see something on the screen, you need to inherit from SKSpriteNode or another node class with visible properties you can see on the screen.
There's a better solution for you than inheriting from one of the SpriteKit node classes. Instead of using inheritance, use composition. Give the Unit class a property of type SKSpriteNode and use that property to load textures, move the unit, and display the unit on the screen.
class Unit {
sprite: SKSpriteNode
// Rest of class here.
}
I am using SpriteKit and I am loading a SceneKit file that contains a number of sprites with custom classes. The scene never actually loads though because it reaches the first custom class and throws the fatalerror from the required init?(coder:) initializer. The custom class implements an initializer though and I am having trouble pinning down why it is choosing that initializer over the one I provided.
Custom Class:
class Bat: SKSpriteNode, GameSprite {
var initialSize: CGSize = CGSize(width: 44, height: 24)
var textureAtlas: SKTextureAtlas = SKTextureAtlas(named: "Enemies")
var flyAnimation = SKAction()
init() {
super.init(texture: nil, color: .clear, size: initialSize)
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
self.physicsBody?.affectedByGravity = false
createAnimations()
self.run(flyAnimation)
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func createAnimations() {
let flyFrames: [SKTexture] = [textureAtlas.textureNamed("bat"),
textureAtlas.textureNamed("bat-fly")]
let flyAction = SKAction.animate(with: flyFrames, timePerFrame: 0.12)
flyAnimation = SKAction.repeatForever(flyAction)
}
func onTap() {}
}
And here is the code attempting to load the scene and then loop through the children and initialize them:
Encounter Manager:
class EncounterManager {
// Store encounter file names
let encounterNames: [String] = [
"EncounterA"
]
// Each encounter is a node, store an array
var encounters: [SKNode] = []
init() {
// Loop through each encounter scene and create a node for the encounter
for encounterFileName in encounterNames {
let encounterNode = SKNode()
// Load the scene file into a SKScene instance and loop through the children
if let encounterScene = SKScene(fileNamed: encounterFileName) {
for child in encounterScene.children {
// Create a copy of the scene's child node to add to our encounter node
// Copy the position, name, and then add to the encounter
let copyOfNode = type(of: child).init()
copyOfNode.position = child.position
copyOfNode.name = child.name
encounterNode.addChild(copyOfNode)
}
}
// Add the populated encounter node to the array
encounters.append(encounterNode)
}
}
// This function will be called from the GameScene to add all the encounter nodes to the world node
func addEncountersToScene(gameScene: SKNode) {
var encounterPosY = 1000
for encounterNode in encounters {
// Spawn the encounters behind the action, with increasing height so they do not collide
encounterNode.position = CGPoint(x: -2000, y: encounterPosY)
gameScene.addChild(encounterNode)
// Double Y pos for next encounter
encounterPosY *= 2
}
}
}
What I have noticed using breakpoints though is that it never gets past loading the scene. It fails on the line if let encounterScene = SKScene(fileNamed: encounterFileName) and the error is the fatal error in the initializer from the Bat class.
Any help understanding why it picks one initializer over the other would be greatly appreciated!
You are doing:
if let encounterScene = SKScene(fileNamed: encounterFileName)
Which calls SKScene's init(fileNamed:) which loads a file and decodes it with SKScene's coder init. That init loads the file and decodes each element in it with the node's coder init.
If you want to load from a file, you need to implement the coder init.
I want to create a SKShapeNode at a higher level than the touchesBegan of a SpriteNode so when I want to add the SKShapeNode to the screen from the touchesBegun event on this sprite, the shape already exists, and I simply add it to the screen from within the touchesBegan override.
TL;DR, in my SKSpriteNode, I'm trying to pre-build the SKShapeNode that will be used as an animated ring when the Sprite is touched.
I'd like to create the SKShapeNode with variable/constants, so I can easily edit its values...
So in the root of the subclass I have variables for color, size, and linewidth, ready to be used in the creation of the SKShapeNode...
class Balls: SKSpriteNode {
var ringSize: CGFloat = 64
var ringColor: SKColor = SKColor.white
var ringWidth: CGFloat = 16
....
Further down, but still at the root of the class, I create my ring:
let ring = SKShapeNode(circleOfRadius: ringSize)
And am instantly greeted with the lovingly cryptic:
Can not use instance member 'ringSize' within property initializer,
property initializers run before 'self' is available.
Fine. Ok. I get it. You want to think that a functional call to a class to create a property should be done before values are assigned to self. Neither here nor there, I think I'm getting cunning and can get around that by wrapping everything in a function:
class Balls: SKSpriteNode {
var ringSize: CGFloat = 64
var ringColor: SKColor = SKColor.white
var ringWidth: CGFloat = 16
var myRing = SKShapeNode()
func createRing() -> SKShapeNode{
let ring = SKShapeNode(circleOfRadius: ringSize)
ring.strokeColor = ringColor
ring.lineWidth = ringWidth
return ring
}
This generates no errors, and my excitement builds.
So I add another line, to create the actual ring:
....
myRing = createRing()
Dead again:
! Expected
declaration
I have absolutely no idea what this means and began to randomly attempt weird things.
One of them is heading into my already messy convenience initializer and adding myRing = createRing() in there... and this WORKS!
How and why does this work, and is this the best/right/proper way to be circling the drain of initialization?
:: EDIT:: UPDATE :: Full Code Context ::
Here's the full class with my bizarre and misunderstood initialisers.
import SpriteKit
class Circle: SKSpriteNode {
var ringSize: CGFloat = 96
var ringColor: SKColor = SKColor.white
var ringWidth: CGFloat = 8
var myRing = SKShapeNode()
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
}
convenience init() {
self.init(color: SKColor.clear, size: CGSize(width: 100, height: 100))
myRing = createRing()
addChild(myRing)
print("I'm on the screen")
explodeGroup = create_explosionActionGroup()
}
convenience init(color: UIColor, size: CGSize, position: CGPoint) {
self.init(color: color, size: size)
self.position = position
myRing = createRing()
explodeGroup = create_explosionActionGroup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createRing() -> SKShapeNode{
let ring = SKShapeNode(circleOfRadius: ringSize)
ring.strokeColor = ringColor
ring.lineWidth = ringWidth
return ring
}
Your createRing() method is inside Ball class so you need to create an instance of Ball first.
Simple way - You can change creation of instance to
let ball = Balls()
let myRing = ball.createRing()
I'm slightly confused as to where you placed the
myRing = createRing()
line of code but I'm wondering if this setup would help solve your problem
lazy var myRing: SKShapeNode = {
let ring = SKShapeNode(circleOfRadius: ringSize)
ring.strokeColor = ringColor
ring.lineWidth = ringWidth
return ring
}()
This way myRing would be created when it was accessed which should be after the Balls class is instantiated which would mean that ringSize, ringColor and ringWidth would all exist.
Based on your update I think your best bet might be to just make your three ring variables ‘static let’ instead. That way they will exist and have the set value before initializing the main class. The errors you’re seeing are because you created instance variables. Those will only exist when the instance has been initialized. So if you tried to call the ring method as the declaration of the variable or if you did it within the init before self/super init is called then the instance variables wouldn’t be accessible. The most recent code you’ve added should be working because you create the instance before attempting to generate the ring. I hope that makes sense and helps.
And am instantly greeted with the lovingly cryptic:
Can not use instance member 'ringSize' within property initializer, property initializers run before 'self' is available.
So one way around this problem would be to make the default ringSize available another way, e.g.
static let defaultRingSize: CGFloat = 64
var ringSize: CGFloat = Circle.defaultRingSize
let ring = SKShapeNode(circleOfRadius: Circle.defaultRingSize)
... but I question why you even have a var ringSize property like that. Shouldn't you have a didSet observer on it, so that if you change its value, you can update the shape of ring?
Dead again:
! Expected declaration
You weren't clear, in your question, how you actually triggered this, but I guess you tried something like this:
class Circle: SKSpriteNode {
var ringSize: CGFloat = 96
var myRing = SKShapeNode()
myRing = createRing() // “Expected declaration” error on this line
The problem here is that you've placed a statement in the body of your class, but only declarations are allowed in the body.
One of them is heading into my already messy convenience initializer and adding myRing = createRing() in there... and this WORKS!
How and why does this work
All of your class's own instance variables must be initialized before a super.init call. Since myRing has a default value, the compiler effectively inserts the initialization of myRing before the call to super.init in your designated initializer, like this:
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
// Compiler-inserted initialization of myRing to the default
// value you specified:
myRing = SKShapeNode()
super.init(texture: texture, color: color, size: size)
}
Since you declared var myRing, you can then change it later to the customized SKShapeNode you really want.
is this the best/right/proper way to be circling the drain of initialization?
Well, “circling the drain” means “failing”, so I guess you're asking if this is “the best/right/proper way” to fail at initialization… I suppose it's not the best way to fail, since you didn't actually fail in the end.
Or maybe you meant “I hate the way Swift does initialization so I'm going to throw some shade”, in which case, you ain't seen nothin' yet.
But maybe you really meant “is this the best/right/proper way to initialize my instance”, in which case, well, “best” and “right” and “proper” are pretty subjective.
But I can objectively point out that you're creating an SKShapeNode (as the default value of myRing) just to immediately throw it away and create another SKShapeNode. So that's a waste. You've also got calls to createRing in both of your convenience initializers, but you could factor them out into the designated initializer.
But I wouldn't even do it quite like that. SKShapeNode's path property is settable, so you can just create a default SKShapeNode and then change its path after the call to super.init. That also makes it easier to handle changes to ringSize and the other properties, because you can funnel all the changes through a single method that knows how to make myRing match the properties.
Here's how I'd probably write your class:
import SpriteKit
class Circle: SKSpriteNode {
var ringSize: CGFloat = 96 {
// Use an observer to update myRing if this changes.
didSet { configureMyRing() }
}
var ringColor = SKColor.white {
didSet { configureMyRing() }
}
var ringWidth: CGFloat = 8 {
didSet { configureMyRing() }
}
// This can be a let instead of a var because I'm never going to
// set it to a different object. Note that I'm not bothering to
// initialize myRing's path or any other property here, because
// I can just call configureMyRing in my init (after the call to
// super.init).
let myRing = SKShapeNode()
override init(texture: SKTexture?, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
// Call this now to set up myRing's path and other properties.
configureMyRing()
}
convenience init() {
self.init(color: SKColor.clear, size: CGSize(width: 100, height: 100))
// No need to do anything to myRing now, because my designated
// initializer set it up completely.
addChild(myRing)
print("I'm on the screen")
// Commented out because you didn't provide this property
// or method in your question.
// explodeGroup = create_explosionActionGroup()
}
convenience init(color: SKColor, size: CGSize, position: CGPoint) {
self.init(color: color, size: size)
self.position = position
// Commented out because you didn't provide this property
// or method in your question.
// explodeGroup = create_explosionActionGroup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureMyRing() {
myRing.path = CGPath(ellipseIn: CGRect(x: -ringSize / 2, y: -ringSize / 2, width: ringSize, height: ringSize), transform: nil)
myRing.strokeColor = ringColor
myRing.lineWidth = ringWidth
}
}
I designed a SKSpriteNode subclass Ship, and I want to know how to go about animating it with a couple different images. I'm currently passing in an original image with:
class Ship:SKSpriteNode{
static var shipImage = SKTexture(imageNamed:"Sprites/fullShip.png")
init(startPosition startPos:CGPoint, controllerVector:CGVector){
super.init(texture: Ship.shipImage, color: UIColor.clearColor(), size: Ship.shipImage.size())
but I'm not sure how to loop through an image atlas afterwards. I first attempted using a method inside the class that was then called in the update function:
func animateShip() {
self.runAction(SKAction.repeatActionForever(
SKAction.animateWithTextures(shipAnimationFrames,
timePerFrame: 0.2,
resize: false,
restore: true)),
withKey:"shipBlink")
print("animate")
}
var shipAnimationFrames : [SKTexture]! is declared right above GameScene, and this block is located in didMoveToView
//Ship animation
let shipAnimatedAtlas = SKTextureAtlas(named: "ShipImages")
var blinkFrames = [SKTexture]()
let numImages = shipAnimatedAtlas.textureNames.count
for var i=1; i<=numImages; i += 1 {
let shipTextureName = "samShip\(i).png"
blinkFrames.append(shipAnimatedAtlas.textureNamed(shipTextureName))
}
shipAnimationFrames = blinkFrames
Any help would be awesome!
Texture Atlas
First of all you need to create a texture atlas into Assets.xcassets. You'll put here the images related to the frames of your character.
Subclassing SKSpriteNode
This is how you create you own sprite with a beginAnimation method
class Croc: SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "croc_walk01")
super.init(texture: texture, color: .clearColor(), size: texture.size())
}
func beginAnimation() {
let textureAtlas = SKTextureAtlas(named: "croc")
let frames = ["croc_walk01", "croc_walk02", "croc_walk03", "croc_walk04"].map { textureAtlas.textureNamed($0) }
let animate = SKAction.animateWithTextures(frames, timePerFrame: 0.1)
let forever = SKAction.repeatActionForever(animate)
self.runAction(forever)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
As you can see the beginAnimation method creates a texture atlas using the same name of the texture atlas I previously created in Assets.xcassets.
Then the an array of textures is created (frames) and used as parameter to create the animate action.
Starting the animation
Now you need to create your sprite and invoke beginAnimation just once. You do NOT have to invoke beginAnimations inside any update method otherwise you will create a new animation every new frame. The is wrong. beginAnimation must be called only once.
Here's an example
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let croc = Croc()
croc.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
addChild(croc)
croc.beginAnimation()
}
}
I'm relatively new to Swift, and I wanted to know if there was a way to reference a class's property inside of a separate class initializer? For example: if I have a class Person with the property position, is there a way to initialize a Pants class such that its position is the same as Person's? Here's my code:
class Pants:SKSpriteNode{
init(){
let pants = SKTexture(imageNamed: "Sprites/pants.jpg")
pants.setScale(0.5)
super.init(texture: pants, color: UIColor.clearColor(), size: pants.size())
//self.position.x = aPerson.position.x + (aPerson.size.width / 2)
//self.position.y = aPerson.position.y - (aPerson.size.height * 0.04)
self.position = Person.getPos()//CGPoint(x: 200,y: 200)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
At first I tried referencing aPerson which is an instance of Person but I received the error: Instance member aPerson cannot be used on type GameScene. I think understand why it doesn't make much sense to reference an instance in this case- as the instance may not exist by the time of reference? But I don't really know what this error message means- any clarification would be great. I then thought to use a static getter method within the Person class that just returned it's position property. This also doesn't seem to work. Any suggestions would be awesome!
One solution is to add a parameter to your initializer (as suggested by Paul Griffiths in a comment above):
class Pants: SKSpriteNode {
init(aPerson: Person) {
let pants = SKTexture(imageNamed: "Sprites/pants.jpg")
pants.setScale(0.5)
super.init(texture: pants, color: UIColor.clearColor(), size: pants.size())
self.position.x = aPerson.position.x + (aPerson.size.width / 2)
self.position.y = aPerson.position.y - (aPerson.size.height * 0.04)
self.position = aPerson.getPos()//CGPoint(x: 200,y: 200)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then wherever you want to create a Pants instance, you must pass a person:
let somePerson = Person()
let pants = Pants(aPerson: somePerson)
I assume Pants are worn by Person? so instead, work relative, not absolute.
Make Pants a child node of person, then all you need to worry about is the distance from the center of Person, to the Pant line. If this will always be a constant number (Like 10 pixels below center) then hard code it. If the Pant line changes, then pass in the pant line like #Santa Claus suggests
====Assume some code here please======
person.pantline = -10;
person.addChild(Pants(pantline:person.pantline))
=====================================
class Pants: SKSpriteNode {
convenience init(pantline: Int) {
self.init(imageNamed: "Sprites/pants.jpg")
self.setScale(0.5) //Why?
self.anchorPoint = CGPointMake(0.5,1)
self.position.y = pantline
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
}
override init (texture: SKTexture, color: UIColor, size: CGSize)
{
super.init(texture: texture, color: color, size: size)
}
}