|

Building a Game Server in C++ #5: Joining games

Series Navigation<< Building a Game Server in C++ #4: Let’s talk protocolBuilding a Game Server in C++ #6: A generic client >>

Our server has reached a point where users can authenticate, browse games and active game worlds, create new worlds and terminate them. In this article we will give them the ability to join games by adding a cmdJoin to our command arsenal.

First things first, we have to add it to commandType and commandStrType:

enum commandType { /* ... */ cmdJoin, };

map<string,commandType> commandStrType={
	/* ... */
	{"join",cmdJoin},
}; 

As I already mentioned, in order for a user to join a game, they must be authenticated and performed a successful UDP handshake. Moreover, they are obviously expected to pass a valid world id as a parameter. Having said that, let’s implement the command’s case in clientCommand():

		case cmdJoin: {
		
			if(""==getSessionUsername(id)) return {false,"err "+encodeToken("Not authenticated\n")};
			if(!getSessionUDPAddr(id).sin_addr.s_addr) return {false,"err "+encodeToken("UDP handshake required\n")};
		
			string wid_s=p.getToken();
			if(!wid_s.length()) return {false,"err "+encodeToken("A world id is required\n")};
			
			int wid=atoi(wid_s.c_str());
			if(wid_s!=to_string(wid)) return {false,"err "+encodeToken("Invalid world id "+wid_s+"\n")};
			
			lock_guard<recursive_mutex> wLock(worldMutex);
			if(!worlds[wid]) return {false,"err "+encodeToken("Unknown world id "+wid_s+"\n")};
			
			Game *g=worlds[wid]->game;
			sockaddr_in a=sessions[id]->udpAddr;
			pair<uint32_t,uint16_t> p={a.sin_addr.s_addr,a.sin_port};
			
			lock_guard<recursive_mutex> pLock(playerMutex);
			if(players[p]) {
				players[p]->game->deletePlayer(id);
				delete players[p];
			}
			players[p]=new playerEntry{g,id};
			
			g->addPlayer(id);
			
			return {false,"ok\n"};
		
		}

That’s about it. Now we can (and should) remove:

  • the temporary code in handleUDP() which joins the user to a new RPSGame upon handshake
  • the joinGame() method, since its function is now embedded in cmdJoin

But, although the joining procedure is complete, there is another issue we have to address. A game should be able to notify the server when a user must be disassociated with it. Four occasions come to mind:

  • When the game is terminated using cmdKill: Games will not have the ability to destroy their instances, only the server can do that by effectively calling the game’s destructor. What that means is that, during destruction, the game should implement any cleanup procedure, including “kicking” its players.
  • When the game must refuse the joining player: As I also mentioned, the server doesn’t ask a game to accept a player, it just notifies it. If for whatever reason the player cannot join, the game has the opportunity to communicate with the user before kicking them. In other words, the player is always considered to have joined the game, if only for a very short period of time.
  • When the game actively kicks the player: This can happen for several reasons, including abuse or timeout. In practice, it’s pretty much the same as the above.
  • When the player explicitly quits the game: This is a functionality that the vast majority of games, if not all of them, are expected to implement.

In order to do that, we will expose one more public method to games:

class GameInterface {

	public:

  		/* ... */
  
		virtual void unjoinPlayer(int)=0;
	
};

class Server: public SessionInterface, public GameInterface {

	public:
	
  		/* ... */
  
		void unjoinPlayer(int);

  		/* ... */

};

void Server::unjoinPlayer(int id) {
  
	lock_guard<recursive_mutex> sLock(sessionMutex);
	if(!sessions[id]) return;
	
	sockaddr_in a=sessions[id]->udpAddr;
	pair<uint32_t,uint16_t> p={a.sin_addr.s_addr,a.sin_port};
	lock_guard<recursive_mutex> pLock(playerMutex);
	delete players[p];
	players.erase(p);
  
}

That should do it for the Server class, but RPSGame has to reflect those changes. When the game instance is deleted, it should notify the server to disassociate the player from it:

class RPSGame: public Game {

  public:
  		
  		/* ... */
  
		~RPSGame();
	
  		/* ... */
  
};

RPSGame::~RPSGame() { 
	if(!opponent) return;
	server->sendPlayer(opponent,{"Sorry, the game is shutting down\n"});
	server->unjoinPlayer(opponent); 
}

We simply check if we have an opponent. If so, we notify the user that the game is shutting down, and the server to unjoin them.

While we’re at it, there’s still one more thing to take care of. Up until now, we spawned a new game for every user that performed a UDP handshake, and joined that user to it. Therefore, addPlayer() only had to update the opponent field. From this point forward, users may try to join games that already have an opponent, and addPlayer() must deal with the situation:

void RPSGame::addPlayer(int player) { 
	if(opponent) {
		server->sendPlayer(player,{"Sorry, I already have an opponent...\n"});
		server->unjoinPlayer(player); 
		return;
	}
	opponent=player; 
	server->sendPlayer(player,{"Welcome to Rock Paper Scissors!\n"});
}

Notice that, if we’re already playing an opponent, we call unjoinPlayer() to notify the server that we rejected the user.

Now, at first I thought I’d add some kind of exit option in playerSent(), where a player can send some special character in order to quit the game. However, since a user can either disconnect or join another game at any time, there’s not really any significant reason to do that.

To test all this, I will first login as mario from telnet:

$ telnet localhost 9999
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
id 3
auth mario Its-aMe
ok
run rps
world 1
run rps
world 2
run rps
world 3
run rps
world 4
run rps
world 5
run rps
world 6
run rps
world 7
run rps
world 8
run rps
world 9
run rps
world 10
kill 3
ok
kill 4
ok
kill 6
ok
kill 9
ok
worlds
world 1;rps
world 2;rps
world 5;rps
world 7;rps
world 8;rps
world 10;rps
join 1
ok

Ok, I finally created six instances of RPSGame and, having completed a handshake from another terminal, I joined the first one. Now let’s connect as luigi from yet another terminal:

$ telnet localhost 9999
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
id 4
auth luigi OkeyDokey
ok
worlds
world 1;rps
world 2;rps
world 5;rps
world 7;rps
world 8;rps
world 10;rps
join 1
err UDP_handshake_required
join 1
ok
join 2
ok
kill 2
ok

I obviously did a handshake and played from a fourth terminal, so here’s the output from that:

$ nc -u localhost 9999
4
Sorry, I already have an opponent...
Welcome to Rock Paper Scissors!
rsppsrrrps
pr score: 1-0
ps score: 1-1
rp score: 1-2
pp Tie!
ss Tie!
pr score: 2-2
pr score: 3-2
rr Tie!
rp score: 3-3
ps score: 3-4
Sorry, the game is shutting down

These tests are getting pretty frustrating. Having to open numerous terminals and running telnet/nc has become too cumbersome by now. That’s why in the next article I will put together a generic client to make my life a bit easier.

Leave a Reply

Your email address will not be published. Required fields are marked *