Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
Classes/RocketController.m
/* |
File: RocketController.m |
Abstract: Controls the logic, controls, networking, and view of the actual game. |
Version: 1.0 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2010 Apple Inc. All Rights Reserved. |
*/ |
#import "RocketController.h" |
#import <GameKit/GKVoiceChatService.h> |
#define kGravity 2.0 |
#define kRestitution 0.99 |
#define kBallSpeed 4.0 |
#define kMaxV 35.0 |
#define kMinV 18.0 |
#define kWorldX 320.0 |
#define kWorldY 320.0 |
#define kBorder 10.0 |
#define kStartY kWorldY/3.0 |
#define kOffscreen 3.0 |
#define kThrustOffset 60.0 |
#define kThrustRatio 12.0 |
#define kTimestep 0.033 |
#define randomV() -2.0+4.0*(Float32)random()/(Float32)RAND_MAX |
typedef struct { |
CFSwappedFloat32 playerY; |
CFSwappedFloat32 playerV; |
CFSwappedFloat32 ballY; |
CFSwappedFloat32 ballV; |
} Packet; |
@implementation RocketController |
@synthesize stateLabel; |
@synthesize playerScoreLabel; |
@synthesize enemyScoreLabel; |
@synthesize touchLabel; |
#pragma mark View Controller Methods |
/* The designated initializer. Override if you create the controller |
programmatically and want to perform customization that is not appropriate |
for viewDidLoad. */ |
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil manager:(SessionManager *)aManager |
{ |
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { |
manager = [aManager retain]; |
manager.gameDelegate = self; |
srandomdev(); |
} |
return self; |
} |
- (void)viewDidLoad |
{ |
[super viewDidLoad]; |
self.title = [manager displayNameForPeer:manager.currentConfPeerID]; |
UIBarButtonItem *endButton = [[UIBarButtonItem alloc] |
initWithTitle:@"End Game" |
style:UIBarButtonItemStylePlain |
target:self |
action:@selector(endButtonHit)]; |
self.navigationItem.leftBarButtonItem = endButton; |
[endButton release]; |
self.navigationItem.hidesBackButton = YES; |
stateLabel.text = @"Connecting..."; |
rocketStart = FALSE; |
playerTalking = FALSE; |
enemyTalking = FALSE; |
playerScore = 0; |
enemyScore = 0; |
// Create the game graphics view |
CGRect rect = CGRectMake(0.0, 0.0, kWorldX, kWorldY); |
rocketView = [[RocketView alloc] initWithFrame:rect]; |
[self.view addSubview:rocketView]; |
player.bounds.size = CGSizeMake(kSize,kSize*kRocketHeightRatio); |
enemy.bounds.size = CGSizeMake(kSize,kSize*kRocketHeightRatio); |
ball.bounds.size = CGSizeMake(kSize,kSize); |
[self resetRockets:FALSE initialVelocity:0.0]; |
[rocketView updatePlayer:player.bounds enemy:enemy.bounds ball:ball.bounds |
playerThrust:player.thrust enemyThrust:enemy.thrust]; |
} |
- (void)viewDidUnload |
{ |
self.stateLabel = nil; |
self.playerScoreLabel = nil; |
self.enemyScoreLabel = nil; |
self.touchLabel = nil; |
[rocketView release]; |
rocketView = nil; |
} |
- (void)didReceiveMemoryWarning |
{ |
// Releases the view if it doesn't have a superview. |
[super didReceiveMemoryWarning]; |
// Release any cached data, images, etc that aren't in use. |
} |
- (void)dealloc |
{ |
[rocketView release]; |
rocketView = nil; |
manager.gameDelegate = nil; |
[manager release]; |
manager = nil; |
[super dealloc]; |
} |
#pragma mark - |
#pragma mark Connection and Timer Logic |
// Update the call timer once a second. |
- (void) updateElapsedTime:(NSTimer *) timer |
{ |
int hour, minute, second; |
NSTimeInterval elapsedTime = [NSDate timeIntervalSinceReferenceDate] - startTime; |
hour = elapsedTime / 3600; |
minute = (elapsedTime - hour * 3600) / 60; |
second = (elapsedTime - hour * 3600 - minute * 60); |
callTimerLabel.text = [NSString stringWithFormat:@"%2d:%02d:%02d", hour, minute, second]; |
} |
// Called when the user hits the end call toolbar button. |
-(void) endButtonHit |
{ |
[manager disconnectCurrentCall]; |
} |
#pragma mark - |
#pragma mark SessionManagerGameDelegate Methods |
- (void) voiceChatWillStart:(SessionManager *)session |
{ |
stateLabel.text = @"Starting Voice Chat"; |
} |
- (void) session:(SessionManager *)session didConnectAsInitiator:(BOOL)shouldStart |
{ |
[[UIApplication sharedApplication] setIdleTimerDisabled:YES]; |
stateLabel.text = @"Connected"; |
// Schedule the game to update at 30fps and the call timer at 1fps. |
if (nil == callTimer) { |
callTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0 target:self |
selector:@selector(updateElapsedTime:) userInfo:nil repeats:YES] retain]; |
rocketTimer = [[NSTimer scheduledTimerWithTimeInterval:kTimestep target:self |
selector:@selector(updateRockets:) userInfo:nil repeats:YES] retain]; |
startTime = [NSDate timeIntervalSinceReferenceDate]; |
// If the user is starting the voice chat, let the other player be the one |
// who starts the game. That way both players are starting at the same time. |
if (shouldStart) { |
[self resetRockets:TRUE initialVelocity:randomV()]; |
[self sendPacket:PacketTypeStart]; |
rocketStart = TRUE; |
} |
} |
} |
// If hit end call or the call failed or timed out, clear the state and go back a screen. |
- (void) willDisconnect:(SessionManager *)session |
{ |
stateLabel.text = @"Disconnected"; |
rocketStart = FALSE; |
playerTalking = FALSE; |
enemyTalking = FALSE; |
[callTimer invalidate]; |
[rocketTimer invalidate]; |
[callTimer release]; |
[rocketTimer release]; |
callTimer = nil; |
rocketTimer = nil; |
[rocketView release]; |
rocketView = nil; |
[[UIApplication sharedApplication] setIdleTimerDisabled:NO]; |
self.navigationController.navigationBar.barStyle = UIBarStyleDefault; |
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault]; |
[self.navigationController popViewControllerAnimated:YES]; |
manager.gameDelegate = nil; |
[manager release]; |
manager = nil; |
} |
// The GKSession got a packet and sent it to the game, so parse it and update state. |
- (void) session:(SessionManager *)session didReceivePacket:(NSData*)data ofType:(PacketType)packetType |
{ |
Packet incoming; |
if ([data length] == sizeof(Packet)) { |
[data getBytes:&incoming length:sizeof(Packet)]; |
switch (packetType) { |
case PacketTypeStart: |
// The other player started the game, so unpause our graphics |
// and set the initial ball velocity. |
rocketStart = TRUE; |
[self resetRockets:FALSE initialVelocity:CFConvertFloat32SwappedToHost(incoming.ballV)]; |
break; |
case PacketTypeBounce: |
// The other player hit the ball back towards us. |
enemy.bounds.origin.y = CFConvertFloat32SwappedToHost(incoming.playerY); |
enemy.velocity = CFConvertFloat32SwappedToHost(incoming.playerV); |
ball.velocity.y = CFConvertFloat32SwappedToHost(incoming.ballV); |
ball.velocity.x = -kBallSpeed; |
ball.bounds.origin.y = CFConvertFloat32SwappedToHost(incoming.ballY); |
ball.bounds.origin.x = kWorldX-kSize*2.0-kBorder; |
break; |
case PacketTypeScore: |
// The other player missed, we scored, and the ball is being served |
// towards us. |
[self resetRockets:FALSE initialVelocity:CFConvertFloat32SwappedToHost(incoming.ballV)]; |
playerScore++; |
playerScoreLabel.text = [NSString stringWithFormat:@"%u",(unsigned int)playerScore]; |
break; |
case PacketTypeTalking: |
// The other player is speaking and doesn't want to thrust. |
enemyTalking = YES; |
enemy.thrust = 0.0; |
break; |
case PacketTypeEndTalking: |
// The other player is ready to play again. |
enemyTalking = NO; |
break; |
default: |
break; |
} |
} |
} |
#pragma mark - |
#pragma mark Game Network Logic |
// Send the same information each time, just with a different header |
-(void) sendPacket:(PacketType)packetType |
{ |
Packet outgoing; |
outgoing.playerY = CFConvertFloat32HostToSwapped(player.bounds.origin.y); |
outgoing.playerV = CFConvertFloat32HostToSwapped(player.velocity); |
outgoing.ballY = CFConvertFloat32HostToSwapped(ball.bounds.origin.y); |
outgoing.ballV = CFConvertFloat32HostToSwapped(ball.velocity.y); |
NSData *packet = [[NSData alloc] initWithBytes:&outgoing length:sizeof(Packet)]; |
[manager sendPacket:packet ofType:packetType]; |
[packet release]; |
} |
#pragma mark Game Control and Graphics Logic |
// The bulk of the game logic. moves objects around and sends packets if necessary. |
-(void) updateRockets:(NSTimer*)timer |
{ |
// If the game hasn't started, the graphics will just be stationary. |
if (rocketStart) { |
// Allow for frameskipping in case a timer is delayed. |
NSTimeInterval tempTimestamp = [NSDate timeIntervalSinceReferenceDate]; |
int timesteps = (int)((tempTimestamp-frameskipTimestamp)/kTimestep); |
frameskipTimestamp = tempTimestamp; |
timesteps = ((timesteps < 1) ? 1 : timesteps); |
while (timesteps--) { |
// Apply acceleration to velocity and velocity to position. |
[self moveRockets]; |
[self moveBall]; |
// Check if the rockets or ball are hitting the ceiling or floor. |
[self collideRocketsWithWorld]; |
[self collideBallWithWorld]; |
// Check if the ball hit a rocket or scored. |
[self checkScoring]; |
} |
// Tell the graphics to update. |
[rocketView updatePlayer:player.bounds enemy:enemy.bounds ball:ball.bounds |
playerThrust:player.thrust enemyThrust:enemy.thrust]; |
} |
} |
// Apply acceleration to velocity and velocity to position. |
-(void) moveRockets |
{ |
player.velocity += kGravity; |
enemy.velocity += kGravity; |
// Add the acceleration of the voice thrust to the rockets if the player |
// is not touching the screen. |
if (!playerTalking) { |
float level = [[GKVoiceChatService defaultVoiceChatService] inputMeterLevel]; |
// The decibel level may be 0.0 if there is no audio |
// otherwise its a negative number, with closer to 0.0 being louder |
player.thrust = (level == 0.0) ? 0.0 : (level+kThrustOffset)/kThrustRatio; |
player.velocity -= player.thrust; |
} |
if (!enemyTalking) { |
float level = [[GKVoiceChatService defaultVoiceChatService] outputMeterLevel]; |
enemy.thrust = (level == 0.0) ? 0.0 : (level+kThrustOffset)/kThrustRatio; |
enemy.velocity -= enemy.thrust; |
} |
// clamp the rocket speeds. |
player.velocity = player.velocity > kMaxV ? kMaxV : (player.velocity < -kMaxV ? -kMaxV : player.velocity); |
enemy.velocity = enemy.velocity > kMaxV ? kMaxV : (enemy.velocity < -kMaxV ? -kMaxV : enemy.velocity); |
// Apply a time step of velocity to the object positions |
player.bounds.origin.y += player.velocity; |
enemy.bounds.origin.y += enemy.velocity; |
} |
// Apply acceleration to velocity and velocity to position. |
-(void) moveBall |
{ |
ball.velocity.y += kGravity; |
// clamp the ball's velocity to keep the game playable. |
ball.velocity.y = ((ball.velocity.y > kMaxV) ? kMaxV : ((ball.velocity.y < -kMaxV) ? -kMaxV : ball.velocity.y)); |
ball.bounds.origin.y += ball.velocity.y; |
ball.bounds.origin.x += ball.velocity.x; |
} |
// Check if the rockets are hitting the ceiling or floor. |
-(void) collideRocketsWithWorld |
{ |
// Keep the objects from shooting off the top or bottom of the screen. |
if (player.bounds.origin.y < 0.0) { |
player.bounds.origin.y = 0.0; |
player.velocity = 0.0; |
} else if (player.bounds.origin.y > kWorldY-kSize*kRocketHeightRatio) { |
player.bounds.origin.y = kWorldX-kSize*kRocketHeightRatio; |
player.velocity = 0.0; |
} |
if (enemy.bounds.origin.y < 0.0) { |
enemy.bounds.origin.y = 0.0; |
enemy.velocity = 0.0; |
} else if (enemy.bounds.origin.y > kWorldY-kSize*kRocketHeightRatio) { |
enemy.bounds.origin.y = kWorldX-kSize*kRocketHeightRatio; |
enemy.velocity = 0.0; |
} |
} |
// Check if the ball is hitting the ceiling or floor. |
-(void) collideBallWithWorld |
{ |
// Note that the ball loses a little energy with every bounce. |
if (ball.bounds.origin.y < 0.0) { |
ball.velocity.y = -ball.velocity.y*kRestitution; |
} else if (ball.bounds.origin.y > kWorldY-kSize) { |
ball.bounds.origin.y = kWorldY-kSize; |
ball.velocity.y = -ball.velocity.y*kRestitution; |
// Make sure there is some bounce, and it isn't just rolling. |
ball.velocity.y = ball.velocity.y > -kMinV ? -kMinV : ball.velocity.y; |
} |
} |
// Check if the ball hit a rocket or scored. |
-(void) checkScoring |
{ |
// In range to check for collision or scoring. |
if (ball.bounds.origin.x < kSize+kBorder) { |
// If there is collision, send the bounce packet. |
if (CGRectIntersectsRect(player.bounds, ball.bounds)) { |
ball.velocity.x = kBallSpeed; |
ball.velocity.y += player.velocity; |
ball.bounds.origin.x = kSize+kBorder; |
[self sendPacket:PacketTypeBounce]; |
// If the ball goes far enough, its a goal, reset and send a |
// score packet. |
} else if (ball.bounds.origin.x < kBorder+kSize/2.0) { |
[self resetRockets:TRUE initialVelocity:randomV()]; |
[self sendPacket:PacketTypeScore]; |
enemyScore++; |
enemyScoreLabel.text = [NSString stringWithFormat:@"%d",enemyScore]; |
} |
} else if (ball.bounds.origin.x > kWorldX*kOffscreen) { |
// If the ball gets way off screen, it means we stopped getting |
// packets due to a network problem, so disconnect. |
[manager disconnectCurrentCall]; |
} |
} |
// Set the rockets and the ball to an initial position. |
-(void) resetRockets:(BOOL)serve initialVelocity:(Float32)vel |
{ |
[rocketView newBall]; |
player.bounds.origin = CGPointMake(kBorder,kWorldY-kRocketHeightRatio*kSize); |
player.velocity = 0.0; |
player.thrust = 0.0; |
enemy.bounds.origin = CGPointMake(kWorldX-kBorder-kSize,kWorldY-kRocketHeightRatio*kSize); |
enemy.velocity = 0.0; |
enemy.thrust = 0.0; |
if (serve) { |
ball.bounds.origin = CGPointMake(kSize+kBorder,kStartY); |
ball.velocity = CGPointMake(kBallSpeed,vel); |
} else { |
ball.bounds.origin = CGPointMake(kWorldX-2.0*kSize-kBorder,kStartY); |
ball.velocity = CGPointMake(-kBallSpeed,vel); |
} |
frameskipTimestamp = [NSDate timeIntervalSinceReferenceDate]; |
} |
// Stop thrusting the rocket when the user touches the screen, allowing them |
// to talk without making the game impossible to play. |
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event |
{ |
playerTalking = YES; |
player.thrust = 0.0; |
touchLabel.text = @"Untouch screen to resume rocketing."; |
[self sendPacket:PacketTypeTalking]; |
} |
// Resume thrusting the rocket. |
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event |
{ |
playerTalking = NO; |
touchLabel.text = @"Touch screen to speak without rocketing."; |
[self sendPacket:PacketTypeEndTalking]; |
} |
// Same as touchesEnded. |
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event |
{ |
[self touchesEnded:touches withEvent:event]; |
} |
@end |
Copyright © 2011 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2011-03-15