DemoBots/Entities/GroundBot.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
A ground-based `TaskBot` with a distance attack. This `GKEntity` subclass allows for convenient construction of an entity with appropriate `GKComponent` instances. |
*/ |
import SpriteKit |
import GameplayKit |
class GroundBot: TaskBot, ChargeComponentDelegate, ResourceLoadableType { |
// MARK: Static Properties |
/// The size to use for the `GroundBot`s animation textures. |
static var textureSize = CGSize(width: 120.0, height: 120.0) |
/// The size to use for the `GroundBot`'s shadow texture. |
static var shadowSize = CGSize(width: 90.0, height: 40.0) |
/// The actual texture to use for the `GroundBot`'s shadow. |
static var shadowTexture: SKTexture = { |
let shadowAtlas = SKTextureAtlas(named: "Shadows") |
return shadowAtlas.textureNamed("GroundBotShadow") |
}() |
/// The offset of the `GroundBot`'s shadow from its center position. |
static var shadowOffset = CGPoint(x: 0.0, y: -40.0) |
/// The animations to use when a `GroundBot` is in its "good" state. |
static var goodAnimations: [AnimationState: [CompassDirection: Animation]]? |
/// The animations to use when a `GroundBot` is in its "bad" state. |
static var badAnimations: [AnimationState: [CompassDirection: Animation]]? |
// MARK: TaskBot Properties |
override var goodAnimations: [AnimationState: [CompassDirection: Animation]] { |
return GroundBot.goodAnimations! |
} |
override var badAnimations: [AnimationState: [CompassDirection: Animation]] { |
return GroundBot.badAnimations! |
} |
// MARK: GroundBot Properties |
/// The position in the scene that the `GroundBot` should target with its attack. |
var targetPosition: float2? |
// MARK: Initialization |
required init(isGood: Bool, goodPathPoints: [CGPoint], badPathPoints: [CGPoint]) { |
super.init(isGood: isGood, goodPathPoints: goodPathPoints, badPathPoints: badPathPoints) |
// Determine initial animations and charge based on the initial state of the bot. |
let initialAnimations: [AnimationState: [CompassDirection: Animation]] |
let initialCharge: Double |
if isGood { |
guard let goodAnimations = GroundBot.goodAnimations else { |
fatalError("Attempt to access GroundBot.goodAnimations before they have been loaded.") |
} |
initialAnimations = goodAnimations |
initialCharge = 0.0 |
} |
else { |
guard let badAnimations = GroundBot.badAnimations else { |
fatalError("Attempt to access GroundBot.badAnimations before they have been loaded.") |
} |
initialAnimations = badAnimations |
initialCharge = GameplayConfiguration.GroundBot.maximumCharge |
} |
// Create components that define how the entity looks and behaves. |
let renderComponent = RenderComponent() |
addComponent(renderComponent) |
let orientationComponent = OrientationComponent() |
addComponent(orientationComponent) |
let shadowComponent = ShadowComponent(texture: GroundBot.shadowTexture, size: GroundBot.shadowSize, offset: GroundBot.shadowOffset) |
addComponent(shadowComponent) |
let animationComponent = AnimationComponent(textureSize: GroundBot.textureSize, animations: initialAnimations) |
addComponent(animationComponent) |
let intelligenceComponent = IntelligenceComponent(states: [ |
TaskBotAgentControlledState(entity: self), |
GroundBotRotateToAttackState(entity: self), |
GroundBotPreAttackState(entity: self), |
GroundBotAttackState(entity: self), |
TaskBotZappedState(entity: self) |
]) |
addComponent(intelligenceComponent) |
let physicsBody = SKPhysicsBody(circleOfRadius: GameplayConfiguration.TaskBot.physicsBodyRadius, center: GameplayConfiguration.TaskBot.physicsBodyOffset) |
let physicsComponent = PhysicsComponent(physicsBody: physicsBody, colliderType: .TaskBot) |
addComponent(physicsComponent) |
let chargeComponent = ChargeComponent(charge: initialCharge, maximumCharge: GameplayConfiguration.GroundBot.maximumCharge) |
chargeComponent.delegate = self |
addComponent(chargeComponent) |
let movementComponent = MovementComponent() |
addComponent(movementComponent) |
// Connect the `PhysicsComponent` and the `RenderComponent`. |
renderComponent.node.physicsBody = physicsComponent.physicsBody |
// Connect the `RenderComponent` and `ShadowComponent` to the `AnimationComponent`. |
renderComponent.node.addChild(animationComponent.node) |
animationComponent.shadowNode = shadowComponent.node |
// Specify the offset for beam targeting. |
beamTargetOffset = GameplayConfiguration.GroundBot.beamTargetOffset |
} |
required init?(coder aDecoder: NSCoder) { |
fatalError("init(coder:) has not been implemented") |
} |
// MARK: ContactableType |
override func contactWithEntityDidBegin(_ entity: GKEntity) { |
super.contactWithEntityDidBegin(entity) |
// Retrieve the current state from this `GroundBot` as a `GroundBotAttackState`. |
guard let attackState = component(ofType: IntelligenceComponent.self)?.stateMachine.currentState as? GroundBotAttackState else { return } |
// Use the `GroundBotAttackState` to apply the appropriate damage to the contacted entity. |
attackState.applyDamageToEntity(entity: entity) |
} |
// MARK: RulesComponentDelegate |
override func rulesComponent(rulesComponent: RulesComponent, didFinishEvaluatingRuleSystem ruleSystem: GKRuleSystem) { |
super.rulesComponent(rulesComponent: rulesComponent, didFinishEvaluatingRuleSystem: ruleSystem) |
/* |
A `GroundBot` will attack a location in the scene if the following conditions are met: |
1) Enough time has elapsed since the `GroundBot` last attacked a target. |
2) The `GroundBot` is hunting a target. |
3) The target is within the `GroundBot`'s attack range. |
4) There is no scenery between the `GroundBot` and the target. |
*/ |
guard let scene = component(ofType: RenderComponent.self)?.node.scene else { return } |
guard let intelligenceComponent = component(ofType: IntelligenceComponent.self) else { return } |
guard let agentControlledState = intelligenceComponent.stateMachine.currentState as? TaskBotAgentControlledState else { return } |
// 1) Check if enough time has passed since the `GroundBot`'s last attack. |
guard agentControlledState.elapsedTime >= GameplayConfiguration.GroundBot.delayBetweenAttacks else { return } |
// 2) Check if the current mandate is to hunt an agent. |
guard case let .huntAgent(targetAgent) = mandate else { return } |
// 3) Check if the target is within the `GroundBot`'s attack range. |
guard distanceToAgent(otherAgent: targetAgent) <= GameplayConfiguration.GroundBot.maximumAttackDistance else { return } |
// 4) Check if any walls or obstacles are between the `GroundBot` and its hunt target position. |
var hasLineOfSight = true |
scene.physicsWorld.enumerateBodies(alongRayStart: CGPoint(agent.position), end: CGPoint(targetAgent.position)) { body, _, _, stop in |
if ColliderType(rawValue: body.categoryBitMask).contains(.Obstacle) { |
hasLineOfSight = false |
stop.pointee = true |
} |
} |
if !hasLineOfSight { return } |
// The `GroundBot` is ready to attack the `targetAgent`'s current position. |
targetPosition = targetAgent.position |
intelligenceComponent.stateMachine.enter(GroundBotRotateToAttackState.self) |
} |
// MARK: ChargeComponentDelegate |
func chargeComponentDidLoseCharge(chargeComponent: ChargeComponent) { |
guard let intelligenceComponent = component(ofType: IntelligenceComponent.self) else { return } |
isGood = !chargeComponent.hasCharge |
if !isGood { |
intelligenceComponent.stateMachine.enter(TaskBotZappedState.self) |
} |
} |
// MARK: ResourceLoadableType |
static var resourcesNeedLoading: Bool { |
return goodAnimations == nil || badAnimations == nil |
} |
static func loadResources(withCompletionHandler completionHandler: @escaping () -> ()) { |
// Load `TaskBot`s shared assets. |
super.loadSharedAssets() |
let groundBotAtlasNames = [ |
"GroundBotGoodWalk", |
"GroundBotBadWalk", |
"GroundBotAttack", |
"GroundBotZapped" |
] |
/* |
Preload all of the texture atlases for `GroundBot`. This improves |
the overall loading speed of the animation cycles for this character. |
*/ |
SKTextureAtlas.preloadTextureAtlasesNamed(groundBotAtlasNames) { error, groundBotAtlases in |
if let error = error { |
fatalError("One or more texture atlases could not be found: \(error)") |
} |
/* |
This closure sets up all of the `GroundBot` animations |
after the `GroundBot` texture atlases have finished preloading. |
*/ |
goodAnimations = [:] |
goodAnimations![.walkForward] = AnimationComponent.animationsFromAtlas(atlas: groundBotAtlases[0], withImageIdentifier: "GroundBotGoodWalk", forAnimationState: .walkForward) |
badAnimations = [:] |
badAnimations![.walkForward] = AnimationComponent.animationsFromAtlas(atlas: groundBotAtlases[1], withImageIdentifier: "GroundBotBadWalk", forAnimationState: .walkForward) |
badAnimations![.attack] = AnimationComponent.animationsFromAtlas(atlas: groundBotAtlases[2], withImageIdentifier: "GroundBotAttack", forAnimationState: .attack, bodyActionName: "ZappedShake", shadowActionName: "ZappedShadowShake", repeatTexturesForever: false) |
badAnimations![.zapped] = AnimationComponent.animationsFromAtlas(atlas: groundBotAtlases[3], withImageIdentifier: "GroundBotZapped", forAnimationState: .zapped, bodyActionName: "ZappedShake", shadowActionName: "ZappedShadowShake") |
// Invoke the passed `completionHandler` to indicate that loading has completed. |
completionHandler() |
} |
} |
static func purgeResources() { |
goodAnimations = nil |
badAnimations = nil |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13