|

Φτιάχνοντας έναν Game Server σε C++ #5: Συμμετοχή σε Παιχνίδια

Series Navigation<< Φτιάχνοντας έναν Game Server σε C++ #4: Περί πρωτοκόλλουΦτιάχνοντας έναν Game Server σε C++ #6: Ένας Στοιχειώδης Client >>

Ο server μας έχει φτάσει στο σημείο όπου οι χρήστες μπορούν να διαπιστεύονται, να βλέπουν ποια παιχνίδια υποστηρίζονται και ποια “τρέχουν”, να ξεκινούν νέα στιγμιότυπα και να τα τερματίζουν. Σε αυτό το άρθρο θα τους δώσουμε τη δυνατότητα να συμμετέχουν σε παιχνίδια, προσθέτοντας μια νέα εντολή cmdJoin στο οπλοστάσιό μας.

Πρώτα απ’ ολα, θα πρέπει να την προσθέσουμε στο commandType και το commandStrType:

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

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

Όπως έχω αναφέρει, οι προϋποθέσεις για να μπορεί κάποιος να συμμετέχει σε παιχνίδια είναι η διαπίστευση και το UDP handshake. Επιπλέον, θα πρέπει να δώσουν ένα έγκυρο αναγνωριστικό ενεργού στιγμιότυπου σαν όρισμα. Με αυτά τα δεδομένα, ας υλοποιήσουμε το case της εντολής στην 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"};
		
		}

Αυτό ήταν όλο. Τώρα μπορούμε, και θα πρέπει, να αφαιρέσουμε:

  • τον προσωρινό κώδικα στην handleUDP() που ξεκινά ένα RPSGame κατά το handshake
  • τη μέθοδο inGame(), αφού ο κώδικάς της πλέον ενσωματώθηκε στην cmdJoin

Αλλά, παρόλο που η διαδικασία της συμμετοχής είναι ολοκληρωμένη, υπάρχει άλλο ένα θέμα που πρέπει να δούμε. Τα παιχνίδια θα πρέπει να μπορούν να ειδοποιούν τον server όταν ένας χρήστης δεν συμμετέχει πλέον. Τέσσερις περιστάσεις για παράδειγμα:

  • Όταν ένα στιγμιότυπο τερματίζεται μέσω της cmdKill: Τα παιχνίδια δεν θα έχουν τη δυνατότητα να τερματίζονται, μόνο ο server θα μπορεί να το κάνει αυτό καλώντας τον destructor τους. Με άλλα λόγια, κατά την καταστροφή του στιγμιότυπου, το παιχνίδι θα πρέπει να υλοποιεί κάποια διαδικασία τακτοποίησης, συμπεριλαμβανομένης της αποσύνδεσης των παιχτών του.
  • Όταν ένα παιχνίδι πρέπει να αρνηθεί συμμετοχή στο χρήστη: Όπως επίσης έχω αναφέρει, ο server δε ζητά από το παιχνίδι να δεχτεί το χρήστη, απλά το ενημερώνει για τη συμμετοχή. Αν για κάποιο λόγο ο χρήστης δεν μπορεί να συμμετάσχει, το παιχνίδι έχει την ευκαιρία να επικοινωνήσει μαζί του πριν τον αποσυνδέσει. Αυτό σημαίνει ότι ο χρήστης θεωρείται πάντα συνδεδεμένος, έστω και για ελάχιστο χρόνο.
  • Όταν το παιχνίδι διώχνει το χρήστη: Αυτό μπορεί να συμβεί για διάφορους λόγους, για παράδειγμα ανάρμοστη συμπεριφορά ή timeout.
  • Όταν ο παίχτης ρητά βγαίνει από το παιχνίδι: Αυτή είναι μια λειτουργικότητα όπου η πλειοψηφία των παιχνιδιών, αν όχι όλα, αναμένεται να υλοποιούν.

Για να το πετύχουμε αυτό, πρέπει να κάνουμε διαθέσιμη άλλη μία μέθοδο στα παιχνίδια:

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);
  
}

Αυτό μας καλύπτει για την κλάση Server, αλλά το RPSGame θα πρέπει να συγχρονιστεί με αυτές τις αλλαγές. Όταν ένα στιγμιότυπο καταστρέφεται, θα πρέπει να ειδοποιεί τον server για την αποσύνδεση του παίχτη:

class RPSGame: public Game {

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

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

Απλά ελέγχουμε αν έχουμε αντίπαλο. Αν ναι, ειδοποιούμε το χρήστη ότι το παιχνίδι κλείνει, και τον server για την αποσύνδεση.

Έχουμε και κάτι άλλο να τακτοποιήσουμε στο ίδιο θέμα. Μέχρι τώρα, ξεκινούσαμε ένα νέο στιγμιότυπο κατά το UDP handshake, και συνδέαμε το χρήστη με αυτό. Κατά συνέπεια, η addPlayer() έπρεπε απλά να ενημερώσει το πεδίο opponent. Στο εξής, οι χρήστες μπορεί να θελήσουν να συμμετέχουν σε παιχνίδια που ήδη έχουν αντιπάλους, και η addPlayer() πρέπει να μπορεί να το διαχειριστεί:

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"});
}

Όπως παρατηρείτε, αν έχουμε ήδη αντίπαλο, καλούμε την unjoinPlayer() για να ειδοποήσιουμε τον server ότι απορρίψαμε το χρήστη.

Τωρα, στην αρχή σκέφτηκα να υλοποιήσω κάποιου είδους διαδικασία εξόδου στην playerSent(), οπου ο παίχτης θα μπορεί να στείλει κάποιον ειδικό χαρακτήρα για να βγει από το παιχνίδι. Αλλά, εφόσον ο χρήστης μπορεί να αποσυνδεθεί από τον server ή να συμμετάσχει σε άλλο παιχνίδι οποιαδήποτε στιγμή, δεν έχει και πολύ νόημα να το κάνω.

Για να τα δοκιμάσουμε όλα αυτά, πρώτα θα κάνω login ως mario μέσω 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

Οκ, τελικά ξεκίνησα έξι στιγμιότυπα του RPSGame, και έχοντας κάνει handshake από ένα άλλο τερματικό, έκανα join στο πρώτο. Τώρα ας συνδεθούμε ως luigi από ένα ακόμα τερματικό:

$ 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

Προφανώς έκανα handshake και έπαιξα το παιχνίδι από ένα τέταρτο τερματικό, και εδώ είναι το output από εκείνο:

$ 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

Αυτές οι δοκιμές έχουν καταντήσει εκνευριστικές. Έχει γίνει πολύ επίπονο να ανοίγω πολλά τερματικά και να τρέχω telnet/nc. Γι αυτό, στο επόμενο άρθρο, θα φτιάξω έναν πολύ στοιχειώδη client για να κάνω τη ζωή μου λίγο πιο εύκολη.

Αφήστε μια απάντηση

Η ηλ. διεύθυνση σας δεν δημοσιεύεται. Τα υποχρεωτικά πεδία σημειώνονται με *