binlee0903 / se13_team_project

GNU General Public License v2.0
1 stars 3 forks source link

OnlineTetrisServer 제작 #91

Closed hyotaime closed 6 months ago

hyotaime commented 6 months ago
hyotaime commented 6 months ago

현재 OnlineTetrisServer 진행사항입니다. TetrisGame과의 상호작용 pulse, observeState는 임시로 작성해두었습니다. Reference: #85

// ClientDTO.java
package org.se13.server;

public class ClientDTO {
    public ClientDTO(int userId, TetrisClient client) {
        this.userId = userId;
        this.client = client;
    }

    public int getUserId() {
        return userId;
    }
    public TetrisClient getClient() {
        return client;
    }

    private final int userId;
    private final TetrisClient client;
}

// OnlineTetrisServer.java
package org.se13.server;

import org.se13.view.tetris.TetrisState;
import org.se13.game.TetrisGame;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class OnlineTetrisServer implements TetrisServer {
    public TetrisServerImpl(TetrisGame game) {
        this.game = game;
    }

    public void createRoom(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public Socket waitForClient() throws IOException {
        return serverSocket.accept();
    }

    @Override
    public TetrisActionHandler connect(TetrisClient client) {
        clients.add(new ClientDTO(client.getUserId(), client));
        return this::handleAction;
    }

    @Override
    public void disconnect(TetrisClient client) {
        clients.removeIf(dto -> dto.getUserId() == client.getUserId());
    }

    @Override
    public void responseGameOver(int score, boolean isItemMode, String difficulty) {
        // 게임 종료 시, 클라이언트에게 게임 종료 메시지를 전송
    }

    private void handleAction(TetrisActionPacket packet) {
        game.pulse(packet.action());
        TetrisState state = game.observeState();
        clients.stream()
                .filter(dto -> dto.getUserId() == packet.userId())
                .findFirst().ifPresent(clientDTO -> clientDTO.getClient().response(state));
    }

    private ServerSocket serverSocket;
    private final List<ClientDTO> clients = new ArrayList<>();
    private final TetrisGame game;
}
hyotaime commented 6 months ago

LocalBattleTetrisServer를 기반으로 소켓을 더해 OnlineTetrisServer를 다시 작성하였습니다. 소켓통신을 컴퓨터네트워크에 배운걸 참고해서 작성해보긴 했는데 제대로 잘 작동할지 모르겠습니다... 클라이언트 부분은 TetrisClient.java의 repository를 server로 교체하여 구현할 계획입니다. 혹시나 제가 틀리게 작성하였거나 부족한 부분, 또는 개선할 점들이 있다면 코멘트 부탁드립니다. 참고해서 작성하도록 하겠습니다.

// OnlineTetrisServer.java
package org.se13.server;

import org.se13.game.action.TetrisAction;
import org.se13.game.event.*;
import org.se13.game.rule.GameLevel;
import org.se13.game.rule.GameMode;
import org.se13.game.tetris.TetrisGame;
import org.se13.view.tetris.TetrisGameEndData;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class OnlineTetrisServer implements TetrisServer {
    public OnlineTetrisServer(GameLevel level, GameMode mode, int port) throws IOException {
        this.level = level;
        this.mode = mode;
        this.tetrisTimer = new Timer();
        this.sessions = new HashMap<>();
        this.handlers = new HashMap<>();
        this.clientSockets = new ArrayList<>();
        this.serverSocket = new ServerSocket(port);
        startServer();
    }

    @Override
    public void responseGameOver(int score, boolean isItemMode, String difficulty) {
        endData = new LinkedList<>();
        sessions.forEach((playerId, session) -> {
            endData.add(session.stopBattleGame());
            session.stopGame();
        });
        tetrisTimer.cancel();
        broadcastToClients("Game Over");
    }

    @Override
    public TetrisActionHandler connect(TetrisClient client) {
        sessions.put(client.getUserId(), new TetrisSession(client, new TetrisGame(level, mode, this)));
        handlers.put(client.getUserId(), createHandlers());

        return packet -> {
            switch (packet.action()) {
                case START -> handleStartGame(packet.userId());
                case IMMEDIATE_BLOCK_PLACE,
                     ROTATE_BLOCK_CW,
                     MOVE_BLOCK_LEFT,
                     MOVE_BLOCK_DOWN,
                     MOVE_BLOCK_RIGHT -> handleInputAction(packet.userId(), packet.action());
            }
        };
    }

    @Override
    public void disconnect(TetrisClient client) {
        int userId = client.getUserId();
        sessions.remove(userId);
        // 클라이언트 소켓을 목록에서 제거
        Socket socket = clientSockets.remove(userId);
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void handleStartGame(int userId) {
        {
            TetrisSession session = sessions.get(userId);
            if (session == null) {
                broadcast(new ServerErrorEvent("세션이 종료되었습니다."), userId);
                return;
            }

            if (session.isPlayerReady()) {
                broadcast(new ServerErrorEvent("이미 레디중입니다."), userId);
                return;
            }

            session.setReady(true);
        }

        AtomicBoolean isAllPlayerReady = new AtomicBoolean(true);

        sessions.forEach((_userId, session) -> {
            if (!session.isPlayerReady()) {
                isAllPlayerReady.set(false);
            }
        });

        if (isAllPlayerReady.get()) {
            sessions.forEach((playerId, session) -> {
                session.startGame(handlers.get(playerId));
            });

            schedule();
        }
    }

    private TetrisEventHandler createHandlers() {
        return (userId, event) -> {
            switch (event) {
                case UpdateTetrisState state -> broadcast(state, userId);
                case AttackedTetrisBlocks state -> handleAttacked(userId, state);
                case AttackingTetrisBlocks blocks -> handleAttacking(userId, blocks);
                case InsertAttackBlocksEvent insertEvent -> handleInsertEvent(userId, insertEvent);
                default -> {
                }
            }
        };
    }

    private void handleInsertEvent(int userID, InsertAttackBlocksEvent insertEvent) {
        sessions.forEach((playerId, session) -> {
            if (playerId == userID) {
                session.insertAttackedBlocks(insertEvent);
            }
        });
    }

    private void handleAttacked(int userID, AttackedTetrisBlocks state) {
        sessions.forEach((playerId, session) -> {
            if (playerId == userID) {
                session.attacked(state);
            }
        });
    }

    private void handleAttacking(int userID, AttackingTetrisBlocks blocks) {
        sessions.forEach((playerId, session) -> {
            if (playerId != userID) {
                session.attackedByPlayer(blocks);
            }
        });
    }

    private void schedule() {
        tetrisTimer = new Timer();
        tetrisTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                long nanoTime = System.nanoTime();
                sessions.forEach((_userId, session) -> {
                    session.pulse(nanoTime);
                });
            }
        }, 0, 16);
    }

    private void broadcast(TetrisEvent event, int userId) {
        TetrisSession session = sessions.get(userId);
        if (session == null) return;

        session.response(event);
    }

    private void broadcast(TetrisEvent event) {
        sessions.forEach((userId, _room) -> {
            broadcast(event, userId);
        });
    }

    // 네트워크 통신을 위한 소켓 서버를 시작
    private void startServer() {
        new Thread(() -> {
            while (true) {
                try {
                    Socket clientSocket = serverSocket.accept();
                    clientSockets.add(clientSocket);
                    new ClientHandler(clientSocket).start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    // 클라이언트와 통신을 담당하는 클래스
    private class ClientHandler extends Thread {
        private Socket socket;
        private BufferedReader in;
        private PrintWriter out;

        public ClientHandler(Socket socket) throws IOException {
            this.socket = socket;
            this.in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            this.out = new PrintWriter(socket.getOutputStream(), true);
        }

        @Override
        public void run() {
            try {
                String message;
                while ((message = in.readLine()) != null) {
                    // 클라이언트로부터 받은 메시지를 처리하는 로직
                    handleClientMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        private void handleClientMessage(String message) {
            // 메시지를 TetrisAction으로 변환하여 처리
            TetrisAction action = TetrisAction.valueOf(message.split(",")[1]);
            handleInputAction(getUserIdFromMessage(message), action);
        }

        private int getUserIdFromMessage(String message) {
            // 메시지에서 사용자 ID를 추출
            return Integer.parseInt(message.split(",")[0].split(":")[1]);
        }

        public void sendMessage(String message) {
            out.println(message);
        }
    }

    // 클라이언트에게 메시지를 전송
    private void broadcastToClients(String message) {
        for (Socket clientSocket : clientSockets) {
            try {
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                out.println(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 클라이언트의 입력을 처리
    private void handleInputAction(int userId, TetrisAction action) {
        TetrisSession session = sessions.get(userId);
        if (session == null) {
            broadcast(new ServerErrorEvent("세션이 종료되었습니다." + userId));
            return;
        }

        session.requestInput(action);

        // 결과를 클라이언트에게 전송
        broadcastToClients("User " + userId + " performed action: " + action);
    }

    private final int maxUser = 2;

    private GameLevel level;
    private GameMode mode;

    private Timer tetrisTimer;
    private Map<Integer, TetrisSession> sessions;
    private Map<Integer, TetrisEventHandler> handlers;

    private List<TetrisGameEndData> endData;

    // 네트워크 통신을 위한 소켓
    private ServerSocket serverSocket;
    private List<Socket> clientSockets;
}
someh2705 commented 6 months ago

코드를 조금 추가했습니다.

클라이언트 -> 서버 접속도 이 코드와 비슷하게 처리해주시면 됩니다.

hyotaime commented 6 months ago

OnlineBattleTetrisServer를 사용하면 BattleScreenController의 server가 LocalBattleTetrisServer로 되어있기 때문에 LevelSelectScreenController에서 setArgument를 이용해 온라인 배틀을 만들 수 없어 해결이 필요합니다. Ref. #103

binlee0903 commented 6 months ago

BattleScreenController의 server를 인터페이스로 교체했습니다. 머지하시고 사용하시면 될 것 같습니다.

hyotaime commented 6 months ago

BattleScreenController의 server를 인터페이스로 교체했습니다. 머지하시고 사용하시면 될 것 같습니다.

확인했습니다. 감사합니다.

hyotaime commented 6 months ago

https://github.com/hyotaime/se13_team_project/commits/develop/online/

현재까지 작성한 코드에서 클라이언트을 2개 띄우고 서버를 실행시켜 테스트 해 보았습니다.

기존의 로컬 테트리스에서 입력을 화면에 반영하는 handleInputActionwrite로 변경하여 입력을 서버에 전송하도록 하였습니다. 클라이언트의 입력이 서버에 정상적으로 전달되는 것은 확인하였습니다.

하지만 각 클라이언트가 별개의 게임을 실행하여 대전 모드가 정상적으로 진행되지 않습니다. startOnlineTetrisGamesetArgument가 player 2명을 파라미터로 필요로 하고 있어 각 클라이언트에서 player1을 connectToServer(onlineServer)하고 player2는 서버에서 받아와 서로 클라이언트 정보가 교환된 상태에서 Battle화면으로 넘어가야할 것 같은데 서로 다른 클라이언트를 동일한 게임에 연결시켜주는 방법을 잘 모르겠습니다.

또한 입력 -> 서버 -> 각 클라이언트로의 반영이 이루어 지려면 서버에서 event를 broadcast할 때 어떤 유저의 event인지 구분해서 userId를 담아 보내야 하는데 어디서 userId를 가져올 수 있을까요

someh2705 commented 6 months ago

테트리스 서버에 접속을 하면 Socket이 생성됩니다. 이 소켓으로는 "나의 게임 상태" 만 전송 받는게 아니에요 "나와 게임하고 있는 플레이어의 상태"도 서버가 전송해줘야 합니다. 제가 올려드렸던 예제코드엔 매칭큐가 있어서 선착순 2명이 같은 게임에 접속하게 짰던 기억이 있네요 클라이언트 쪽에서는 만약 내가 첫번째 접속한 유저면 두번째로 접속한 플레이어를 기다릴거고, 내가 두번째로 접속한 플레이어라면 먼저 접속한 플레이어의 상태를 받아오게 짜시면 됩니다.

클라이언트끼리 userId를 고유하게 만들 수 있어야해요 보통은 로그인 과정을 거치니 고유하게 만들 수 있죠 서버에 접속을하면 userId를 생성해서 클라이언트에게 전송하는거에요 다만 로그인이 어렵다면 랜덤Int값을 쓰는 방안도 있습니다

TetrisServerApplication에서 userId를 1씩 증가시키면서 설정해주니 게임 시작하기 전에 UserLoginEvent 같은걸 전송해주면서 클라이언트에게 userId를 전송해주면 되겠네요

someh2705 commented 6 months ago

OnlineBattleTetrisServer 이 클래스가 필요했었나요? 제가 상상했던거는 BattleScreenController에 소켓 기반 TetrisActionRepository랑 TetrisEventRepository를 넘겨주는 방식을 생각했거든요

실제 게임은 TetrisServerApplication에서 LocalBattleTetrisServer를 사용하고 있으니 접속한 클라이언트에게 상태만 전송하면 됩니다

someh2705 commented 6 months ago

아 BattleScreenController에 TetrisServer가 들어갔군요,,, 의도했던게 이게 아닌데 수정을 해야할 거 같네요,,,