How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 3
This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on Google+ and Twitter. Welcome back to our monster 7-part tutorial series on creating a multiplayer card game over Bluetooth or Wi-Fi using UIKit! If you are new to this series, check out the […] By Matthijs Hollemans.
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 Simple Playing Card Game with Multiplayer and Bluetooth, Part 3
55 mins
The Game Class
As I mentioned before, the Game class is the main data model object for the game. It also handles incoming network packets from GKSession. You’re going to build a very basic version of this class to get the game started. Throughout the rest of this tutorial, you’ll expand the Game and GameViewController classes until Snap! is fully complete.
Add a new Objective-C class to the project, subclass of NSObject, named Game. I suggest you place the Game.h and Game.h source files in a new group called “Data Model.” Replace the contents of Game.h with:
@class Game;
@protocol GameDelegate <NSObject>
- (void)game:(Game *)game didQuitWithReason:(QuitReason)reason;
@end
@interface Game : NSObject <GKSessionDelegate>
@property (nonatomic, weak) id <GameDelegate> delegate;
@property (nonatomic, assign) BOOL isServer;
- (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID;
- (void)quitGameWithReason:(QuitReason)reason;
@end
Here you declare the GameDelegate protocol, which you’ve already seen a bit about (GameViewController plays that role), and the Game class. So far you’ve only added the startClientGameWithSession… method to the class, and a delegate and isServer property.
Replace the contents of Game.m with:
#import "Game.h"
typedef enum
{
GameStateWaitingForSignIn,
GameStateWaitingForReady,
GameStateDealing,
GameStatePlaying,
GameStateGameOver,
GameStateQuitting,
}
GameState;
@implementation Game
{
GameState _state;
GKSession *_session;
NSString *_serverPeerID;
NSString *_localPlayerName;
}
@synthesize delegate = _delegate;
@synthesize isServer = _isServer;
- (void)dealloc
{
#ifdef DEBUG
NSLog(@"dealloc %@", self);
#endif
}
#pragma mark - Game Logic
- (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID
{
}
- (void)quitGameWithReason:(QuitReason)reason
{
}
#pragma mark - GKSessionDelegate
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
#ifdef DEBUG
NSLog(@"Game: peer %@ changed state %d", peerID, state);
#endif
}
- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
#ifdef DEBUG
NSLog(@"Game: connection request from peer %@", peerID);
#endif
[session denyConnectionFromPeer:peerID];
}
- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
#ifdef DEBUG
NSLog(@"Game: connection with peer %@ failed %@", peerID, error);
#endif
// Not used.
}
- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
#ifdef DEBUG
NSLog(@"Game: session failed %@", error);
#endif
}
#pragma mark - GKSession Data Receive Handler
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context
{
#ifdef DEBUG
NSLog(@"Game: receive data from peer: %@, data: %@, length: %d", peerID, data, [data length]);
#endif
}
@end
This is the bare minimum you need to do to get the app compiling again. None of the methods from the Game class do anything useful yet. Also notice you’ve declared a new enum, GameState, that contains the different states that Game can occupy. More about the Game’s state machine later.
Run the app again. When you connect the client to the server, you’ll briefly see the “Connecting…” message (for as long as it takes to set up the connection to the server, which is usually only a fraction of a second), and then the app switches to the game screen. You won’t notice much difference because the layout stays mostly the same (on purpose), except that the main label now says “Center Label,” and is white instead of green.
Also notice that the client disappears from the server’s table view. This happens because on the client side, the JoinViewController closes, which deallocates the MatchmakingClient object. That object is the only one holding on to the GKSession object, so that gets deallocated as well, and the connection is immediately broken as soon as you leave the Join Game screen. (You can verify this in the server’s debug output; the state of the client peer becomes 3, which is GKPeerStateDisconnected.)
You’ll fix this right now.
Replace Game.m’s startClientGameWithSession… method with:
- (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID
{
self.isServer = NO;
_session = session;
_session.available = NO;
_session.delegate = self;
[_session setDataReceiveHandler:self withContext:nil];
_serverPeerID = peerID;
_localPlayerName = name;
_state = GameStateWaitingForSignIn;
[self.delegate gameWaitingForServerReady:self];
}
This method takes control of the GKSession object and makes “self,” i.e. the Game object, become the new GKSessionDelegate as well as the data-receive-handler. (You’ve already added those delegate methods, but they are currently empty.)
You copy the server’s peer ID and the player’s name into your own instance variables, and then set the game state to “waiting for sign-in.” This means the client will now wait for a specific message from the server. Finally, you tell the GameDelegate that you’re ready for the game to start. That employs a new delegate method, so add that to Game.h in the GameDelegate @protocol:
- (void)gameWaitingForServerReady:(Game *)game;
The delegate for Game is the GameViewController, so you should implement this method there:
- (void)gameWaitingForServerReady:(Game *)game
{
self.centerLabel.text = NSLocalizedString(@"Waiting for game to start...", @"Status text: waiting for server");
}
That’s all it does. It replaces the text of the center label with “Waiting for game to start…”. Run the app again. After connecting the client, you should see this:
Because you’re now keeping the GKSession object alive after the Join Game screen closes, the table view of the server should still show the client’s device name.
There’s not much to do yet for the client at this point, but you can at least make the exit button work. Replace the following method in Game.m:
- (void)quitGameWithReason:(QuitReason)reason
{
_state = GameStateQuitting;
[_session disconnectFromAllPeers];
_session.delegate = nil;
_session = nil;
[self.delegate game:self didQuitWithReason:reason];
}
Even though the game hasn’t really started yet on the server, you can still exit. To the server, this looks just like a disconnect, and it will remove the client from its table view.
You already implemented game:didQuitWithReason: in GameViewController, which causes the app to return to the main screen. Because you put some NSLogging into the dealloc methods, you can see in Xcode’s debug output pane that everything gets deallocated properly. Test it out!
Starting the Game On the Server
Starting the game on the server is not too different from what you just did. The host taps the Start button from the Host Game screen. In response, you should create a Game object and fire up the GameViewController.
HostViewController has a startAction: method that’s wired up to the Start button. Right now this method does nothing. Replace it with:
- (IBAction)startAction:(id)sender
{
if (_matchmakingServer != nil && [_matchmakingServer connectedClientCount] > 0)
{
NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([name length] == 0)
name = _matchmakingServer.session.displayName;
[_matchmakingServer stopAcceptingConnections];
[self.delegate hostViewController:self startGameWithSession:_matchmakingServer.session playerName:name clients:_matchmakingServer.connectedClients];
}
}
The Start button will only work if there is a valid MatchmakingServer object (which is usually the case except when there is no Wi-Fi or Bluetooth available), and there is at least one connected client – it’s no fun playing against yourself! When these conditions are met, you get the player’s name from the text field, tell the MatchmakingServer not to accept any new clients, and tell the delegate (MainViewController) that it should start a server game.
stopAcceptingConnections is new, so add it to MatchmakingServer.h:
- (void)stopAcceptingConnections;
And to MatchmakingServer.m:
- (void)stopAcceptingConnections
{
NSAssert(_serverState == ServerStateAcceptingConnections, @"Wrong state");
_serverState = ServerStateIgnoringNewConnections;
_session.available = NO;
}
Unlike endSession, this doesn’t tear down the GKSession object. It just moves the MatchmakingServer into the “ignoring new connections” state, so that it no longer accepts new connections when the GKPeerStateConnected or GKPeerStateDisconnected callbacks happen. Setting the GKSession’s available property to NO also means that the existence of the service is no longer broadcast.
startAction: also called a new delegate method, so add its signature to HostViewController.h:
- (void)hostViewController:(HostViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients;
And add the implementation for this method in MainViewController.m:
- (void)hostViewController:(HostViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
_performAnimations = NO;
[self dismissViewControllerAnimated:NO completion:^
{
_performAnimations = YES;
[self startGameWithBlock:^(Game *game)
{
[game startServerGameWithSession:session playerName:name clients:clients];
}];
}];
}
This is very similar to what you did for clients, except you’re calling a new method on the Game class. Add this method to Game.h:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients;
As well as to Game.m:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
self.isServer = YES;
_session = session;
_session.available = NO;
_session.delegate = self;
[_session setDataReceiveHandler:self withContext:nil];
_state = GameStateWaitingForSignIn;
[self.delegate gameWaitingForClientsReady:self];
}
This is very much the same thing you do for the client game, except that you set the isServer property to YES, and you call another GameDelegate method. Add this method to the @protocol in Game.h:
- (void)gameWaitingForClientsReady:(Game *)game;
And implement it in GameViewController.m:
- (void)gameWaitingForClientsReady:(Game *)game
{
self.centerLabel.text = NSLocalizedString(@"Waiting for other players...", @"Status text: waiting for clients");
}
That’s it! Now when you press the Start button on the host, the HostViewController gets discreetly dismissed and the GameViewController appears:
Your app can now connect a server (host) to multiple clients. But the devices just sit there with the clients waiting for the host to start the game. There’s nothing much for a player to do except tap the exit button. Because you already implemented exitAction:, and both the client and server share most of the code in Game and GameViewController, tapping the exit button on the server should end the game.
Note: When you exit the server, the client may take a few seconds to recognize that the server is disconnected. It will also remain stuck on the “Waiting for game to start…” screen, because you haven’t yet implemented any disconnection logic in the Game class.
Disconnection logic used to be handled by the MatchmakingServer and MatchmakingClient classes, but now that you’ve started the game, these objects have served their purpose and are no longer being used. The Game object has taken over the duties of GKSessionDelegate.
Note: When you exit the server, the client may take a few seconds to recognize that the server is disconnected. It will also remain stuck on the “Waiting for game to start…” screen, because you haven’t yet implemented any disconnection logic in the Game class.
Disconnection logic used to be handled by the MatchmakingServer and MatchmakingClient classes, but now that you’ve started the game, these objects have served their purpose and are no longer being used. The Game object has taken over the duties of GKSessionDelegate.