Objective-C/fox2 Shared/AAPLGameController.m
/* |
Copyright (C) 2018 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
This class serves as the app's source of control flow. |
*/ |
#import "AAPLGameController.h" |
#import "AAPLCharacter.h" |
#import "AAPLOverlay.h" |
#import "AAPLMenu.h" |
#import <GameController/GameController.h> |
#if TARGET_OS_IOS |
#import "AAPLControlOverlay.h" |
#endif |
#import <GameplayKit/GameplayKit.h> |
#import "AAPLPlayerComponent.h" |
#import "AAPLChaserComponent.h" |
#import "AAPLScaredComponent.h" |
// Some settings |
#define EnableGamePlayKit 1 |
#define DefaultCameraTransitionDuration 1.0 |
#define NumberOfFiends 100 |
#define CameraOrientationSensitivity 0.05f |
// the scene and renderer are settable locally |
@interface AAPLGameController () |
@property (strong, nonatomic) SCNScene *scene; |
@property (strong, nonatomic) id <SCNSceneRenderer> sceneRenderer; |
@end |
// Particle identifiers |
typedef enum |
{ |
AAPLParticleNameCollect, |
AAPLParticleNameCollectBig, |
AAPLParticleNameKeyApparition, |
AAPLParticleNameEnemyExplosion, |
AAPLParticleNameUnlockDoor, |
AAPLParticleNameCount, |
}AAPLParticleName; |
// Audio identifiers |
typedef enum |
{ |
AAPLAudioSourceNameCollect, |
AAPLAudioSourceNameCollectBig, |
AAPLAudioSourceNameUnlockDoor, |
AAPLAudioSourceNameHitEnemy, |
AAPLAudioSourceNameCount, |
}AAPLAudioSourceName; |
@implementation AAPLGameController |
{ |
// Overlays |
AAPLOverlay *_overlay; |
//character |
AAPLCharacter *_character; |
//camera and targets |
SCNNode *_cameraNode; |
SCNNode *_lookAtTarget; |
SCNNode *_lastActiveCamera; |
SCNNode *_activeCamera; |
vector_float3 _lastCameraFrontDir; |
BOOL _playingCinematic; |
//triggers |
SCNNode *_lastTrigger; |
BOOL _firstTriggerDone; |
//enemies |
SCNNode *_enemy1; |
SCNNode *_enemy2; |
//friends |
SCNNode *_friends[NumberOfFiends]; |
float _friendsSpeed[NumberOfFiends]; |
NSUInteger _friendCount; |
BOOL _friendsAreFree; |
//collected objects |
NSUInteger _collectedKeys; |
NSUInteger _collectedGems; |
BOOL _keyIsVisible; |
// particles |
NSArray *_particleSystems[AAPLParticleNameCount]; |
// audio |
SCNAudioSource *_audioSources[AAPLAudioSourceNameCount]; |
// GameplayKit |
GKScene *_gkScene; |
//dPad |
vector_float2 _cameraDirection; |
// Game controller |
GCController* _gamePadCurrent; |
GCControllerDirectionPad* _gamePadLeft; |
GCControllerDirectionPad* _gamePadRight; |
// update delta time |
NSTimeInterval _lastUpdateTime; |
} |
#pragma mark - |
#pragma mark Setup |
- (void)setupGameController { |
if ([[GCController controllers] count]) { |
GCController* controller = [[GCController controllers] firstObject]; |
[self registerGameController:controller]; |
} |
[NSNotificationCenter.defaultCenter addObserver:self |
selector:@selector(handleControllerDidConnectNotification:) |
name:GCControllerDidConnectNotification |
object:nil]; |
[NSNotificationCenter.defaultCenter addObserver:self |
selector:@selector(handleControllerDidDisconnectNotification:) |
name:GCControllerDidDisconnectNotification |
object:nil]; |
} |
- (void)setupCharacter |
{ |
_character = [[AAPLCharacter alloc] initWithScene:self.scene]; |
// keep a pointer to the physicsWorld from the character because we will need it when updating the character's position |
_character.physicsWorld = self.scene.physicsWorld; |
[self.scene.rootNode addChildNode:_character.node]; |
} |
- (void)setupPhysics |
{ |
//make sure all objects only collide with the character |
[self.scene.rootNode enumerateHierarchyUsingBlock:^(SCNNode * _Nonnull node, BOOL * _Nonnull stop) { |
node.physicsBody.collisionBitMask = AAPLBitmaskCharacter; |
}]; |
} |
- (void)setupCollisions |
{ |
// load the collision mesh from another scene and merge into main scene |
SCNScene *collisionScene = [SCNScene sceneNamed:@"Art.scnassets/collision.scn"]; |
[collisionScene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.opacity = 0.0f; // make the collision invisible |
[self.scene.rootNode addChildNode:child]; |
}]; |
} |
// the follow camera behavior make the camera to follow the character, with a constant distance, altitude and smoothed motion |
- (void)setupFollowCamera:(SCNNode *)cameraNode |
{ |
// look at "lookAtTarget" |
SCNLookAtConstraint* lookAtConstraint = [SCNLookAtConstraint lookAtConstraintWithTarget:_lookAtTarget]; |
lookAtConstraint.gimbalLockEnabled = YES; // keep horizon horizonal |
lookAtConstraint.influenceFactor = 0.07; |
// distance constraints |
SCNDistanceConstraint *follow = [SCNDistanceConstraint distanceConstraintWithTarget:_lookAtTarget]; |
// configure distance |
const float distance = vector_length(cameraNode.simdPosition); |
follow.minimumDistance = distance; |
follow.maximumDistance = distance; |
// configure a constraint to maintain a constant altitude relative to the character |
const float desiredAltitude = fabs(cameraNode.worldPosition.y); |
__weak AAPLGameController* weakSelf = self; |
SCNTransformConstraint *keepAltitude = [SCNTransformConstraint positionConstraintInWorldSpace:YES withBlock:^SCNVector3(SCNNode * _Nonnull node, SCNVector3 position) { |
AAPLGameController* strongSelf = weakSelf; |
if (!strongSelf) |
return position; |
position.y = _character.baseAltitude + desiredAltitude; |
return position; |
}]; |
// acceleration constraint to smooth the camera motion |
SCNAccelerationConstraint *accelerationConstraint = [SCNAccelerationConstraint accelerationConstraint]; |
accelerationConstraint.maximumLinearVelocity = 1500.0; |
accelerationConstraint.maximumLinearAcceleration = 50.0; |
accelerationConstraint.damping = 0.05; |
// use a custom constraint to let the user orbit the camera around the character |
SCNNode* transformNode = [SCNNode node]; |
SCNTransformConstraint* orientationUpdateConstraint = [SCNTransformConstraint transformConstraintInWorldSpace:YES withBlock:^SCNMatrix4(SCNNode * _Nonnull node, SCNMatrix4 transform) { |
AAPLGameController* strongSelf = weakSelf; |
// Slowly update the acceleration constraint influence factor to smoothly reenable the acceleration. |
accelerationConstraint.influenceFactor = MIN(1, accelerationConstraint.influenceFactor + 0.01); |
if (!strongSelf) |
return transform; |
if (strongSelf->_activeCamera != node) |
return transform; |
const vector_float3 targetPosition = strongSelf->_lookAtTarget.presentationNode.simdWorldPosition; |
const vector_float2 cameraDirection = strongSelf->_cameraDirection; |
if (vector_all(cameraDirection == 0)) { |
return transform; |
} |
// Disable the acceleration constraint. |
accelerationConstraint.influenceFactor = 0; |
const vector_float3 characterWorldUp = strongSelf->_character.node.presentationNode.simdWorldUp; |
transformNode.transform = transform; |
const simd_quatf q = simd_mul( |
simd_quaternion(CameraOrientationSensitivity * cameraDirection.x, characterWorldUp), |
simd_quaternion(CameraOrientationSensitivity * cameraDirection.y, transformNode.simdWorldRight)); |
[transformNode simdRotateBy:q aroundTarget:targetPosition]; |
return transformNode.transform; |
}]; |
cameraNode.constraints = @[follow, keepAltitude, accelerationConstraint, orientationUpdateConstraint, lookAtConstraint]; |
} |
// the axis aligned behavior look at the character but remains aligned using a specified axis |
- (void)setupAxisAlignedCamera:(SCNNode *)cameraNode |
{ |
const float distance = vector_length(cameraNode.simdPosition); |
const vector_float3 originalAxisDirection = cameraNode.simdWorldFront; |
_lastCameraFrontDir = originalAxisDirection; |
const vector_float3 symetricAxisDirection = {-originalAxisDirection.x, originalAxisDirection.y, -originalAxisDirection.z}; |
// define a custom constraint for the axis alignment |
__weak AAPLGameController* weakSelf = self; |
SCNTransformConstraint *axisAlignConstraint = [SCNTransformConstraint positionConstraintInWorldSpace:YES withBlock:^SCNVector3(SCNNode * _Nonnull node, SCNVector3 position) { |
AAPLGameController* strongSelf = weakSelf; |
if (!strongSelf) |
return position; |
vector_float3 axisOrigin = _lookAtTarget.presentationNode.simdWorldPosition; |
vector_float3 referenceFrontDirection = |
strongSelf->_activeCamera == node ? _lastCameraFrontDir : strongSelf->_activeCamera.presentationNode.simdWorldFront; |
vector_float3 axis = vector_dot(originalAxisDirection, referenceFrontDirection) > 0 ? originalAxisDirection : symetricAxisDirection; |
vector_float3 constrainedPosition = axisOrigin - distance * axis; |
return SCNVector3FromFloat3(constrainedPosition); |
}]; |
SCNAccelerationConstraint* accelerationConstraint = [SCNAccelerationConstraint accelerationConstraint]; |
accelerationConstraint.maximumLinearAcceleration = 20; |
accelerationConstraint.decelerationDistance = .5; |
accelerationConstraint.damping = 0.05; |
// look at constraint |
SCNLookAtConstraint* lookAtConstraint = [SCNLookAtConstraint lookAtConstraintWithTarget:_lookAtTarget]; |
lookAtConstraint.gimbalLockEnabled = YES; // keep horizon horizontal |
cameraNode.constraints = @[axisAlignConstraint, lookAtConstraint, accelerationConstraint]; |
} |
- (void) setupCameraNode:(SCNNode *)node |
{ |
NSString *cameraName = node.name; |
if([cameraName hasPrefix:@"camTrav"]){ |
[self setupAxisAlignedCamera:node]; |
} |
else if([cameraName hasPrefix:@"camLookAt"]){ |
[self setupFollowCamera:node]; |
} |
} |
- (void)setupCamera |
{ |
//create a node that we will use as the lookAt target |
//it will be placed slighlty above the character |
_lookAtTarget = [SCNNode node]; |
__weak AAPLGameController* weakSelf = self; |
_lookAtTarget.constraints = @[[SCNTransformConstraint positionConstraintInWorldSpace:YES withBlock:^SCNVector3(SCNNode *node, SCNVector3 position) { |
AAPLGameController* strongSelf = weakSelf; |
if (!strongSelf) |
return position; |
position = strongSelf->_character.node.worldPosition; |
position.y = strongSelf->_character.baseAltitude + 0.5; |
return position; |
}]]; |
[self.scene.rootNode addChildNode:_lookAtTarget]; |
[self.scene.rootNode enumerateHierarchyUsingBlock:^(SCNNode * _Nonnull node, BOOL * _Nonnull stop) { |
if(node.camera){ |
[self setupCameraNode:node]; |
} |
}]; |
// create our main camera |
// the one we will use during the game |
_cameraNode = [SCNNode node]; |
_cameraNode.camera = [SCNCamera camera]; |
_cameraNode.name = @"mainCamera"; |
_cameraNode.camera.zNear = 0.1; |
[self.scene.rootNode addChildNode:_cameraNode]; |
// start with this camera |
[self setActiveCamera:@"camLookAt_cameraGame" animationDuration:0.0]; |
} |
- (void)setupEnemies |
{ |
#if EnableGamePlayKit |
_gkScene = [GKScene sceneWithFileNamed:@"Art.scnassets/scene.gks" rootNode:_scene]; |
#endif |
_enemy1 = [_scene.rootNode childNodeWithName:@"enemy1" recursively:YES]; |
_enemy2 = [_scene.rootNode childNodeWithName:@"enemy2" recursively:YES]; |
// animate enemies (move up and down) |
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"]; |
anim.fromValue = [NSValue valueWithSCNVector3:SCNVector3Make(0, 0.1, 0)]; |
anim.toValue = [NSValue valueWithSCNVector3:SCNVector3Make(0, -0.1, 0)]; |
anim.additive = YES; |
anim.repeatCount = INFINITY; |
anim.autoreverses = YES; |
anim.duration = 1.2; |
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
[_enemy1 addAnimation:anim forKey:nil]; |
[_enemy2 addAnimation:anim forKey:nil]; |
// setup GameplayKit |
#if EnableGamePlayKit |
GKEntity *playerEntity = [GKEntity entity]; |
[_gkScene addEntity:playerEntity]; |
[playerEntity addComponent:[GKSCNNodeComponent componentWithNode:_character.node]]; |
AAPLPlayerComponent *playerComponent = [[AAPLPlayerComponent alloc] init]; |
playerComponent.autoMoveNode = NO; |
playerComponent.character = _character; |
[playerEntity addComponent:playerComponent]; |
[playerComponent positionAgentFromNode]; |
for (GKEntity *entity in _gkScene.entities) |
{ |
AAPLChaserComponent *chaser = (AAPLChaserComponent *)[entity componentForClass:AAPLChaserComponent.class]; |
if (chaser){ |
chaser.player = playerComponent; |
[chaser positionAgentFromNode]; |
} |
AAPLScaredComponent *scared = (AAPLScaredComponent *)[entity componentForClass:AAPLScaredComponent.class]; |
if (scared){ |
scared.player = playerComponent; |
[scared positionAgentFromNode]; |
} |
} |
#endif |
} |
- (NSArray *)loadParticleSystemsAtPath:(NSString *)path |
{ |
NSString *directory = [path stringByDeletingLastPathComponent]; |
NSString *fileName = [path lastPathComponent]; |
NSString *ext = [path pathExtension]; |
if([ext isEqualToString:@"scnp"]){ |
return @[[SCNParticleSystem particleSystemNamed:fileName inDirectory:directory]]; |
} |
else{ |
NSMutableArray *particles = [NSMutableArray array]; |
SCNScene *scene = [SCNScene sceneNamed:fileName inDirectory:directory options:nil]; |
[scene.rootNode enumerateHierarchyUsingBlock:^(SCNNode * _Nonnull node, BOOL * _Nonnull stop) { |
if(node.particleSystems){ |
[particles addObjectsFromArray:node.particleSystems]; |
} |
}]; |
return particles; |
} |
return nil; |
} |
- (void)setupParticleSystems |
{ |
_particleSystems[AAPLParticleNameCollect] = [self loadParticleSystemsAtPath:@"Art.scnassets/particles/collect.scnp"]; |
_particleSystems[AAPLParticleNameCollectBig] = [self loadParticleSystemsAtPath:@"Art.scnassets/particles/key_apparition.scn"]; |
_particleSystems[AAPLParticleNameEnemyExplosion] = [self loadParticleSystemsAtPath:@"Art.scnassets/particles/enemy_explosion.scn"]; |
_particleSystems[AAPLParticleNameKeyApparition] = [self loadParticleSystemsAtPath:@"Art.scnassets/particles/key_apparition.scn"]; |
_particleSystems[AAPLParticleNameUnlockDoor] = [self loadParticleSystemsAtPath:@"Art.scnassets/particles/unlock_door.scn"]; |
} |
- (void)setupPlatforms |
{ |
#define PLATFORM_MOVE_OFFSET 1.5 |
#define PLATFORM_MOVE_SPEED 0.5 |
__block float alternate = 1; |
// animate the platforms |
[self.scene.rootNode enumerateHierarchyUsingBlock:^(SCNNode * _Nonnull node, BOOL * _Nonnull stop) { |
if([node.name isEqualToString:@"mobilePlatform"] && node.childNodes.count > 0){ |
node.position = SCNVector3Make(node.position.x - (alternate * PLATFORM_MOVE_OFFSET/2), node.position.y, node.position.z); |
SCNAction *moveAction = [SCNAction moveBy:SCNVector3Make(alternate * PLATFORM_MOVE_OFFSET, 0, 0) duration:1/PLATFORM_MOVE_SPEED]; |
moveAction.timingMode = SCNActionTimingModeEaseInEaseOut; |
[node runAction:[SCNAction repeatActionForever:[SCNAction sequence:@[moveAction, moveAction.reversedAction]]]]; |
alternate = -alternate; // alternate movement of platforms to unsynchronize them |
// tweak platform particles |
[node enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
if([child.name isEqualToString:@"particles_platform"]){ |
child.particleSystems[0].orientationDirection = SCNVector3Make(0, 1, 0); |
} |
}]; |
} |
}]; |
} |
#pragma mark - |
#pragma mark Camera transitions |
// transition to the specified camera |
// this method will reparent the main camera under the camera named "cameraNamed" |
// and trigger the animation to smoothly move from the current position to the new position |
- (void)setActiveCamera:(NSString *)cameraName animationDuration:(CFTimeInterval)duration |
{ |
SCNNode *camera = [_scene.rootNode childNodeWithName:cameraName recursively:YES]; |
if(!camera || _activeCamera == camera) |
return; |
_lastActiveCamera = _activeCamera; |
_lastCameraFrontDir = _activeCamera.presentationNode.simdWorldFront; |
_activeCamera = camera; |
// save old transform in world space |
SCNMatrix4 oldTransform = _cameraNode.presentationNode.worldTransform; |
// re-parent |
[camera addChildNode:_cameraNode]; |
// compute the old transform relative to our new parent node (yeah this is the complex part) |
SCNMatrix4 parentTransform = camera.presentationNode.worldTransform; |
SCNMatrix4 parentInv = SCNMatrix4Invert(parentTransform); |
// with this new transform our position is unchanged in workd space (i.e we did re-parent but didn't move). |
_cameraNode.transform = SCNMatrix4Mult(oldTransform, parentInv); |
// now animate the transform to identity to smoothly move to the new desired position |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:duration]; |
[SCNTransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]]; |
_cameraNode.transform = SCNMatrix4Identity; // anim |
// use the parameters described by the target camera and that was configured by the artist |
SCNCamera *cameraTemplate = camera.camera; |
if(cameraTemplate){ |
_cameraNode.camera.fieldOfView = cameraTemplate.fieldOfView; |
_cameraNode.camera.wantsDepthOfField = cameraTemplate.wantsDepthOfField; |
_cameraNode.camera.sensorHeight = cameraTemplate.sensorHeight; |
_cameraNode.camera.fStop = cameraTemplate.fStop; |
_cameraNode.camera.focusDistance = cameraTemplate.focusDistance; |
_cameraNode.camera.bloomIntensity = cameraTemplate.bloomIntensity; |
_cameraNode.camera.bloomThreshold = cameraTemplate.bloomThreshold; |
_cameraNode.camera.bloomBlurRadius = cameraTemplate.bloomBlurRadius; |
_cameraNode.camera.wantsHDR = cameraTemplate.wantsHDR; |
_cameraNode.camera.wantsExposureAdaptation = cameraTemplate.wantsExposureAdaptation; |
_cameraNode.camera.vignettingPower = cameraTemplate.vignettingPower; |
_cameraNode.camera.vignettingIntensity = cameraTemplate.vignettingIntensity; |
} |
[SCNTransaction commit]; |
} |
- (void)setActiveCamera:(NSString *)cameraName |
{ |
[self setActiveCamera:cameraName animationDuration:DefaultCameraTransitionDuration]; |
} |
#pragma mark - |
#pragma mark Audio |
- (void)playSound:(AAPLAudioSourceName)audioName |
{ |
[self.scene.rootNode addAudioPlayer:[SCNAudioPlayer audioPlayerWithSource:_audioSources[audioName]]]; |
} |
- (void)setupAudio |
{ |
// Get an arbitrary node to attach the sounds to. |
SCNNode *node = self.scene.rootNode; |
// ambience |
SCNAudioSource *audioSource = [SCNAudioSource audioSourceNamed:@"audio/ambience.mp3"]; |
audioSource.loops = YES; |
audioSource.volume = 0.8; |
audioSource.positional = NO; |
audioSource.shouldStream = YES; |
[node addAudioPlayer:[SCNAudioPlayer audioPlayerWithSource:audioSource]]; |
// volcano |
SCNNode *volcanoNode = [self.scene.rootNode childNodeWithName:@"particles_volcanoSmoke_v2" recursively:YES]; |
audioSource = [SCNAudioSource audioSourceNamed:@"audio/volcano.mp3"]; |
audioSource.loops = YES; |
audioSource.volume = 5.0; |
[volcanoNode addAudioPlayer:[SCNAudioPlayer audioPlayerWithSource:audioSource]]; |
// other sounds |
_audioSources[AAPLAudioSourceNameCollect] = [SCNAudioSource audioSourceNamed:@"audio/collect.mp3"]; |
_audioSources[AAPLAudioSourceNameCollectBig] = [SCNAudioSource audioSourceNamed:@"audio/collectBig.mp3"]; |
_audioSources[AAPLAudioSourceNameUnlockDoor] = [SCNAudioSource audioSourceNamed:@"audio/unlockTheDoor.m4a"]; |
_audioSources[AAPLAudioSourceNameHitEnemy] = [SCNAudioSource audioSourceNamed:@"audio/hitEnemy.wav"]; |
// adjust volumes |
_audioSources[AAPLAudioSourceNameUnlockDoor].positional = NO; |
_audioSources[AAPLAudioSourceNameCollect].positional = NO; |
_audioSources[AAPLAudioSourceNameCollectBig].positional = NO; |
_audioSources[AAPLAudioSourceNameHitEnemy].positional = NO; |
_audioSources[AAPLAudioSourceNameUnlockDoor].volume = 0.5; |
_audioSources[AAPLAudioSourceNameCollect].volume = 4.0; |
_audioSources[AAPLAudioSourceNameCollectBig].volume = 4.0; |
} |
#pragma mark - |
#pragma mark Init |
- (instancetype)initWithSCNView:(SCNView *)scnView { |
self = [super init]; |
if (self) { |
self.sceneRenderer = scnView; |
self.sceneRenderer.delegate = self; |
// Uncomment to show statistics such as fps and timing information |
// scnView.showsStatistics = YES; |
// setup overlay |
_overlay = [AAPLOverlay sceneWithSize:scnView.bounds.size]; |
[_overlay setupWithController:self]; |
scnView.overlaySKScene = _overlay; |
//load the main scene |
self.scene = [SCNScene sceneNamed:@"Art.scnassets/scene.scn"]; |
//setup physics |
[self setupPhysics]; |
//setup collisions |
[self setupCollisions]; |
//load the character |
[self setupCharacter]; |
//setup enemies |
[self setupEnemies]; |
//setup friends |
[self addFriends:3]; |
//setup platforms |
[self setupPlatforms]; |
//setup particles |
[self setupParticleSystems]; |
//configure the lighting |
SCNLight *light = [self.scene.rootNode childNodeWithName:@"DirectLight" recursively:YES].light; |
light.shadowCascadeCount = 3; // turn on cascade shadows |
light.shadowCascadeSplittingFactor = 0.5; |
light.maximumShadowDistance = 20.0; |
light.shadowMapSize = CGSizeMake(512,512); |
//setup camera |
[self setupCamera]; |
//setup game controller |
[self setupGameController]; |
//configure quality |
[self configureRenderingQuality:scnView]; |
//assign the scene to the view |
self.sceneRenderer.scene = self.scene; |
//select the point of view to use |
self.sceneRenderer.pointOfView = _cameraNode; |
//register ourself as the physics contact delegate to receive contact notifications |
self.sceneRenderer.scene.physicsWorld.contactDelegate = (id <SCNPhysicsContactDelegate>) self; |
//setup audio |
[self setupAudio]; |
//preload everything |
[self.sceneRenderer prepareObject:self.scene shouldAbortBlock:nil]; |
} |
return self; |
} |
- (void)resetPlayerPosition { |
[_character resetCharacterPosition]; |
} |
#pragma mark - cinematic |
- (void)startCinematic |
{ |
_playingCinematic = YES; |
_character.node.paused = YES; |
} |
- (void)stopCinematic |
{ |
_playingCinematic = NO; |
_character.node.paused = NO; |
} |
#pragma mark - particles |
- (NSArray *)particleSystemsWithName:(AAPLParticleName)name |
{ |
return _particleSystems[name]; |
} |
- (void)addParticlesWithName:(AAPLParticleName)name withTransform:(SCNMatrix4)transform |
{ |
NSArray *particles = [self particleSystemsWithName:name]; |
for(SCNParticleSystem *ps in particles){ |
[self.scene addParticleSystem:ps withTransform:transform]; |
} |
} |
#pragma mark - Triggers |
// "triggers" are triggered when a character enter a box with the collision mask AAPLBitmaskTrigger |
- (void)execTrigger:(SCNNode *)triggerNode animationDuration:(CFTimeInterval)duration |
{ |
//exec trigger |
if([triggerNode.name hasPrefix:@"trigCam_"]){ |
NSString *cameraName = [triggerNode.name substringFromIndex:8]; |
//NSLog(@"switch to camera :%@", cameraName); |
[self setActiveCamera:cameraName animationDuration:duration]; |
} |
//action |
if([triggerNode.name hasPrefix:@"trigAction_"]){ |
if(_collectedKeys > 0){ |
NSString *actionName = [triggerNode.name substringFromIndex:11]; |
//NSLog(@"trigger action :%@", actionName); |
if([actionName isEqualToString:@"unlockDoor"]){ |
[self unlockDoor]; |
} |
} |
} |
} |
- (void)trigger:(SCNNode *)triggerNode |
{ |
if(_playingCinematic) |
return; |
if(_lastTrigger != triggerNode){ |
_lastTrigger = triggerNode; |
// the very first trigger should not animate (initial camera position) |
[self execTrigger:triggerNode animationDuration:_firstTriggerDone ? DefaultCameraTransitionDuration : 0]; |
_firstTriggerDone = YES; |
} |
} |
#pragma mark - Friends |
- (void)updateFriendsWithDeltaTime:(CFTimeInterval)deltaTime |
{ |
float pathCurve = 0.4f; |
// update pandas |
for (NSInteger i = 0; i < _friendCount; i++) { |
SCNNode *friend = _friends[i]; |
vector_float3 pos = SCNVector3ToFloat3(friend.position); |
float offsetx = pos.x - sinf(pathCurve * pos.z); |
pos.z += _friendsSpeed[i] * deltaTime * 0.5; |
pos.x = sinf(pathCurve * pos.z) + offsetx; |
friend.position = SCNVector3FromFloat3(pos); |
[self ensureNoPenetrationOfIndex:i]; |
} |
} |
- (void)animateFriends |
{ |
//animations |
SCNAnimationPlayer *walkAnimation = [AAPLCharacter loadAnimationFromSceneNamed:@"Art.scnassets/character/max_walk.scn"]; |
[SCNTransaction begin]; |
for(int i=0; i < _friendCount; i++){ |
//unsynchronize |
walkAnimation = [walkAnimation copy]; |
walkAnimation.speed = _friendsSpeed[i]; |
[_friends[i] addAnimationPlayer:walkAnimation forKey:@"walk"]; |
[walkAnimation play]; |
} |
[SCNTransaction commit]; |
} |
- (void)addFriends:(NSUInteger)count |
{ |
if(count + _friendCount > NumberOfFiends){ |
count = NumberOfFiends - _friendCount; |
} |
SCNScene *friendScene = [SCNScene sceneNamed:@"Art.scnassets/character/max.scn"]; |
SCNNode *friendModel = [friendScene.rootNode childNodeWithName:@"Max_rootNode" recursively:YES]; |
friendModel.name = @"friend"; |
// prepare 3 different geometries with different colors |
SCNGeometry *geometries[3]; |
SCNNode *geometryNode = [friendModel childNodeWithName:@"Max" recursively:YES]; |
geometryNode.geometry.firstMaterial.diffuse.intensity = 0.5; |
geometries[0] = [geometryNode.geometry copy]; |
geometries[1] = [geometryNode.geometry copy]; |
geometries[2] = [geometryNode.geometry copy]; |
geometries[0].firstMaterial = [geometries[0].firstMaterial copy]; |
geometryNode.geometry.firstMaterial.diffuse.contents = @"Art.scnassets/character/max_diffuseB.png"; |
geometries[1].firstMaterial = [geometries[1].firstMaterial copy]; |
geometryNode.geometry.firstMaterial.diffuse.contents = @"Art.scnassets/character/max_diffuseC.png"; |
geometries[2].firstMaterial = [geometries[2].firstMaterial copy]; |
geometryNode.geometry.firstMaterial.diffuse.contents = @"Art.scnassets/character/max_diffuseD.png"; |
//remove physics from our friends |
[friendModel enumerateHierarchyUsingBlock:^(SCNNode * _Nonnull node, BOOL * _Nonnull stop) { |
node.physicsBody = nil; |
}]; |
//todo: use a marker |
SCNVector3 friendPosition = SCNVector3Make(-5.84, -0.75, 3.354); |
#define FRIEND_AREA_WIDTH 1.4 |
#define FRIEND_AREA_LENGTH 5.0 |
// group them |
SCNNode *friends = [self.scene.rootNode childNodeWithName:@"friends" recursively:NO]; |
if(!friends){ |
friends = [SCNNode node]; |
friends.name = @"friends"; |
[self.scene.rootNode addChildNode:friends]; |
} |
//animations |
SCNAnimationPlayer *idleAnimation = [AAPLCharacter loadAnimationFromSceneNamed:@"Art.scnassets/character/max_idle.scn"]; |
for(int i=0; i < count; i++){ |
SCNNode* friend = [friendModel clone]; |
//replace texture |
int textureIndex = (int)(3.0 * (rand()/(float)RAND_MAX)); |
SCNNode *geometryNode = [friend childNodeWithName:@"Max" recursively:YES]; |
geometryNode.geometry = geometries[textureIndex]; |
//place our friend |
friend.position = SCNVector3Make(friendPosition.x + (1.4 * (rand()/(float)RAND_MAX)-0.5),friendPosition.y, friendPosition.z - (FRIEND_AREA_LENGTH * (rand()/(float)RAND_MAX))); |
//unsynchronize |
idleAnimation = [idleAnimation copy]; |
idleAnimation.speed = 1.5 + 1.5 * rand()/(float)RAND_MAX; |
[friend addAnimationPlayer:idleAnimation forKey:@"idle"]; |
[idleAnimation play]; |
[friends addChildNode:friend]; |
_friendsSpeed[_friendCount] = idleAnimation.speed; |
_friends[_friendCount++] = friend; |
} |
for (NSInteger i = 0; i < _friendCount; i++) { |
[self ensureNoPenetrationOfIndex:i]; |
} |
} |
// iterates on every friend and move them if they intersect friend at index i |
- (void)ensureNoPenetrationOfIndex:(NSInteger)i |
{ |
vector_float3 pos = SCNVector3ToFloat3(_friends[i].position); |
// ensure no penetration |
float pandaRadius = 0.15f; |
float pandaDiameter = pandaRadius * 2.f; |
for (NSInteger j = 0; j < _friendCount; ++j) { |
if (j == i) |
continue; |
if (!_friends[j]) |
continue; |
vector_float3 otherPos = SCNVector3ToFloat3(_friends[j].position); |
vector_float3 v = otherPos - pos; |
float dist = vector_length(v); |
if (dist < pandaDiameter) { // penetration |
float pen = pandaDiameter - dist; |
pos -= vector_normalize(v) * pen; |
} |
} |
//ensure within the box X[-6.662 -4.8] Z<3.354 |
if(_friends[i].position.z <= 3.354){ |
pos.x = MAX(pos.x, -6.662); |
pos.x = MIN(pos.x, -4.8); |
} |
_friends[i].position = SCNVector3FromFloat3(pos); |
} |
#pragma mark - Game actions |
- (void)unlockDoor |
{ |
if(_friendsAreFree) //already unlocked |
return; |
[self startCinematic]; //pause the scene |
//play sound |
[self playSound:AAPLAudioSourceNameUnlockDoor]; |
//cinematic02 |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.0]; |
[SCNTransaction setCompletionBlock:^{ |
//trigger particles |
SCNNode *door = [self.scene.rootNode childNodeWithName:@"door" recursively:YES]; |
SCNNode *particle_door = [self.scene.rootNode childNodeWithName:@"particles_door" recursively:YES]; |
[self addParticlesWithName:AAPLParticleNameUnlockDoor withTransform:particle_door.worldTransform]; |
//audio |
[self playSound:AAPLAudioSourceNameCollectBig]; |
//add friends |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.0]; |
[self addFriends:NumberOfFiends]; |
[SCNTransaction commit]; |
//open the door |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.0]; |
[SCNTransaction setCompletionBlock:^{ |
//animate characters |
[self animateFriends]; |
// update state |
_friendsAreFree = YES; |
// show end screen |
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ |
[self showEndScreen]; |
}); |
// this is the END! |
}]; |
door.opacity = 0.0; |
[SCNTransaction commit]; |
}]; |
// change the point of view |
[self setActiveCamera:@"CameraCinematic02" animationDuration:1.0]; |
[SCNTransaction commit]; |
} |
- (void)showKey |
{ |
_keyIsVisible = YES; |
// get the key node |
SCNNode *key = [self.scene.rootNode childNodeWithName:@"key" recursively:YES]; |
//sound fx |
[self playSound:AAPLAudioSourceNameCollectBig]; |
//particles |
[self addParticlesWithName:AAPLParticleNameKeyApparition withTransform:key.worldTransform]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.0]; |
[SCNTransaction setCompletionBlock:^{ |
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ |
[self keyDidAppear]; |
}); |
}]; |
key.opacity = 1.0; // show the key |
[SCNTransaction commit]; |
} |
- (void)keyDidAppear |
{ |
[self execTrigger:_lastTrigger animationDuration:0.75]; //revert to previous camera |
[self stopCinematic]; |
} |
- (void)keyShouldAppear |
{ |
[self startCinematic]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.0]; |
[SCNTransaction setCompletionBlock:^{ |
[self showKey]; |
}]; |
[self setActiveCamera:@"CameraCinematic01" animationDuration:3.0]; |
[SCNTransaction commit]; |
} |
- (void)collect:(SCNNode *)collectable |
{ |
if(collectable.physicsBody != nil){ |
//the Key |
if([collectable.name isEqualToString:@"key"]){ |
if(!_keyIsVisible){ //key not visible yet |
return; |
} |
// play sound |
[self playSound:AAPLAudioSourceNameCollect]; |
[_overlay didCollectKey]; |
_collectedKeys++; |
} |
//the gems |
else if([collectable.name isEqualToString:@"CollectableBig"]){ |
_collectedGems++; |
// play sound |
[self playSound:AAPLAudioSourceNameCollect]; |
// update the overlay |
[_overlay setCollectedGemsCount:_collectedGems]; |
if(_collectedGems == 1) { |
//we collect a gem, show the key after 1 second |
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ |
[self keyShouldAppear]; |
}); |
} |
} |
collectable.physicsBody = nil; //not collectable anymore |
// particles |
[self addParticlesWithName:AAPLParticleNameKeyApparition withTransform:collectable.worldTransform]; |
[collectable removeFromParentNode]; |
} |
} |
#pragma mark - Controlling the character |
- (void)controllerJump:(BOOL)controllerJump |
{ |
_character.jump = controllerJump; |
} |
- (void)controllerAttack |
{ |
if (!_character.isAttacking) { |
[_character attack]; |
} |
} |
- (void)setCharacterDirection:(vector_float2)characterDirection |
{ |
float l = vector_length(characterDirection); |
if( l > 1.0f ) { |
characterDirection = characterDirection / l; |
} |
_character.direction = characterDirection; |
} |
- (vector_float2)characterDirection |
{ |
return _character.direction; |
} |
- (vector_float2)cameraDirection { |
return _cameraDirection; |
} |
- (void)setCameraDirection:(vector_float2)direction { |
float l = vector_length(direction); |
if( l > 1.0f ) { |
direction = direction / l; |
} |
direction.y = 0; |
_cameraDirection = direction; |
} |
#pragma mark - Update |
- (void)renderer:(id <SCNSceneRenderer>)renderer updateAtTime:(NSTimeInterval)time |
{ |
// compute delta time |
if (_lastUpdateTime == 0) |
_lastUpdateTime = time; |
NSTimeInterval deltaTime = time - _lastUpdateTime; |
_lastUpdateTime = time; |
// Update Friends |
if(_friendsAreFree) |
[self updateFriendsWithDeltaTime:deltaTime]; |
// stop here if cinematic |
if(_playingCinematic == YES) |
return; |
// update characters |
[_character updateAtTime:time withRenderer:renderer]; |
#if EnableGamePlayKit |
// update enemies |
for (GKEntity *entity in _gkScene.entities) { |
[entity updateWithDeltaTime:deltaTime]; |
} |
#endif |
} |
#pragma mark - contact delegate |
- (void)physicsWorld:(SCNPhysicsWorld *)world didBeginContact:(SCNPhysicsContact *)contact |
{ |
// triggers |
if(contact.nodeA.physicsBody.categoryBitMask == AAPLBitmaskTrigger){ |
[self trigger:contact.nodeA]; |
} |
if(contact.nodeB.physicsBody.categoryBitMask == AAPLBitmaskTrigger){ |
[self trigger:contact.nodeB]; |
} |
// collectables |
if(contact.nodeA.physicsBody.categoryBitMask == AAPLBitmaskCollectable){ |
[self collect:contact.nodeA]; |
} |
if(contact.nodeB.physicsBody.categoryBitMask == AAPLBitmaskCollectable){ |
[self collect:contact.nodeB]; |
} |
} |
#pragma mark - Congratulating the Player |
- (void)showEndScreen { |
// Play the congrat sound. |
SCNAudioSource *victoryMusic = [SCNAudioSource audioSourceNamed:@"audio/Music_victory.mp3"]; |
victoryMusic.volume = 0.5; |
[self.scene.rootNode addAudioPlayer:[SCNAudioPlayer audioPlayerWithSource:victoryMusic]]; |
[_overlay showEndScreen]; |
} |
#pragma mark - Configure rendering quality |
- (void)turnOffPhysicallyBasedMaterials |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
for(SCNMaterial *material in child.geometry.materials){ |
material.lightingModelName = SCNLightingModelBlinn; |
material.roughness.contents = nil; |
material.metalness.contents = nil; |
} |
}]; |
// IBL is useless... |
self.scene.lightingEnvironment.contents = nil; |
//... so add an ambient light to compensate |
SCNNode *ambient = [SCNNode node]; |
ambient.light = [SCNLight light]; |
ambient.light.type = SCNLightTypeAmbient; |
ambient.light.intensity = 600; |
[self.scene.rootNode addChildNode:ambient]; |
} |
- (void)turnOffDepthOfField |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.camera.wantsDepthOfField = false; |
}]; |
} |
- (void)turnOffHDR |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.camera.wantsHDR = false; |
}]; |
} |
- (void)turnOffSoftShadows |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.light.shadowSampleCount = MIN(child.light.shadowSampleCount, 1); |
}]; |
} |
- (void)turnOffCascadeShadows |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.light.shadowCascadeCount = 0; |
child.light.shadowMapSize = CGSizeMake(1024, 1024); |
}]; |
} |
- (void)turnOffPostProcess |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.camera.bloomIntensity = 0; |
child.camera.wantsExposureAdaptation = NO; |
child.camera.vignettingIntensity = 0; |
}]; |
} |
- (void)turnOffShadow |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.light.castsShadow = NO; |
}]; |
} |
- (void)turnOffOverlays |
{ |
self.sceneRenderer.overlaySKScene = nil; |
} |
- (void)turnOffDiffuse |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
for(SCNMaterial *material in child.geometry.materials){ |
// material.lightingModelName = SCNLightingModelBlinn; |
material.diffuse.contents = [SCNColor whiteColor]; |
} |
}]; |
} |
- (void)turnOffNormalMaps |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
for(SCNMaterial *material in child.geometry.materials){ |
material.normal.contents = [SCNColor blackColor]; |
} |
}]; |
} |
- (void)turnOffLightMaps |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
for(SCNMaterial *material in child.geometry.materials){ |
material.selfIllumination.contents = [SCNColor blackColor]; |
material.ambientOcclusion.contents = [SCNColor whiteColor]; |
} |
}]; |
} |
- (void)turnOffSpecularMaps |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
for(SCNMaterial *material in child.geometry.materials){ |
material.specular.contents = [SCNColor blackColor]; |
} |
}]; |
} |
- (void)turnOffVertexShaderModifiers |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
NSMutableDictionary *sm = [child.geometry.shaderModifiers mutableCopy]; |
[sm setValue:nil forKey:SCNShaderModifierEntryPointGeometry]; |
child.geometry.shaderModifiers = sm; |
for(SCNMaterial *material in child.geometry.materials){ |
NSMutableDictionary *sm = [material.shaderModifiers mutableCopy]; |
[sm setValue:nil forKey:SCNShaderModifierEntryPointGeometry]; |
material.shaderModifiers = sm; |
} |
}]; |
} |
- (void)turnOffLighting |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
if(child.light && child.light.type != SCNLightTypeAmbient) |
child.hidden = YES; |
}]; |
} |
- (void)turnOffEXRForMaterialProperty:(SCNMaterialProperty *)property |
{ |
if([property.contents isKindOfClass:NSString.class]){ |
NSString *path = property.contents; |
if([path.pathExtension isEqualToString:@"exr"]){ |
path = [[path stringByDeletingPathExtension] stringByAppendingPathExtension:@"png"]; |
property.contents = path; |
} |
} |
} |
- (void)turnOffEXR |
{ |
[self turnOffEXRForMaterialProperty:self.scene.background]; |
[self turnOffEXRForMaterialProperty:self.scene.lightingEnvironment]; |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
for(SCNMaterial *material in child.geometry.materials){ |
[self turnOffEXRForMaterialProperty:material.selfIllumination]; |
} |
}]; |
} |
- (void)turnOffShaderModifiers |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
child.geometry.shaderModifiers = nil; |
for(SCNMaterial *material in child.geometry.materials){ |
material.shaderModifiers = nil; |
} |
}]; |
} |
- (void)turnOffVegetation |
{ |
[self.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) { |
if ([child.geometry.firstMaterial.name hasPrefix:@"plante"]){ |
child.hidden = YES; |
} |
}]; |
} |
- (void)configureRenderingQuality:(SCNView*)scnView |
{ |
BOOL isLowPowerDevice = false; |
#if TARGET_OS_TV |
isLowPowerDevice = YES; |
#elif !TARGET_OS_IPHONE |
isLowPowerDevice = [scnView.device isLowPower]; |
#endif |
if(isLowPowerDevice){ |
[self turnOffEXR]; //tvOS doesn't support exr maps |
[self turnOffNormalMaps]; |
[self turnOffSpecularMaps]; |
[self turnOffHDR]; |
[self turnOffDepthOfField]; |
[self turnOffSoftShadows]; |
[self turnOffPostProcess]; |
[self turnOffOverlays]; |
[self turnOffVertexShaderModifiers]; |
[self turnOffVegetation]; |
scnView.preferredFramesPerSecond = 30.0; |
} |
} |
#pragma mark - Debug menu |
- (void)fStopChanged:(CGFloat)value |
{ |
self.sceneRenderer.pointOfView.camera.fStop = value; |
} |
- (void)focusDistanceChanged:(CGFloat)value |
{ |
self.sceneRenderer.pointOfView.camera.focusDistance = value; |
} |
- (void)debugMenuSelectCameraAtIndex:(NSUInteger)index |
{ |
if(index == 0) |
{ |
SCNNode *key = [self.scene.rootNode childNodeWithName:@"key" recursively:YES]; |
key.opacity = 1.0; |
} |
[self setActiveCamera:[NSString stringWithFormat:@"CameraDof%lu",(unsigned long)index]]; |
} |
#pragma mark - GameController |
- (void)handleControllerDidConnectNotification:(NSNotification*)notification { |
if (_gamePadCurrent) |
return; |
GCController *gameController = notification.object; |
[self registerGameController:gameController]; |
} |
- (void)handleControllerDidDisconnectNotification:(NSNotification*)notification { |
GCController *gameController = notification.object; |
if (gameController != _gamePadCurrent) |
return; |
[self unregisterGameController]; |
for (GCController* controller in [GCController controllers]) { |
if (gameController != controller && controller.extendedGamepad) |
[self registerGameController:controller]; |
} |
} |
- (void)registerGameController:(GCController *)gameController { |
GCControllerButtonInput *buttonA = nil; |
GCControllerButtonInput *buttonB = nil; |
if( gameController.extendedGamepad != nil ) { |
_gamePadLeft = gameController.extendedGamepad.leftThumbstick; |
_gamePadRight = gameController.extendedGamepad.rightThumbstick; |
buttonA = gameController.extendedGamepad.buttonA; |
buttonB = gameController.extendedGamepad.buttonB; |
} else if( gameController.gamepad != nil ) { |
_gamePadLeft = gameController.gamepad.dpad; |
buttonA = gameController.gamepad.buttonA; |
buttonB = gameController.gamepad.buttonB; |
} else if( gameController.microGamepad != nil ) { |
_gamePadLeft = gameController.microGamepad.dpad; |
buttonA = gameController.microGamepad.buttonA; |
buttonB = gameController.microGamepad.buttonX; |
} |
__weak AAPLGameController* weakController = self; |
[_gamePadLeft setValueChangedHandler:^(GCControllerDirectionPad * _Nonnull dpad, float xValue, float yValue) { |
[weakController setCharacterDirection:(vector_float2){xValue, -yValue}]; |
}]; |
[_gamePadRight setValueChangedHandler:^(GCControllerDirectionPad * _Nonnull dpad, float xValue, float yValue) { |
[weakController setCameraDirection:(vector_float2){-xValue, yValue}]; |
}]; |
[buttonA setValueChangedHandler:^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { |
[weakController controllerJump:pressed]; |
}]; |
[buttonB setValueChangedHandler:^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { |
if (pressed) { |
[weakController controllerAttack]; |
} |
}]; |
#if TARGET_OS_IOS |
if (_gamePadLeft && _gamePadRight) { |
[_overlay hideVirtualPad]; |
} |
#endif |
} |
- (void)unregisterGameController { |
_gamePadLeft = nil; |
_gamePadRight = nil; |
_gamePadCurrent = nil; |
#if TARGET_OS_IOS |
[_overlay showVirtualPad]; |
#endif |
} |
#if TARGET_OS_IOS |
#pragma mark - AAPLPadOverlayDelegate |
- (void)padOverlayVirtualStickInteractionDidStart:(AAPLPadOverlay*)padNode { |
if (padNode == _overlay.controlOverlay.leftPad) { |
[self setCharacterDirection:(vector_float2){padNode.stickPosition.x, -padNode.stickPosition.y}]; |
} |
if (padNode == _overlay.controlOverlay.rightPad) { |
[self setCameraDirection:(vector_float2){-padNode.stickPosition.x, padNode.stickPosition.y}]; |
} |
} |
- (void)padOverlayVirtualStickInteractionDidChange:(AAPLPadOverlay*)padNode { |
if (padNode == _overlay.controlOverlay.leftPad) { |
[self setCharacterDirection:(vector_float2){padNode.stickPosition.x, -padNode.stickPosition.y}]; |
} |
if (padNode == _overlay.controlOverlay.rightPad) { |
[self setCameraDirection:(vector_float2){-padNode.stickPosition.x, padNode.stickPosition.y}]; |
} |
} |
- (void)padOverlayVirtualStickInteractionDidEnd:(AAPLPadOverlay*)padNode { |
if (padNode == _overlay.controlOverlay.leftPad) { |
[self setCharacterDirection:(vector_float2){0, 0}]; |
} |
if (padNode == _overlay.controlOverlay.rightPad) { |
[self setCameraDirection:(vector_float2){0, 0}]; |
} |
} |
- (void)willPressButtonOverlay:(AAPLButtonOverlay*_Nonnull)button { |
if (button == _overlay.controlOverlay.buttonA) { |
[self controllerJump:YES]; |
} |
if (button == _overlay.controlOverlay.buttonB) { |
[self controllerAttack]; |
} |
} |
- (void)didPressButtonOverlay:(AAPLButtonOverlay*_Nonnull)button { |
if (button == _overlay.controlOverlay.buttonA) { |
[self controllerJump:NO]; |
} |
} |
#endif // TARGET_OS_IOS |
#if TARGET_OS_TV |
// Stubs for missing delegate callabacks |
- (void) padOverlayVirtualStickInteractionDidChange:(AAPLPadOverlay *)padNode { |
} |
- (void) padOverlayVirtualStickInteractionDidEnd:(AAPLPadOverlay *)padNode { |
} |
- (void) padOverlayVirtualStickInteractionDidStart:(AAPLPadOverlay *)padNode { |
} |
- (void) didPressButtonOverlay:(nonnull AAPLButtonOverlay *)button { |
} |
- (void) willPressButtonOverlay:(nonnull AAPLButtonOverlay *)button { |
} |
#endif |
@end |
Copyright © 2018 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2018-04-05