How To Make a Multiplayer iPhone Game Hosted on Your Own Server Part 1

A while back, I wrote a tutorial on How To Make A Simple Multiplayer Game with Game Center. That tutorial showed you how to use Game Center to create a peer-to-peer match. This means that packets of game data were sent directly between the connected devies, and there was no central game server. However, sometimes […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 5 of 6 of this article. Click here to view the first page.

Reading Socket Data

Just like you saw in the Python script, when you receive a NSStreamEventHasBytesAvailable you don't know how many bytes you'll receive. It might be exactly the length of one message, but it could be shorter or greater than a message also.

So we need to add some code very similar to what was in the Python script, to add the data to an input buffer, and process it only when a complete message has arrived.

Let's see what this looks like in code. Make the following changes to NetworkController.h:

// Inside @interface
NSMutableData *_inputBuffer;

// After @interface
@property (retain) NSMutableData *inputBuffer;

This creates a data buffer for the input buffer as mentioned above.

Switch to NetworkController.m and make the following changes:

// Add in synthesize section
@synthesize inputBuffer = _inputBuffer;

// Add at top of connect
self.inputBuffer = [NSMutableData data];

// Add in disconnect, at end of _inputStream != nil case:
self.inputBuffer = nil;

// Add placeholder method right after init
- (void)processMessage:(NSData *)data {
    // TODO: Process message
    NSLog(@"w00t got a message!  Implement me!");
}

// Add right above inputStreamHandleEvent
- (void)checkForMessages {
    while (true) {
        if (_inputBuffer.length < sizeof(int)) {
            return;
        }
        
        int msgLength = *((int *) _inputBuffer.bytes);
        msgLength = ntohl(msgLength);
        if (_inputBuffer.length < msgLength) {
            return;
        }
        
        NSData * message = [_inputBuffer subdataWithRange:NSMakeRange(4, msgLength)];
        [self processMessage:message];
        
        int amtRemaining = _inputBuffer.length - msgLength - sizeof(int);
        if (amtRemaining == 0) {
            self.inputBuffer = [[[NSMutableData alloc] init] autorelease];
        } else {
            NSLog(@"Creating input buffer of length %d", amtRemaining);
            self.inputBuffer = [[[NSMutableData alloc] initWithBytes:_inputBuffer.bytes+4+msgLength length:amtRemaining] autorelease];
        }        
        
    }
}

// In inputStreamHandleEvent, NSStreamEventHasBytesAvailable case, right after "Input stream has bytes..."
NSInteger       bytesRead;
uint8_t         buffer[32768];

bytesRead = [self.inputStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1) {
    NSLog(@"Network read error");
} else if (bytesRead == 0) {
    NSLog(@"No data read, reconnecting");
    [self reconnect];
} else {                
    NSLog(@"Read %d bytes", bytesRead);
    [_inputBuffer appendData:[NSData dataWithBytes:buffer length:bytesRead]];
    [self checkForMessages];
}

Let's start at the bottom.

When the NSStreamEventHasBytesAvailable comes in, we read some random bytes that have come from the other side.
So we take whatever's come, append them onto the input buffer, and call another method to check if we have any full messages yet.

The checkForMessages method makes sure there's at least 4 bytes in the input buffer (for the length). If there is, it reads out the first four bytes and stores them into an integer we call msgLength (converted to host byte order).

We make sure we have that amount of data in the input buffer, and if so make a data buffer with that sub-range. We then pass it to a processMessage method for futher processing.

That's it - compile and run your code and restart the server, and you should see some output like this in your game's console log:

[SNIP] Input stream has bytes...
[SNIP] Read 5 bytes
[SNIP] w00t got a message!  Implement me!

Unmarshalling Data

Now that we have a buffer of data, we have to unmarshal it. Let's write a helper class for this called MessageReader. It will be just like the Python version!

Go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class MessageReader.m, and click Save.

Replace MessageReader.h with the following:

#import <Foundation/Foundation.h>

@interface MessageReader : NSObject {
    NSData * _data;
    int _offset;
}

- (id)initWithData:(NSData *)data;

- (unsigned char)readByte;
- (int)readInt;
- (NSString *)readString;

@end

This keeps track of the data buffer we're reading and the current offset where we're reading from, and some helper methods to read various types of data.

Switch to MessageReader.m and replace the contents with the following:

#import "MessageReader.h"

@implementation MessageReader

- (id)initWithData:(NSData *)data {
    if ((self = [super init])) {
        _data = [data retain];
        _offset = 0;
    }
    return self;
}

- (unsigned char)readByte {
    unsigned char retval = *((unsigned char *) (_data.bytes + _offset));
    _offset += sizeof(unsigned char);
    return retval;
}

- (int)readInt {
    int retval = *((unsigned int *) (_data.bytes + _offset));
    retval = ntohl(retval);
    _offset += sizeof(unsigned int);
    return retval;
}

- (NSString *)readString {
    int strLen = [self readInt];
    NSString *retval = [NSString stringWithCString:_data.bytes+_offset encoding:NSUTF8StringEncoding];
    _offset += strLen;
    return retval;
    
}

- (void)dealloc {
    [_data release];
    [super dealloc];
}

@end

You should understand how this works pretty well by now, so I'm not going to discuss this further here.

Now, we're going to use this to parse the MessageNotInMatch message. Once we recieve this, we'll bring up the Game Center matchmaker view controller and let it try to find a match for us. Once it finds a match, we'll just log out the player IDs it finds.

Start by opening NetworkController.h and make the following changes:

// Add final three network states
NetworkStatePendingMatch,
NetworkStatePendingMatchStart,
NetworkStateMatchActive,

// Add inside NetworkControllerDelegate
- (void)setNotInMatch;

// Modify @interface to add GKMatchmakerViewController protocol
@interface NetworkController : NSObject <NSStreamDelegate, GKMatchmakerViewControllerDelegate> {

// Inside @interface
UIViewController *_presentingViewController;
GKMatchmakerViewController *_mmvc;

// After @interface
@property (retain) UIViewController *presentingViewController;
@property (retain) GKMatchmakerViewController *mmvc;

// After @properties
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers 
                 viewController:(UIViewController *)viewController;

Here we add the final three network states. NetworkStatePendingMatch means we're waiting for the Game Center matchmaker to do its thing, NetworkStatePendingMatchStart means we're waiting for the server to actually start the match, and NetworkStateMathActive means the game is on!

We also set the NetworkController as implementing the GKMatchmakerViewControllerDelegate and create a few instance variables/properties it needs. We also create a method the layer will use when it's ready to look for a match.

Next switch to NetworkController.m and make the following changes:

// Add to top of file
#import "MessageReader.h"

// In @synthesize section
@synthesize presentingViewController = _presentingViewController;
@synthesize mmvc = _mmvc;

// Add right after setState:
- (void)dismissMatchmaker {
    [_presentingViewController dismissModalViewControllerAnimated:YES];
    self.mmvc = nil;
    self.presentingViewController = nil;
}

// Replace processMessage with the following
- (void)processMessage:(NSData *)data {
    MessageReader * reader = [[[MessageReader alloc] initWithData:data] autorelease];
    
    unsigned char msgType = [reader readByte];
    if (msgType == MessageNotInMatch) {
        [self setState:NetworkStateReceivedMatchStatus];
        [_delegate setNotInMatch];
    }
}

// Add code to bottom of file
#pragma mark - Matchmaking

- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers 
                 viewController:(UIViewController *)viewController {
    
    if (!_gameCenterAvailable) return;
    
    [self setState:NetworkStatePendingMatch];
    
    self.presentingViewController = viewController;
    [_presentingViewController dismissModalViewControllerAnimated:NO];
    
    if (FALSE) {
        
        // TODO: Will add code here later!
        
    } else {
        GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease]; 
        request.minPlayers = minPlayers;     
        request.maxPlayers = maxPlayers;
        
        self.mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];    
        _mmvc.hosted = YES;
        _mmvc.matchmakerDelegate = self;
        
        [_presentingViewController presentModalViewController:_mmvc animated:YES];
    }
}

// The user has cancelled matchmaking
- (void)matchmakerViewControllerWasCancelled:(GKMatchmakerViewController *)viewController {
    NSLog(@"matchmakerViewControllerWasCancelled");
    [self dismissMatchmaker];
}

// Matchmaking has failed with an error
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFailWithError:(NSError *)error {
    NSLog(@"didFailWithError: %@", error.localizedDescription);
    [self dismissMatchmaker];
}

// Players have been found for a server-hosted game, the game should start
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindPlayers:(NSArray *)playerIDs {
    NSLog(@"didFindPlayers");
    for (NSString *playerID in playerIDs) {
        NSLog(@"%@", playerID);
    }        
    if (_state == NetworkStatePendingMatch) {
        [self dismissMatchmaker]; 
        // TODO: Send message to server to start match, with given player Ids
    }
}

// An invited player has accepted a hosted invite.  Apps should connect through the hosting server and then update the player's connected state (using setConnected:forHostedPlayer:)
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didReceiveAcceptFromHostedPlayer:(NSString *)playerID __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0) {
    NSLog(@"didReceiveAcceptFromHostedPlayer");    
}

There's a lot of code here, but most of this is review from the previous Game Center tutorial.

The biggest significant new piece is we're implementing the processMessage method to use the new MessageReader to parse the first byte to figure out the message type. If it's the MessageNotInMatch method, we notify the delegate (the HelloWorldLayer in this case).

Before we switch to the HelloWorldLayer, we need to make a property for the RootViewController in the AppDelegate, because this is needed to present the matchmaker view controller. So switch to AppDelegate.h and add the property:

@property (nonatomic, retain) RootViewController * viewController;

Then switch to AppDelegate.m and synthesize it:

@synthesize viewController;

Finally switch to HelloWorldLayer.m and make the following changes:

// Add to top of file
#import "AppDelegate.h"
#import "NetworkController.h"

// Add final cases to stateChanged
case NetworkStatePendingMatch:
    debugLabel.string = @"Pending Match";
    break;
case NetworkStateMatchActive:
    debugLabel.string = @"Match Active";
    break;   
case NetworkStatePendingMatchStart:
    debugLabel.string = @"Pending Start";
    break;                  

// Add right above dealloc
- (void)setNotInMatch {
    AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;                
    [[NetworkController sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController];
}

The most important part here is setNotInMatch - when this is called, we tell the network controller to look for a match.

Phew - finally done! Compile and run, and you'll see the matchmaker GUI show up:

Displaying the GKMatchmakerViewController

If you run your game on the simulator and a device at the same time, you can actually use it to look for an auto-match, and once it finds a match you'll see the player IDs it matched up logged out:

CatRace[5407:707] didFindPlayers
CatRace[5407:707] G:1036727375
CatRace[5407:707] G:1417937643

Allright, we have end-to-end communication! We send and received a message on our game, and also sent and received a message from our server. It will be a cakewalk from here!

Well, let's face it - as much of a cakewalk as any networked game can be :P

Contributors

Over 300 content creators. Join our team.