Käigupõhise mängu eripärad

Käigupõhisus on mänguloogika, kus mängijad ei käi samaaegselt vaid kordamööda. Ehk üks mängija korraga valib oma tegevuse enne teisi ning see käiguvalik on teistele mängijatele nähtav, et ajavahe ei omandaks strateegilist mõju.

Seetõttu on käigupõhised mängud sageli:

  • Taktikalisemad: Mängijad saavad oma samme hoolikalt ette planeerida ja vastase tegevusi hoolikamalt jälgida (näiteks male ja pokker).

  • Sobivad aeglasemate refleksidega mängijatele: Kiire reageerimine vahetult toimuvatele sündmustele ei ole osa strateegiast.

../_images/turnbased.gif

Ebarealistlik võistluse stseen, sest mängijste käigukord ei toimu samal ajal.

Käigupõhised mängud kipuvad olema:

  • Reegliterohked.

  • Aeglasema tempoga multiplayer-mängudes: Pikemate ooteaegade vältimiseks on soovitatav rakendada ajapiiranguid. Mängijad, kes limiidist üle lähevad, kaotavad oma käigu.

  • Üksluisemad pärast mitmeid läbimängimisi: Käigumustrid muutuvad sageli korduvaks ja seetõttu ettearvatavaks.

Alternatiivid "üks mängija, üks käik" struktuurile

  1. Ajapõhine käikude arv: Käigu jooksul on lubatud teha ükskõik kui palju käike, kuid kindla ajavahemiku piires.

  2. Fikseeritud arv käike: Ühe käigu asemel võib olla suurem, kuid fikseeritud arv mängukäike.

  3. Progressikellad: visualiseerimaks sündmusi, mis kestavad kauem kui üks käik. Need aitavad mängijatel jälgida pikaajalisi arenguid, nagu tegelase edenemine lõppeesmärgi suunas, vastase lähenemise või ajaliselt piiratud võimaluste avamine.

../_images/progress_clock.png ../_images/progress_clock1.png

Progressikellade näide nii võrdlev kell kui ka Inventory laadne.

Käigukorra järjekord

  1. Liitumisjärjekorra alusel.

  2. Rollide järgi ehk mängusisene hierarhia määrab käigujärjekorra (näiteks "Dungeons & Dragons").

  3. Juhuslikkuse alusel, näiteks täringuviskega mängus "Wizard101".

Ettearvatavuse vähendamine

  • Käigukorra muutmine: “Dungeons & Dragons” mängus on rünnak “attacks of opportunity” mängijatele võimalust käia oma käik enne oma korda ette ära.

  • Käigupõhise ja reaalajas mängu hübriid: sarnaselt vabalöögiga jalgpallis ja vabaviskega korvpallis on niimoodi strateegiate kombineerimise eesmärk teha taktikalisemaks mängufaase, millest mängutulemus suuresti sõltub ja dünaamilisemaks teise näiteks ringiuitamist. Näiteks “Fallout”’is on lahingufaas käigupõhine, kuid ülejäänud mäng toimub reaalajas.

  • Mängukäikudele lisapiirangute ja -võimaluste seadmine: erilised käikud, näiteks “King Arthur: The Role-Playing Wargame” saab hooneid ehitada vaid talvel, mis saabub iga neljanda käigu järel. Sõjamängus “Napoleon” on iga mängija kolmas mängukäik “öökäik,” mille jooksul lahingupidamine pole lubatud.

../_images/combat.png

Muidu reaalajas toimuv mäng muudetakse käigupõhiseks võistluse stseeni ajal, et paremat sihtimist võimaldada.

Serveri kood käigupõhiseks virtuaalseks lauamänguks

Packetite ja Player klassi loomine

Kõigepealt loome 4 Packetit ja Player klass. Käigupõhist kaardimängu luues pole vaja mängija koordinaate saata ehk PositionPacket on üleliigne.

  1. ConnectionPacket

  2. PositionPacket

  3. ScorePacket

  4. CurrentActivePlayerPacket

ConnectionPacket ja CurrentActivePlayerPacket sisaldavad ühte muutujat, milleks on int id muutujat ja meetodeid getId ning setId.

PositionPacket ja ScorePacket sisaldavad kõik kahte muutujat. PositionPacket sisaldab int id ja Vector2 (mängija x ja y koordinaadid) ning mõlemal muutujal on get ja set meetodid. ScorePacket'il on int id ja int score muutujad, kuid ühtegi meetodi jaoks pole vajadust.

Player klass

Muutujad - private Vector2 position - private int id - private boolean yourTurn = false - private int currentPoints (algväärtuse seame konstruktoris nulliks)

Kõigile muutujatele peale yourTurn loome lisaks harilikul viisil get ja set meetodid väljaarvatud setCurrentPoints meetod, kus punkte määratakse juurdeliitmisega olemasolevale punktisummale. Loome ka updatePosition meetodi, sest kliendi pool täpsustame x ja y koordinaate.

public class Player {
    private Vector2 position;  // lisaks get ja set meetod
    private int id;  // lisaks get ja set meetod
    private int currentPoints;  // lisaks get meetod
    private boolean yourTurn = false

    public Player() {
        this.currentPoints = 0;
    }
    public void setCurrentPoints(int points) {
        this.currentPoints += points;
    }
    public void flipTurn() {
        yourTurn = !yourTurn;
    }
    public void updatePosition(Vector2 position) {
        this.position = position;
    }
}

ChatServer klass

Muutujad - private static Server server - private HashMap<Integer, Player> playersId = new HashMap<>() - private int currentActiveIndex = 0 - private boolean gameStarted = false

ChatServer konstruktoris loome uue serveri instantsi ja registreerime selle Network'i.

public ChatServer() throws IOException {
   Log.set(Log.LEVEL_WARN);
   server = new Server();
   Network.register(server);

Chatserver konstruktorisse panema ka Event Listener'i, mille sees on kolm meetodit:

  1. connected

  2. received

  3. disconnected

Connected meetod

Alustuseks Loome uue Player instantsi ja lisame ta playersId HashMap'i ning saadame ConnectionPacket'i teistele mängijatele.

public void connected(Connection c) {
    if (!gameStarted) {
        Player player = new Player();
        player.setId(c.getID());
        playersId.put(c.getID(), player);
        ConnectionPacket packet = new ConnectionPacket();
        packet.setId(c.getID());
        // teavitame teisi uuest mängijast
        server.sendToAllExceptTCP(c.getID(), packet);

        for (Player player : playersId.values()) {
            if (player.getId() != c.getID()) {
               // saadame teiste mängijate positsioonid uuele mängijale
               ConnectionPacket connectionPacket = new ConnectionPacket();
               connectionPacket.setId(player.getId());
               server.sendToTCP(c.getID(), connectionPacket);
            }
        }
    }
}

connected meetod lõppeb ühenduse sulgemisega kui proovitakse uut mängu alustada, serveris, kus juba mäng käib.

} else {
    c.close();
}

Disconnected meetod

Siin ühtegi Packet'it ei saada.

public void disconnected(Connection c) {
    playersId.remove(c.getID());
    sendMessageToAllExcept(c.getID(), "User #%d has left the chat.".formatted(c.getID()));
    if (playersId.isEmpty()) {
        playersId = new HashMap<>();
        currentActiveIndex = 0;
    }
}

Received meetod

Sama Event Listeneri sees kontrollime, kas kliendi poolt on saadetud PositionPacket, et uuendada mängija asukohta ja jagada seda asukohta teiste mängijatega.

if (o instanceof PositionPacket pkg) {
   Player player = playersId.get(c.getID());
   Vector2 vector2 = pkg.getVector2();
   player.updatePosition(vector2);
   server.sendToAllExceptTCP(c.getID(), pkg);
}

ScorePacket, et uuendada nii mängija skoori, kui ka määrata, millise mängija kord nüüd on käia (ehk saadame ka CurrentActivePlayerPacket'i), sest ScorePacket saadetakse käigu lõpus.

if (o instanceof ScorePacket pkg) {
   Player player = playersId.get(pkg.id);
   player.setCurrentPoints(pkg.score);

   ScorePacket scorePacket = new ScorePacket();
   scorePacket.id = pkg.id;
   scorePacket.score = pkg.score;
   server.sendToAllExceptTCP(c.getID(), scorePacket);
   currentActiveIndex = (currentActiveIndex + 1) % playersId.size();
   List<Integer> id = new ArrayList<>(playersId.keySet());
   int currentId = id.get(currentActiveIndex);

   CurrentActivePlayerPacket packet = new CurrentActivePlayerPacket();
   packet.setId(currentId);
   server.sendToAllUDP(packet);
}

Seejärel saame Event Listener’i sulgeda ning lisame konstruktorisse veel serveri käivitamise.

server.start();
server.bind(Network.TCP_PORT, Network.UTP_PORT);

Peale konstruktori vajab ChatServer klass lisaks veel sendMessageToAllExcept meetodit.

private void sendMessageToAllExcept(int clientId, String text) {
    Network.Message msg = new Network.Message();
    msg.text = text;
    server.sendToAllExceptUDP(clientId, msg);
}

Ära unusta Network klassis kyro.register teha kõigile Packetitele, Player klassile ning Chatserver'ile.

Kliendi kood käigupõhiseks virtuaalseks lauamänguks

Kõik Packetid ja Player klass, mida serveri repository's kasutasime on identsed kliendipoolsega. Vaid updatePosition meetod on kliendi klassis teistsugune.

public void updatePosition(float x, float y) {
        this.position.x = x;
        this.position.y = y;
}

MyClient klass

Loome MyClient klassi ChatServer klassiga suhtlemiseks. kui serveri pool oli kolm meetodit Event Listener'i all, siis kliendi pool on vaid received meetod, kuid siid väljaspool konstrukorit on siin klassis rohkem get ja set meetodeid.

Muutujad - private final Client client - private final int clientID - private boolean yourTurn = false - private boolean firstPlayer = false - private final HashMap<Integer, Player> playersId = new HashMap<>()

public MyClient(LobbyScreen lobbyScreen) {
    client = new Client();
    Network.register(client);

    client.addListener(new Listener.ThreadedListener(new Listener() {
    @Override
    public void received(Connection connection, Object object) {
        if (object instanceof ConnectionPacket pkg) {
            Player Player = new Player();
            Player.setId(pkg.getId());
            playersId.put(pkg.getId(), Player);
        }
        if (object instanceof PositionPacket pkg) {
            Player player = playersId.get(pkg.getId());
            if (player != null) {
                ee.taltech.mygdxgame.packets.Vector2 vector2 = pkg.getVector2();
                float x = vector2.x;
                float y = vector2.y;
                player.updatePosition(x, y);
            }
        }
        if (object instanceof ScorePacket pkg) {
            int score = pkg.score;
            int id = pkg.id;

            Player player = playersId.get(id);
            player.setCurrentPoints(score);
        }
        if (object instanceof CurrentActivePlayerPacket pkg) {
            yourTurn = clientID == pkg.getId();
        }
    }
}));
client.start();

try {
    client.connect(5000, "193.40.255.27", 8080, 8081);
} catch (IOException e) {
    throw new RuntimeException(e);
}
clientID = client.getID();
}
public int getID() {
    return client.getID();
}
public void sendUDP(Object object) {
    client.sendUDP(object);
}
public void sendTCP(Object object) {
    client.sendTCP(object);
}
public Map<Integer, Player> getPlayersId() {
    return playersId;
}
public boolean isYourTurn() {
    return yourTurn;
}
public Client getClient() {
    return client;
}

Põhimängu klass

Mängijate positsiooni uuendamine ja joonistamine toimub mängu põhiklassis. Kõigepealt on vaja muutujat private Map<int, Player> playersId.

Meil on vaja render meetodis enne mängija käiku (ehk enne batch.begin()) mängija asukohta uuendada, saates PositionPacketi.

if (client.getPlayersId().size() > 0) {
    client.getPlayersId().get(clientID).setPosition(this.x, this.y);
    PositionPacket positionPacket = new PositionPacket();
    positionPacket.setId(clientID);
    positionPacket.setVector2(new ee.taltech.mygdxgame.packets.Vector2(this.x, this.y));
    client.sendUDP(positionPacket);
}

Peale mängija käiguvalikut on vaja saata ScorePacket.

ScorePacket scorePkg = new ScorePacket();
scorePkg.id = clientID;
scorePkg.score = 0;
getClient().sendTCP(scorePkg);

Järgmisena saab kõiki mängijaid luua, andes neule uue positsiooni Player klassi meetodi kaudu (järgnevas koodis on kahe mängija ja solo-player positsiooni uuendamise näide).

for (Map.Entry<Integer, Player> entry : client.getPlayersId().entrySet()) {
    int id = entry.getKey();
    Player player = entry.getValue();

    float playerX = client.getPlayersId().get(id).getPosition().x;
    float playerY = client.getPlayersId().get(id).getPosition().y;
    if (client.getPlayersId().size() == 2) {
        player = new Player();
        player.setTexture(pic2);
        player.setPosition(new Vector2(playerX, playerY));
    } else {
        player.updatePosition(playerX, playerY);
        player.setTexture(pic1);
    }
}

Siis saame mängijaid joonistama hakata nende asukoha järgi.

batch.begin();
for (Player player : client.getPlayersId().values()) {
Vector2 playerPosition = player.getPosition();
    if (client.getPlayersId() == 1) {
        batch.draw(pic1, playerPosition.x, playerPosition.y);
    }
    if (client.getPlayersId() == 2) {
        batch.draw(pic2, playerPosition.x, playerPosition.y);
    }
}
batch.end()

PS! Hetkel joonistame render meetodis mängijad iga kord uuesti Texture (PNG) järgi. Protsessori-sõbralikum oleks:

  1. Luua üks Atlas-fail kõigi karakterivalikute Texture'itega.

  2. Lobby Screenil teha (Event) ChangeListener, mis vahetab Actor'eid mängija karakterivaliku järgi.

  3. Teha Sprite'i päriv klass, kus super märgusõnaga konstruktoris saab Atlas-failist leida valitud karakteri screen.getAtlas().findRegion()'iga.

  4. Main klassis teha SpriteBatchile muutuja.

  5. Siis saame peamise mänguklassi render'is kõiki mängijaid joonistada üherealise koodiga: main.getPlayers().getValue().getSprite().draw(main.getBatch()).