Φτιάχνοντας έναν Game Server σε C++ #5: Συμμετοχή σε Παιχνίδια
Ο 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 για να κάνω τη ζωή μου λίγο πιο εύκολη.
