Lobby (uus)

1. Sissejuhatus

1.1. Kirjeldus

See juhend kirjeldab kuidas luua lihtsama lobbi kasutades LibGDX ja Kryonet'i

Kui tekkivad probleemid, siis saab avada originaalkoodi ja otsida mis läks valesti.

1.2. Milleks on lobbi vaja?

Lobbi on oluline osa multiplayer'i loomiseks mängudes. Lobbi annab meile võimalust, et teha mitu mängi instantsi, et kõik mängijad saaksid mängida eraldi teistest mängijatest

1.3. Lisa info

Kasulikud materjalid:

2. Mängija registreerimine

images/registreeri.png

2.1. Loo vormi mängija loomiseks

Esialguks kliendi kaustas on vaja luua uue pakki screen ja lisada uue klassi MainScreen. See ekraan on mõeldud mängijate loomiseks. Loo sinna uue stage'i ja lisa 3 toimijad (Label, TextField ja TextButton)

MainScreen.java

@Override
public void show() {
    Stage stage = new Stage();
    Gdx.input.setInputProcessor(stage);
    Label label = new Label("Write your name:", skin);
    TextField field = new TextField("", skin);
    TextButton button = new TextButton("START", skin);
    button.addListener(new RegisterPlayerClickListener(field.getName()));
    Table table = new Table();
    table.setFillParent(true);
    table.defaults().space(10);
    table.add(label).uniform().fillX();
    table.row();
    table.add(field);
    table.row();
    table.add(button).uniform().fillX();

    stage.addActor(table);
}

@Override
public void render(float delta) {
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    Gdx.gl.glClearColor(0.157f, 0.196f, 0.522f, 1);
    stage.act(delta);
    stage.draw();
}

2.2. Saada UDP päringu serverile

Enne päringu saatmist peame serveriga ühenduma. Selleks loome uue ClientLauncher klassi

2.2.1 ClientLauncher

ClientLauncher.java

 public class ClientLauncher extends Client {

    private static ClientLauncher instance;

    private ClientLauncher() {
        KryoHelper.registerClasses(getKryo());
        connectToServer();
    }

    public static ClientLauncher getInstance() {
        if (instance == null) {
            instance = new ClientLauncher();
        }
        return instance;
    }

    public void connectToServer() {
        start();
        try {
            connect(DEFAULT_CONNECTION_TO_SERVER_TIMEOUT, DEFAULT_HOST, DEFAULT_TCP_PORT, DEFAULT_UDP_PORT);
        } catch (IOException e) {
            System.out.println(e);
        }
    }
}

2.2.2. Constants

Lisa Constants klassi shared kausta, et hoida konstantseid väärtusi

Constants.java

 public class Constants {

    public static final int DEFAULT_CONNECTION_TO_SERVER_TIMEOUT = 5000;
    public static final int DEFAULT_TCP_PORT = 8080;
    public static final int DEFAULT_UDP_PORT = 8081;
    public static final String DEFAULT_HOST = "localhost";
}

2.3. Töötle nuppu vajutamist

Nüüd lisa uue klassi RegisterPlayerClickListener, mis pärib ClickListener ja hoiab endas TextField field.

RegisterPlayerClickListener.java

 @AllArgsConstructor
public class RegisterPlayerClickListener extends ClickListener {

    private TextField field;

    @Override
    public void clicked(InputEvent event, float x, float y) {
        String username = field.getText();
        if (username.isBlank()) return;
        ClientLauncher.getInstance().sendUDP(new RegisterPlayerPacket(username));
    }
}

Kui ClientLauncher.getInstance() on kasutatud esimest korda, siis luuakse uue ClientLauncher klassist objekti ja kohe ühendatakse serveriga ja saadatakse päringu.

2.4. Registeeri klassid kasutades Kryo

Kui praegu proovida seda koodi, siis tuleb viga. Selleks, et seda parandada peab Kryonet'i registreerima RegisterPlayerPacket. Loome uue KryoHelper klassi shared kausta.

KryoHelper.java

 public class KryoHelper {

    public static void registerClasses(Kryo kryo) {
        kryo.register(RegisterPlayerPacket.class);
        // Register more classes here.
    }
}

*Kõik klassid, mis saadetakse kliendilt serverile ja serverilt kliendile peavad olema registreeritud.

Kasuta see meetod serveril ja kliendil.

ClientLauncher.java

private ClientLauncher() {
   KryoHelper.registerClasses(getKryo());
   ...
}

ServerLauncher.java

public ServerLauncher() {
   KryoHelper.registerClasses(getKryo());
   ...
}

Nüüd klassid registreeritakse nii serveril, kui ka kliendil.

2.5. Lombok

Selles juhendis hakkan kasutama Lombok

Selleks, et lisada Lombok ava build.gradle faili ja lisa dependencies alla

build.gradle

annotationProcessor "org.projectlombok:lombok:1.18.34"
compileOnly "org.projectlombok:lombok:1.18.34"

*Võid versiooni muuta uueks, kui on vajadus

2.6. Päringu töötlemine serveris

Lisame uue ServerListener klassi.

ServerListener.java

public class ServerListener implements Listener {

   @Override
   public void received(Connection connection, Object object) {
       switch (object) {
           case RegisterPlayerPacket packet -> // if object is instance of RegisterPlayerPacket
               ServerLauncher.getInstance().registerPlayer(connection.getID(), packet.getName());
           default ->
               // leave it like this
               System.out.println("PACKET SKIPPED");
       }
   }
}

ServerLauncher.java

public ServerLauncher() {
   ...
   addListener(new ServerListener());
}

Ja lisa shared kausta mängija klassi.

Player.java

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Player {

    private int id;
    private String name;
}

2.7. Vastuse töötlemine kliendis

Lisame uue ClientListener klassi.

ClientListener.java

public class ClientListener implements Listener {

    @Override
    public void received(Connection connection, Object object) {
        switch (object) {
            case RegisterPlayerPacket packet ->
                Main.getInstance().createPlayer(connection.getID(), packet.getName());
            default -> System.out.println("Skipped package");
        }
    }
}

ClientLauncher.java

private ClientLauncher() {
    addListener(new ClientListener());
    ...
}

Nüüd salvestame mängija ja muudame ekraani.

Main.java

private Player currentPlayer;

public void createPlayer(int id, String username) {
    currentPlayer = new Player(id, username);
    Gdx.app.postRunnable(new SetScreenRunnable(new LobbiesListScreen()));
}

2.8. Kasuta Runnable ekraanide muutmiseks

SetScreenRunnable.java

@AllArgsConstructor
public class SetScreenRunnable implements Runnable {

    private Screen screen;

    @Override
    public void run() {
        Main.getInstance().setScreen(screen);
    }
}

*Runnable kasutamine on kohustuslik, sest mäng ja kryonet töötavad erinevatel Thread'idel


3. Loo uus lobbi

Järgmiseks etappiks realiseerime lobbide loomine.

images/loo.png

3.1. Lisa lobbi class

Loo lobby klass shared kaustas.

Lobby.java

@Getter
@NoArgsConstructor
public class Lobby {

    private static int nextId = 0;

    private int id;
    private String name;
    private Map<Integer, Player> players;

    public Lobby(Player player) {
        this.id = nextId++;
        this.name = generateLobbyName(player);
        this.players = new HashMap<>();
        joinLobby(player);
    }

    public void joinLobby(Player player) {
        players.put(player.getId(), player);
    }

    private String generateLobbyName(Player player) {
        return String.format("%s's lobby", player.getName());
    }
}

3.2. Töötle nuppu vajutamist

LobbiesListScreen.java

@Override
protected void createInterface() {
   ...
   TextButton addLobbyButton = new TextButton("Add Lobby", skin);
   addLobbyButton.addListener(new CreateLobbyClickListener());
   ...
}

CreateLobbyClickListener.java

public class CreateLobbyClickListener extends ClickListener {

    @Override
    public void clicked(InputEvent event, float x, float y) {
        ClientLauncher.getInstance().sendUDP(new CreateLobbyPacket());
    }
}

Saadame lihtsalt tühja CreateLobbyPacket, sest meile polegi midagi vaja välja arvatud mängija ID, mida me saame koos päringuga vaikimisi.

CreateLobbyPacket.java

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class CreateLobbyPacket {

    private Lobby lobby;
}

*Hoiame lobby muutuja vastuseks serverilt. Kuid päringuks saadame tühja paketti

3.3. Päringu töötlemine serveris

ServerListener.java

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ...
        case CreateLobbyPacket packet ->
                ServerLauncher.getInstance().getGame().createLobby(connection.getID());
        ...
    }
}

Lisame uue kujutise lobbideks, et hoida neid serveril.

Game.java

private Map<Integer, Lobby> lobbies = new HashMap<>();

public void createLobby(int id) {
    Player player = players.get(id);
    Lobby lobby = new Lobby(player);
    lobbies.put(lobby.getId(), lobby);
    ServerLauncher.getInstance().sendToUDP(id, new CreateLobbyPacket(lobby));
}

3.4. Vastuse töötlemine kliendis

ClientListener.java

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ...
        case CreateLobbyPacket packet ->
                Main.getInstance().joinLobby(Main.getInstance().getCurrentPlayer(), packet.getLobby());
        ...
    }
}

Main.java

public void joinLobby(Lobby lobby) {
    currentLobby = lobby;
    Gdx.app.postRunnable(new SetScreenRunnable(new LobbyScreen(currentLobby)));
}

4. Lobbist väljumine

Lobbist väljumise implementatsioon on peaaegu sama nagu lobbi loomine

images/valju.png

4.1. Töötle nuppu vajutamist

LobbyScreen.java

@Override
protected void createInterface() {
   ...
   TextButton backButton = new TextButton("", skin);
   backButton.addListener(new LeaveLobbyClickListener(lobby.getId()));
   ...
 }

LeaveLobbyClickListener.java

@AllArgsConstructor
public class LeaveLobbyClickListener extends ClickListener {

    private int id;

    @Override
    public void clicked(InputEvent event, float x, float y) {
        ClientLauncher.getInstance().sendUDP(new LeaveLobbyPacket(id));
    }
}

4.2. Töötle nuppu vajutamist

Main.java

public void removePlayer(int id) {
    currentLobby = null;
    Gdx.app.postRunnable(new SetScreenRunnable(new LobbiesListScreen()));
}

4.3. Päringu töötlemine serveris

ServerListener.java

@Override
   public void received(Connection connection, Object object) {
   switch (object) {
      ...
      case LeaveLobbyPacket packet ->
             ServerLauncher.getInstance().getGame().leaveLobby(connection.getID(), packet.getId());
      ...
   }
}

Game.java

public void leaveLobby(int playerId, int lobbyId) {
    Lobby lobby = lobbies.get(lobbyId);
    lobby.kickPlayer(playerId);
    if (lobby.getPlayersNumber() == 0) {
        lobbies.remove(lobbyId);
    }
    ServerLauncher.getInstance().sendToUDP(playerId, new LeaveLobbyPacket(playerId));
}

4.4. Lobbi klassi värskendamine

Lobby.java

public void kickPlayer(int playerId) {
    players.remove(playerId);
}

public int getPlayersNumber() {
    return players.keySet().size();
}

*See pole kohustuslik, et saata midagi tagasi kliendile, sest lobbi väljumist me saame töötelda ka kliendis.


5. Näita lobbid

images/naita.png

5.1. Hoia muutujad

Selleks, et näidata lobbid, peame hoidma järgmised muutujaid

LobbiesListScreen.java

private Table lobbies;
private Map<Integer, Table> lobbyActors = new HashMap<>();

* lobbies tabel hakkab hoidma endas toimijad. Ja lobbyActors kujutis on selleks, et oleks lihtsam leida need toimijaid ja kustutada neid.

5.2. Küsi lobbid

Pärast seda, kui sa said LobbiesListScreen, küsi serverilt näidata lobbid

LobbiesListScreen.java

@Override
protected void createInterface() {
    ...
    ClientLauncher.getInstance().sendUDP(new GetLobbiesPacket());
}

GetLobbiesPacket.java

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class GetLobbiesPacket implements Packet {

    private Map<Integer, Lobby> lobbies;
}

*Kui me saadame serverile päringut, siis me saadame tühja paketti, vastuseks saame sama paketti, aga juba koos lobbidega

5.3. Päringu töötlemine serveris

ServerListener.java

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ...
        case GetLobbiesPacket packet ->
                ServerLauncher.getInstance().getGame().getLobbies(connection.getID());
        ...
    }
}

Game.java

public void getLobbies(int id) {
    ServerLauncher.getInstance().sendToUDP(id, new GetLobbiesPacket(lobbies));
}

5.4. Koodi värskendamine

Nüüd on vaja natuke muuta meie koodi. Täpsemalt, värskendame createLobby koodi. Pärast lobbi loomist saadame kõikidele lobbi otsijatele uue lobbi.

public void createLobby(int id) {
    ...
    ServerLauncher.getInstance().sendToAllExceptUDP(id, new GetLobbiesPacket(lobbies));
}

5.5. Optimiseerimine

Praegu saadetakse paketti kõigidele mängijatele välja arvatud mängija kes lõi uue lobbi. Selleks, et seda optimiseerida, saaks lisada mängijale uue muutuja, mis hoiaks tema oleku ja kui olek oleks näiteks State.SEARCH siis saata temale paketti.

5.6. Vastuse töötlemine kliendil

ClientListener.java

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ...
        case GetLobbiesPacket packet ->
                Main.getInstance().updateLobbies(packet.getLobbies());
        ...
    }
}

Esialgus vaatame kas mängija on praegu LobbiesListScreen ekraanil, et vältida vigu ja siis lisame uue lobbi ekraanile.

Main.java

public void updateLobbies(Map<Integer, Lobby> lobbies) {
    if (getScreen() instanceof LobbiesListScreen lobbiesListScreen) {
        lobbiesListScreen.clearLobbies();
        for (Lobby lobby : lobbies.values()) {
            lobbiesListScreen.addLobby(lobby);
        }
    }
}

LobbiesListScreen.java

private Table lobbies;
private final Map<Integer, Table> lobbyActors = new HashMap<>();

public void addLobby(Lobby lobby) {
    TextButton lobbyButton = new TextButton(lobby.getName(), skin);
    lobbies.add(lobbyButton).expandX().fillX();
    lobbies.row();
    lobbyActors.put(lobby.getId(), lobbyButton);
}

public void removeLobby(int lobbyId) {
    Table lobby = lobbyActors.remove(lobbyId);
    lobbies.removeActor(lobby);
}

public void clearLobbies() {
    lobbies.clear();
    lobbyActors.clear();
}

6. Lobbi eemaldamine ekraanilt

6.1 Koodi värskendamine

Kui lobbis on 0 mängijad, siis on vaja seda kustutada ekraanilt.

Game.java

public void leaveLobby(int playerId, int lobbyId) {
    ...
    if (lobby.getPlayersNumber() == 0) {
        ...
        ServerLauncher.getInstance().sendToAllExceptUDP(playerId, new DeleteLobbyPacket(lobbyId));
    }
}

6.2. Vastuse töötlemine kliendis

ClientListener.java

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ...
        case DeleteLobbyPacket packet ->
                Main.getInstance().deleteLobby(packet.getLobbyId());
        ...
    }
}

Main.java

public void deleteLobby(int lobbyId) {
    if (getScreen() instanceof LobbiesListScreen lobbiesListScreen) {
        lobbiesListScreen.removeLobby(lobbyId);
    }
}

Nüüd lobbi kaob ekraanilt, kui seda kustutakse.


7. Lobbi sisenemine

images/sisenemine.png

7.1. Töötle ekraanis oleva lobbi vajutamist

JoinLobbyClickListener.java

@AllArgsConstructor
public class JoinLobbyClickListener extends ClickListener {

    private final int lobbyId;

    @Override
    public void clicked(InputEvent event, float x, float y) {
        ClientLauncher.getInstance().sendUDP(new JoinLobbyPacket(lobbyId));
    }
}

Lisame see listener iga lobbile.

LobbiesListScreen.java

public void addLobby(Lobby lobby) {
    TextButton lobbyButton = new TextButton(lobby.getName(), skin);
    lobbyButton.addListener(new JoinLobbyClickListener(lobby.getId()));
    ...
}

7.2. Päringu töötlemine serveris

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ...
        case JoinLobbyPacket packet ->
                ServerLauncher.getInstance().getGame().joinLobby(connection.getID(), packet.getLobbyId());
        ...
    }
}

Lisame mängija lobbisse ja saadame kõigidele mängijatele lobbis, et uus mängija sisenes.

Game.java

public void joinLobby(int id, int lobbyId) {
    Player player = players.get(id);
    Lobby lobby = lobbies.get(lobbyId);
    lobby.joinLobby(player);
    for (Player currentPlayer : lobby.getPlayers().values()) {
        ServerLauncher.getInstance().sendToUDP(currentPlayer.getId(), new PlayerJoinedLobbyPacket(player, lobby));
    }
}

PlayerJoinedLobbyPacket.java

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class PlayerJoinedLobbyPacket {

    private Player player;
    private Lobby lobby;
}

7.3. Vastuse töötlemine kliendis

7.3.1 Töötle vastust

ClientListener.java

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ....
        case PlayerJoinedLobbyPacket packet ->
                Main.getInstance().joinLobby(packet.getPlayer(), packet.getLobby());
        ...
    }
}

7.3.2 Koodi värskendamine

Main.java

public void joinLobby(Player player, Lobby lobby) {
    if (player.getId() == currentPlayer.getId()) {
        currentLobby = lobby;
        Gdx.app.postRunnable(new SetScreenRunnable(new LobbyScreen(currentLobby)));
    } else {
        if (getScreen() instanceof LobbyScreen lobbyScreen) {
            lobbyScreen.addPlayer(player);
        }
    }
}

LobbyScreen.java

private final Map<Integer, Actor> players = new HashMap<>();

public void addPlayer(Player player) {
    Label playerNameLabel = new Label(player.getName(), skin);
    playersTable.add(playerNameLabel).expandX().fillX();
    players.put(player.getId(), playerNameLabel);
    playersTable.row();
}

Ja kui mängija sisenes lobbisse, siis lisame teda kõigi mängijate ekraanidele.

LobbyScreen.java

@Override
protected void createInterface() {
    ...
    for (Player player : lobby.getPlayers().values()) {
        addPlayer(player);
    }
}

8. Väljunud mängija eemaldamine ekraanilt

8.1. Koodi värskendamine

Game.java

public void leaveLobby(int playerId, int lobbyId) {
    ...
    if (lobby.getPlayersNumber() == 0) {
        ...
    }
    ServerLauncher.getInstance().sendToAllUDP(new LeaveLobbyPacket(playerId));
}

Nüüd, kui mängija väljub lobbist, siis saadetakse sellest info teistele lobbis asuvatele mängijatele.

8.2. Vastuse töötlemine kliendis

ClientLauncher.java

@Override
public void received(Connection connection, Object object) {
    switch (object) {
        ...
        case LeaveLobbyPacket packet ->
                Main.getInstance().removePlayer(packet.getId());
        ...
    }
}

Main.java

public void removePlayer(int id) {
    if (id == ClientLauncher.getInstance().getID()) {
        currentLobby = null;
        Gdx.app.postRunnable(new SetScreenRunnable(new LobbiesListScreen()));
    } else {
        if (getScreen() instanceof LobbyScreen lobbyScreen) {
            lobbyScreen.removePlayer(id);
        }
    }
}

LobbyScreen.java

public void removePlayer(int id) {
    Actor player = players.remove(id);
    playersTable.removeActor(player);
}

Lõpuks peaks olema täiesti töötav lobbi, kuhu saavad mängijad siseneda ja väljuda sellest.

Kui tekkivad probleemid, siis saab avada originaalkoodi ja otsida mis läks valesti.

images/lobby.gif