Search code examples
swiftoopsprite-kitinstantiationself

Instantiating a property that calls on self before super.init() is called


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)

Solution

  • 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 in SKNode. It returns the scene the current node belongs to.

    You could do something similar and make your life a lot easier

    Ship

    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.

    Gun

    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 }()
    }
    

    Test

    let gun = Gun()
    let ship = Ship()
    ship.name = "Enteprise"
    ship.addChild(gun)
    
    print(gun.ship?.name) // Optional("Enterprise")
    

    Considerations

    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.

    Performance

    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.

    Using computed properties

    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 }
    }