/* |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
|
Abstract: |
|
This class simulates the player character. It manages the character's animations and simulates movement and jumping. |
|
*/ |
|
#import <SpriteKit/SpriteKit.h> |
#import <GLKit/GLKit.h> |
|
#import "AAPLMathUtils.h" |
#import "AAPLPlayerCharacter.h" |
#import "AAPLGameLevel.h" |
#import "AAPLGameSimulation.h" |
#import "AAPLSceneView.h" |
|
typedef NS_ENUM(NSInteger, AAPLCharacterAnimation) { |
AAPLCharacterAnimationDie = 0, |
AAPLCharacterAnimationRun, |
AAPLCharacterAnimationJump, |
AAPLCharacterAnimationJumpFalling, |
AAPLCharacterAnimationJumpLand, |
AAPLCharacterAnimationIdle, |
AAPLCharacterAnimationGetHit, |
AAPLCharacterAnimationBored, |
AAPLCharacterAnimationRunStart, |
AAPLCharacterAnimationRunStop, |
AAPLCharacterAnimationCount |
}; |
|
@interface AAPLPlayerCharacter () { |
BOOL _isWalking; |
CGFloat jumpForce; |
CGFloat jumpDuration; |
CGFloat jumpForceOrig; |
CGFloat dustWalkingBirthRate; |
} |
|
@property (nonatomic) BOOL inJumpAnimation; |
@property (nonatomic) CGFloat groundPlaneHeight; |
@property (nonatomic) GLKVector3 velocity; |
@property (nonatomic) CGFloat baseWalkSpeed; |
|
@property (strong, nonatomic) SCNNode *cameraHelper; |
@property (assign, nonatomic) BOOL ChangingDirection; |
|
+ (NSString *)keyForAnimationType:(AAPLCharacterAnimation)animType; |
|
@end |
|
@implementation AAPLPlayerCharacter |
|
+ (NSString *)keyForAnimationType:(AAPLCharacterAnimation)animType |
{ |
|
switch (animType) { |
case AAPLCharacterAnimationBored: |
return @"bored-1"; |
case AAPLCharacterAnimationDie: |
return @"die-1"; |
case AAPLCharacterAnimationGetHit: |
return @"hit-1"; |
case AAPLCharacterAnimationIdle: |
return @"idle-1"; |
case AAPLCharacterAnimationJump: |
return @"jump_start-1"; |
case AAPLCharacterAnimationJumpFalling: |
return @"jump_falling-1"; |
case AAPLCharacterAnimationJumpLand: |
return @"jump_land-1"; |
case AAPLCharacterAnimationRun: |
return @"run-1"; |
case AAPLCharacterAnimationRunStart: |
return @"run_start-1"; |
case AAPLCharacterAnimationRunStop: |
return @"run_stop-1"; |
case AAPLCharacterAnimationCount: |
return nil; |
} |
return nil; |
} |
|
- (id)initWithNode:(SCNNode *)characterNode |
{ |
self = [super initWithNode:characterNode]; |
if (self) { |
self.categoryBitMask = NodeCategoryLava; |
|
// Setup walking parameters. |
_velocity = GLKVector3Make(0, 0, 0); |
_isWalking = NO; |
_ChangingDirection = NO; |
_baseWalkSpeed = .0167f; |
_jumpBoost = 0.0f; |
self.walkSpeed = _baseWalkSpeed * 2; |
self.jumping = NO; |
_groundPlaneHeight = 0.0f; |
self.walkDirection = WalkDirectionRight; |
|
// Create a node to help position the camera and attach to self. |
self.cameraHelper = [SCNNode node]; |
[self addChildNode:self.cameraHelper]; |
self.cameraHelper.position = SCNVector3Make(1000, 200, 0); |
|
// Create a capsule used for generic collision. |
_collideSphere = [SCNNode node]; |
_collideSphere.position = SCNVector3Make(0, 80, 0); |
SCNGeometry *geo = [SCNCapsule capsuleWithCapRadius:90 height:160]; |
SCNPhysicsShape *shape2 = [SCNPhysicsShape shapeWithGeometry:geo options:nil]; |
_collideSphere.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeKinematic shape:shape2]; |
|
// We only want to collide with bananas, coins, and coconuts. Ground collision is handled elsewhere. |
_collideSphere.physicsBody.collisionBitMask = |
GameCollisionCategoryBanana | |
GameCollisionCategoryCoin | |
GameCollisionCategoryCoconut | |
GameCollisionCategoryLava; |
|
// Put ourself into the player category so other objects can limit their scope of collision checks. |
_collideSphere.physicsBody.categoryBitMask = GameCollisionCategoryPlayer; |
[self addChildNode:_collideSphere]; |
|
// Load our dust poof. |
self.dustPoof = [AAPLGameSimulation loadParticleSystemWithName:@"dust"]; |
self.dustWalking = [AAPLGameSimulation loadParticleSystemWithName:@"dustWalking"]; |
dustWalkingBirthRate = self.dustWalking.birthRate; |
|
// Load the animations and store via a lookup table. |
[self setupIdleAnimation]; |
[self setupRunAnimation]; |
[self setupJumpAnimation]; |
[self setupBoredAnimation]; |
[self setupHitAnimation]; |
|
[self playIdle:NO]; |
} |
|
return self; |
} |
|
#pragma mark - Animation Setup |
|
- (void)setupIdleAnimation |
{ |
CAAnimation *idleAnimation = [self loadAndCacheAnimation:@"art.scnassets/characters/explorer/idle" |
forKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationIdle]]; |
if (idleAnimation != nil) { |
idleAnimation.repeatCount = FLT_MAX; |
idleAnimation.fadeInDuration = 0.15f; |
idleAnimation.fadeOutDuration = 0.15f; |
} |
} |
|
- (void)setupRunAnimation |
{ |
NSString *runKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationRun]; |
NSString *runStartKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationRunStart]; |
NSString *runStopKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationRunStop]; |
|
CAAnimation *runAnim = [self loadAndCacheAnimation:@"art.scnassets/characters/explorer/run" |
forKey:runKey]; |
CAAnimation *runStartAnim = [self loadAndCacheAnimation:@"art.scnassets/characters/explorer/run_start" |
forKey:runStartKey]; |
CAAnimation *runStopAnim = [self loadAndCacheAnimation:@"art.scnassets/characters/explorer/run_stop" |
forKey:runStopKey]; |
runAnim.repeatCount = FLT_MAX; |
runStartAnim.repeatCount = 0; |
runStopAnim.repeatCount = 0; |
|
runAnim.fadeInDuration = 0.05f; |
runAnim.fadeOutDuration = 0.05f; |
runStartAnim.fadeInDuration = 0.05f; |
runStartAnim.fadeOutDuration = 0.05f; |
runStopAnim.fadeInDuration = 0.05f; |
runStopAnim.fadeOutDuration = 0.05f; |
|
|
SCNAnimationEventBlock stepLeftBlock = ^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { |
[[AAPLGameSimulation sim] playSound:@"leftstep.caf"]; |
}; |
SCNAnimationEventBlock stepRightBlock = ^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { |
[[AAPLGameSimulation sim] playSound:@"rightstep.caf"]; |
}; |
|
SCNAnimationEventBlock startWalkStateBlock = ^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { |
if (_inRunAnimation == YES) { |
_isWalking = YES; |
} else { |
[self.mainSkeleton removeAnimationForKey:runKey fadeOutDuration:0.15f]; |
} |
}; |
SCNAnimationEventBlock stopWalkStateBlock = ^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { |
_isWalking = NO; |
[self turnOffWalkingDust]; |
if (_ChangingDirection == YES) { |
_inRunAnimation = NO; |
self.inRunAnimation = YES; |
self.ChangingDirection = NO; |
_walkDirection = (_walkDirection == WalkDirectionLeft) ? WalkDirectionRight : WalkDirectionLeft; |
} |
}; |
|
runStopAnim.animationEvents = @[[SCNAnimationEvent animationEventWithKeyTime:1.0 block:stopWalkStateBlock]]; |
runAnim.animationEvents = @[[SCNAnimationEvent animationEventWithKeyTime:0.0 block:startWalkStateBlock], |
[SCNAnimationEvent animationEventWithKeyTime:0.25 block:stepRightBlock], |
[SCNAnimationEvent animationEventWithKeyTime:0.75 block:stepLeftBlock]]; |
} |
|
- (void)setupJumpAnimation |
{ |
NSString *jumpKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationJump]; |
NSString *fallingKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationJumpFalling]; |
NSString *landKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationJumpLand]; |
NSString *idleKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationIdle]; |
|
[self loadAndCacheAnimation:@"art.scnassets/characters/explorer/jump_start" forKey:jumpKey]; |
[self loadAndCacheAnimation:@"art.scnassets/characters/explorer/jump_falling" forKey:fallingKey]; |
[self loadAndCacheAnimation:@"art.scnassets/characters/explorer/jump_land" forKey:landKey]; |
|
CAAnimation *jumpAnimation = [self cachedAnimationForKey:jumpKey]; |
CAAnimation *fallAnimation = [self cachedAnimationForKey:fallingKey]; |
CAAnimation *landAnimation = [self cachedAnimationForKey:landKey]; |
|
jumpAnimation.fadeInDuration = 0.15f; |
jumpAnimation.fadeOutDuration = 0.15f; |
fallAnimation.fadeInDuration = 0.15f; |
landAnimation.fadeInDuration = 0.15f; |
landAnimation.fadeOutDuration = 0.15f; |
|
jumpAnimation.repeatCount = 0; |
fallAnimation.repeatCount = 0; |
landAnimation.repeatCount = 0; |
|
jumpForce = jumpForceOrig = 7.0f; |
jumpDuration = jumpAnimation.duration; |
SCNAnimationEventBlock leaveGroundBlock = ^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { |
_velocity = GLKVector3Add(_velocity, GLKVector3Make(0, jumpForce * 2.1, 0)); |
self.launching = NO; |
self.inJumpAnimation = NO; |
}; |
SCNAnimationEventBlock pause = ^(CAAnimation *animation, id animatedObject, BOOL playingBackward) { |
[self.mainSkeleton pauseAnimationForKey:fallingKey]; |
}; |
|
jumpAnimation.animationEvents = @[[SCNAnimationEvent animationEventWithKeyTime:0.25f block:leaveGroundBlock]]; |
fallAnimation.animationEvents = @[[SCNAnimationEvent animationEventWithKeyTime:0.5f block:pause]]; |
|
// Animation Sequence is to Jump -> Fall -> Land -> Idle. |
[self chainAnimation:jumpKey toAnimation:fallingKey]; |
[self chainAnimation:landKey toAnimation:idleKey]; |
} |
|
- (void)setupBoredAnimation |
{ |
[self loadAndCacheAnimation:@"art.scnassets/characters/explorer/bored" |
forKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationBored]]; |
CAAnimation *animation = [self cachedAnimationForKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationBored]]; |
if (animation != nil) { |
animation.repeatCount = FLT_MAX; |
} |
} |
|
- (void)setupHitAnimation |
{ |
[self loadAndCacheAnimation:@"art.scnassets/characters/explorer/hit" |
forKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationGetHit]]; |
CAAnimation *animation = [self cachedAnimationForKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationGetHit]]; |
if (animation != nil) { |
animation.repeatCount = FLT_MAX; |
} |
} |
|
#pragma mark - |
|
- (BOOL)isRunning |
{ |
return _isWalking; |
} |
|
- (void)playIdle:(BOOL)stop |
{ |
[self turnOffWalkingDust]; |
|
CAAnimation *anim = [self cachedAnimationForKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationIdle]]; |
[anim setRepeatCount:MAXFLOAT]; |
[anim setFadeInDuration:0.1]; |
[anim setFadeOutDuration:0.1]; |
[self.mainSkeleton addAnimation:anim forKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationIdle]]; |
} |
|
- (void)playLand |
{ |
NSString *fallKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationJumpFalling]; |
NSString *key = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationJumpLand]; |
CAAnimation *anim = [self cachedAnimationForKey:key]; |
anim.timeOffset = 0.65f; |
[self.mainSkeleton removeAnimationForKey:fallKey fadeOutDuration:0.15f]; |
self.inJumpAnimation = NO; |
if (_isWalking) { |
_inRunAnimation = NO; |
self.inRunAnimation = YES; |
} else { |
[self.mainSkeleton addAnimation:anim forKey:key]; |
} |
|
[[AAPLGameSimulation sim] playSound:@"Land.wav"]; |
} |
|
- (void)update:(NSTimeInterval)deltaTime |
{ |
GLKMatrix4 mtx = SCNMatrix4ToGLKMatrix4(self.transform); |
|
GLKVector3 gravity = GLKVector3Make(0, -90, 0); |
GLKVector3 gravitystep = GLKVector3MultiplyScalar(gravity, deltaTime); |
|
_velocity = GLKVector3Add(_velocity, gravitystep); |
|
GLKVector3 minMovement = GLKVector3Make(0, -50, 0); |
GLKVector3 maxMovement = GLKVector3Make(100, 100, 100); |
_velocity = GLKVector3Maximum(_velocity, minMovement); |
_velocity = GLKVector3Minimum(_velocity, maxMovement); |
|
mtx = GLKMatrix4TranslateWithVector3(mtx, _velocity); |
_groundPlaneHeight = [self getGroundHeight:mtx]; |
|
if (mtx.m31 < _groundPlaneHeight) { |
if (self.launching == NO && _velocity.y < 0.0f) { |
if (self.jumping == YES) { |
self.jumping = NO; |
if (self.dustPoof != nil) { |
[self addParticleSystem:self.dustPoof]; |
self.dustPoof.loops = NO; |
} |
[self playLand]; |
_jumpBoost = 0.0f; |
} |
} |
|
// tie to ground. |
mtx.m31 = _groundPlaneHeight; |
|
_velocity.y = 0.0f; |
} |
|
self.transform = SCNMatrix4FromGLKMatrix4(mtx); |
|
//-- move the camera |
SCNNode *camera = [[[AAPLGameSimulation sim] gameLevel] camera].parentNode; |
|
if (camera != nil) { |
//interpolate |
SCNVector3 pos = SCNVector3Make(self.position.x + ((self.walkDirection == WalkDirectionRight) ? 250 : -250), |
(self.position.y + 261) - (0.85f * (self.position.y - _groundPlaneHeight)), |
(self.position.z + 1500)); |
SCNMatrix4 desiredTransform = AAPLMatrix4SetPosition(camera.transform, pos); |
camera.transform = AAPLMatrix4Interpolate(camera.transform, desiredTransform, 0.025); |
} |
} |
|
/*! Given our current location, |
shoot a ray downward to collide with our ground mesh or lava mesh |
*/ |
- (CGFloat)getGroundHeight:(GLKMatrix4)mtx |
{ |
SCNVector3 start = SCNVector3Make(mtx.m30, mtx.m31 + 1000, mtx.m32); |
SCNVector3 end = SCNVector3Make(mtx.m30, mtx.m31 - 3000, mtx.m32); |
|
NSArray *hits = [[AAPLGameSimulation sim].physicsWorld rayTestWithSegmentFromPoint:start |
toPoint:end |
options:@{SCNPhysicsTestCollisionBitMaskKey : @(GameCollisionCategoryGround | GameCollisionCategoryLava), |
SCNPhysicsTestSearchModeKey : SCNPhysicsTestSearchModeClosest}]; |
if (hits.count > 0) { |
// take the first hit. make that the ground. |
for (SCNHitTestResult *result in hits) { |
if (result.node.physicsBody.categoryBitMask & ~(GameCollisionCategoryGround|GameCollisionCategoryLava)) |
continue; |
return result.worldCoordinates.y; |
} |
} |
|
// 0 is ground if we didn't hit anything. |
return 0; |
|
} |
|
SCNMatrix4 AAPLMatrix4Interpolate(SCNMatrix4 scnm0, SCNMatrix4 scnmf, CGFloat factor) |
{ |
GLKMatrix4 m0 = SCNMatrix4ToGLKMatrix4(scnm0); |
GLKMatrix4 mf = SCNMatrix4ToGLKMatrix4(scnmf); |
GLKVector4 p0 = GLKMatrix4GetColumn(m0, 3); |
GLKVector4 pf = GLKMatrix4GetColumn(mf, 3); |
GLKQuaternion q0 = GLKQuaternionMakeWithMatrix4(m0); |
GLKQuaternion qf = GLKQuaternionMakeWithMatrix4(mf); |
|
GLKVector4 pTmp = GLKVector4Lerp(p0, pf, factor); |
GLKQuaternion qTmp = GLKQuaternionSlerp(q0, qf, factor); |
GLKMatrix4 rTmp = GLKMatrix4MakeWithQuaternion(qTmp); |
|
SCNMatrix4 transform = { rTmp.m00, rTmp.m01, rTmp.m02, 0.0, |
rTmp.m10, rTmp.m11, rTmp.m12, 0.0, |
rTmp.m20, rTmp.m21, rTmp.m22, 0.0, |
pTmp.x, pTmp.y, pTmp.z, 1.0 }; |
|
return transform; |
} |
|
/*! Jump with variable heights based on how many times this method gets called. |
*/ |
- (void)performJumpAndStop:(BOOL)stop |
{ |
jumpForce = 13.0f; |
if (stop == YES) { |
return; |
} |
|
_jumpBoost += 0.0005f; |
CGFloat maxBoost = self.walkSpeed * 2.0f; |
if (_jumpBoost > maxBoost) { |
_jumpBoost = maxBoost; |
} else { |
_velocity.y += 0.55f; |
} |
|
if (self.jumping == NO) { |
self.jumping = YES; |
self.launching = YES; |
self.inJumpAnimation = YES; |
} |
} |
|
- (void)setInJumpAnimation:(BOOL)jumpAnimState |
{ |
if (_inJumpAnimation == jumpAnimState) { |
return; |
} |
|
_inJumpAnimation = jumpAnimState; |
if (_inJumpAnimation == YES) { |
// Launching YES means we are in the preflight jump animation. |
self.launching = YES; |
|
CAAnimation *anim = [self cachedAnimationForKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationJump]]; |
[self.mainSkeleton removeAllAnimations]; |
[self.mainSkeleton addAnimation:anim forKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationJump]]; |
[self turnOffWalkingDust]; |
} else { |
self.launching = NO; |
} |
} |
|
- (void)setInRunAnimation:(BOOL)runAnimState |
{ |
if (_inRunAnimation == runAnimState) { |
return; |
} |
_inRunAnimation = runAnimState; |
|
// If we are running, then |
if (_inRunAnimation == YES) { |
self.walkSpeed = _baseWalkSpeed * 2; |
|
NSString *runKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationRun]; |
NSString *idleKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationIdle]; |
|
CAAnimation *runAnim = [self cachedAnimationForKey:runKey]; |
[self.mainSkeleton removeAnimationForKey:idleKey fadeOutDuration:0.15f]; |
[self.mainSkeleton addAnimation:runAnim forKey:runKey]; |
// add or turn on the flow of dust particles. |
if (self.dustWalking != nil) { |
if ([self.particleSystems containsObject:self.dustWalking] == NO) { |
[self addParticleSystem:self.dustWalking]; |
} else { |
self.dustWalking.birthRate = dustWalkingBirthRate; |
} |
} |
} else { |
// Fade out run and move to run stop. |
NSString *runKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationRun]; |
NSString *runStopKey = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationIdle]; |
CAAnimation *runStopAnim = [self cachedAnimationForKey:runStopKey]; |
runStopAnim.fadeInDuration = 0.15f; |
runStopAnim.fadeOutDuration = 0.15f; |
[self.mainSkeleton removeAnimationForKey:runKey fadeOutDuration:0.15f]; |
[self.mainSkeleton addAnimation:runStopAnim forKey:runStopKey]; |
self.walkSpeed = _baseWalkSpeed; |
[self turnOffWalkingDust]; |
_isWalking = NO; |
} |
} |
|
- (void)turnOffWalkingDust |
{ |
// Stop the flow of dust by turning the birthrate to 0. |
if (self.dustWalking != nil && [self.particleSystems containsObject:self.dustWalking]) { |
self.dustWalking.birthRate = 0; |
} |
} |
|
- (void)setWalkDirection:(WalkDirection)walkDirection |
{ |
// If we changed directions and are already walking |
// then play the run stop animation once. |
if (walkDirection != _walkDirection && _isWalking == YES && self.launching == NO && self.jumping == NO) { |
if (self.ChangingDirection == NO) { |
[self.mainSkeleton removeAllAnimations]; |
NSString *key = [AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationRunStop]; |
CAAnimation *anim = [self cachedAnimationForKey:key]; |
[self.mainSkeleton addAnimation:anim forKey:key]; |
self.ChangingDirection = YES; |
self.walkSpeed = _baseWalkSpeed; |
} |
} else { |
_walkDirection = walkDirection; |
} |
} |
|
- (void)setInHitAnimation:(BOOL)GetHitAnimState |
{ |
_inHitAnimation = GetHitAnimState; |
|
// Play the get hit animation. |
CAAnimation *anim = [self cachedAnimationForKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationGetHit]]; |
[anim setRepeatCount:0]; |
[anim setFadeInDuration:0.15]; |
[anim setFadeOutDuration:0.15]; |
[self.mainSkeleton addAnimation:anim forKey:[AAPLPlayerCharacter keyForAnimationType:AAPLCharacterAnimationGetHit]]; |
|
_inHitAnimation = NO; |
|
[[AAPLGameSimulation sim] playSound:@"coconuthit.caf"]; |
} |
|
@end |