Examples (iOS)
Examples (Android)

 

» Tris (Tic-Tac-Toe)

» Overview

The Tris example shows how to create a mutiplayer version of the classic "Tic-Tac-Toe" gamefor iPhone using SmartFoxServer 2X.

The following game flow is implemented.

  1. Users log in and join a Room called "The Lobby" by default; it's a sort of lobby where users can meet and chat before they start or join a game.
  2. A user creates a Room where he can play the tic-tac-toe game against another user; as soon as the game is created, he/she will enter the new Room and wait for the second player.
  3. When the second player joins the game (by selecting it in the list), the game starts.
  4. The application alternatively sets the player's turn and after every move it checks if there's a winner.
  5. The game ends when no more moves are available or one of the player makes a row of three; at the end of the game the user can go back in the main lobby Room.

This example shows how to create a SmartFoxServer Room of type "game". Game Rooms are specifically aimed at representing games (or matches) as they can contain two different types of users: players and spectators. A Game Room also provides player indexes: each user joining the room is automatically assigned a unique player index which facilitates the tasks of starting and stopping the game (spectators are not indexed instead).
In this example spectators are not supported.

In this implementation of the Tris game, all the logic is confined on the client: Room Variables are used to keep the game state and Object Messages are exchanged by clients to syncronize the game boards.

Room Variables are one of the three server variable objects available in SmartFoxServer to store data to be shared among the clients of an application or game (the other two are User Variables and Buddy Variables, which we aren't deal with in this example). What's the difference between a server variable and a regular one? The main difference is that the first one (as the word says) is stored on the server and broadcasted to the other clients. In particular, a Room Variable is broadcasted to all the users in the same Room where it is set. This means that a change in a Room Variable will be reflected on all clients within the same Room so that, for example, they can start a game in synch.

An Object Message is a specific kind of message exchanged between clients, allowing data to be transferred from one user to another. In this game the message contains informations on the "move" played by the user.

The implementation of the Tris game for other platforms instead features a server-side Extension which is in charge of handling the game logic, as opposed to keeping the logic on the client-side only. In general, using a server-side Extension is a more flexibile and secure option. Check the corresponding tutorials for more informations on this topic.

In this example, after the application is launched, the user is prompted to enter a user name and connect. Once submitted, the user is connected to SmartFoxServer 2X and a logged into the BasicExamples Zone available by default. As a logged in user, we can then participate in the lobby Room where we can chat, create & join games and play a full tic-tac-toe round.

>> DOWNLOAD the source files <<

» Installation

» Source code setup

The example assets are fully contained in a project file and don't require a specific setup: simply open the Tris.xcodeproj file contained in the folder with Xcode.

» Running the example

In order to run the application follow these steps:

  1. make sure your SmartFoxServer 2X installation contains the BasicExamples Zone definition;
  2. start SmartFoxServer 2X;
  3. make sure the client will connect to the right IP address by editing the <ip> entry in the xml file available under the Supporting Files folder in Xcode;
  4. select the iPhone Simulator from the Scheme dropdown and click the Run button to launch the simulator.

» Code highlights

Similar to the Simple Chat example, the SmartFox client for the application is instantiated within the AppDelegate and SFS events are delegated to the AppDelegate as well. Once the application loads, a connection is automatically made with SmartFoxServer in the AppDelegate's application method:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    self.viewController = [[[LogInViewController alloc] initWithNibName:@"LogInViewController" bundle:nil] autorelease];
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];

	/**
     * Create a connection to SmartFoxServer as soon as the application has launched.
     * First, allocate a SmartFox2XClient instance
     */
	_smartFox = [[SmartFox2XClient alloc] initSmartFoxWithDebugMode:YES delegate:self];

    /*
     * Load our XML config file and once done, attempt a connection
     */
	[_smartFox loadConfig:@"config.xml" connectOnSuccess:YES];

    return YES;
}
You'll notice that the client loads the config.xml file and connects to SFS on success. Once connected, a message is presented to the user that they are able to log on.

You'll also notice, that the interface decleration in the header file for the AppDelegate includes three protocol implementations:

@interface AppDelegate : UIResponder <UIApplicationDelegate,UIAlertViewDelegate,ISFSEvents>
The inclusion of ISFSEvents is what enables us to listen for SFS2X events in the AppDelegate.

The login button press is handled by onLoginTouchUp in the LoginViewController implementation which simply calls the AppDelegate's login method. The following listener handles the related event when the login is performed successfully.

- (void)onLogin:(SFSEvent *)evt
{
	[_smartFox send:[JoinRoomRequest requestWithId:@"The Lobby"]];
    [self switchToView:@"LobbyViewController" uiViewController:[LobbyViewController alloc]];
}
Once logged in, a JoinRoomRequest is sent to connect to the Room named "The Lobby" and, if successful, the application will run through the onRoomJoin method which will switch the view to the LobbyViewController.

The LobbyViewController is devided up into five distinct parts: chat entry, chat log, user list, room list, and create game. Our UI components are defined in the header file:

@property (nonatomic, retain) IBOutlet UITextField *textField; //Our chat entry
@property (nonatomic, retain) IBOutlet UIScrollView *scrollView; //Our chat log
@property (nonatomic, retain) IBOutlet UITableView *users; //Our user list
@property (nonatomic, retain) IBOutlet UITableView *games; //Our game list
Our LobbyViewController implementation contains numerous methods to handle events coming from the AppDelegate (remember, the SFS2X client is a property of the AppDelegate and handles events there). When a public message is recieved, the event gets handled in the AppDelegate and is subsequently passed to the LobbyViewController for display.

The same methodology applies for users entering and exiting the lobby Room. Each time there is a change in the Room users, the onUserCountChange event is handled in the AppDelegate, which calls a similarly named method in the LobbyViewController to refresh the users list. Also contained in the lobby, is a button to create a new game. The "New Game" button is connected on Touch Down to the newGame method. The newGame method simply switches views to the SpawnGameViewController.

The LobbyViewController also acts as a delegate to both the user list and the Room list enabling us to listen for UITableView events. We simply fork the logic based on which table is the target.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	if (tableView == users)
    {
		SFSUser *user = [[_smartFox.lastJoinedRoom userList] objectAtIndex:indexPath.row];

		if (user.id != [_smartFox.mySelf id])
        {
			_alertType = 1;
			UIAlertView *prompt = [[UIAlertView alloc] initWithTitle:@"Private Message" message:[NSString stringWithFormat:@"Send a private message to %@", user.name] delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Send", nil];

            prompt.alertViewStyle = UIAlertViewStylePlainTextInput;

			[prompt show];
			[prompt release];
		}
		else
        {
			[tableView deselectRowAtIndexPath:indexPath animated:YES];
		}
	}
	else
    {
		[tableView deselectRowAtIndexPath:indexPath animated:YES];

		SFSRoom *room = [[_smartFox.roomManager getRoomListFromGroup:@"games"] objectAtIndex:indexPath.row];

		if (room == nil || ![room isGame])
        {
			return;
		}

		if ([room isPasswordProtected])
        {
			_lastSelectedRoom = room.name;
			_alertType = 2;
			UIAlertView *prompt = [[UIAlertView alloc] initWithTitle:@"Password for room" message:@"Enter a password" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Go", nil];

            prompt.alertViewStyle = UIAlertViewStyleSecureTextInput;

			[prompt show];
			[prompt release];
		}
		else
        {
			[_smartFox send:[JoinRoomRequest requestWithId:room.name pass:@"" roomIdToLeave:[NSNumber numberWithInt:[_smartFox.lastJoinedRoom id]] asSpect:NO]];
		}
	}
}

The SpawnGameViewController is a simple form to generate a game with a name and optional password. Again, the AppDelegate will pass SFS events through to us when they occur for the current view to handle.

Once in a game, the first player (the Room creator) must wait for an opponent and after the game Room view has loaded, the player sets a Room Variable for the opponent to read. As soon as an opponent enters the Room, the onUserEnterRoom event is fired, and we can update the player list and start the game after the second Room Variable update is handled:

- (void)onRoomVariablesUpdate:(SFSEvent *)evt
{
	SFSRoom *room = [evt.params objectForKey:@"room"];

	if ([room containsVariable:@"player1"] && [room containsVariable:@"player2"])
    {
		if ([_smartFox.lastJoinedRoom userCount] == 1)
        {
			return;
		}
		if (!_gameStarted)
        {
			[self updatePlayersNames];

			if (_alert && _alert.visible)
            {
				[_alert dismissWithClickedButtonIndex:5 animated:NO];
				_alert = nil;
			}

			_gameStarted = YES;
			_whoseTurn = 1;
			[self waitMove];
		}
	}
	else
	{
		_gameStarted = NO;
		_moveCount = 0;
		[self resetGameBoard];
		[self updatePlayersNames];

        _turnText.text = @"Status: Waiting for opponent";

		NSInteger otherPlayerId = ([_smartFox.mySelf playerId] == 1) ? 2 : 1;

		if (_alert && _alert.visible)
        {
			[_alert dismissWithClickedButtonIndex:5 animated:NO];
			_alert = nil;
		}

		_alertType = 1;
		_alert = [[UIAlertView alloc] initWithTitle:@"Info"
                                            message:[NSString stringWithFormat:@"Waiting for player %ld\n\npress [cancel] to leave the game", otherPlayerId]
                                           delegate:self
                                  cancelButtonTitle:@"Cancel"
                                  otherButtonTitles: nil];
		[_alert show];
		[_alert release];
	}

}

Once the game has started, player moves are sent to each other using Object Messages. Object Messages are a great way to send information to all players in a Room. Once a player selects a tile on the gameboard, we assemble an object to be sent to the Room. First, we listen for a tile to be touched in the onTileTouch method. Then, we send the data of which tile was selected to the createBall method to draw a ball on our screen and call moveDone to send the selected tile to the room for our opponent to read:

- (void)moveDone:(NSInteger)x y:(NSInteger)y
{
	_moveCount++;
	[self checkBoard];
	[self nextTurn];

	SFSObject *obj = [[SFSObject newInstance] autorelease];
	[obj putUtfString:@"type" value:@"move"];
	[obj putUtfString:@"x" value:[NSString stringWithFormat:@"%ld", x + 1]];
	[obj putUtfString:@"y" value:[NSString stringWithFormat:@"%ld", y + 1]];

	[_smartFox send:[ObjectMessageRequest requestWithObject:obj]];
}
Our AppDelegate will recieve an onObjectMessage event and pass the event to our GameViewController where it is to be handled:
- (void)onObjectMessage:(SFSEvent *)evt
{
	SFSObject *obj = [evt.params objectForKey:@"message"];

	if ([[obj getUtfString:@"type"] isEqualToString:@"move"])
    {
		NSInteger x = [obj getInt:@"x"] - 1;
		NSInteger y = [obj getInt:@"y"] - 1;
		_board[y][x].status = ([_smartFox.mySelf playerId] == 1) ? @"R" : @"G";

		[self createBall:([_smartFox.mySelf playerId] == 1) ? @"redBall.png" : @"greenBall.png" x:_board[y][x].button.center.x y:_board[y][x].button.center.y];
		_moveCount++;
		[self checkBoard];
		[self nextTurn];
	}
	if ([[obj getUtfString:@"type"] isEqualToString:@"restart"])
    {
		[self restartGame];
	}
}

» More resources

You can learn more about the topics discussed in this tutorial by consulting the following resources: