0

I am trying to make a simple game: Space ship on the bottom of the screen shooting asteroids "falling" from the top of the screen.

I am learning ECS and GameplayKit, and have been trying to turn shields into a component. I've heavily relied on Apple's DemoBots sample app, and have lifted the PhysicsComponent, ColliderType, and ContactNotifiableType from the sample code.

A shield needs to render the assets assoicated with it (one for full shields and one for half shields), a different physics body from the ship because it's radius is noticeably larger than the ship, and to keep track of it's state. To do this I wrote:

final class ShieldComponent: GKComponent {
    enum ShieldLevel: Int {
        case full = 0, half, none
    }

    var currentShieldLevel: ShieldLevel = .full {
        didSet {
            switch currentShieldLevel {
            case .full:
                node.isHidden = false
                node.texture = SKTexture(image: #imageLiteral(resourceName: "shield"))
            case .half:
                node.isHidden = false
                node.texture = SKTexture(image: #imageLiteral(resourceName: "damagedShield"))
            case .none:
                node.isHidden = true
            }
        }
    }

    let node: SKSpriteNode

    override init() {
        node = SKSpriteNode(imageNamed: "shield")
        super.init()

        node.physicsBody = {
            let physicsBody = SKPhysicsBody(circleOfRadius: node.frame.size.width / 2)
            physicsBody.pinned = true
            physicsBody.allowsRotation = false
            physicsBody.affectedByGravity = false

            ColliderType.definedCollisions[.shield] = [
                .obstacle,
                .powerUp
            ]

            physicsBody.categoryBitMask = ColliderType.shield.rawValue
            physicsBody.contactTestBitMask = ColliderType.obstacle.rawValue
            physicsBody.collisionBitMask = ColliderType.obstacle.rawValue
            return physicsBody
        }()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func loseShields() {
        if let newShieldLevel = ShieldLevel(rawValue: self.currentShieldLevel.rawValue + 1) {
            self.currentShieldLevel = newShieldLevel
        }
    }

    func restoreShields() {
        self.currentShieldLevel = .full
    }
}

And in my Ship initializer I do this:

    let shieldComponent = ShieldComponent()
    renderComponent.node.addChild(shieldComponent.node)

It would great if I could reuse the RenderComponent, and PhysicsComponent from DemoBots have I have with my ship and asteroid GKEntity subclasses, but components cannot have components. I had made ShieldComponent a ContactNotifiableType, but because the shield node does not actually belong to the ship entity.

I know I'm clearly coming at this wrong, and I'm at a loss of how to correct this. I'm hoping to get an example of how to make a shield component.

jjatie
  • 5,152
  • 5
  • 34
  • 56

1 Answers1

1

You must understand that components are meant to handle only one behaviour. so git rid of the physics code in your init() function and instead Build a Physics component similar to the one in DemoBots.

Tweak Your render Component to your liking. The problem with using DemoBots code is that its not perfectly Suited. So lets tweak it

class RenderComponent: GKComponent {
// MARK: Properties

// The `RenderComponent` vends a node allowing an entity to be rendered in a scene.
@objc let node = SKNode()

var sprite = SKSpriteNode

// init
init(imageNamed name: String) {
    self.sprite = SKSpriteNode(imageNamed: name)
}
// MARK: GKComponent

override func didAddToEntity() {
    node.entity = entity
}

override func willRemoveFromEntity() {
    node.entity = nil
}

}

final class ShieldComponent: GKComponent {


    var node : SKSpriteNode
    //add reference to ship entity
    weak var ship: Ship?


    enum ShieldLevel: Int {
        case full = 0, half, none
    }

    var currentShieldLevel: ShieldLevel = .full {
        didSet {
            switch currentShieldLevel {
            case .full:
                node.isHidden = false
                node.texture = SKTexture(image: #imageLiteral(resourceName: "shield"))
            case .half:
                node.isHidden = false
                node.texture = SKTexture(image: #imageLiteral(resourceName: "damagedShield"))
            case .none:
                node.isHidden = true
            }
        }
    }

    // Grab the visual component from the entity. Unwrap it with a Guard. If the Entity doesnt have the component you get an error.
    var visualComponentRef : RenderComponent {
        guard let renderComponent = ship?.component(ofType: RenderComponent.self) else {
            fatalError("entity must have a render component")
        }
    }

    override init(shipEntity ship: Ship) {
    let visualComponent = RenderComponent(imageNamed: "imageName")
    node = visualComponent.sprite
        self.ship = ship
    super.init()

       // get rid of this. Use a Physics Component for this, Kep your components to one behaviour only. Make them as dumb as possible.
//    node.physicsBody = {
//        let physicsBody = SKPhysicsBody(circleOfRadius: node.frame.size.width / 2)
//        physicsBody.pinned = true
//        physicsBody.allowsRotation = false
//        physicsBody.affectedByGravity = false
//
//        ColliderType.definedCollisions[.shield] = [
//            .obstacle,
//            .powerUp
//        ]
//
//        physicsBody.categoryBitMask = ColliderType.shield.rawValue
//        physicsBody.contactTestBitMask = ColliderType.obstacle.rawValue
//        physicsBody.collisionBitMask = ColliderType.obstacle.rawValue
//        return physicsBody
//    }()
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

func loseShields() {
    if let newShieldLevel = ShieldLevel(rawValue: self.currentShieldLevel.rawValue + 1) {
        self.currentShieldLevel = newShieldLevel
    }
}

func restoreShields() {
    self.currentShieldLevel = .full
    }

};

Make sure to look at how I changed my components interaction with the entity. You can create a reference Object to Ship Entity Directly. Or you can check weather or not the ShieldComponent has an entity with the entity? property. (beware. it is an optional, so unwrap it.

Once you have the Entity reference you can then search it for other Components and retrieve The using component(ofType:_) property. eg ship?.component(ofType: RenderComponent.self)

Other than this, I think you have a decent shield component

Chris Karani
  • 414
  • 5
  • 14
  • Thank you for the detailed response. The ship has a physics component. (One of) My problem is that the physics body is different for the ship when there's an active shield. It seems to me that the logic for the shield physics body should be in the shield. – jjatie Oct 02 '17 at 18:41
  • One more thing, I've read in a few places that shields should generally be part of a health component. If I were to add a health component to the ship, would you recommend it incorporate this shield logic too (instead of having a separate shields component)? – jjatie Oct 02 '17 at 18:43