Skip to content

Commit ec5127f

Browse files
committed
Add fox sample project
1 parent 4433882 commit ec5127f

File tree

177 files changed

+1734
-194
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

177 files changed

+1734
-194
lines changed

Game.xcodeproj/project.pbxproj

Lines changed: 160 additions & 16 deletions
Large diffs are not rendered by default.
Binary file not shown.

Game.xcworkspace/xcuserdata/Julio.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,16 @@
22
<Bucket
33
type = "0"
44
version = "2.0">
5+
<Breakpoints>
6+
<BreakpointProxy
7+
BreakpointExtensionID = "Xcode.Breakpoint.ExceptionBreakpoint">
8+
<BreakpointContent
9+
shouldBeEnabled = "Yes"
10+
ignoreCount = "0"
11+
continueAfterRunningActions = "No"
12+
scope = "0"
13+
stopOnStyle = "0">
14+
</BreakpointContent>
15+
</BreakpointProxy>
16+
</Breakpoints>
517
</Bucket>

Game/Character.swift

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
/*
2+
Copyright (C) 2015 Apple Inc. All Rights Reserved.
3+
See LICENSE.txt for this sample’s licensing information
4+
5+
Abstract:
6+
This class manages the main character, including its animations, sounds and direction.
7+
*/
8+
9+
import SceneKit
10+
11+
enum GroundType: Int {
12+
case Grass
13+
case Rock
14+
case Water
15+
case InTheAir
16+
case Count
17+
}
18+
19+
private typealias ParticleEmitter = (node: SCNNode, particleSystem: SCNParticleSystem, birthRate: CGFloat)
20+
21+
class Character {
22+
23+
// MARK: Initialization
24+
25+
init() {
26+
27+
// MARK: Load character from external file
28+
29+
// The character is loaded from a .scn file and stored in an intermediate
30+
// node that will be used as a handle to manipulate the whole group at once
31+
32+
let characterScene = SCNScene(named: "game.scnassets/panda.scn")!
33+
let characterTopLevelNode = characterScene.rootNode.childNodes[0]
34+
node.addChildNode(characterTopLevelNode)
35+
36+
37+
// MARK: Configure collision capsule
38+
39+
// Collisions are handled by the physics engine. The character is approximated by
40+
// a capsule that is configured to collide with collectables, enemies and walls
41+
42+
let (min, max) = node.boundingBox
43+
let collisionCapsuleRadius = CGFloat(max.x - min.x) * 0.4
44+
let collisionCapsuleHeight = CGFloat(max.y - min.y)
45+
46+
let characterCollisionNode = SCNNode()
47+
characterCollisionNode.name = "collider"
48+
characterCollisionNode.position = SCNVector3(0.0, collisionCapsuleHeight * 0.51, 0.0) // a bit too high so that the capsule does not hit the floor
49+
characterCollisionNode.physicsBody = SCNPhysicsBody(type: .Kinematic, shape:SCNPhysicsShape(geometry: SCNCapsule(capRadius: collisionCapsuleRadius, height: collisionCapsuleHeight), options:nil))
50+
characterCollisionNode.physicsBody!.contactTestBitMask = BitmaskSuperCollectable | BitmaskCollectable | BitmaskCollision | BitmaskEnemy
51+
node.addChildNode(characterCollisionNode)
52+
53+
54+
// MARK: Load particle systems
55+
56+
// Particle systems were configured in the SceneKit Scene Editor
57+
// They are retrieved from the scene and their birth rate are stored for later use
58+
59+
func particleEmitterWithName(name: String) -> ParticleEmitter {
60+
let emitter: ParticleEmitter
61+
emitter.node = characterTopLevelNode.childNodeWithName(name, recursively:true)!
62+
emitter.particleSystem = emitter.node.particleSystems![0]
63+
emitter.birthRate = emitter.particleSystem.birthRate
64+
emitter.particleSystem.birthRate = 0
65+
emitter.node.hidden = false
66+
return emitter
67+
}
68+
69+
fireEmitter = particleEmitterWithName("fire")
70+
smokeEmitter = particleEmitterWithName("smoke")
71+
whiteSmokeEmitter = particleEmitterWithName("whiteSmoke")
72+
73+
74+
// MARK: Load sound effects
75+
76+
reliefSound = SCNAudioSource(name: "aah_extinction.mp3", volume: 2.0)
77+
haltFireSound = SCNAudioSource(name: "fire_extinction.mp3", volume: 2.0)
78+
catchFireSound = SCNAudioSource(name: "ouch_firehit.mp3", volume: 2.0)
79+
80+
for i in 0..<10 {
81+
if let grassSound = SCNAudioSource(named: "game.scnassets/sounds/Step_grass_0\(i).mp3") {
82+
grassSound.volume = 0.5
83+
grassSound.load()
84+
steps[GroundType.Grass.rawValue].append(grassSound)
85+
}
86+
87+
if let rockSound = SCNAudioSource(named: "game.scnassets/sounds/Step_rock_0\(i).mp3") {
88+
rockSound.load()
89+
steps[GroundType.Rock.rawValue].append(rockSound)
90+
}
91+
92+
if let waterSound = SCNAudioSource(named: "game.scnassets/sounds/Step_splash_0\(i).mp3") {
93+
waterSound.load()
94+
steps[GroundType.Water.rawValue].append(waterSound)
95+
}
96+
}
97+
98+
99+
// MARK: Configure animations
100+
101+
// Some animations are already there and can be retrieved from the scene
102+
// The "walk" animation is loaded from a file, it is configured to play foot steps at specific times during the animation
103+
104+
characterTopLevelNode.enumerateChildNodesUsingBlock { (child, _) in
105+
for key in child.animationKeys { // for every animation key
106+
let animation = child.animationForKey(key)! // get the animation
107+
animation.usesSceneTimeBase = false // make it system time based
108+
animation.repeatCount = Float.infinity // make it repeat forever
109+
child.addAnimation(animation, forKey: key) // animations are copied upon addition, so we have to replace the previous animation
110+
}
111+
}
112+
113+
walkAnimation = CAAnimation.animationWithSceneNamed("game.scnassets/walk.scn")
114+
walkAnimation.usesSceneTimeBase = false
115+
walkAnimation.fadeInDuration = 0.3
116+
walkAnimation.fadeOutDuration = 0.3
117+
walkAnimation.repeatCount = Float.infinity
118+
walkAnimation.speed = Character.speedFactor
119+
walkAnimation.animationEvents = [
120+
SCNAnimationEvent(keyTime: 0.1) { (_, _, _) in self.playFootStep() },
121+
SCNAnimationEvent(keyTime: 0.6) { (_, _, _) in self.playFootStep() }]
122+
}
123+
124+
// MARK: Retrieving nodes
125+
126+
let node = SCNNode()
127+
128+
// MARK: Controlling the character
129+
130+
static let speedFactor = Float(1.538)
131+
132+
private var groundType = GroundType.InTheAir
133+
private var previousUpdateTime = NSTimeInterval(0.0)
134+
private var accelerationY = SCNFloat(0.0) // Simulate gravity
135+
136+
private var directionAngle: SCNFloat = 0.0 {
137+
didSet {
138+
if directionAngle != oldValue {
139+
node.runAction(SCNAction.rotateToX(0.0, y: CGFloat(directionAngle), z: 0.0, duration: 0.1, shortestUnitArc: true))
140+
}
141+
}
142+
}
143+
144+
func walkInDirection(direction: float3, time: NSTimeInterval, scene: SCNScene, groundTypeFromMaterial: SCNMaterial -> GroundType) -> SCNNode? {
145+
// delta time since last update
146+
if previousUpdateTime == 0.0 {
147+
previousUpdateTime = time
148+
}
149+
150+
let deltaTime = Float(min(time - previousUpdateTime, 1.0 / 60.0))
151+
let characterSpeed = deltaTime * Character.speedFactor * 0.84
152+
previousUpdateTime = time
153+
154+
let initialPosition = node.position
155+
156+
// move
157+
if direction.x != 0.0 && direction.z != 0.0 {
158+
// move character
159+
let position = float3(node.position)
160+
node.position = SCNVector3(position + direction * characterSpeed)
161+
162+
// update orientation
163+
directionAngle = SCNFloat(atan2(direction.x, direction.z))
164+
165+
isWalking = true
166+
}
167+
else {
168+
isWalking = false
169+
}
170+
171+
// Update the altitude of the character
172+
173+
var position = node.position
174+
var p0 = position
175+
var p1 = position
176+
177+
let maxRise = SCNFloat(0.08)
178+
let maxJump = SCNFloat(10.0)
179+
p0.y -= maxJump
180+
p1.y += maxRise
181+
182+
// Do a vertical ray intersection
183+
var groundNode: SCNNode?
184+
let results = scene.physicsWorld.rayTestWithSegmentFromPoint(p1, toPoint: p0, options:[SCNPhysicsTestCollisionBitMaskKey: BitmaskCollision | BitmaskWater, SCNPhysicsTestSearchModeKey: SCNPhysicsTestSearchModeClosest])
185+
186+
if let result = results.first {
187+
var groundAltitude = result.worldCoordinates.y
188+
groundNode = result.node
189+
190+
let groundMaterial = result.node.childNodes[0].geometry!.firstMaterial!
191+
groundType = groundTypeFromMaterial(groundMaterial)
192+
193+
if groundType == .Water {
194+
if isBurning {
195+
haltFire()
196+
}
197+
198+
// do a new ray test without the water to get the altitude of the ground (under the water).
199+
let results = scene.physicsWorld.rayTestWithSegmentFromPoint(p1, toPoint: p0, options:[SCNPhysicsTestCollisionBitMaskKey: BitmaskCollision, SCNPhysicsTestSearchModeKey: SCNPhysicsTestSearchModeClosest])
200+
201+
let result = results[0]
202+
groundAltitude = result.worldCoordinates.y
203+
}
204+
205+
let threshold = SCNFloat(1e-5)
206+
let gravityAcceleration = SCNFloat(0.18)
207+
208+
if groundAltitude < position.y - threshold {
209+
accelerationY += SCNFloat(deltaTime) * gravityAcceleration // approximation of acceleration for a delta time.
210+
if groundAltitude < position.y - 0.2 {
211+
groundType = .InTheAir
212+
}
213+
}
214+
else {
215+
accelerationY = 0
216+
}
217+
218+
position.y -= accelerationY
219+
220+
// reset acceleration if we touch the ground
221+
if groundAltitude > position.y {
222+
accelerationY = 0
223+
position.y = groundAltitude
224+
}
225+
226+
// Finally, update the position of the character.
227+
node.position = position
228+
229+
}
230+
else {
231+
// no result, we are probably out the bounds of the level -> revert the position of the character.
232+
node.position = initialPosition
233+
}
234+
235+
return groundNode
236+
}
237+
238+
// MARK: Animating the character
239+
240+
private var walkAnimation: CAAnimation!
241+
242+
private var isWalking: Bool = false {
243+
didSet {
244+
if oldValue != isWalking {
245+
// Update node animation.
246+
if isWalking {
247+
node.addAnimation(walkAnimation, forKey: "walk")
248+
} else {
249+
node.removeAnimationForKey("walk", fadeOutDuration: 0.2)
250+
}
251+
}
252+
}
253+
}
254+
255+
private var walkSpeed: Float = 1.0 {
256+
didSet {
257+
// remove current walk animation if any.
258+
let wasWalking = isWalking
259+
if wasWalking {
260+
isWalking = false
261+
}
262+
263+
walkAnimation.speed = Character.speedFactor * walkSpeed
264+
265+
// restore walk animation if needed.
266+
isWalking = wasWalking
267+
}
268+
}
269+
270+
// MARK: Dealing with fire
271+
272+
private var isBurning = false
273+
private var isInvincible = false
274+
275+
private var fireEmitter: ParticleEmitter! = nil
276+
private var smokeEmitter: ParticleEmitter! = nil
277+
private var whiteSmokeEmitter: ParticleEmitter! = nil
278+
279+
func catchFire() {
280+
if isInvincible == false {
281+
isInvincible = true
282+
node.runAction(SCNAction.sequence([
283+
SCNAction.playAudioSource(catchFireSound, waitForCompletion: false),
284+
SCNAction.repeatAction(SCNAction.sequence([
285+
SCNAction.fadeOpacityTo(0.01, duration: 0.1),
286+
SCNAction.fadeOpacityTo(1.0, duration: 0.1)
287+
]), count: 7),
288+
SCNAction.runBlock { _ in self.isInvincible = false } ]))
289+
}
290+
291+
isBurning = true
292+
293+
// start fire + smoke
294+
fireEmitter.particleSystem.birthRate = fireEmitter.birthRate
295+
smokeEmitter.particleSystem.birthRate = smokeEmitter.birthRate
296+
297+
// walk faster
298+
walkSpeed = 2.3
299+
}
300+
301+
func haltFire() {
302+
if isBurning {
303+
isBurning = false
304+
305+
node.runAction(SCNAction.sequence([
306+
SCNAction.playAudioSource(haltFireSound, waitForCompletion: true),
307+
SCNAction.playAudioSource(reliefSound, waitForCompletion: false)])
308+
)
309+
310+
// stop fire and smoke
311+
fireEmitter.particleSystem.birthRate = 0
312+
SCNTransaction.animateWithDuration(1.0) {
313+
self.smokeEmitter.particleSystem.birthRate = 0
314+
}
315+
316+
// start white smoke
317+
whiteSmokeEmitter.particleSystem.birthRate = whiteSmokeEmitter.birthRate
318+
319+
// progressively stop white smoke
320+
SCNTransaction.animateWithDuration(5.0) {
321+
self.whiteSmokeEmitter.particleSystem.birthRate = 0
322+
}
323+
324+
// walk normally
325+
walkSpeed = 1.0
326+
}
327+
}
328+
329+
// MARK: Dealing with sound
330+
331+
private var reliefSound: SCNAudioSource
332+
private var haltFireSound: SCNAudioSource
333+
private var catchFireSound: SCNAudioSource
334+
335+
private var steps = [[SCNAudioSource]](count: GroundType.Count.rawValue, repeatedValue: [])
336+
337+
private func playFootStep() {
338+
if groundType != .InTheAir { // We are in the air, no sound to play.
339+
// Play a random step sound.
340+
let soundsCount = steps[groundType.rawValue].count
341+
let stepSoundIndex = min(soundsCount - 1, Int(Float(rand()) / Float(RAND_MAX) * Float(soundsCount)))
342+
node.runAction(SCNAction.playAudioSource(steps[groundType.rawValue][stepSoundIndex], waitForCompletion: false))
343+
}
344+
}
345+
346+
}

0 commit comments

Comments
 (0)