Networking

Sissejuhatus ning tehnoloogia valik

Selles peatükis vaatame, kuidas toimub andmevahetus mitme mängijaga mängudes serveri ja klientide vahel. Peatüki eesmärk on anda ülevaade klient-server mudelist, mänguseisundi sünkroniseerimisest, pakettide saatmisest ning TCP ja UDP kasutamisest.

Kursuse projektis on soovituslik kasutada Kryoneti teeki ja LibGDX raamistikku. Kryonet sobib hästi väiksemate mitme mängijaga mängude jaoks, sest selle seadistamine on võrdlemisi lihtne ja suurem osa kursuse juhenditest toetub sellele. Samuti on paljud abiõppejõud projektides Kryoneti kasutanud, mistõttu on selle kohta lihtsam kursuse raames abi saada.

Näidetes kasutatakse varasemat BoxheadTypeGame projekti. Tegemist on mitme aasta taguse projektiga, seega ei tasu seda üks ühele oma töö aluseks võtta. Näited on mõeldud eelkõige üldise idee ja struktuuri mõistmiseks.

Klient-server mudel ja rollijaotus mängus

Mitme mängijaga mängudes on vaja pidevalt vahetada infot mängijate, objektide, kuulide, vastaste ja mänguseisundi kohta. Üks levinumaid viise selle lahendamiseks on klient-server mudel.

Klient-server mudelis vastutab server üldjuhul mänguloogika ja mänguseisundi eest. Klient vastutab peamiselt kasutaja sisendi, kasutajaliidese ja graafika kuvamise eest. Näiteks mängija liikumise otsus saadetakse kliendist serverisse, server uuendab mänguseisundit ning saadab vajaliku info tagasi klientidele.

Selline jaotus aitab vältida olukorda, kus igal mängijal on mängust erinev versioon. Kui server on keskne tõeallikas, on lihtsam hoida kõik kliendid samas mänguseisundis.

Tüüpiline rollijaotus on järgmine:

Ülesanne

Server

Klient

Mängumaailma seisu hoidmine

JAH

JAH (koopia serverist)

Objektide liikumise loogika

JAH

EI

Klaviatuuri/sisendi lugemine

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 ainus võimalik lahendus, kuid väiksemate kursuseprojektide puhul on see tavaliselt piisavalt lihtne ja töökindel.

Mängu arhitektuur

Mitme mängijaga mängu saab üles ehitada mitmel viisil. Siin vaatame kahte võimalikku lähenemist, mida saab kasutada klient-server loogika planeerimisel.

Esimene lähenemine

Projektis BoxheadTypeGame kasutatakse maailma mudelit ehk World klassi. See klass hoiab mängu hetkeseisu ning on olemas nii kliendi kui ka serveri poolel.

World klass vastutab näiteks järgmiste tegevuste eest:

  • kaardi ja objektide loomine;

  • mängijate ja vastaste haldamine;

  • kuulide ja kokkupõrgete töötlemine.

Selline lahendus võib sobida väiksema projekti või prototüübi jaoks, sest struktuur on lihtne ja kiiresti arusaadav. Samas võib projekti kasvades üks suur World klass muutuda raskesti hallatavaks.

Teine lähenemine

Projektis Example game on struktuur jagatud mitmeks mooduliks:

  • core: mängu põhilogika, näiteks mängija, areeni ja ekraanide haldamine;

  • server: serveripoolne loogika, võrguühendus ja mänguseisundi uuendamine;

  • shared: ühised klassid ja sõnumid, mida kasutavad nii klient kui server.

Selline lähenemine on modulaarsem. See teeb projekti paremini loetavaks ja lihtsustab meeskonnatööd, sest erinevad osad on üksteisest selgemalt eraldatud.

Andmete liikumise skeem klientide ja serveri vahel modulaarse arhitektuuri korral.

Andmete liikumise skeem klientide ja serveri vahel modulaarse arhitektuuri korral.

Skeemil on näidatud üks võimalik andmete liikumise viis:

  1. Mängija vajutab klaviatuuri või hiire nuppe. Sisendit töötleb PlayerInputManager.

  2. Sisendist luuakse sõnum, näiteks PlayerMovementMessage või PlayerShootingMessage.

  3. Sõnum saadetakse serverisse, kus seda töötlevad vastavad kuulajad, näiteks PlayerMovementListener ja PlayerShootingListener.

  4. Server uuendab mänguseisundit ning saadab vajaliku info klientidele tagasi.

Probleemid ja optimeerimine

Võrgusuhtluses võib probleem tekkida siis, kui serverisse saadetakse liiga palju infot liiga kiiresti. Näiteks ei ole mõistlik saata eraldi paketti iga väikese hiireliigutuse või iga üksiku kaadrimuutuse kohta. Liigne pakettide hulk võib võrku koormata ja põhjustada mängus viivitust.

Kursuseprojektid on tavaliselt väiksema mahuga, mistõttu ei pruugi see probleem kohe välja tulla. Sellegipoolest tasub juba alguses mõelda, mida on tegelikult vaja üle võrgu saata ja kui tihti seda teha.

Võimalikud lihtsad optimeerimised on näiteks:

  • saata pakette kindla intervalliga, mitte igal kaadril;

  • saata ainult muutunud andmeid;

  • hoida paketid võimalikult väikesed;

  • vältida ebavajalike objektide või pikkade andmestruktuuride saatmist.

Võimalused Javas kliendi-serveri loomiseks

Javas saab kliendi ja serveri vahelist suhtlust realiseerida mitme teegi abil. Selle kursuse raames on peamine soovitus kasutada Kryoneti.

Kryonet on kursuseprojekti jaoks hea valik, sest:

  • seda on lihtne seadistada;

  • see sobib väiksematele mänguprojektidele;

  • see võimaldab saata Java objekte klientide ja serveri vahel;

  • suurem osa kursuse juhenditest ja näidetest kasutab Kryoneti;

  • abiõppejõududel on selle teegiga varasem kogemus.

Kryoneti näited on hea koht, kust alustada, kui teegi kasutamisel tekib küsimusi.

Netty on samuti Java võrguraamistik, kuid see on madalama taseme ja keerulisema seadistusega. Netty sobib pigem suurematele või tehniliselt keerukamatele võrgurakendustele. Kursuseprojekti jaoks ei ole Netty üldjuhul vajalik.

Kryoneti ja Netty lihtsustatud võrdlus:

Omadus

Kryonet

Netty

Lihtsus

Lihtne kasutada ja seadistada

Rohkem käsitsi seadistamist

Abstraktsioonitase

Kõrgem, palju detaile on peidetud

Madalam, annab rohkem kontrolli

Sobib

Väiksematele mängudele ja prototüüpidele

Suurematele või keerukamatele võrgurakendustele

Kursuse tugi

Enamik juhendeid ja näiteid kasutab Kryoneti

Kursusematerjalides pigem kõrvaline

Seetõttu on kursuseprojekti puhul mõistlik eelistada Kryoneti.

Võrgupõhimõtted ja protokollid

Klient ja server suhtlevad omavahel pakettide kaudu. Pakett on väike andmeüksus, mille kaudu saadetakse näiteks mängija asukoht, laskmise info või mänguseisundi muudatus.

Võrgusuhtluses kasutatakse IP-aadresse ja porte. IP-aadress määrab, millise seadmega ühendust luuakse, ning port määrab, milline rakendus või teenus selle ühenduse vastu võtab.

Mänguprojektis kasutatakse tavaliselt TCP ja UDP protokolle. TCP sobib olukordadesse, kus oluline on andmete usaldusväärne kohalejõudmine. UDP sobib olukordadesse, kus olulisem on kiirus ja üksikute pakettide kadumine ei riku mängu toimimist.

Protokoll

TCP

UDP

Eelis

Usaldusväärne, õige järjekord

Kiire

Puudus

Suurem viivitus

Võib kaduda / vale järjekord

Millal kasutada

Kui täpsus on oluline

Kui kiirus on olulisem

Näide mängust

Ühendus, oleku muutused

Kuulid, efektid

Kliendi ja serveri realiseerimine

Serveri poolel tuleb luua serveriühendus ja määrata pordid, mille kaudu kliendid saavad serveriga suhelda. Näiteprojektis on vastav fail siin.

Server class

Server class

Kliendi poolel tuleb määrata serveri IP-aadress ja pordid, kuhu ühendus luuakse. Näiteprojektis on kliendipoolse ühenduse näide siin.

Paketid ja andmeedastus

Kui ühendus on loodud, tuleb määrata, millist infot klient ja server omavahel vahetavad. Selleks kasutatakse pakette.

Pakett võiks sisaldada ainult seda infot, mida konkreetse tegevuse jaoks vaja on. Näiteks mängija liikumise pakett võib sisaldada mängija koordinaate ja liikumissuunda. Laskmise pakett võib sisaldada kuuli alguspunkti, suunda ja kahjustust.

Näiteprojektis on paketid eraldi Java klassidena: pakettide näited.

Pakette tasub hoida võimalikult väikestena, sest väiksemaid andmeid saab kiiremini saata ja töödelda. Samuti on hea hoida pakettide struktuur ühtlane. Näiteks võib luua ühise abstraktse baasklassi, mida kõik paketiklassid laiendavad.

Pakettide töötlemine serveris

Server peab vastu võtma klientide saadetud paketid ja nende põhjal mänguseisundit uuendama. Kryonetis kasutatakse selleks kuulajat (listener).

server.addListener(new Listener() {
    public void received(Connection connection, Object object) {
        // Pakettide töötlemise loogika läheb siia.
    }
});

Kui server saab paketi, kontrollitakse selle tüüpi ning käivitatakse vastav loogika. Näiteks ühenduse loomise pakett võib lisada uue mängija, liikumise pakett võib uuendada mängija asukohta ning laskmise pakett võib luua uue kuuli.

Allolev näide pärineb varasemast projektist ja näitab, kuidas server võtab pakette vastu ning teeb nende põhjal mängumaailmas muudatusi.

  // 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:

Reaalsed projektid: