I'm building both Ship
and also GenericGun
classes in my first Swift game, and I just ran into a problem instantiating one of Ships
properties. The property in question, aGun
calls upon self
as its value but the error appears because while the property must be set before the call to super.init()
, the property relies on self
which can only be accessed after super.init()
is called. I played around a bunch and found that using an optional on the variable makes the error disappear, but I'm not sure why and don't know if it will work long term. Here's my gun class:
class genericGun{
var theShip:Ship
var theGameScene:GameScene
init(gameScene:GameScene, shipInstance:Ship){
theShip = shipInstance
theGameScene = gameScene
}
func addLaser(){
let aLaser = Laser(laserPosition: theShip.position)
theShip.lasers.append(aLaser)
}
//If statement on user touch to call this
func shoot(){
//Pull out the laser from the ship
let availableLaser = theShip.lasers.removeLast()
let constY:CGFloat = theShip.position.y
availableLaser.position = CGPoint(x: theShip.position.x, y:constY)
//Set its speed
availableLaser.physicsBody?.velocity = CGVector(dx: 400.0,dy: 0)
//Add it to the scene
theGameScene.addChild(availableLaser)
theShip.canShoot = false
func printHey(){print("Hey!!!!!!!!!!!!!!!!!!!!!!")}
let sayHey = SKAction.runBlock{printHey()}
let reloadTime = SKAction.waitForDuration(1)
let loadGun = SKAction.sequence([reloadTime, sayHey])
theShip.runAction(SKAction.repeatActionForever(loadGun))
}
}
The Laser Class:
class Laser:SKSpriteNode{
init(laserPosition:CGPoint){
let laser = SKTexture(imageNamed: "Sprites/laser.jpg")
super.init(texture: laser, color: UIColor.clearColor(), size: laser.size())
//Laser physics
self.physicsBody = SKPhysicsBody(circleOfRadius: laser.size().width/2)
self.physicsBody?.dynamic = true
self.physicsBody?.categoryBitMask = PhysicsCategory.Laser
self.physicsBody?.contactTestBitMask = PhysicsCategory.Alien
self.physicsBody?.collisionBitMask = PhysicsCategory.None
self.physicsBody?.collisionBitMask = 0;
self.physicsBody?.usesPreciseCollisionDetection = true
self.physicsBody?.linearDamping = 0.0;
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The Ship Class:
class Ship:SKSpriteNode{
static var shipState = "norm"
//A dictionary with String keys and AnyType array values
static var shipTypes: [String: [Any]] = [
"norm":[SKTexture(imageNamed:"Sprites/fullShip.png"), SKTexture(imageNamed:"Sprites/laser.jpg"),7],
"rapid":[SKTexture(imageNamed:"Sprites/fullShip.png"),7],
"bazooka":[SKTexture(imageNamed:"Sprites/fullShip.png"),7]
]
var moveSpeed:CGFloat
var lives:Int
var lasers = [SKSpriteNode]()
var canShoot = false
var aGun: genericGun? = nil
var theGameScene:GameScene
static var shipImage = SKTexture(imageNamed:"Sprites/fullShip.png")//: Int = Int(shipTypes[shipState]![0])
init(gameScene:GameScene, startPosition startPos:CGPoint, controllerVector:CGVector){
self.lives = 3
self.moveSpeed = 200
theGameScene = gameScene
//Call super initilizer
super.init(texture: Ship.shipImage, color: UIColor.clearColor(), size: Ship.shipImage.size())
self.aGun = genericGun(gameScene: theGameScene, shipInstance: self)
self.setScale(0.2)
//Position is an property of SKSpriteNode so super must be called first
self.position = startPos
//Physics of the ship
self.physicsBody = SKPhysicsBody(circleOfRadius: self.size.width/2)
self.physicsBody?.dynamic = true
self.physicsBody?.collisionBitMask = 0//PhysicsCategory.Ship
self.physicsBody?.contactTestBitMask = PhysicsCategory.Ship
self.physicsBody?.allowsRotation = false
self.physicsBody?.angularVelocity = CGFloat(0)
self.physicsBody?.affectedByGravity = false //TBD
self.physicsBody?.velocity.dx = controllerVector.dx * moveSpeed
self.physicsBody?.velocity.dy = controllerVector.dy * moveSpeed
}
func updateVelocity(v:CGVector){
if(v == CGVector(dx:0,dy:0)){
self.physicsBody?.velocity = CGVector(dx: 0,dy: 0)
}
self.physicsBody?.velocity.dx = v.dx * moveSpeed
self.physicsBody?.velocity.dy = v.dy * moveSpeed
}
func updateLaserPos(){
// laser.position = self.position
}
func updateShipProperties(shipVelocity v:CGVector,laserStartPos laserStart:CGPoint){
updateVelocity(v)
updateLaserPos()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And this is where the ship would be instantiated:
class GameScene: SKScene, SKPhysicsContactDelegate {
var aShip = Ship(gameScene:GameScene(), startPosition: CGPoint(x:50,y:200),controllerVector: controlVector)
If you are designing your game in a way that a Ship
needs a Gun
to be created and a Gun
needs a Ship
before the initialisation you are going into a lot of trouble.
SpriteKit already solved this kind of problem with the
scene
property available inSKNode
. It returns thescene
the current node belongs to.
You could do something similar and make your life a lot easier
class Ship: SKSpriteNode {
lazy var gun: Gun? = { return self.children.flatMap { $0 as? Gun }.first }()
}
As you can see I created a lazy property, when you invoke the gun
property of a Ship
it automatically gets populated with the first Gun
found among its children.
You can do the same with the Gun
object, as you can see it has a lazy var ship
that gets populated with its parent conditionally casted to Ship
.
class Gun: SKSpriteNode {
lazy var ship: Ship? = { return self.parent as? Ship }()
}
let gun = Gun()
let ship = Ship()
ship.name = "Enteprise"
ship.addChild(gun)
print(gun.ship?.name) // Optional("Enterprise")
Following what SpriteKit did with the scene
property, I made the properties gun
and ship
optionals. It means that if a Gun
is not direct child of a Ship
then its ship
property will return nil
.
Similarly if a Ship
doesn't have a Gun
among its children, then its gun
property will return nil
.
Since the ship
and gun
property are lazy
they will need a very small amount of time (the first time they are read) to be populated. You will not notice the delay but just keep it in mind.
Instead of making ship
and gun
lazy properties, you can defined them as computed properties. In this case if you move a Gun from a Ship to another you'll get consistent results.
On the other hand in this case each time you read ship
and gun
you'll need to way a very (very very very) little amount of time.
class Ship: SKSpriteNode {
var gun: Gun? { return self.children.flatMap { $0 as? Gun }.first }
}
class Gun: SKSpriteNode {
var ship: Ship? { return self.parent as? Ship }
}