/* |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
|
Abstract: |
|
This class manages most of the game logic, including setting up the scene and keeping score. |
|
*/ |
|
#import <GLKit/GLKit.h> |
#import <SpriteKit/SpriteKit.h> |
#import <SceneKit/SceneKit.h> |
#import <GameKit/GameKit.h> |
|
#import "AAPLMathUtils.h" |
#import "AAPLGameLevel.h" |
#import "AAPLPlayerCharacter.h" |
#import "AAPLMonkeyCharacter.h" |
#import "AAPLAppDelegate.h" |
#import "AAPLSceneView.h" |
#import "AAPLGameSimulation.h" |
#import "AAPLCoconut.h" |
#import "AAPLInGameScene.h" |
|
typedef enum { |
AAPLShadowReceiverCategory = 2, |
} AAPLCategoryBitMasks; |
|
#define BANANA_SCALE_LARGE 0.5 * 10./4. |
#define BANANA_SCALE 0.5 |
|
@interface AAPLGameLevel () { |
SCNVector3 _lightOffsetFromCharacter; |
SCNVector3 _screenSpaceplayerPosition; |
SCNVector3 _worldSpaceLabelScorePosition; |
} |
|
@property (strong, nonatomic) SCNNode *rootNode; |
@property (strong, nonatomic) SCNNode *sunLight; |
@property (strong, nonatomic) SCNAction *bananaIdleAction; |
@property (strong, nonatomic) SCNAction *hoverAction; |
@property (strong, nonatomic) NSMutableArray *pathPositions; |
@property (strong, nonatomic) SCNNode *bananaCollectable; |
@property (strong, nonatomic) SCNNode *largeBananaCollectable; |
@property (strong, nonatomic) AAPLSkinnedCharacter *monkeyProtoObject; |
@property (strong, nonatomic) SCNNode *coconutProtoObject; |
@property (strong, nonatomic) SCNNode *palmTreeProtoObject; |
@property (strong, nonatomic) NSMutableArray *monkeys; |
|
@end |
|
@implementation AAPLGameLevel |
|
- (BOOL)isHighEnd |
{ |
//todo: return YES on OSX, iPad air, iphone 5s - NO otherwie |
return YES; |
} |
|
/*! Helper Method for creating a large banana |
Create model, Add particle system, Add persistent SKAction, Add / Setup collision |
*/ |
- (SCNNode *)createLargeBanana |
{ |
if (self.largeBananaCollectable == nil) { |
NSString *bananaPath = [AAPLGameSimulation pathForArtResource:@"level/banana.dae"]; |
SCNNode *node = [AAPLGameSimulation loadNodeWithName:@"banana" |
fromSceneNamed:bananaPath]; |
|
node.scale = SCNVector3Make(BANANA_SCALE_LARGE, BANANA_SCALE_LARGE, BANANA_SCALE_LARGE); |
|
SCNSphere *sphereGeometry = [SCNSphere sphereWithRadius:100]; |
SCNPhysicsShape *physicsShape = [SCNPhysicsShape shapeWithGeometry:sphereGeometry options:nil]; |
node.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeKinematic shape:physicsShape]; |
|
// Only collide with player and ground |
node.physicsBody.collisionBitMask = GameCollisionCategoryPlayer | GameCollisionCategoryGround; |
|
// Declare self in the coin category |
node.physicsBody.categoryBitMask = GameCollisionCategoryCoin; |
|
// Rotate forever. |
SCNAction *rotateCoin = [SCNAction rotateByX:0 y:8 z:0 duration:2.0f]; |
SCNAction *repeat = [SCNAction repeatActionForever:rotateCoin]; |
|
node.rotation = SCNVector4Make(0, 1, 0, M_PI_2); |
[node runAction:repeat]; |
|
self.largeBananaCollectable = node; |
} |
|
SCNNode *node = [self.largeBananaCollectable clone]; |
|
SCNParticleSystem *newSystem = [AAPLGameSimulation loadParticleSystemWithName:@"sparkle"]; |
[node addParticleSystem:newSystem]; |
|
return node; |
} |
|
/*! Helper Method for creating a small banana |
*/ |
- (SCNNode *)createBanana |
{ |
//Create model |
if (self.bananaCollectable == nil) { |
self.bananaCollectable = [AAPLGameSimulation loadNodeWithName:@"banana" fromSceneNamed:[AAPLGameSimulation pathForArtResource:@"level/banana.dae"]]; |
|
self.bananaCollectable.scale = SCNVector3Make(BANANA_SCALE, BANANA_SCALE, BANANA_SCALE); |
|
SCNSphere *sphereGeometry = [SCNSphere sphereWithRadius:40]; |
SCNPhysicsShape *physicsShape = [SCNPhysicsShape shapeWithGeometry:sphereGeometry options:nil]; |
|
self.bananaCollectable.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeKinematic shape:physicsShape]; |
|
// Only collide with player and ground |
self.bananaCollectable.physicsBody.collisionBitMask = GameCollisionCategoryPlayer | GameCollisionCategoryGround; |
// Declare self in the banana category |
self.bananaCollectable.physicsBody.categoryBitMask = GameCollisionCategoryBanana; |
|
// Rotate and Hover forever. |
self.bananaCollectable.rotation = SCNVector4Make(0.5, 1, 0.5, -M_PI_4); |
SCNAction *idleHoverGroupAction = [SCNAction group:@[self.bananaIdleAction, self.hoverAction]]; |
SCNAction *repeatForeverAction = [SCNAction repeatActionForever:idleHoverGroupAction]; |
[self.bananaCollectable runAction:repeatForeverAction]; |
} |
|
return [self.bananaCollectable clone]; |
} |
|
- (void)setupPathColliders |
{ |
// Collect all the nodes that start with path_ under the dummy_front object. |
// Set those objects as Physics category ground and create a static concave mesh collider. |
// The simulation will use these as the ground to walk on. |
SCNNode *front = [self.rootNode childNodeWithName:@"dummy_front" recursively:YES]; |
[front enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
if ([child.name hasPrefix:@"path_"]) { |
SCNNode *path = child.childNodes.firstObject; //the geometry is attached to the first child node of the node named path_* |
|
path.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeStatic shape:[SCNPhysicsShape shapeWithGeometry:path.geometry options:@{SCNPhysicsShapeTypeKey : SCNPhysicsShapeTypeConcavePolyhedron}]]; |
path.physicsBody.categoryBitMask = GameCollisionCategoryGround; |
} |
}]; |
} |
|
- (NSArray *)collectSortedPathNodes |
{ |
// Gather all the children under the dummy_master |
// Sort left to right, in the world. |
SCNNode *pathNodes = [self.rootNode childNodeWithName:@"dummy_master" recursively:YES]; |
|
NSArray *sortedNodes = [pathNodes.childNodes sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { |
SCNNode *dummyA = obj1; |
SCNNode *dummyB = obj2; |
|
if (dummyA.position.x > dummyB.position.x) { |
return NSOrderedDescending; |
} |
return NSOrderedAscending; |
}]; |
return sortedNodes; |
} |
|
- (void)convertPathNodesIntoPathPositions |
{ |
// Walk the path, sampling every little bit, creating a path to follow. |
// We use this path to move along left to right and right to left. |
NSArray *sortedNodes; |
sortedNodes = [self collectSortedPathNodes]; |
|
self.pathPositions = [[NSMutableArray alloc] init]; |
[self.pathPositions addObject:[NSValue valueWithSCNVector3:SCNVector3Make(0, 0, 0)]]; |
|
for (SCNNode *d in sortedNodes) { |
if ([d.name hasPrefix:@"dummy_path_"] == NO) { |
continue; |
} |
[self.pathPositions addObject:[NSValue valueWithSCNVector3:d.position]]; |
} |
[self.pathPositions addObject:[NSValue valueWithSCNVector3:SCNVector3Make(0, 0, 0)]]; |
} |
|
- (void)resamplePathPositions |
{ |
// Calc the phatom end control point. |
SCNVector3 controlPointA = [self.pathPositions[self.pathPositions.count - 2] SCNVector3Value]; |
SCNVector3 controlPointB = [self.pathPositions[self.pathPositions.count - 3] SCNVector3Value]; |
SCNVector3 controlPoint; |
|
controlPoint.x = controlPointA.x + (controlPointA.x - controlPointB.x); |
controlPoint.y = controlPointA.y + (controlPointA.y - controlPointB.y); |
controlPoint.z = controlPointA.z + (controlPointA.z - controlPointB.z); |
|
self.pathPositions[self.pathPositions.count - 1] = [NSValue valueWithSCNVector3:controlPoint]; |
|
// Calc the phatom begin control point. |
controlPointA = [self.pathPositions[1] SCNVector3Value]; |
controlPointB = [self.pathPositions[2] SCNVector3Value]; |
|
controlPoint.x = controlPointA.x + (controlPointA.x - controlPointB.x); |
controlPoint.y = controlPointA.y + (controlPointA.y - controlPointB.y); |
controlPoint.z = controlPointA.z + (controlPointA.z - controlPointB.z); |
self.pathPositions[0] = [NSValue valueWithSCNVector3:controlPoint]; |
|
NSMutableArray *newPath = [[NSMutableArray alloc] init]; |
SCNVector3 lastPosition; |
CGFloat minDistanceBetweenPoints = 10.0; |
NSUInteger steps = 10000; |
for (NSInteger i = 0; i < steps; i++) { |
CGFloat t = (CGFloat)i / (CGFloat)steps; |
SCNVector3 currentPostion = [self locationAlongPath:t]; |
if (i == 0) { |
[newPath addObject:[NSValue valueWithSCNVector3:currentPostion]]; |
lastPosition = currentPostion; |
} else { |
CGFloat dist = GLKVector3Distance(SCNVector3ToGLKVector3(currentPostion), SCNVector3ToGLKVector3(lastPosition)); |
if (dist > minDistanceBetweenPoints) { |
[newPath addObject:[NSValue valueWithSCNVector3:currentPostion]]; |
lastPosition = currentPostion; |
} |
} |
} |
|
// Last Step. Return the path position array for our pathing system to query. |
self.pathPositions = newPath; |
} |
|
- (void)calculatePathPositions |
{ |
|
[self setupPathColliders]; |
|
[self convertPathNodesIntoPathPositions]; |
|
[self resamplePathPositions]; |
} |
|
/*! Given a relative percent along the path, return back the world location vector. |
*/ |
- (SCNVector3)locationAlongPath:(CGFloat)percent |
{ |
if (self.pathPositions.count <= 3) { |
return SCNVector3Make(0, 0, 0); |
} |
|
NSUInteger numSections = self.pathPositions.count - 3; |
CGFloat dist = percent * (CGFloat)numSections; |
|
NSUInteger currentPointIndex = MIN((NSUInteger)floorf(dist), numSections - 1); |
dist -= (CGFloat)currentPointIndex; |
GLKVector3 a = SCNVector3ToGLKVector3([self.pathPositions[currentPointIndex] SCNVector3Value]); |
GLKVector3 b = SCNVector3ToGLKVector3([self.pathPositions[currentPointIndex + 1] SCNVector3Value]); |
GLKVector3 c = SCNVector3ToGLKVector3([self.pathPositions[currentPointIndex + 2] SCNVector3Value]); |
GLKVector3 d = SCNVector3ToGLKVector3([self.pathPositions[currentPointIndex + 3] SCNVector3Value]); |
|
SCNVector3 location; |
|
#define CatmullRomValue(a, b, c, d, dist) \ |
(((-a + 3.0 * b - 3.0 * c + d) * (dist * dist * dist)) + \ |
((2.0 * a - 5.0 * b + 4.0 * c - d) * (dist * dist)) + \ |
((-a + c) * dist) + \ |
(2.0 * b)) * 0.5; \ |
|
location.x = CatmullRomValue(a.x, b.x, c.x, d.x, dist); |
location.y = CatmullRomValue(a.y, b.y, c.y, d.y, dist); |
location.z = CatmullRomValue(a.z, b.z, c.z, d.z, dist); |
|
return location; |
} |
|
/*! Direction player facing given the current walking direction. |
*/ |
- (SCNVector4)getDirectionFromPosition:(SCNVector3)currentPosition |
{ |
|
SCNVector3 target = [self locationAlongPath:self.timeAlongPath - 0.05]; |
|
GLKMatrix4 lookat = GLKMatrix4MakeLookAt(currentPosition.x, currentPosition.y, currentPosition.z, target.x, target.y, target.z, 0, 1, 0); |
GLKQuaternion q = GLKQuaternionMakeWithMatrix4(lookat); |
|
CGFloat angle = GLKQuaternionAngle(q); |
if (self.playerCharacter.walkDirection == WalkDirectionLeft) { |
angle -= M_PI; |
} |
return SCNVector4Make(0, 1, 0, angle); |
} |
|
/* Helper method for getting main player's direction |
*/ |
- (SCNVector4)getPlayerDirectionFromCurrentPosition |
{ |
return [self getDirectionFromPosition:self.playerCharacter.position]; |
} |
|
// Helper Method for loading the Swinging Torch |
// |
// Load the dae from disk |
// Attach to origin |
- (void)createSwingingTorch |
{ |
|
SCNNode *torchSwing = [AAPLGameSimulation loadNodeWithName:@"dummy_master" fromSceneNamed:[AAPLGameSimulation pathForArtResource:@"level/torch.dae"]]; |
[self.rootNode addChildNode:torchSwing]; |
} |
|
// createLavaAnimation |
// |
// Find the lava nodes in the scene. |
// Add a concave collider to each lava mesh |
// UV animate the lava texture in the vertex shader. |
- (void)createLavaAnimation |
{ |
NSArray *lavaNodes = [self.rootNode childNodesPassingTest:^BOOL(SCNNode *child, BOOL *stop) { |
if ([child.name hasPrefix:@"lava_0"] == YES) { |
return YES; |
} |
return NO; |
}]; |
|
for (SCNNode *lava in lavaNodes) { |
NSArray *childrenWithGeometry = [lava childNodesPassingTest:^BOOL(SCNNode *child, BOOL *stop) { |
if (child.geometry != nil) { |
*stop = YES; |
return YES; |
} |
|
return NO; |
}]; |
|
SCNNode *lavaGeometry = childrenWithGeometry[0]; |
|
lavaGeometry.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeStatic shape:[SCNPhysicsShape shapeWithGeometry:lavaGeometry.geometry options:@{SCNPhysicsShapeTypeKey : SCNPhysicsShapeTypeConcavePolyhedron}]]; |
lavaGeometry.physicsBody.categoryBitMask = GameCollisionCategoryLava; |
lavaGeometry.categoryBitMask = NodeCategoryLava; |
|
NSString *shaderCode = |
@"uniform float speed;\n" |
@"#pragma body\n" |
@"_geometry.texcoords[0] += vec2(sin(_geometry.position.z*0.1 + u_time * 0.1) * 0.1, -1.0* 0.05 * u_time);\n"; |
lavaGeometry.geometry.shaderModifiers = @{ SCNShaderModifierEntryPointGeometry : shaderCode}; |
} |
} |
|
/*! Create an action that rotates back and forth. |
*/ |
- (SCNAction *)bananaIdleAction |
{ |
if (_bananaIdleAction == nil) { |
SCNAction *rotateAction = [SCNAction rotateByX:0 y:M_PI_2 z:0 duration:1.0f]; |
rotateAction.timingMode = SCNActionTimingModeEaseInEaseOut; |
SCNAction *reversed = [rotateAction reversedAction]; |
_bananaIdleAction = [SCNAction sequence:@[rotateAction, reversed]]; |
} |
return _bananaIdleAction; |
} |
|
/*! Create an action that hovers up and down slightly. |
*/ |
- (SCNAction *)hoverAction |
{ |
if (_hoverAction == nil) { |
SCNAction *floatAction = [SCNAction moveByX:0 y:10.0f z:0 duration:1.0f]; |
SCNAction *floatAction2 = [floatAction reversedAction]; |
floatAction.timingMode = SCNActionTimingModeEaseInEaseOut; |
floatAction2.timingMode = SCNActionTimingModeEaseInEaseOut; |
_hoverAction = [SCNAction sequence:@[floatAction, floatAction2]]; |
} |
return _hoverAction; |
} |
|
/*! Create an action that pulses the opacity of a node. |
*/ |
- (SCNAction *)pulseAction |
{ |
CGFloat duration = 8.0f / 6.0f; |
SCNAction *pulseAction = [SCNAction repeatActionForever: |
[SCNAction sequence:@[[SCNAction fadeOpacityTo:0.3 duration:duration], |
[SCNAction fadeOpacityTo:0.5 duration:duration], |
[SCNAction fadeOpacityTo:1.0 duration:duration], |
[SCNAction fadeOpacityTo:0.7 duration:duration], |
[SCNAction fadeOpacityTo:0.4 duration:duration], |
[SCNAction fadeOpacityTo:0.8 duration:duration]]]]; |
return pulseAction; |
} |
|
/* Create a simple point light |
*/ |
- (SCNLight *)torchLight |
{ |
SCNLight *light = [SCNLight light]; |
light.type = SCNLightTypeOmni; |
light.color = [SKColor orangeColor]; |
light.attenuationStartDistance = 350; |
light.attenuationEndDistance = 400; |
light.attenuationFalloffExponent = 1; |
return light; |
} |
|
/*! Create a torch node that has a particle effect and point light attached. |
*/ |
- (SCNNode *)createTorchNode |
{ |
static SCNNode *template; |
|
if (template == nil) { |
template = [SCNNode node]; |
|
SCNGeometry *geometry = [SCNBox boxWithWidth:20 height:100 length:20 chamferRadius:0]; |
geometry.firstMaterial.diffuse.contents = [SKColor brownColor]; |
template.geometry = geometry; |
|
SCNNode *particleEmitter = [SCNNode node]; |
particleEmitter.position = SCNVector3Make(0, 50, 0); |
|
SCNParticleSystem *fire = [SCNParticleSystem particleSystemNamed:@"torch.scnp" |
inDirectory:@"art.scnassets/level/effects"]; |
[particleEmitter addParticleSystem:fire]; |
|
particleEmitter.light = [self torchLight]; |
|
[template addChildNode:particleEmitter]; |
} |
|
return [template clone]; |
} |
|
// CreateLevel |
// |
// Load the level dae from disk |
// Setup and construct the level. ( Should really be done offline in an editor ). |
- (SCNNode *)createLevel |
{ |
|
self.rootNode = [SCNNode node]; |
|
// load level dae and add all root children to the scene. |
SCNScene *scene = [SCNScene sceneNamed:@"level.dae" inDirectory:[AAPLGameSimulation pathForArtResource:@"level/"] options: @{SCNSceneSourceConvertToYUpKey : @YES}]; |
for (SCNNode *node in scene.rootNode.childNodes) { |
[self.rootNode addChildNode:node]; |
} |
|
// retrieve the main camera |
self.camera = [self.rootNode childNodeWithName:@"camera_game" recursively:YES]; |
|
// create our path that the player character will follow. |
[self calculatePathPositions]; |
|
//-- Sun/Moon light |
self.sunLight = [self.rootNode childNodeWithName:@"FDirect001" recursively:YES]; |
self.sunLight.eulerAngles = SCNVector3Make(7.1 * M_PI_4, M_PI_4, 0); |
self.sunLight.light.shadowSampleCount = 1; //to match iOS while testing: to be removed from the sample code |
_lightOffsetFromCharacter = SCNVector3Make(1500, 2000, 1000); |
|
//workaround directional light deserialization issue |
self.sunLight.light.zNear = 100; |
self.sunLight.light.zFar = 5000; |
self.sunLight.light.orthographicScale = 1000; |
|
if (![self isHighEnd]) { |
//use blob shadows on low end devices |
self.sunLight.light.shadowMode = SCNShadowModeModulated; |
self.sunLight.light.categoryBitMask = 0x2; |
self.sunLight.light.orthographicScale = 60; |
self.sunLight.eulerAngles = SCNVector3Make(M_PI_2, 0, 0); |
_lightOffsetFromCharacter = SCNVector3Make(0, 2000, 0); |
|
self.sunLight.light.gobo.contents = @"art.scnassets/techniques/blobShadow.jpg"; |
self.sunLight.light.gobo.intensity = 0.5; |
|
SCNNode *middle = [self.rootNode childNodeWithName:@"dummy_front" recursively:YES]; |
[middle enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
child.categoryBitMask = 0x2; |
}]; |
} |
|
//-- Torches |
float torchesPos[8] = {0, -1, 0.092467, -1, -1, 0.5, 0.7920, 0.953830}; |
|
for (int i = 0; i < 8; i++) { |
if (torchesPos[i] == -1) continue; |
SCNVector3 location = [self locationAlongPath:torchesPos[i]]; |
location.y += 50; |
location.z += 150; |
|
SCNNode *node = [self createTorchNode]; |
|
node.position = location; |
[self.rootNode addChildNode:node]; |
} |
|
// After load, we add nodes that are dynamic / animated / or otherwise not static. |
[self createLavaAnimation]; |
[self createSwingingTorch]; |
[self animateDynamicNodes]; |
|
// Create our player character |
SCNNode *characterRoot = [AAPLGameSimulation loadNodeWithName:nil fromSceneNamed:@"art.scnassets/characters/explorer/explorer_skinned.dae"]; |
self.playerCharacter = [[AAPLPlayerCharacter alloc] initWithNode:characterRoot]; |
self.timeAlongPath = 0; |
self.playerCharacter.position = [self locationAlongPath:self.timeAlongPath]; |
self.playerCharacter.rotation = [self getPlayerDirectionFromCurrentPosition]; |
[self.rootNode addChildNode:self.playerCharacter]; |
|
// Optimize lighting and shadows |
// only the charadcter should cast shadows |
[self.rootNode enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
child.castsShadow = NO; |
}]; |
[self.playerCharacter enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
child.castsShadow = YES; |
}]; |
|
// Add some monkeys to the scene. |
[self addMonkeyAtPosition:SCNVector3Make(0, -30, -400) andRotation:0]; |
[self addMonkeyAtPosition:SCNVector3Make(3211, 146, -400) andRotation:-M_PI_4]; |
[self addMonkeyAtPosition:SCNVector3Make(5200, 330, 600) andRotation:0]; |
|
//- Volcano |
SCNNode *oldVolcano = [self.rootNode childNodeWithName:@"volcano" recursively:YES]; |
NSString *volcanoDaeName = [AAPLGameSimulation pathForArtResource:@"level/volcano_effects.dae"]; |
SCNNode *newVolcano = [AAPLGameSimulation loadNodeWithName:@"dummy_master" |
fromSceneNamed:volcanoDaeName]; |
[oldVolcano addChildNode:newVolcano]; |
oldVolcano.geometry = nil; |
oldVolcano = [newVolcano childNodeWithName:@"volcano" recursively:YES]; |
oldVolcano = oldVolcano.childNodes[0]; |
|
//-- Animate our dynamic volcano node. |
NSString *shaderCode = |
@"uniform float speed;\n" |
@"_geometry.color = vec4(a_color.r, a_color.r, a_color.r, a_color.r);\n" |
@"_geometry.texcoords[0] += (vec2(0.0, 1.0) * 0.05 * u_time);\n"; |
|
NSString *fragmentShaderCode = |
@"#pragma transparent\n"; |
|
//dim background |
SCNNode *back = [self.rootNode childNodeWithName:@"dumy_rear" recursively:YES]; |
[back enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
child.castsShadow = NO; |
|
for (SCNMaterial *material in child.geometry.materials) { |
material.lightingModelName = SCNLightingModelConstant; |
material.multiply.contents = [SKColor colorWithWhite:0.3 alpha:1.0]; |
material.multiply.intensity = 1; |
} |
}]; |
|
//remove lighting from middle plane |
{ |
SCNNode *back = [self.rootNode childNodeWithName:@"dummy_middle" recursively:YES]; |
[back enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
for (SCNMaterial *material in child.geometry.materials) { |
material.lightingModelName = SCNLightingModelConstant; |
} |
}]; |
} |
|
[newVolcano enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
if (child != oldVolcano && child.geometry != nil) { |
child.geometry.firstMaterial.lightingModelName = SCNLightingModelConstant; |
child.geometry.firstMaterial.multiply.contents = [SKColor whiteColor]; |
child.geometry.shaderModifiers = @{ SCNShaderModifierEntryPointGeometry : shaderCode, |
SCNShaderModifierEntryPointFragment : fragmentShaderCode }; |
} |
}]; |
|
|
if (![self isHighEnd]) { |
[self.rootNode enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
for (SCNMaterial *m in child.geometry.materials) { |
m.lightingModelName = SCNLightingModelConstant; |
} |
}]; |
|
[self.playerCharacter enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
for (SCNMaterial *material in child.geometry.materials) { |
material.lightingModelName = SCNLightingModelLambert; |
} |
}]; |
} |
|
self.coconuts = [[NSMutableArray alloc] init]; |
return self.rootNode; |
} |
|
/*! Given a world position and rotation, load the monkey dae and place it into the world. |
*/ |
- (void)addMonkeyAtPosition:(SCNVector3)worldPos andRotation:(CGFloat)rotation |
{ |
if (self.monkeys == nil) { |
self.monkeys = [[NSMutableArray alloc] init]; |
} |
|
SCNNode *palmTree = [self createMonkeyPalmTree]; |
palmTree.position = worldPos; |
palmTree.rotation = SCNVector4Make(0, 1, 0, rotation); |
[self.rootNode addChildNode:palmTree]; |
|
AAPLSkinnedCharacter *monkey = (AAPLSkinnedCharacter *)[palmTree childNodeWithName:@"monkey" recursively:YES]; |
if (monkey != nil) { |
[self.monkeys addObject:monkey]; |
} |
} |
|
/*! Load the palm tree that the monkey is attached to. |
*/ |
- (SCNNode *)createMonkeyPalmTree |
{ |
static SCNNode *s_palmTreeProtoObject = nil; |
|
if (s_palmTreeProtoObject == nil) { |
NSString *palmTreeDae = [AAPLGameSimulation pathForArtResource:@"characters/monkey/monkey_palm_tree.dae"]; |
s_palmTreeProtoObject = [AAPLGameSimulation loadNodeWithName:@"PalmTree" |
fromSceneNamed:palmTreeDae]; |
} |
|
SCNNode *monkeyNode = [AAPLGameSimulation loadNodeWithName:nil fromSceneNamed:@"art.scnassets/characters/monkey/monkey_skinned.dae"]; |
|
AAPLMonkeyCharacter *monkey = [[AAPLMonkeyCharacter alloc] initWithNode:monkeyNode]; |
[monkey createAnimations]; |
|
SCNNode *palmTree = [s_palmTreeProtoObject clone]; |
[palmTree addChildNode:monkey]; |
|
return palmTree; |
} |
|
- (void)animateDynamicNodes |
{ |
|
NSMutableArray *dynamicNodesWithVertColorAnimation = [NSMutableArray array]; |
|
[self.rootNode enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) { |
NSRange range = [child.parentNode.name rangeOfString:@"vine"]; |
if ([child.geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticColor] == nil) { |
} else if (range.location != NSNotFound) { |
[dynamicNodesWithVertColorAnimation addObject:child]; |
} |
}]; |
|
//-- Animate our dynamic node. |
NSString *shaderCode = |
@"uniform float timeOffset;\n" |
@"#pragma body\n" |
@"float speed = 20.05;\n" |
@"_geometry.position.xyz += (speed * sin(u_time + timeOffset) * _geometry.color.rgb);\n" |
; |
|
for (SCNNode *dynamicNode in dynamicNodesWithVertColorAnimation) { |
dynamicNode.geometry.shaderModifiers = @{ SCNShaderModifierEntryPointGeometry : shaderCode}; |
CABasicAnimation *explodeAnimation = [CABasicAnimation animationWithKeyPath:@"timeOffset"]; |
explodeAnimation.duration = 2.0; |
explodeAnimation.repeatCount = FLT_MAX; |
explodeAnimation.autoreverses = YES; |
explodeAnimation.toValue = @(AAPLRandomPercent()); |
explodeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
[dynamicNode.geometry addAnimation:explodeAnimation forKey:@"sway"]; |
} |
} |
|
/*! Reset the game simulation for the start of the game or restart after you have completed the level. |
*/ |
- (void)resetLevel |
{ |
_score = 0; |
_secondsRemaining = 120; |
_coinsCollected = 0; |
_bananasCollected = 0; |
|
self.timeAlongPath = 0; |
self.playerCharacter.position = [self locationAlongPath:self.timeAlongPath]; |
self.playerCharacter.rotation = [self getPlayerDirectionFromCurrentPosition]; |
self.hitByLavaReset = NO; |
|
// Remove dynamic objects from the level. |
[SCNTransaction begin]; |
|
for (SCNNode *b in self.coconuts) { |
[b removeFromParentNode]; |
} |
|
for (SCNNode *b in self.bananas) { |
[b removeFromParentNode]; |
} |
|
for (SCNNode *largeBanana in self.largeBananas) { |
[largeBanana removeFromParentNode]; |
} |
[SCNTransaction commit]; |
|
// Add dynamic objects to the level, like bananas and large bananas |
self.bananas = [[NSMutableSet alloc] init]; |
self.coconuts = [[NSMutableArray alloc] init]; |
|
for (int i = 0; i < 10; i++) { |
SCNNode *banana = [self createBanana]; |
[self.rootNode addChildNode:banana]; |
SCNVector3 location = [self locationAlongPath:(i + 1) / 20.0 - 0.01]; |
location.y += 50; |
banana.position = location; |
|
[self.bananas addObject:banana]; |
} |
|
self.largeBananas = [[NSMutableSet alloc] init]; |
|
for (int i = 0; i < 6; i++) { |
SCNNode *largeBanana = [self createLargeBanana]; |
[self.rootNode addChildNode:largeBanana]; |
SCNVector3 location = [self locationAlongPath: AAPLRandomPercent()]; |
location.y += 50; |
largeBanana.position = location; |
[self.largeBananas addObject:largeBanana]; |
} |
|
[[AAPLGameSimulation sim] playMusic:@"music.caf"]; |
[[AAPLGameSimulation sim] playMusic:@"night.caf"]; |
} |
|
/*! Change the game state to the postgame. |
*/ |
- (void)doGameOver |
{ |
self.playerCharacter.inRunAnimation = NO; |
[[AAPLGameSimulation sim] setGameState:AAPLGameStatePostGame]; |
} |
|
- (void)collideWithLava |
{ |
if (self.hitByLavaReset == YES) |
return; |
|
self.playerCharacter.inRunAnimation = NO; |
|
[[AAPLGameSimulation sim] playSound:@"ack.caf"]; |
|
// Blink for a second |
SCNAction *blinkOffAction = [SCNAction fadeOutWithDuration:0.15f]; |
SCNAction *blinkOnAction = [SCNAction fadeInWithDuration:0.15f]; |
SCNAction *cycle = [SCNAction sequence:@[blinkOffAction, blinkOnAction]]; |
SCNAction *repeatCycle = [SCNAction repeatAction:cycle count:7]; |
|
self.hitByLavaReset = YES; |
|
[self.playerCharacter runAction:repeatCycle completionHandler:^{ |
self.timeAlongPath = 0; |
self.playerCharacter.position = [self locationAlongPath:self.timeAlongPath]; |
self.playerCharacter.rotation = [self getPlayerDirectionFromCurrentPosition]; |
self.hitByLavaReset = NO; |
}]; |
} |
|
- (void)moveCharacterAlongPathWith:(NSTimeInterval)deltaTime currentState:(AAPLGameState)currentState |
{ |
if (self.playerCharacter.isRunning == YES) { |
if (currentState == AAPLGameStateInGame) { |
CGFloat walkSpeed = self.playerCharacter.walkSpeed; |
if (self.playerCharacter.isJumping == YES) { |
walkSpeed += self.playerCharacter.jumpBoost; |
} |
|
self.timeAlongPath += (deltaTime * walkSpeed * (self.playerCharacter.walkDirection == WalkDirectionRight ? 1 : -1)); |
|
// limit how far the player can go in left and right directions. |
if (self.timeAlongPath < 0.0f) { |
self.timeAlongPath = 0.0f; |
} else if (self.timeAlongPath > 1.0f) { |
self.timeAlongPath = 1.0f; |
} |
|
SCNVector3 newPosition = [self locationAlongPath:self.timeAlongPath]; |
self.playerCharacter.position = SCNVector3Make(newPosition.x, self.playerCharacter.position.y, newPosition.z); |
if (self.timeAlongPath >= 1.0) { |
[self doGameOver]; |
} |
} else { |
self.playerCharacter.inRunAnimation = NO; |
} |
} |
} |
|
- (void)updateSunLightPosition |
{ |
SCNVector3 lightPos = _lightOffsetFromCharacter; |
SCNVector3 charPos = self.playerCharacter.position; |
lightPos.x += charPos.x; |
lightPos.y += charPos.y; |
lightPos.z += charPos.z; |
self.sunLight.position = lightPos; |
} |
|
/*! Main game logic |
*/ |
- (void)update:(NSTimeInterval)deltaTime withRenderer:(id <SCNSceneRenderer>)aRenderer { |
|
// Based on gamestate: |
// ingame: Move the character if running. |
// ingame: prevent movement of the character past our level bounds. |
// ingame: perform logic for the player character. |
// any: move the directional light with any player movement. |
// ingame: update the coconuts kinematically. |
// ingame: perform logic for each monkey. |
// ingame: because our camera could have moved, update the transforms needs to fly |
// collected bananas from the player (world space) to score (screen space) |
// |
|
AAPLAppDelegate *appDelegate = [AAPLAppDelegate sharedAppDelegate]; |
AAPLGameState currentState = [[AAPLGameSimulation sim] gameState]; |
|
// Move character along path if walking. |
[self moveCharacterAlongPathWith:deltaTime currentState:currentState]; |
|
// Based on the time along path, rotation the character to face the correct direction. |
self.playerCharacter.rotation = [self getPlayerDirectionFromCurrentPosition]; |
if (currentState == AAPLGameStateInGame) { |
[self.playerCharacter update:deltaTime]; |
} |
|
// Move the light |
[self updateSunLightPosition]; |
|
if (currentState == AAPLGameStatePreGame || |
currentState == AAPLGameStatePostGame || |
currentState == AAPLGameStatePaused) |
return; |
|
for (AAPLSkinnedCharacter *monkey in self.monkeys) { |
[monkey update:deltaTime]; |
} |
|
// Update timer and check for Game Over. |
_secondsRemaining -= deltaTime; |
if (_secondsRemaining < 0.0) { |
[self doGameOver]; |
} |
|
// update the player's SP position. |
SCNVector3 playerPosition = AAPLMatrix4GetPosition(self.playerCharacter.worldTransform); |
_screenSpaceplayerPosition = [appDelegate.scnView projectPoint:playerPosition]; |
|
// Update the SP position of the score label |
CGPoint pt = self.scoreLabelLocation; |
#if TARGET_OS_IPHONE |
// Unflip coordinate system on iOS. |
pt.y = appDelegate.scnView.frame.size.height - pt.y; |
#endif |
_worldSpaceLabelScorePosition = [appDelegate.scnView unprojectPoint:SCNVector3Make(pt.x, pt.y, _screenSpaceplayerPosition.z)]; |
} |
|
- (void)collectBanana:(SCNNode *)banana |
{ |
// Flyoff the banana to the screen space position score label. |
// Don't increment score until the banana hits the score label. |
|
// ignore collisions |
banana.physicsBody = nil; |
_bananasCollected++; |
|
NSInteger variance = 60; |
CGFloat apexY = ((_worldSpaceLabelScorePosition.y * 0.8f)) + ((rand() % variance) - (variance / 2)); |
_worldSpaceLabelScorePosition.z = banana.position.z; |
SCNVector3 apex = SCNVector3Make(banana.position.x + 10 + ((rand() % variance) - (variance / 2)), apexY, banana.position.z); |
|
SCNAction *startFlyOff = [SCNAction moveTo:apex duration:0.25f]; |
startFlyOff.timingMode = SCNActionTimingModeEaseOut; |
|
CGFloat duration = 0.25f; |
SCNAction *endFlyOff = [SCNAction customActionWithDuration:duration actionBlock:^(SCNNode *node, CGFloat elapsedTime) { |
|
CGFloat t = elapsedTime / duration; |
SCNVector3 v = { |
apex.x + ((_worldSpaceLabelScorePosition.x - apex.x) * t), |
apex.y + ((_worldSpaceLabelScorePosition.y - apex.y) * t), |
apex.z + ((_worldSpaceLabelScorePosition.z - apex.z) * t)}; |
node.position = v; |
}]; |
|
endFlyOff.timingMode = SCNActionTimingModeEaseInEaseOut; |
SCNAction *flyoffSequence = [SCNAction sequence:@[startFlyOff, endFlyOff]]; |
|
[banana runAction:flyoffSequence completionHandler:^{ |
[self.bananas removeObject:banana]; |
[banana removeFromParentNode]; |
// Add to score. |
_score++; |
[[AAPLGameSimulation sim] playSound:@"deposit.caf"]; |
if (self.bananas.count == 0) { |
// Game Over |
[self doGameOver]; |
} |
}]; |
} |
|
- (void)collectLargeBanana:(SCNNode *)largeBanana |
{ |
// When the player hits a large banana, explode it into smaller bananas. |
// We explode into a predefined pattern: square, diamond, letterA, letterB |
|
// ignore collisions |
largeBanana.physicsBody = nil; |
_coinsCollected++; |
|
[self.largeBananas removeObject:largeBanana]; |
[largeBanana removeAllParticleSystems]; |
[largeBanana removeFromParentNode]; |
|
// Add to score. |
_score+=100; |
NSArray *square = @[@1, @1, @1, @1, @1, |
@1, @1, @1, @1, @1, |
@1, @1, @1, @1, @1, |
@1, @1, @1, @1, @1, |
@1, @1, @1, @1, @1]; |
NSArray *diamond = @[@0, @0, @1, @0, @0, |
@0, @1, @1, @1, @0, |
@1, @1, @1, @1, @1, |
@0, @1, @1, @1, @0, |
@0, @0, @1, @0, @0]; |
NSArray *letterA = @[@1, @0, @0, @1, @0, |
@1, @0, @0, @1, @0, |
@1, @1, @1, @1, @0, |
@1, @0, @0, @1, @0, |
@0, @1, @1, @0, @0]; |
|
NSArray *letterB = @[@1, @1, @0, @0, @0, |
@1, @0, @1, @0, @0, |
@1, @1, @0, @0, @0, |
@1, @0, @1, @0, @0, |
@1, @1, @0, @0, @0]; |
NSArray *choices = @[square, diamond, letterA, letterB]; |
|
CGFloat vertSpacing = 40; |
CGFloat spacing = 0.0075; |
NSArray *choice = choices[rand() % choices.count]; |
for (int y = 0; y < 5; y++) { |
for (int x = 0; x < 5; x++) { |
int place = [choice[(y * 5) + x] intValue]; |
if (place != 1) |
continue; |
|
SCNNode *banana = [self createBanana]; |
|
[self.rootNode addChildNode:banana]; |
banana.position = largeBanana.position; |
banana.physicsBody.categoryBitMask = GameCollisionCategoryNoCollide; |
banana.physicsBody.collisionBitMask = GameCollisionCategoryGround; |
|
SCNVector3 endPoint = [self locationAlongPath:self.timeAlongPath + (spacing * (x + 1))]; |
endPoint.y += (vertSpacing * (y + 1)); |
|
SCNAction *flyoff = [SCNAction moveTo:endPoint duration:AAPLRandomPercent() * 0.25f]; |
flyoff.timingMode = SCNActionTimingModeEaseInEaseOut; |
|
// Prevent collision until the banana gets to the final resting spot. |
[banana runAction:flyoff completionHandler:^{ |
banana.physicsBody.categoryBitMask = GameCollisionCategoryBanana; |
banana.physicsBody.collisionBitMask = GameCollisionCategoryGround | GameCollisionCategoryPlayer; |
[[AAPLGameSimulation sim] playSound:@"deposit.caf"]; |
}]; |
[self.bananas addObject:banana]; |
} |
} |
} |
|
- (void)collideWithCoconut:(SCNNode *)coconut point:(SCNVector3)contactPoint |
{ |
|
// No more collisions. Let it bounce away and fade out. |
coconut.physicsBody.collisionBitMask = 0; |
[coconut runAction:[SCNAction sequence:@[ |
[SCNAction waitForDuration:1.0], |
[SCNAction fadeOutWithDuration:1.0], |
[SCNAction removeFromParentNode], |
]] |
completionHandler:^{ |
[self.coconuts removeObject:coconut]; |
}]; |
|
// Decrement score |
NSUInteger amountToDrop = self.score / 10; |
if (amountToDrop < 1) |
amountToDrop = 1; |
if (amountToDrop > 10) |
amountToDrop = 10; |
if (amountToDrop > _score) |
amountToDrop = _score; |
_score -= amountToDrop; |
|
// Throw bananas |
CGFloat spacing = 40; |
for (int x = 0; x < amountToDrop; x++) { |
SCNNode *banana = [self createBanana]; |
|
[self.rootNode addChildNode:banana]; |
banana.position = contactPoint; |
banana.physicsBody.categoryBitMask = GameCollisionCategoryNoCollide; |
banana.physicsBody.collisionBitMask = GameCollisionCategoryGround; |
SCNVector3 endPoint = SCNVector3Make(0, 0, 0); |
endPoint.x -= (spacing * x) + spacing; |
|
SCNAction *flyoff = [SCNAction moveBy:endPoint duration:AAPLRandomPercent() * 0.750f]; |
flyoff.timingMode = SCNActionTimingModeEaseInEaseOut; |
|
[banana runAction:flyoff completionHandler:^{ |
banana.physicsBody.categoryBitMask = GameCollisionCategoryBanana; |
banana.physicsBody.collisionBitMask = GameCollisionCategoryGround | GameCollisionCategoryPlayer; |
}]; |
[self.bananas addObject:banana]; |
} |
|
[self.playerCharacter setInHitAnimation:YES]; |
} |
|
@end |