1. Sissejuhatus ning tehnoloogia valik¶
Selles peatükis vaatleme, kuidas multiplayer-mängudes toimub info vahetamine serveri ja kliendi vahel. Räägime sellest, kuidas sünkroniseerida mängumaailma seisundit ning kuidas kasutada TCP ja UDP protokolle sõltuvalt olukorrast — olgu fookuses kiirus või töökindlus. Antud näidete kaudu saad esmase arusaama sellest, kuidas käib mänguloogika jaotamine serveri ja kliendi vahel ning millised on levinumad probleemid ja lahendused.
Näidete juures kasutame Kryonet teeki (library) ja Libgdx raamistikku (framework). Näited tulevad ühest eelmisel aastal tehtud projektist. Projektis on kohti, mida oleks saanud teha efektiivsemalt, nii et ei soovita midagi üks ühele maha teha.
2. Klient-server mudel ja rollijaotus mängus¶
Multiplayer mängude tegemisel on keskseks osaks info jagamine. Info jagamist saab realiseerida kõige lihtsamalt klient-server (client-server) mudeli abil, kuigi on ka teisi võimalusi, kuidas info jagamist realiseerida. Selle mudeli peamine idee seisneb selles, et mängu loogika - näiteks objekti liikumine x, y teljel - toimub serveris ja visuaalne pool toimub clientis. Selleks, et multiplayer mängu teha, tuleks kohe alguses hakata raliseerima projekti selle mudeli alusel, muidu hiljem läheb selle tegemine väga keeruliseks.
Multiplayer mängude puhul on oluline selgelt määratleda, millised ülesanded täidab server ja millised klient. Kui neid rolle kohe alguses õigesti ei lahuta, võib projekt kiiresti muutuda keerukaks ja raskesti hallatavaks. Server keskendub üldjuhul mängu loogikale ja andmete sünkroniseerimisele, samal ajal kui klient hoolitseb kasutajaliidese ja visuaalse poole eest.
Allolevas tabelis on toodud tüüpiline jaotus serveri ja kliendi rollide vahel:
Ülesanne |
Server |
Klient |
---|---|---|
Mängu maailma hetke seisu hoidmine |
JAH |
JAH (koopia serverist) |
Objektide liikumise loogika |
JAH |
EI |
Klaviatuuri/sisestuse kuulamine |
EI |
JAH |
Graafika renderdamine |
EI |
JAH |
Info saatmine teistele klientidele |
JAH |
EI |
Pakettide vastuvõtmine |
JAH |
JAH |
Heliefektide mängimine |
EI |
JAH |
Võrguühenduste haldamine |
JAH |
JAH |
See jaotus ei ole kivisse raiutud, kuid väiksemate mängude puhul on just selline lihtsustatud mudel väga tõhus ja piisav. Täpsemalt hakkame konkreetseid rakendusnüansse ja näiteid uurima järgmistes jaotistes.
3. Mängu arhitektuur¶
Selles peatükis vaatleme kaht võimalikku client-server loogika teostuse varianti mitme mängijaga mängu jaoks. Igal lähenemisel on omad plussid ja miinused ning valik sõltub projekti eesmärkidest ja ulatusest.
Esimene lähenemine
Esimeses projektis BoxheadTypeGame maailma mudel "World klass", mis hoiab mängu hetke seisu - state - ja mis on nii kliendis kui ka serveris. Nii et see klass ühendab endas mitmeid funktsioone:
Kaardi ja objektide initsialiseerimine
Mängijate ja zombide haldamine
Kuulide ja kokkupõrgete töötlemine
Selleks, et mulitplayer mäng ilusasti toimiks, peavad igal kliendil maailmamudelid ühtima.
Selline lähenemine sobib väiksemate projektide või prototüüpide jaoks, kus on oluline lihtsus ja kiire arendus. Kuid projekti kasvades võib tekkida vajadus modulaarsema arhitektuuri järele, et parandada koodi loetavust ja hooldatavust.
Teine lähenemine
Teises projektis (Example game) on struktuur jagatud mitmeks mooduliks, millest igaüks vastutab oma valdkonna eest:
core: mängu põhilogika, sh mängija, areeni ja ekraanide haldamine
server: serveripoolne osa, mis töötleb mänguloogikat, kokkupõrkeid ja võrguüritusi
shared: ühised klassid ja sõnumid, mida kasutavad nii klient kui server
See lähenemine võimaldab paremat skaleeritavust ja lihtsustab meeskonnatööd, võimaldades arendajatel keskenduda eraldi komponentidele, ilma et see mõjutaks teisi süsteemi osi.

Andmete liikumise skeem klientide ja serveri vahel modulaarse arhitektuuri korral.¶
Kuidas see töötab?
Skeemil on näidatud, kuidas andmed liiguvad kliendi ja serveri vahel:
1. Mängija vajutab klahve klaviatuuril või hiirel. Neid tegevusi töötleb PlayerInputManager.
2. Tegevused muudetakse sõnumiteks (nt PlayerMovementMessage, PlayerShootingMessage) ja saadetakse serverisse.
3. Server võtab sõnumid vastu spetsiaalsete kuulajate kaudu (Player1MovementListener, Player2ShootingListener) ja uuendab mängu seisu:
muudab mängijate liikumissuunda
loob uusi kuule
kontrollib kuulide ja mängijate kokkupõrkeid BulletCollisionHandler kaudu
4. Probleemid ja optimeerimine¶
Suurtemate projektide jaoks võib nii lihtsakoelisel lähenemisel probleeme tekkida, näiteks võib juhtuda selline olukord, kus saadetakse liiga palju informatsiooni liiga kiiresti serverisse ja tänu sellele võib võrk (network) olla ülekoormatud, mis tekitab mängus viivitusi (lag). Selline olukord võib tekkida näiteks siis, kui iga väiksemgi mängija tegevus — näiteks iga hiire liigutus või klahvivajutus — saadab eraldi MovePacket-i serverisse.
Selle aine raames aga sellise probleemiga väga kokku õnneks ei puutu, kuna mängud on väiksemahulised. Kui probleem tekib, siis saab tõenäoliselt kuskil lihtsa optimeerimise teha, et probleem ära kaoks.
5. Võimalused Javas kliendi-serveri loomiseks¶
Javas on palju võimalusi, kuidas klienti ja serveri vahelist suhtlust realiseerida. Kõige lihtsam oleks seda teha Kryonet või Netty teekide abil - siinkohal vihje, et kui tekib arusaamatusi teekide toimimise kohta, siis vaadake nende dokumentatasioone ja tavaliselt on ka dokumentatsioonis tehtud lihtsamaid näiteid kuidas teeki kasutada. Siin on näiteks Kryoneti enda poolt tehtud näited. Samuti on ka Nettyl teegi kasutamise näited.
Kui mõtled, kumba valida, siis võib abiks olla järgmine väike võrdlus:
Omadus |
KryoNet |
Netty |
---|---|---|
Lihtsus |
Väga lihtne kasutada ja seadistada |
Rohkem kontrolli, keerulisem seadistada |
Abstraktsioonitase |
Kõrge (palju ära peidetud) |
Madalam (rohkem käsitsi tööd) |
Sobib |
Kiireks prototüüpimiseks, väiksematele mängudele |
Suuremahulistele rakendustele, suure koormuse puhul |
Dokumentatsioon |
Piisav, koos näidetega |
Põhjalik, rohkem näiteid ja juhendeid |
6. Võrgupõhimõtted ja protokollid¶
Klient-serveri mudeli toimimise loogika peaks olema teile tuttav ainest Arvutivõrkude alused (ICA0013), aga siiski kordaks siinkohal põhilised asjad üle. Põhimõtteliselt on klient ja server kahes eraldi arvutis, mis suhtlevad omavahel saates pakette (packet). Arvutid suhtlevad IP aadresside ja portide abil. Port võtab andmeid vastu teatud rakenduse/protsessi jaoks.
Teie rakendusel läheb tavaliselt vaja kahte porti, üks TCP ja teine UDP saatmiseks.
7. Kliendi ja Serveri realiseerimine¶
Etteantud pildil on näha Serveripoolse serveri klassi tegemist, kus punasega on ära märgitud TCP ja UDP portide määramine nendele määratud arvude järgi. Näiteprojektis on fail siin.

Server class¶
Kliendi pool on pilt umbes sama, kuid on vaja ka määrata ip aadress, kuhu packeteid saadetakse. Siin on näidatud kuidas kliendi poolne suhtlus toimib.
8. Paketid ja andmeedastus¶
Kui UDP ja TCP on ära määratud, siis tuleb mõelda sellele, mis informatsiooni saadetakse kliendi ja serveri vahel. Üleüldiselt tehakse nii, et saadetaks pakette serveri ja kliendi vahel. Pakette võib olla mitut erinevat tüüpi, igas paketis saadetakse erinevat informatsiooni, näiteks tegelaste liikumise jaoks on pakett, mis määrab ära tegelase koordinaadid, või nupumängus on pakett, mis määrab ära kelle käik on, mis server saadab klientidele kui mängu olek muutub. Näiteks kui mäng on pealtvaates, kus saab tegelasi liigutada, siis oleks üks asi saadetav info tegelas(t)e x ja y koordinaadid. Siin on näiteid pakettidest. Etteantud projektis on paketid tehtud eraldi java klassideks. Paketid võiksid olla võimalikult väiksed, nii et neid saaks võimalikult kiiresti saata. Mida teie saaksite etteantud projektist paremini teha oleks, et iga pakett laiendab (extend) abstraktset klassi mitte tavalist klassi, see on stiili suhtes parem.
9. Pakettide töötlemine serveris¶
Järgmise asjana oleks teil vaja realiseerida suhtlse süsteem serveris ja kliendis, mis võtab paketid lahti ja realiseerib nendega mängu loogikat. Krypnetis käib asi nii, et on vaja teha serverile kuulamise instants (instance) serveri klassi sisse:
server.addListener(new Listener() {
public void received(Connection connection, Object object) {
// packetite suhtluse loogika läheb siia
}
});
Näidisprojektis on see koht ära näidatud all oleval koodis. Koodis on
siis näha kuidas Server saab kliendi (Connection
) käest paketi
(Object
), siis tehakse mingisugune muudatus maailmale ja saadetakse
see teistele mängjatele/mängjale.
server.sendTCP(mängja ip, muudatuse packet)
// Add listener to handle receiving objects.
server.addListener(new Listener() {
// Receive packets from clients.
public void received(Connection connection, Object object){
if (object instanceof PacketConnect && serverWorld.getClients().size() < 3) {
PacketConnect packetConnect = (PacketConnect) object;
playerCount += 1;
System.out.println("Received message from the client: " + packetConnect.getPlayerName());
// Creates new PlayerGameCharacter instance for the connection.
PlayerGameCharacter newPlayerGameCharacter = PlayerGameCharacter
.createPlayerGameCharacter(playerGameCharacterX, playerGameCharacterY,
packetConnect.getPlayerName(), serverWorld, connection.getID());
// Add new PlayerGameCharacter instance to all connections.
addCharacterToClientsGame(connection, newPlayerGameCharacter);
// Each PlayerGameCharacter instance has a different x coordinate.
if (playerCount <= 3) {
playerGameCharacterX += INCREASE_X_COORDINATE;
} else {
playerGameCharacterX = 280f;
playerCount = 0;
}
} else if (object instanceof PacketConnect && serverWorld.getClients().size() == 3) {
// If server is full then send a packet notifying that player cant join game.
server.sendToTCP(connection.getID(), new PacketServerIsFull());
} else if (object instanceof PacketUpdateCharacterInformation) {
PacketUpdateCharacterInformation packet = (PacketUpdateCharacterInformation) object;
// Update PlayerGameCharacter's coordinates and direction.
// Send PlayerGameCharacter's new coordinate and direction to all connections.
sendUpdatedGameCharacter(connection.getID(), packet.getX(), packet.getY(), packet.getDirection());
} else if (object instanceof PacketBullet) {
PacketBullet packetBullet = (PacketBullet) object;
// Creates new PistolBullet instance.
PistolBullet newPistolBullet = new PistolBullet();
Rectangle newRectangle = new Rectangle(packetBullet.getBulletXCoordinate(),
packetBullet.getBulletYCoordinate(), 5f, 5f);
newPistolBullet.makePistolBullet(newRectangle, packetBullet.getBulletTextureString(),
packetBullet.getMovingDirection(), packetBullet.getDamage());
// Add new PistolBullet to the Server's world.
if (!serverWorld.isUpdatingBullets()) {
serverWorld.addBullet(newPistolBullet);
}
}
}
...
Lisamaterjalid:¶
Videod:
Realsed projektid: