common/AAPLGameSimulation.m

/*
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 See LICENSE.txt for this sample’s licensing information
 
 Abstract:
 
  This class manages the global state of the game. It handles SCNSceneRendererDelegate methods for participating in the update/render loop, polls for input (directly for game controllers and via AAPLSceneView for key/touch input), and delegates game logic to the AAPLGameLevel object.
  
 */
 
#import <GLKit/GLKit.h>
#import <GameController/GameController.h>
 
#import "AAPLGameSimulation.h"
#import "AAPLSceneView.h"
#import "AAPLPlayerCharacter.h"
#import "AAPLInGameScene.h"
#import "AAPLGameLevel.h"
#import "AAPLAppDelegate.h"
#import "AAPLCoconut.h"
 
AAPLGameSimulation *_sharedSimulation = nil;
 
@interface AAPLGameSimulation ()
{
    CGFloat _walkSpeed;
    NSTimeInterval _previousUpdateTime;
    NSTimeInterval _previousPhysicsUpdateTime;
    NSTimeInterval _deltaTime;
}
 
@property (strong, nonatomic) SCNTechnique *desaturationTechnique;
 
@end
 
@implementation AAPLGameSimulation
 
// Singleton for easy lookup
+ (AAPLGameSimulation *)sim
{
    if (_sharedSimulation == nil) {
        _sharedSimulation = [[AAPLGameSimulation alloc] init];
    }
 
    return _sharedSimulation;
}
 
- (void)setupTechniques
{
 
    // The scene can be de-saturarted as a full screen effect.
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"art.scnassets/techniques/desaturation" withExtension:@"plist"];
    self.desaturationTechnique = [SCNTechnique techniqueWithDictionary:[NSDictionary dictionaryWithContentsOfURL:url]];
    [self.desaturationTechnique setValue:@0.0 forKey:@"Saturation"];
}
 
- (id)init
{
    self = [super init];
 
    if (self) {
 
        // We create one level in our simulation.
        self.gameLevel = [[AAPLGameLevel alloc] init];
        _gameState = AAPLGameStatePaused;
 
        // Register ourself as a listener to physics callbacks.
        SCNNode *levelNode = [self.gameLevel createLevel];
        [self.rootNode addChildNode:levelNode];
        [self.physicsWorld setContactDelegate:self];
        self.physicsWorld.gravity = SCNVector3Make(0, -800, 0);
 
        [self setupTechniques];
    }
 
    return self;
}
 
- (void)setPostGameFilters
{
    [SCNTransaction begin];
 
    [self.desaturationTechnique setValue:@1.0 forKey:@"Saturation"];
 
    [SCNTransaction setAnimationDuration:1.0];
 
    [SCNTransaction commit];
 
    AAPLAppDelegate *appDelegate = [AAPLAppDelegate sharedAppDelegate];
    [appDelegate.scnView setTechnique:self.desaturationTechnique];
}
 
- (void)setPauseFilters
{
    [SCNTransaction begin];
 
    [self.desaturationTechnique setValue:@1.0 forKey:@"Saturation"];
 
    [SCNTransaction setAnimationDuration:1.0];
    [self.desaturationTechnique setValue:@0.0 forKey:@"Saturation"];
 
    [SCNTransaction commit];
 
    AAPLAppDelegate *appDelegate = [AAPLAppDelegate sharedAppDelegate];
    [appDelegate.scnView setTechnique:self.desaturationTechnique];
}
 
- (void)setPregameFilters
{
 
    [SCNTransaction begin];
 
    [self.desaturationTechnique setValue:@1.0 forKey:@"Saturation"];
 
    [SCNTransaction setAnimationDuration:1.0];
    [self.desaturationTechnique setValue:@0.0 forKey:@"Saturation"];
 
    [SCNTransaction commit];
 
    AAPLAppDelegate *appDelegate = [AAPLAppDelegate sharedAppDelegate];
    [appDelegate.scnView setTechnique:self.desaturationTechnique];
}
 
- (void)setIngameFilters
{
    [SCNTransaction begin];
 
    [self.desaturationTechnique setValue:@0.0 forKey:@"Saturation"];
 
    [SCNTransaction setAnimationDuration:1.0];
    [self.desaturationTechnique setValue:@1.0 forKey:@"Saturation"];
    [SCNTransaction commit];
 
    SCNAction *dropTechnique = [SCNAction waitForDuration:1.0f];
 
    AAPLAppDelegate *appDelegate = [AAPLAppDelegate sharedAppDelegate];
    [appDelegate.scnView.scene.rootNode runAction:dropTechnique completionHandler:^{
        [appDelegate.scnView setTechnique:nil];
    }];
}
 
- (void)setGameState:(AAPLGameState)gameState
{
    // Ignore redundant state changes.
    if (_gameState == gameState)
        return;
 
    // Change the UI system according to gameState.
    [self.gameUIScene setGameState:gameState];
 
    // Only reset the level from a non paused mode.
    if (gameState == AAPLGameStateInGame && _gameState != AAPLGameStatePaused) {
        [self.gameLevel resetLevel];
    }
    _gameState = gameState;
 
    // Based on the new game state... set the saturation value
    // that the techniques will use to render the scenekit view.
    if (_gameState == AAPLGameStatePostGame) {
        [self setPostGameFilters];
    } else if (_gameState == AAPLGameStatePaused) {
        [[AAPLGameSimulation sim] playSound:@"deposit.caf"];
        [self setPauseFilters];
    } else if (_gameState == AAPLGameStatePreGame) {
        [self setPregameFilters];
    } else {
        [[AAPLGameSimulation sim] playSound:@"ack.caf"];
        [self setIngameFilters];
    }
}
 
/*! Our main input pump for the app.
 */
- (void)renderer:(id <SCNSceneRenderer>)aRenderer updateAtTime:(NSTimeInterval)time
{
 
    if (_previousUpdateTime == 0.0) {
        _previousUpdateTime = time;
    }
    _deltaTime = time - _previousUpdateTime;
    _previousUpdateTime = time;
 
    AAPLSceneView *aView = (AAPLSceneView *)aRenderer;
 
    bool pressingLeft = NO;
    bool pressingRight = NO;
    bool pressingJump = NO;
 
    GCGamepad *gamePad = self.controller.gamepad;
    GCExtendedGamepad *extGamePad = self.controller.extendedGamepad;
 
    if (gamePad.dpad.left.pressed == YES || extGamePad.leftThumbstick.left.pressed == YES)
        pressingLeft = YES;
 
    if (gamePad.dpad.right.pressed == YES || extGamePad.leftThumbstick.right.pressed == YES)
        pressingRight = YES;
 
    if (gamePad.buttonA.pressed == YES ||
        gamePad.buttonB.pressed == YES ||
        gamePad.buttonX.pressed == YES ||
        gamePad.buttonY.pressed == YES ||
        gamePad.leftShoulder.pressed == YES ||
        gamePad.rightShoulder.pressed == YES)
        pressingJump = YES;
 
    if ([aView.keysPressed containsObject:AAPLLeftKey] == YES)
        pressingLeft = YES;
 
    if ([aView.keysPressed containsObject:AAPLRightKey] == YES)
        pressingRight = YES;
 
    if ([aView.keysPressed containsObject:AAPLJumpKey] == YES)
        pressingJump = YES;
 
    if (self.gameState == AAPLGameStateInGame && self.gameLevel.hitByLavaReset == NO) {
        if (pressingLeft) {
            self.gameLevel.playerCharacter.walkDirection = WalkDirectionLeft;
        } else if (pressingRight) {
            self.gameLevel.playerCharacter.walkDirection = WalkDirectionRight;
        }
 
        if (pressingLeft || pressingRight) {
            //Run if not running
            self.gameLevel.playerCharacter.inRunAnimation = YES;
        } else {
            //Stop running if running
            self.gameLevel.playerCharacter.inRunAnimation = NO;
        }
 
        if (pressingJump) {
            [self.gameLevel.playerCharacter performJumpAndStop:NO];
        } else {
            [self.gameLevel.playerCharacter performJumpAndStop:YES];
        }
    } else if (self.gameState == AAPLGameStatePreGame || self.gameState == AAPLGameStatePostGame) {
        if (pressingJump) {
            [self setGameState:AAPLGameStateInGame];
        }
    }
 
 
}
 
/*! Our main simulation pump for the app.
 */
- (void)renderer:(id <SCNSceneRenderer>)aRenderer didSimulatePhysicsAtTime:(NSTimeInterval)time
{
    [self.gameLevel update:_deltaTime withRenderer:(AAPLSceneView *)aRenderer];
}
 
#pragma mark - Collision handling
 
- (void)physicsWorld:(SCNPhysicsWorld *)world didBeginContact:(SCNPhysicsContact *)contact
{
    if (self.gameState == AAPLGameStateInGame) {
        // Player to banana, large banana, or coconut
        if (contact.nodeA == self.gameLevel.playerCharacter.collideSphere) {
            [self playerCollideWithContact:contact.nodeB point:contact.contactPoint];
            return;
        } else if (contact.nodeB == self.gameLevel.playerCharacter.collideSphere) {
            [self playerCollideWithContact:contact.nodeA point:contact.contactPoint];
            return;
        }
 
        // Coconut to anything but the player.
        if ((contact.nodeB.physicsBody.categoryBitMask == GameCollisionCategoryCoconut)) {
            [self handleCollideForCoconut:(AAPLCoconut *)contact.nodeB];
        } else if ((contact.nodeA.physicsBody.categoryBitMask == GameCollisionCategoryCoconut)) {
            [self handleCollideForCoconut:(AAPLCoconut *)contact.nodeA];
        }
    }
}
 
- (void)playerCollideWithContact:(SCNNode *)node point:(SCNVector3)contactPoint
{
    if ([self.gameLevel.bananas containsObject:node] == YES) {
        [self.gameLevel collectBanana:node];
    } else if ([self.gameLevel.largeBananas containsObject:node] == YES) {
        [self.gameLevel collectLargeBanana:node];
    } else if (node.physicsBody.categoryBitMask == GameCollisionCategoryCoconut) {
        [self.gameLevel collideWithCoconut:node point:contactPoint];
    } else if (node.physicsBody.categoryBitMask == GameCollisionCategoryLava) {
        [self.gameLevel collideWithLava];
    }
}
 
- (void)handleCollideForCoconut:(AAPLCoconut *)coconut
{
    // Remove coconut from the world after it has time to fall offscreen.
    [coconut runAction:[SCNAction waitForDuration:3.0f] completionHandler:^{
        [coconut removeFromParentNode];
        [self.gameLevel.coconuts removeObject:coconut];
    }];
}
 
#pragma mark - Game Controller handling
 
- (void)controllerDidConnect:(NSNotification *)note
{
    GCController *controller = [note object];
 
    // Assign the last in controller.
    self.controller = controller;
}
 
- (void)controllerDidDisconnect:(NSNotification *)note
{
    self.controller = nil;
 
    AAPLGameState currentState = [[AAPLGameSimulation sim] gameState];
 
    // Pause the if we are in game and the controller was disconnected.
    if (currentState == AAPLGameStateInGame) {
        [[AAPLGameSimulation sim] setGameState:AAPLGameStatePaused];
    }
}
 
- (void)setController:(GCController *)controller
{
    _controller = controller;
 
    if (_controller == nil) {
        return;
    }
 
    [_controller setControllerPausedHandler:^(GCController *myController) {
        AAPLGameState currentState = [[AAPLGameSimulation sim] gameState];
 
        if (currentState == AAPLGameStatePaused) {
            [[AAPLGameSimulation sim] setGameState:AAPLGameStateInGame];
        } else if (currentState == AAPLGameStateInGame) {
            [[AAPLGameSimulation sim] setGameState:AAPLGameStatePaused];
        }
    }];
}
 
#pragma mark - Sound & Music
 
- (void)playSound:(NSString *)soundFileName
{
    if (soundFileName == nil)
        return;
 
    NSString *path = [NSString stringWithFormat:@"Sounds/%@", soundFileName];
    [self.gameUIScene runAction:[SKAction playSoundFileNamed:path waitForCompletion:NO]];
}
 
- (void)playMusic:(NSString *)soundFileName
{
    if (soundFileName == nil)
        return;
    if ([self.gameUIScene actionForKey:soundFileName] != nil)
        return;
 
    NSString *path = [NSString stringWithFormat:@"Sounds/%@", soundFileName];
    SKAction *repeatAction = [SKAction repeatActionForever:[SKAction playSoundFileNamed:path waitForCompletion:YES]];
    [self.gameUIScene runAction:repeatAction withKey:soundFileName];
}
 
#pragma mark - Resource Loading convenience
 
NSString *const ArtFolderRoot = @"art.scnassets";
 
+ (NSString *)pathForArtResource:(NSString *)resourceName
{
    return [NSString stringWithFormat:@"%@/%@", ArtFolderRoot, resourceName];
}
 
+ (SCNNode *)loadNodeWithName:(NSString *)name fromSceneNamed:(NSString *)path
{
    // Load the scene from the specified file
    SCNScene *scene = [SCNScene sceneNamed:path
                               inDirectory:nil
                                   options:@{SCNSceneSourceConvertToYUpKey : @YES,
                                             SCNSceneSourceAnimationImportPolicyKey :SCNSceneSourceAnimationImportPolicyPlayRepeatedly}];
 
    // Retrieve the root node
    SCNNode *node = scene.rootNode;
 
    // Search for the node named "name"
    if (name) {
        node = [node childNodeWithName:name recursively:YES];
    } else {
        node = node.childNodes[0];
    }
 
    return node;
}
 
+ (SCNParticleSystem *)loadParticleSystemWithName:(NSString *)name
{
    NSString *path = [NSString stringWithFormat:@"level/effects/%@.scnp", name];
    path = [self pathForArtResource:path];
    path = [[NSBundle mainBundle] pathForResource:path ofType:nil];
    SCNParticleSystem *newSystem = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
 
    path = [NSString stringWithFormat:@"level/effects/%@", [((NSURL *)newSystem.particleImage).path lastPathComponent]];
    path = [self pathForArtResource:path];
    NSURL *url = [NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUnicodeStringEncoding]];
    newSystem.particleImage = url;
    return newSystem;
}
 
@end