/* |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
|
Abstract: |
|
The primary scene class for Adventure |
|
*/ |
|
import SpriteKit |
|
// Set this to true to cheat and end up next to the level boss |
let cheat = false |
|
enum HeroClass { |
case Warrior |
case Archer |
} |
|
enum WorldLayer: Int { |
case Ground = 0, BelowCharacter, Character, AboveCharacter, Top |
} |
|
var sBackgroundTiles = [SKSpriteNode]() |
var sSharedSmallTree = Tree(sprites: [SKSpriteNode](), usingOffset: 0.0) |
var sSharedBigTree = Tree(sprites: [SKSpriteNode](), usingOffset: 0.0) |
var sSharedLeafEmitterA = SKEmitterNode() |
var sSharedLeafEmitterB = SKEmitterNode() |
var sSharedProjectileSparkEmitter = SKEmitterNode() |
var sSharedSpawnEmitter = SKEmitterNode() |
var sSharedCave = Cave(sprites: [SKSpriteNode](), atPosition: CGPointZero, usingOffset: 0.0) |
|
let kWorldTileDivisor = 32 |
let kWorldSize = 4096 |
let kWorldTileSize = kWorldSize / kWorldTileDivisor |
let kWorldCenter = kWorldSize / 2 |
let kLevelMapSize = 256 |
let kLevelMapDivisor = (kWorldSize / kLevelMapSize) |
|
let kStartLives = 3 |
let kWorldLayerCount = 5 |
let kMinTimeInterval = (1.0 / 60.0) |
let kMinHeroToEdgeDistance: CGFloat = 256.0 // minimum distance between hero and edge of camera before moving camera |
|
class AdventureScene: SKScene, SKPhysicsContactDelegate { |
// MARK: Properties |
|
// Where all the parts live. |
var world = SKNode() |
|
// Convenience properties for nodes that are updated during each pass. Populated during loading. |
var heroes: [HeroCharacter] = [] |
var goblinCaves: [Cave] = [] |
var levelBoss: Boss! |
var trees: [Tree] = [] |
var particleSystems: [SKEmitterNode] = [] |
var parallaxSprites: [ParallaxSprite] = [] |
|
// Convenience property containing references to the 5 different layers in the world. |
var layers = [SKNode]() |
|
// Convenience properties for the nodes that make up the HUD. |
var hudAvatar: SKSpriteNode! |
var hudScore: SKLabelNode! |
var hudLifeHearts = [SKSpriteNode]() |
|
var defaultSpawnPoint = CGPointZero |
var defaultPlayer = Player() |
|
// Properties to keep track of details important to scene updates. |
var worldMovedForUpdate = false |
var lastUpdateTimeInterval: NSTimeInterval = 0 |
|
// A closure to be called when didMoveToView(_:) completes. |
var finishedMovingToView: Void -> Void = {} |
|
// MARK: SKView Behaviors |
|
override func didMoveToView(view: SKView) { |
let backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) |
|
// Complete the loading on a background queue to not take up the main queue's resources. |
dispatch_async(backgroundQueue) { |
self.loadHUD() |
|
self.updateHUDForPlayer(self.defaultPlayer) |
|
self.loadWorld() |
|
self.centerWorldOnPosition(self.defaultSpawnPoint) |
|
dispatch_async(dispatch_get_main_queue(), self.finishedMovingToView) |
} |
} |
|
// MARK: Scene Processing Support |
|
override func update(currentTime: NSTimeInterval) { |
var timeSinceLast = currentTime - lastUpdateTimeInterval |
lastUpdateTimeInterval = currentTime |
|
if timeSinceLast > 1 { |
timeSinceLast = kMinTimeInterval |
worldMovedForUpdate = true |
} |
|
updateWithTimeSinceLastUpdate(timeSinceLast) |
|
if defaultPlayer.hero == nil { |
return |
} |
|
let hero = defaultPlayer.hero! |
|
if hero.dying { |
return |
} |
|
if defaultPlayer.moveForward { |
hero.move(.Forward, withTimeInterval: timeSinceLast) |
} |
else if defaultPlayer.moveBackward { |
hero.move(.Back, withTimeInterval: timeSinceLast) |
} |
|
if defaultPlayer.moveLeft { |
hero.move(.Left, withTimeInterval: timeSinceLast) |
} |
else if defaultPlayer.moveRight { |
hero.move(.Right, withTimeInterval: timeSinceLast) |
} |
|
if defaultPlayer.fireAction { |
hero.performAttackAction() |
} |
|
#if os(iOS) |
if defaultPlayer.targetLocation != CGPointZero { |
if defaultPlayer.fireAction { |
hero.faceTo(defaultPlayer.targetLocation) |
} |
|
if defaultPlayer.moveRequested { |
if defaultPlayer.targetLocation != hero.position { |
hero.moveTowards(defaultPlayer.targetLocation, withTimeInterval: timeSinceLast) |
} |
else { |
defaultPlayer.moveRequested = false |
} |
} |
} |
#endif |
} |
|
override func didSimulatePhysics() { |
if let defaultHero = defaultPlayer.hero { |
let heroPosition = defaultHero.position |
var worldPos = world.position |
|
let yCoordinate = worldPos.y + heroPosition.y |
if yCoordinate < kMinHeroToEdgeDistance { |
worldPos.y = worldPos.y - yCoordinate + kMinHeroToEdgeDistance |
worldMovedForUpdate = true |
} else if yCoordinate > (frame.size.height - kMinHeroToEdgeDistance) { |
worldPos.y = worldPos.y + (frame.size.height - yCoordinate) - kMinHeroToEdgeDistance |
worldMovedForUpdate = true |
} |
|
let xCoordinate = worldPos.x + heroPosition.x |
if xCoordinate < kMinHeroToEdgeDistance { |
worldPos.x = worldPos.x - xCoordinate + kMinHeroToEdgeDistance |
worldMovedForUpdate = true |
} else if xCoordinate > (frame.size.width - kMinHeroToEdgeDistance) { |
worldPos.x = worldPos.x + (frame.size.width - xCoordinate) - kMinHeroToEdgeDistance |
worldMovedForUpdate = true |
} |
|
world.position = worldPos |
|
updateAfterSimulatingPhysics() |
|
worldMovedForUpdate = false |
} |
} |
|
func updateWithTimeSinceLastUpdate(timeSinceLast: NSTimeInterval) { |
for hero in heroes { |
hero.updateWithTimeSinceLastUpdate(timeSinceLast) |
} |
|
levelBoss?.updateWithTimeSinceLastUpdate(timeSinceLast) |
|
for cave in goblinCaves { |
cave.updateWithTimeSinceLastUpdate(timeSinceLast) |
} |
} |
|
func updateAfterSimulatingPhysics() { |
var position = defaultPlayer.hero!.position |
|
for tree in trees { |
if tree.position.distanceToPoint(position) < 1024 { |
tree.updateAlphaWithScene(self) |
} |
} |
|
if !worldMovedForUpdate { |
return |
} |
|
for emitter in particleSystems { |
var emitterIsVisible = (emitter.position.distanceToPoint(position) < 1024) |
if !emitterIsVisible && !emitter.paused { |
emitter.paused = true |
} else if emitterIsVisible && emitter.paused { |
emitter.paused = false |
} |
} |
|
for sprite in parallaxSprites { |
if sprite.position.distanceToPoint(position) < 1024 { |
sprite.updateOffset() |
} |
} |
} |
|
// MARK: SKPhysicsContactDelegate |
|
func didBeginContact(contact: SKPhysicsContact) { |
if let character = contact.bodyA.node as? Character { |
character.collidedWith(contact.bodyB) |
} |
|
if let character = contact.bodyB.node as? Character { |
character.collidedWith(contact.bodyA) |
} |
|
let rawProjectileType = ColliderType.Projectile.rawValue |
if contact.bodyA.categoryBitMask & rawProjectileType == rawProjectileType || contact.bodyB.categoryBitMask & rawProjectileType == rawProjectileType { |
if let projectile = (contact.bodyA.categoryBitMask & rawProjectileType) == rawProjectileType ? contact.bodyA.node : contact.bodyB.node { |
projectile.runAction(SKAction.removeFromParent()) |
|
let emitter = sSharedProjectileSparkEmitter.copy() as SKEmitterNode |
addNode(emitter, atWorldLayer: .AboveCharacter) |
emitter.position = projectile.position |
|
runOneShotEmitter(emitter, withDuration: 0.15) |
} |
} |
} |
|
// MARK: Game Start |
|
func startLevel(charClass: HeroClass) { |
defaultPlayer.charClass = charClass |
addHeroForPlayer(defaultPlayer) |
|
if cheat { |
var bossPosition = levelBoss!.position |
bossPosition.x += 128 |
bossPosition.y += 512 |
defaultPlayer.hero!.position = bossPosition |
} |
} |
|
// MARK: Character Support |
|
func canSee(point: CGPoint, from vantagePoint: CGPoint) -> Bool { |
let rayStart = vantagePoint |
let rayEnd = point |
|
var canSee = true |
physicsWorld.enumerateBodiesAlongRayStart(rayStart, end: rayEnd) { body, point, normal, stop in |
if body.categoryBitMask & ColliderType.Wall.rawValue == ColliderType.Wall.rawValue || body.node?.name == "wall" { |
canSee = false |
stop.memory = true |
} |
} |
|
return canSee |
} |
|
// MARK: Hero Interactions |
|
func addHeroForPlayer(player: Player) -> HeroCharacter { |
if let hero = player.hero { |
if !hero.dying { |
hero.removeFromParent() |
} |
} |
|
let spawnPos = defaultSpawnPoint |
|
var hero: HeroCharacter |
switch player.charClass! { |
case .Warrior: |
hero = Warrior(atPosition: spawnPos, withPlayer: player) |
|
case .Archer: |
hero = Archer(atPosition: spawnPos, withPlayer: player) |
} |
|
let emitter = sSharedSpawnEmitter.copy() as SKEmitterNode |
emitter.position = spawnPos |
addNode(emitter, atWorldLayer: .AboveCharacter) |
runOneShotEmitter(emitter, withDuration: 0.15) |
|
hero.fadeIn(2.0) |
hero.addToScene(self) |
heroes.append(hero) |
|
player.hero = hero |
|
return hero |
} |
|
func heroWasKilled(hero: HeroCharacter) { |
for cave in goblinCaves { |
cave.stopGoblinsFromTargettingHero(hero) |
} |
|
let player = hero.player |
|
// Remove this hero from our list of heroes |
for (idx, obj) in enumerate(heroes) { |
if obj === hero { |
heroes.removeAtIndex(idx) |
break |
} |
} |
|
#if os(iOS) |
// Disable touch movement, otherwise new hero will try to move to previously-touched location. |
player.moveRequested = false |
#endif |
|
--player.livesLeft |
|
if player.livesLeft < 0 { |
// In a real game, you'd want to end the game when there are no lives left. |
return |
} |
|
updateHUDAfterHeroDeathForPlayer(hero.player) |
|
let hero = addHeroForPlayer(player) |
|
centerWorldOnCharacter(hero) |
} |
|
// MARK: HUD and Scores |
|
func loadHUD() { |
let hudScene: SKScene = SKScene.nodeWithName("AdventureHUD") |
let hud = hudScene.children[0].copy() as SKNode |
hud.position = CGPoint(x: 0, y: self.frame.size.height) |
addChild(hud) |
hudAvatar = hud.childNodeWithName("hudAvatar") as SKSpriteNode |
hudScore = hud.childNodeWithName("hudScore") as SKLabelNode |
hud.enumerateChildNodesWithName("hudHeart") { node, stop in |
self.hudLifeHearts.append(node as SKSpriteNode) |
} |
} |
|
func updateHUDForPlayer(player: Player) { |
hudScore.text = "SCORE: \(player.score)" |
} |
|
func updateHUDAfterHeroDeathForPlayer(player: Player) { |
// Fade out the relevant heart - one-based livesLeft has already been decremented. |
let heartNumber = player.livesLeft |
|
let heart = hudLifeHearts[heartNumber] |
heart.runAction(SKAction.fadeAlphaTo(0.0, duration: 3.0)) |
} |
|
func addToScore(amount: Int, afterEnemyKillWithProjectile projectile: SKNode) { |
if let player = projectile.userData?[kPlayer] as? Player { |
player.score += amount |
updateHUDForPlayer(player) |
} |
} |
|
// MARK: World Construction |
|
func loadWorld() { |
physicsWorld.gravity = CGVector(dx: 0, dy: 0) |
physicsWorld.contactDelegate = self |
|
let scene: SKScene = SKScene.nodeWithName("AdventureWorld") |
let templateWorld = scene.children[0].copy() as SKNode |
|
world.name = "world" |
addChild(world) |
|
populateLayersFromWorld(templateWorld) |
populateBackgroundTiles() |
populateWallsFromWorld(templateWorld) |
populateCharactersFromWorld(templateWorld) |
populateTreesFromWorld(templateWorld) |
} |
|
func populateLayersFromWorld(fromWorld: SKNode) { |
fromWorld.enumerateChildNodesWithName("layer*") { (node, stop) -> Void in |
let layer = SKNode() |
layer.name = node.name |
self.world.addChild(layer) |
self.layers.append(layer) |
} |
} |
|
func populateBackgroundTiles() { |
for tileNode in sBackgroundTiles { |
addNode(tileNode, atWorldLayer: .Ground) |
} |
} |
|
func populateWallsFromWorld(fromWorld: SKNode) { |
let ground = fromWorld.childNodeWithName("layerGround")! |
ground.enumerateChildNodesWithName("wall") { node, stop in |
// Unwrap the physics body to configure it. |
let wallNode = node.copy() as SKNode |
wallNode.physicsBody!.dynamic = false |
wallNode.physicsBody!.categoryBitMask = ColliderType.Wall.rawValue |
self.addNode(wallNode, atWorldLayer: .Ground) |
} |
} |
|
func populateCharactersFromWorld(fromWorld: SKNode) { |
defaultSpawnPoint = fromWorld.childNodeWithName("//defaultSpawnPoint")!.position |
levelBoss = Boss(atPosition: fromWorld.childNodeWithName("//boss")!.position) |
levelBoss!.addToScene(self) |
|
fromWorld.enumerateChildNodesWithName("//cave") { node, stop in |
let cave = sSharedCave.copy() as Cave |
cave.position = node.position |
cave.zRotation = node.zRotation |
cave.timeUntilNextGenerate = 5.0 + 5.0 * unitRandom() |
|
for _ in 0..<caveCapacity { |
var goblin = Goblin(atPosition: node.position) |
goblin.cave = cave |
cave.inactiveGoblins.append(goblin) |
} |
|
// Make it aware! |
cave.intelligence = SpawnArtificialIntelligence(character: cave, target: nil) |
|
self.goblinCaves.append(cave) |
self.parallaxSprites.append(cave) |
cave.addToScene(self) |
} |
} |
|
func populateTreesFromWorld(fromWorld: SKNode) { |
fromWorld.enumerateChildNodesWithName("//*Tree") { node, stop in |
var tree: Tree |
switch node.name { |
case let name where name == "smallTree": |
tree = sSharedSmallTree.copy() as Tree |
case let name where name == "bigTree": |
tree = sSharedBigTree.copy() as Tree |
|
var emitter: SKEmitterNode |
if arc4random_uniform(2) == 1 { |
emitter = sSharedLeafEmitterA.copy() as SKEmitterNode |
} else { |
emitter = sSharedLeafEmitterB.copy() as SKEmitterNode |
} |
|
emitter.position = node.position |
emitter.paused = true |
self.addNode(emitter, atWorldLayer: .AboveCharacter) |
self.particleSystems.append(emitter) |
default: |
return |
} |
|
tree.position = node.position |
tree.zRotation = unitRandom() |
self.addNode(tree, atWorldLayer: .Top) |
self.parallaxSprites.append(tree) |
self.trees.append(tree) |
} |
} |
|
func addNode(node: SKNode, atWorldLayer layer: WorldLayer) { |
let layerNode = layers[layer.rawValue] |
|
layerNode.addChild(node) |
} |
|
// MARK: Asset Pre-loading |
class func loadSceneAssetsWithCompletionHandler(completionHandler: Void -> Void) { |
let queue = dispatch_get_main_queue() |
|
let backgroundQueue = dispatch_get_global_queue(CLong(DISPATCH_QUEUE_PRIORITY_HIGH), 0) |
dispatch_async(backgroundQueue) { |
AdventureScene.loadBackgroundTiles() |
|
Goblin.loadSharedAssets() |
Warrior.loadSharedAssets() |
Archer.loadSharedAssets() |
Cave.loadSharedAssets() |
HeroCharacter.loadSharedHeroAssets() |
Boss.loadSharedAssets() |
|
sSharedLeafEmitterA = .nodeWithName("Leaves_01") |
sSharedLeafEmitterB = .nodeWithName("Leaves_02") |
sSharedProjectileSparkEmitter = .nodeWithName("ProjectileSplat") |
sSharedSpawnEmitter = .nodeWithName("Spawn") |
|
// Load Trees |
let atlas = SKTextureAtlas(named: "Environment") |
var sprites = [ |
SKSpriteNode(texture: atlas.textureNamed("small_tree_base.png")), |
SKSpriteNode(texture: atlas.textureNamed("small_tree_middle.png")), |
SKSpriteNode(texture: atlas.textureNamed("small_tree_top.png")) |
] |
sSharedSmallTree = Tree(sprites: sprites, usingOffset: 25.0) |
sSharedSmallTree.name = "smallTree" |
|
sprites = [ |
SKSpriteNode(texture: atlas.textureNamed("big_tree_base.png")), |
SKSpriteNode(texture: atlas.textureNamed("big_tree_middle.png")), |
SKSpriteNode(texture: atlas.textureNamed("big_tree_top.png")) |
] |
sSharedBigTree = Tree(sprites: sprites, usingOffset: 150.0) |
sSharedBigTree.name = "bigTree" |
sSharedBigTree.fadeAlpha = true |
|
|
sprites = [ |
sSharedCaveBase.copy() as SKSpriteNode, |
sSharedCaveTop.copy() as SKSpriteNode |
] |
sSharedCave = Cave(sprites: sprites, atPosition: CGPointZero, usingOffset: 50.0) |
sSharedCave.movementSpeed = 0.0 |
sSharedCave.name = "goblinCave" |
|
dispatch_async(queue, completionHandler) |
} |
} |
|
class func loadBackgroundTiles() { |
var tileAtlas = SKTextureAtlas(named: "Tiles") |
|
for y in 0..<kWorldTileDivisor { |
for x in 0..<kWorldTileDivisor { |
let tileNumber = (y * kWorldTileDivisor) + x |
|
let tileNode = SKSpriteNode(texture: tileAtlas.textureNamed("tile\(tileNumber).png")) |
|
let xPosition = CGFloat((x * kWorldTileSize) - kWorldCenter + kWorldTileSize / 2) |
let yPosition = CGFloat((kWorldSize - (y * kWorldTileSize)) - kWorldCenter - kWorldTileSize / 2) |
|
let position = CGPoint(x: xPosition, y: yPosition) |
|
tileNode.position = position |
tileNode.zPosition = -1.0 |
tileNode.blendMode = .Replace |
sBackgroundTiles.append(tileNode) |
} |
} |
} |
|
// MARK: Camera Convenience |
func centerWorldOnPosition(position: CGPoint) { |
world.position = CGPoint(x: -position.x + CGRectGetMidX(frame), |
y: -position.y + CGRectGetMidY(frame)) |
worldMovedForUpdate = true |
} |
|
func centerWorldOnCharacter(character: Character) { |
centerWorldOnPosition(character.position) |
} |
} |