how to write a protocol that will run a function at specific time with - swift - SpriteKit

Viewed 415

Is it possible to create a protocol like this:

protocol SetupProt {
    func setup()
}

then at the end of my didMove function, tell the app to run all the setup functions from all the subclasses that implement this protocol?

Or will I need to store references to all these objects that are implementing the protocol and iterate through them?

Currently I have that protocol in use right now.

Then I have a global array like this:

setups = [SetupProt] = []

I have subclass for "a platform" in my game. I subclass it in the XCODE editor. And within the aDecoder init function, I add that node to this global array...

I do this because the scene property is nil at this moment, so I can't access it yet, it hasn't finished loading I"m guessing.

At the end of my scenes didMove, I iterate through this array:

for set in setups { set.setup() }

And this gets the job done.

I'm wondering if instead of storing an array of all these objects, can I just tell the app: "Hey, run the setup function for anything that is implementing this protocol.

5 Answers

To me; it sounds like you would be better off using a notification/observers...You have multiple objects that you want to respond to a single event. Either in the init or viewDidLoad functions of those objects add:

NotificationCenter.default.addObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name?, object anObject: Any?)

then at the end of your didMove(_:) :

NotificationCenter.default.post(name aName: NSNotification.Name, object anObject: Any?)

This will cause those #selector functions to run. Just make sure you remove the observers when a object is removed from memory Beware of zombies

You are doing this in GameplayKit, so what you want to do is go through your components

entities.foreach{
  ($0.components.filter({$0 as? SetupProtocol != nil}) as? [SetupProtocol]).foreach{
    $0.setup()
  }
}

This will avoid having to attach notification listeners to every item that uses this protocol. Setup is usually a one and done type deal, so to have your instances "listen" for a message that is sent only once could end up being a waste of resources.

What is nice with gameplay kit is technically you do not even need the protocol, you could have a component that is designed for setup, then we can avoid the needless filtering,casting, and looping.

entities.foreach{$0.component(ofType:SetupComponent).setup()}

--Inside SetupComponent

class SetupComponent : GKComponent
{
   func setup()
   { 
      guard let entity = entity as? SetupProtocol else {return}
      entity.setup()
   }
}

You can then take things one step further, and create "buckets" for all your components. This will help you manage things a lot better.

let setupBucket =  entities.flatmap{$0.components.filter({$0 as? SetupProtocol != nil})} as? [SetupProtocol]. //I may need to verify this syntax is correct when I get home, doing it from memory.

You could add things like a setup bucket, update bucket, etc, and all these are are arrays of each type of component. This way if you ever need to call a method on all entities of a given component type, you have the bucket you can run through. Of course you are going to have to properly manage the buckets, and make sure you add/remove when a component is attached/removed to/from an entity.

Now I seem to recall coming across an issue with components in iOS 10 where I had to make sub component classes because subclassing was not properly working, but I do not know the state of it in iOS 11. I can tell you that Scenekit builder components are broken in XCode 9, so I would avoid using this if you can for now (You are on Sprite Kit, I do not know if they broke it on Sprite Kit also)

If your nodes are all children of the SKScene, you can recursively call the methods starting from the scene.

protocol SetupProt {
    func setup()
}

class GameScene: SKScene, SetupProt {

    override func didMove(to view: SKView) {
        super.didMove(to: view)
        // Perform setup
        self.setup()
    }

    // MARK: - Private
    func setup() {
        self.setup(with: self)
    }
    func setup(with node: SKNode ) {
        // Call you method
        (node as? SetupProt)?.setup()

        // Recursively call for every node in scene
        for child in self.children where child is SetupProt {
            self.setup(with: child)
        }
    }
}

I think protocol extension work for you try this. I will give a sample about how to use protocol extensions define optional protocol.

    // Protocol has empty default implementation of the following methods making them optional to implement:
// cancel()
protocol Cancelable {

    /// default implementation is empty.
    func cancel()
}

extension Cancelable {

    func cancel() {}
}

class Plane: Cancelable {
  //Since cancel() have default implementation, that is optional to class Plane
}

let plane = Plane()
plane.cancel()
// Print what you want

If you want multiple instances of a certain class (a node for example) to respond to a single event the best approach is to use a mix of notifications and protocols.

First you need to define your protocol as you did:

protocol SetupProt {
    func setup()
}

Then, create a custom node class that implements the protocol and inside your init, add the node as observer to the Notification that will be sent by your Scene:

class MyNode: SKNode, SetupProt {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        // Add the node as observer for future trigger setup notifications
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(setup),
            name: NSNotification.Name("TriggerSetup"),
            object: nil
        )
    }

    // Protocol implementation
    @objc func setup() {
        // Implement your node setup here
    }
}

Finally, implement your scene and trigger the observers inside your didMove method:

class MyScene: GameScene {

    override func didMove(to view: SKView) {
        super.didMove(to: view)

        // Once your Scene is in place, trigger the setup of all observers
        NotificationCenter.default.post(
            name: NSNotification.Name("TriggerSetup"),
            object: nil
        )
    }

}

By doing this you can setup all your nodes at the same time without the need to have direct reference to them inside your scene. It also avoids the use of iterators.

You can read more here about how Notifications and Observers work.

Related