Objective-C/Common/AAPLCharacter.m
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
This class manages the main character, including its animations, sounds and direction. |
*/ |
@import SceneKit; |
#import "AAPLCharacter.h" |
#import "AAPLGameViewController.h" |
static CGFloat const AAPLCharacterSpeedFactor = 1.538; |
static NSUInteger const AAPLCharacterStepsCount = 11; |
@implementation AAPLCharacter { |
// Character handle |
SCNNode *_node; |
// Controlling the character |
AAPLGroundType _groundType; |
NSTimeInterval _previousUpdateTime; |
CGFloat _walkSpeed; |
CGFloat _accelerationY; |
CGFloat _directionAngle; |
BOOL _isWalking; |
BOOL _isBurning; |
BOOL _isInvincible; |
// Particle systems |
SCNNode *_fireEmitter; |
SCNNode *_smokeEmitter; |
SCNNode *_whiteSmokeEmitter; |
CGFloat _fireEmitterBirthRate; |
CGFloat _smokeEmitterBirthRate; |
CGFloat _whiteSmokeEmitterBirthRate; |
// Sound effects |
SCNAudioSource *_reliefSound; |
SCNAudioSource *_haltFireSound; |
SCNAudioSource *_catchFireSound; |
SCNAudioSource *_steps[AAPLCharacterStepsCount][AAPLGroundTypeCount]; |
// Animations |
CAAnimation *_walkAnimation; |
} |
#pragma mark - Initialization |
- (instancetype)init { |
if (self = [super init]) { |
/// Load character from external file |
_node = [SCNNode node]; |
SCNScene *characterScene = [SCNScene sceneNamed:@"game.scnassets/panda.scn"]; |
SCNNode *characterTopLevelNode = characterScene.rootNode.childNodes[0]; |
[_node addChildNode:characterTopLevelNode]; |
/// Configure collision capsule |
// Collisions are handled by the physics engine. The character is approximated by |
// a capsule that is configured to collide with collectables, enemies and walls |
SCNVector3 min, max; |
[_node getBoundingBoxMin:&min max:&max]; |
CGFloat collisionCapsuleRadius = (max.x - min.x) * 0.4; |
CGFloat collisionCapsuleHeight = (max.y - min.y); |
SCNNode *characterCollisionNode = [SCNNode node]; |
characterCollisionNode.name = @"collider"; |
characterCollisionNode.position = SCNVector3Make(0.0, collisionCapsuleHeight * 0.51, 0.0);// a bit too high to not hit the floor |
characterCollisionNode.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeKinematic shape:[SCNPhysicsShape shapeWithGeometry:[SCNCapsule capsuleWithCapRadius:collisionCapsuleRadius height:collisionCapsuleHeight] options:nil]]; |
characterCollisionNode.physicsBody.contactTestBitMask = AAPLBitmaskSuperCollectable | AAPLBitmaskCollectable | AAPLBitmaskCollision | AAPLBitmaskEnemy; |
[_node addChildNode:characterCollisionNode]; |
/// Load particle systems |
// Particle systems were configured in the SceneKit Scene Editor |
// They are retrieved from the scene and their birth rate are stored for later use |
_fireEmitter = [characterTopLevelNode childNodeWithName:@"fire" recursively:YES]; |
_fireEmitterBirthRate = _fireEmitter.particleSystems[0].birthRate; |
_fireEmitter.particleSystems[0].birthRate = 0; |
_fireEmitter.hidden = NO; |
_smokeEmitter = [characterTopLevelNode childNodeWithName:@"smoke" recursively:YES]; |
_smokeEmitterBirthRate = _smokeEmitter.particleSystems[0].birthRate; |
_smokeEmitter.particleSystems[0].birthRate = 0; |
_smokeEmitter.hidden = NO; |
_whiteSmokeEmitter = [characterTopLevelNode childNodeWithName:@"whiteSmoke" recursively:YES]; |
_whiteSmokeEmitterBirthRate = _whiteSmokeEmitter.particleSystems[0].birthRate; |
_whiteSmokeEmitter.particleSystems[0].birthRate = 0; |
_whiteSmokeEmitter.hidden = NO; |
/// Load sound effects |
_reliefSound = [SCNAudioSource audioSourceNamed:@"game.scnassets/sounds/aah_extinction.mp3"]; |
_reliefSound.volume = 2.0; |
[_reliefSound load]; |
_haltFireSound = [SCNAudioSource audioSourceNamed:@"game.scnassets/sounds/fire_extinction.mp3"]; |
_haltFireSound.volume = 2.0; |
[_haltFireSound load]; |
_catchFireSound = [SCNAudioSource audioSourceNamed:@"game.scnassets/sounds/ouch_firehit.mp3"]; |
_catchFireSound.volume = 2.0; |
[_catchFireSound load]; |
for (NSUInteger i = 0; i < AAPLCharacterStepsCount; i++) { |
_steps[i][AAPLGroundTypeGrass] = [SCNAudioSource audioSourceNamed:[NSString stringWithFormat:@"game.scnassets/sounds/Step_grass_0%d.mp3", (uint32_t)i]]; |
_steps[i][AAPLGroundTypeGrass].volume = 0.5; |
[_steps[i][AAPLGroundTypeGrass] load]; |
_steps[i][AAPLGroundTypeRock] = [SCNAudioSource audioSourceNamed:[NSString stringWithFormat:@"game.scnassets/sounds/Step_rock_0%d.mp3", (uint32_t)i]]; |
[_steps[i][AAPLGroundTypeRock] load]; |
_steps[i][AAPLGroundTypeWater] = [SCNAudioSource audioSourceNamed:[NSString stringWithFormat:@"game.scnassets/sounds/Step_splash_0%d.mp3", (uint32_t)i]]; |
[_steps[i][AAPLGroundTypeWater] load]; |
} |
/// Configure animations |
// Some animations are already there and can be retrieved from the scene |
// The "walk" animation is loaded from a file, it is configured to play foot steps at specific times during the animation |
[characterTopLevelNode enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
for(NSString *key in child.animationKeys) { // for every animation key |
CAAnimation *animation = [child animationForKey:key]; // get the animation |
animation.usesSceneTimeBase = NO; // make it system time based |
animation.repeatCount = FLT_MAX; // make it repeat forever |
[child addAnimation:animation forKey:key]; // animations are copied upon addition, so we have to replace the previous animation |
} |
}]; |
_walkAnimation = [self loadAnimationFromSceneNamed:@"game.scnassets/walk.scn"]; |
_walkAnimation.usesSceneTimeBase = NO; |
_walkAnimation.fadeInDuration = 0.3; |
_walkAnimation.fadeOutDuration = 0.3; |
_walkAnimation.repeatCount = FLT_MAX; |
_walkAnimation.speed = AAPLCharacterSpeedFactor; |
_walkAnimation.animationEvents = @[[SCNAnimationEvent animationEventWithKeyTime:0.1 block:^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { [self playFootStep]; }], |
[SCNAnimationEvent animationEventWithKeyTime:0.6 block:^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { [self playFootStep]; }]]; |
/// Misc |
_walkSpeed = 1.0; |
} |
return self; |
} |
#pragma mark - Retrieving nodes |
- (SCNNode *)node { |
return _node; |
} |
#pragma mark - Controlling the character |
- (void)setDirectionAngle:(CGFloat)directionAngle { |
_directionAngle = directionAngle; |
[_node runAction:[SCNAction rotateToX:0.0 y:directionAngle z:0.0 duration:0.1 shortestUnitArc:YES]]; |
} |
- (SCNNode *)walkInDirection:(vector_float3)direction time:(NSTimeInterval)time scene:(SCNScene *)scene groundTypeFromMaterial:(AAPLGroundType(^)(SCNMaterial *))groundTypeFromMaterial { |
// delta time since last update |
if (_previousUpdateTime == 0.0) { |
_previousUpdateTime = time; |
} |
NSTimeInterval deltaTime = MIN(time - _previousUpdateTime, 1.0 / 60.0); |
CGFloat characterSpeed = deltaTime * AAPLCharacterSpeedFactor * 0.84; |
_previousUpdateTime = time; |
SCNVector3 initialPosition = _node.position; |
// move |
if (direction.x != 0.0 && direction.z != 0.0) { |
// move character |
vector_float3 position = SCNVector3ToFloat3(_node.position); |
_node.position = SCNVector3FromFloat3(position + direction * characterSpeed); |
// update orientation |
self.directionAngle = atan2(direction.x, direction.z); |
self.walking = YES; |
} |
else { |
self.walking = NO; |
} |
// Update the altitude of the character |
SCNVector3 position = _node.position; |
SCNVector3 p0 = position; |
SCNVector3 p1 = position; |
static CGFloat const kMaxRise = 0.08; |
static CGFloat const kMaxJump = 10.0; |
p0.y -= kMaxJump; |
p1.y += kMaxRise; |
// Do a vertical ray intersection |
SCNNode *groundNode = nil; |
NSArray<SCNHitTestResult *> *results = [scene.physicsWorld rayTestWithSegmentFromPoint:p1 toPoint:p0 options:@{SCNPhysicsTestCollisionBitMaskKey: @(AAPLBitmaskCollision | AAPLBitmaskWater), SCNPhysicsTestSearchModeKey : SCNPhysicsTestSearchModeClosest}]; |
SCNHitTestResult *result = results.firstObject; |
if (result) { |
CGFloat groundAltitude = result.worldCoordinates.y; |
groundNode = result.node; |
SCNMaterial *groundMaterial = result.node.childNodes[0].geometry.firstMaterial; |
_groundType = groundTypeFromMaterial(groundMaterial); |
if (_groundType == AAPLGroundTypeWater) { |
if (_isBurning) { |
[self haltFire]; |
} |
// do a new ray test without the water to get the altitude of the ground (under the water). |
results = [scene.physicsWorld rayTestWithSegmentFromPoint:p1 toPoint:p0 options:@{SCNPhysicsTestCollisionBitMaskKey : @(AAPLBitmaskCollision), SCNPhysicsTestSearchModeKey : SCNPhysicsTestSearchModeClosest}]; |
result = results.firstObject; |
groundAltitude = result.worldCoordinates.y; |
} |
static CGFloat const kThreshold = 1e-5; |
static CGFloat const kGravityAcceleration = 0.18; |
if (groundAltitude < position.y - kThreshold) { |
_accelerationY += deltaTime * kGravityAcceleration; // approximation of acceleration for a delta time. |
if (groundAltitude < position.y - 0.2) { |
_groundType = AAPLGroundTypeInTheAir; |
} |
} |
else { |
_accelerationY = 0; |
} |
position.y -= _accelerationY; |
// reset acceleration if we touch the ground |
if (groundAltitude > position.y) { |
_accelerationY = 0; |
position.y = groundAltitude; |
} |
// Finally, update the position of the character. |
_node.position = position; |
} |
else { |
// no result, we are probably out the bounds of the level -> revert the position of the character. |
_node.position = initialPosition; |
} |
return groundNode; |
} |
#pragma mark - Animating the character |
- (void)setWalking:(BOOL)walking { |
if (_isWalking != walking) { |
_isWalking = walking; |
// Update node animation. |
if (_isWalking) { |
[_node addAnimation:_walkAnimation forKey:@"walk"]; |
} |
else { |
[_node removeAnimationForKey:@"walk" fadeOutDuration:0.2]; |
} |
} |
} |
- (void)setWalkSpeed:(CGFloat)walkSpeed { |
_walkSpeed = walkSpeed; |
// remove current walk animation if any. |
BOOL wasWalking = _isWalking; |
if (wasWalking) |
self.walking = NO; |
_walkAnimation.speed = AAPLCharacterSpeedFactor * _walkSpeed; |
// restore walk animation if needed. |
if (wasWalking) |
self.walking = YES; |
} |
#pragma mark - Dealing with fire |
- (void)catchFire { |
if (_isInvincible == NO) { |
_isInvincible = YES; |
[_node runAction:[SCNAction sequence:@[[SCNAction playAudioSource:_catchFireSound waitForCompletion:NO], |
[SCNAction repeatAction:[SCNAction sequence:@[[SCNAction fadeOpacityTo:0.01 duration:0.1], |
[SCNAction fadeOpacityTo:1.0 duration:0.1]]] |
count:7], |
[SCNAction runBlock:^(SCNNode *node) { _isInvincible = NO; }]]]]; |
} |
_isBurning = YES; |
// start fire + smoke |
_fireEmitter.particleSystems[0].birthRate = _fireEmitterBirthRate; |
_smokeEmitter.particleSystems[0].birthRate = _smokeEmitterBirthRate; |
// walk faster |
self.walkSpeed = 2.3; |
} |
- (void)haltFire { |
if (_isBurning) { |
_isBurning = NO; |
[_node runAction:[SCNAction sequence:@[[SCNAction playAudioSource:_haltFireSound waitForCompletion:true], |
[SCNAction playAudioSource:_reliefSound waitForCompletion:false]]]]; |
// stop fire and smoke |
_fireEmitter.particleSystems[0].birthRate = 0; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.0]; |
_smokeEmitter.particleSystems[0].birthRate = 0; |
[SCNTransaction commit]; |
// start white smoke |
_whiteSmokeEmitter.particleSystems[0].birthRate = _whiteSmokeEmitterBirthRate; |
// progressively stop white smoke |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:5.0]; |
_whiteSmokeEmitter.particleSystems[0].birthRate = 0; |
[SCNTransaction commit]; |
// walk normally |
self.walkSpeed = 1.0; |
} |
} |
#pragma mark - Dealing with sound |
- (void)playFootStep { |
if (_groundType != AAPLGroundTypeInTheAir) { // We are in the air, no sound to play. |
// Play a random step sound. |
NSInteger stepSoundIndex = arc4random_uniform(AAPLCharacterStepsCount); |
[_node runAction:[SCNAction playAudioSource:_steps[stepSoundIndex][_groundType] waitForCompletion:NO]]; |
} |
} |
#pragma mark - Utils |
- (CAAnimation *)loadAnimationFromSceneNamed:(NSString *)sceneName { |
SCNScene *scene = [SCNScene sceneNamed:sceneName]; |
// find top level animation |
__block CAAnimation *animation = nil; |
[scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
if (child.animationKeys.count > 0) { |
animation = [child animationForKey:child.animationKeys[0]]; |
*stop = YES; |
} |
}]; |
return animation; |
} |
@end |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13