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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Multiplayer iPhone Game Hosted on Your Own Server Part 1
50 mins
Creating A Simple Server
The next step is to add some code to have our game connect to your own server.
But before you can do that, you’ll need a simple server to connect to, of course!
We’re going to use Python and the Twisted framework to create the server, just like Cesare showed you how to do in the socket based app tutorial. It’s one of the easiest ways to make a server – and let’s face it, Python rules!
You can run your server on a central server you own (I use Linode), or you can run it on your local machine for testing.
Wherever you choose to run the server, make sure you have Python and Twisted installed as discussed in Cesare’s tutorial.
Then create a new file named CatRaceServer.py, and add the following code:
from twisted.internet.protocol import Factory, Protocol
from twisted.internet import reactor
class CatRaceFactory(Factory):
def __init__(self):
self.protocol = CatRaceProtocol
class CatRaceProtocol(Protocol):
def log(self, message):
print message
def connectionMade(self):
self.log("Connection made")
def connectionLost(self, reason):
self.log("Connection lost: %s" % str(reason))
factory = CatRaceFactory()
reactor.listenTCP(1955, factory)
print "Cat Race server started"
reactor.run()
This is a bare-bones server that listens for connections on port 1955 and just prints out when someone connects or disconnects. Bonus points if you know why I chose 1955! :]
Then run your server with the following command:
python CatRaceServer.py
You should see “Cat Race server started” and it should just sit there. You can verify you can actually connect by opening a Terminal on your development machine and issuing the following command (but substitute your own domain name or IP address where your server is running):
~ rwenderlich$ telnet www.razeware.com 1955 Trying 74.207.227.16... Connected to razeware.com. Escape character is '^]'. ^] telnet> quit Connection closed.
On the other side, you should see output like this:
Cat Race server started Connection made Connection lost: [SNIP] Connection was closed cleanly.
If you have trouble connecting, make sure you don’t have a firewall blocking that port – and if you’re running locally, you might need to configure your router to allow you to access the listening port. However, this sort of configuration is outside of the scope of this tutorial!
Once you have it working, read on – time to add the code to have our app connect to the server!
Connecting To Your Server
To connect to the server, we’re going to create a socket connection to that port just like we did in Cesare’s tutorial.
This should be review, so let’s dive right in! Start by making the following changes to NetworkController.h:
// Add to the end of the NetworkState enum
NetworkStateConnectingToServer,
NetworkStateConnected,
// Modify @interface declaration to add NSStreamDelegate
@interface NetworkController : NSObject <NSStreamDelegate> {
// Inside @interface
NSInputStream *_inputStream;
NSOutputStream *_outputStream;
BOOL _inputOpened;
BOOL _outputOpened;
// After @interface
@property (retain) NSInputStream *inputStream;
@property (retain) NSOutputStream *outputStream;
@property (assign) BOOL inputOpened;
@property (assign) BOOL outputOpened;
This creates two new network states. NetworkStateConnectingToServer is when we’re not connected to the server but are trying, and NetworkStateConnected is when we’re successfully connected to the server.
We also create two instance variables/properties for the input and output stream we’ll use to read/write to the socket, and mark the NetworkController as implementing the NSStreamDelegate protocol, so we can receive callbacks for the streams.
Switch to NetworkController.m and make the following changes:
// Add to top of file
@synthesize inputStream = _inputStream;
@synthesize outputStream = _outputStream;
@synthesize inputOpened = _inputOpened;
@synthesize outputOpened = _outputOpened;
// Add after init method
#pragma mark - Server communication
- (void)connect {
[self setState:NetworkStateConnectingToServer];
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"www.razeware.com", 1955, &readStream, &writeStream);
_inputStream = (NSInputStream *)readStream;
_outputStream = (NSOutputStream *)writeStream;
[_inputStream setDelegate:self];
[_outputStream setDelegate:self];
[_inputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];
[_outputStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];
[_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_inputStream open];
[_outputStream open];
}
- (void)disconnect {
[self setState:NetworkStateConnectingToServer];
if (_inputStream != nil) {
self.inputStream.delegate = nil;
[_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_inputStream close];
self.inputStream = nil;
}
if (_outputStream != nil) {
self.outputStream.delegate = nil;
[self.outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.outputStream close];
self.outputStream = nil;
}
}
- (void)reconnect {
[self disconnect];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self connect];
});
}
The connect method is similar to the initNetworkCommunication method in Cesare’s tutorial, except it updates the state and sets a fancy variable called kCFStreamPropertyShouldCloseNativeSocket.
If you set kCFStreamPropertyShouldCloseNativeSocket to true like we do here, it means that when you close the stream it closes the socket too, which is exactly what we want to happen!
Also, don’t forget to replace the DNS name/IP and port in connect with the settings for your own server!
The disconnect method is a handy method that shuts down the input and output streams and puts us in a nice clean state.
Finally, the reconnect method disconnects, and then schedules the connect to be called in 5 seconds. The call to dispatch_after is a fancy trick that uses GCD to schedule a block of code to be run 5 seconds in the future. For more information on GCD, check out the Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.
Once you’re ready, add the following methods right after reconnect:
- (void)inputStreamHandleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted: {
NSLog(@"Opened input stream");
_inputOpened = YES;
if (_inputOpened && _outputOpened && _state == NetworkStateConnectingToServer) {
[self setState:NetworkStateConnected];
// TODO: Send message to server
}
}
case NSStreamEventHasBytesAvailable: {
if ([_inputStream hasBytesAvailable]) {
NSLog(@"Input stream has bytes...");
// TODO: Read bytes
}
} break;
case NSStreamEventHasSpaceAvailable: {
assert(NO); // should never happen for the input stream
} break;
case NSStreamEventErrorOccurred: {
NSLog(@"Stream open error, reconnecting");
[self reconnect];
} break;
case NSStreamEventEndEncountered: {
// ignore
} break;
default: {
assert(NO);
} break;
}
}
- (void)outputStreamHandleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted: {
NSLog(@"Opened output stream");
_outputOpened = YES;
if (_inputOpened && _outputOpened && _state == NetworkStateConnectingToServer) {
[self setState:NetworkStateConnected];
// TODO: Send message to server
}
} break;
case NSStreamEventHasBytesAvailable: {
assert(NO); // should never happen for the output stream
} break;
case NSStreamEventHasSpaceAvailable: {
NSLog(@"Ok to send");
// TODO: Write bytes
} break;
case NSStreamEventErrorOccurred: {
NSLog(@"Stream open error, reconnecting");
[self reconnect];
} break;
case NSStreamEventEndEncountered: {
// ignore
} break;
default: {
assert(NO);
} break;
}
}
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
dispatch_async(dispatch_get_main_queue(), ^(void) {
if (aStream == _inputStream) {
[self inputStreamHandleEvent:eventCode];
} else if (aStream == _outputStream) {
[self outputStreamHandleEvent:eventCode];
}
});
}
Starting at the bottom, the stream:handleEvent method gets called whenever an event happens on the input stream of the output stream, because we registered this class as the delegate of the streams.
One big gotcha here is that this method isn’t guaranteed to be called in the main thread. Because we’re going to be accessing some shared state in this method (instance variables on this class), we use GCD to run the handling code on the main thread.
Then we have the routines to handle the input and output stream events. Right now, they don’t really do that much except reconnect on an error, and handle the NSStreamEventOpenCompleted event.
The NSStreamEventOpenCompleted event is called when the sockets finish opening. So here we set a flag when each side has finished opening, and when both sides are connected we switch the state to connected.
Soon we’ll add some code to send an initial message over to the server when the client first connects, but now let’s try all this new code out.
Make these last changes to NetworkController.m:
// Add inside authenticationChanged, right after _userAuthenticated = TRUE
[self connect];
// Add inside authenticationChanged, right after _userAuthenticated = FALSE
[self disconnect];
This sets the code up to connect to the server after the user is authenticated with Game Center.
And finally, switch to HelloWorldLayer.m and add the following debug lines for the two new network states to the end of stateChanged:
case NetworkStateConnectingToServer:
debugLabel.string = @"Connecting to Server";
break;
case NetworkStateConnected:
debugLabel.string = @"Connected";
break;
Compile and run, and you should see your app successfully connect to your own server!