PROTOCOL.md 16.6 KB

Protocol

서버와의 통신은 socket.io를 사용합니다. 모든 메세지 interface들은 common/message.ts에 정의되어 있습니다. 실제 socket.io단에서 송수신되는 메세지 인터페이스는 RawMessage입니다. type은 메세지의 타입이며, message는 실제 메세지 데이터를 담고 있습니다. type as ServerInboundMessageKey 또는 type as ServerOutboundMessageKey를 사용하여 type: string을 유효한 메세지 타입으로 캐스팅할 수 있습니다.

클라이언트에서 송신하는 메세지는 ServerInboundMessage<key>, 서버에서 송신하는 메세지는 ServerOutboundMessage<key>로 정의되어 있습니다. 이때 key값은 메세지의 타입입니다. 두 메세지 셋에 대해 key값이 같더라도 속성이 다를 수 있음에 유의하세요.

클라이언트에서 송신하는 모든 메세지에 대해, 서버는 요청 결과를 반환합니다. 요청 결과 메세지의 타입은 ServerResponse<key>로 정의되어 있습니다. key값은 ServerInboundMessage<key>key값과 동일합니다.

메세지를 보낼때는 socket.io의 emit을 사용하며, 이때 사용되는 이벤트명은 msg로 모든 메세지에 대해 동일합니다. 특히 클라이언트는 emit의 콜백을 사용하여 ServerResponse<key> 타입의 요청 결과를 수신할 수 있습니다. 자세한 사항은 https://socket.io/docs/v4/emitting-events/Acknowledgements 항목을 참조하세요.

서버에서 메세지를 전송하는 코드는 server/connection/Connection.ts에 구현되어 있고, 메세지 핸들링은 server/message/MessageHandler.tsserver/message/MessageHandlerChain.ts를 통해 이뤄집니다.

로그인

서버에 login을 보냅니다. 본인이 사용하고 싶은 닉네임을 nickname속성에 담아 보내면 됩니다. 닉네임은 공백이 허용되지 않고 최대 12글자입니다. 만약 이 조건을 만족하지 않는다면 요청에 실패하고 사유가 전달됩니다. 요청에 성공한다면 문자열이 반환되는데, 이는 본인의 고유한 uuid값입니다. 앞으로 사용되는 모든 username 속성에 이 값을 사용해주세요. username은 유저의 이름이 아니라 고유한 uuid를 의미하고, nickname은 고유하지 않을 수 있습니다! 상대방의 닉네임은 방 접속시에 얻을 수 있습니다. 오직 UI에 보여주기 위한 용도로만 사용되며, 실제 처리는 모두 username을 통해 이뤄집니다.

로비

로비에서는 모든 방 목록을 확인하고 접속할 수 있습니다. 서버에 roomList를 전송하면 RoomDescription[]가 반환됩니다. 이 메세지는 모든 방에 관한 정보를 가지고 있습니다. 각 방은 고유한 uuid값으로 구분됩니다.

특정한 방에 접속하기 위해서는 서버에 joinRoom을 보내면 됩니다. 요청이 성공하면 RoomInfo가 반환됩니다. RoomInfo에는 본인을 포함한 모든 다른 플레이어들의 정보가 담겨 있습니다.

새로운 방을 만들고 싶다면 createRoom을 보내면 됩니다. 방의 이름을 name 속성에 담아 보내야 합니다. 방의 이름은 공백일 수 없고, 최대 30자까지 가능합니다. 방을 만드는데 성공했다면 방에 자동으로 접속되고 방장을 맡게 됩니다. 이때 반환값은 joinRoom과 동일한 RoomInfo를 받습니다.

RoomInfo에는 각 유저의 닉네임을 nickname 속성으로 담고 있습니다.

방에 접속중인 유저의 목록에 변화가 생기면, updateRoomUser가 수신됩니다. state는 다음 3가지 값 중 하나의 값을 가집니다.

  • added: 새로운 유저가 접속하였습니다. 자신이 접속했을 때에는 수신되지 않습니다. joinRoom의 결과인 RoomInfo를 사용하세요.
  • updated: 기존 유저의 정보가 업데이트되었습니다. 해당 유저의 방장, 준비 여부가 변경되면 수신됩니다.
  • removed: 유저가 방에서 퇴장하였습니다.

해당 메세지에는 변화된 유저를 특정하기 위한 username이 포함되어 있고, 그 유저의 닉네임인 nickname, 방장 여부인 admin, 준비 여부인 ready를 포함하고 있습니다.

유저가 채팅을 입력했다면 chat을 보내면 됩니다. 다른 사람이 채팅을 입력한 경우에도 chat이 수신됩니다(ServerInboundMessage<"chat">ServerOutboundMessage<"chat">이 다르게 정의되었다는 점에 유의하세요). 자신이 보낸 채팅에 대해서는 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다. 채팅 문자열의 양 끝 공백을 제거했을 때 문자열이 빈 문자열이거나, 채팅 문자열의 길이가 300을 초과하는 경우 요청이 실패 처리됩니다.

방을 나가고 싶으면 leaveRoom을 보내면 됩니다.

방장

방을 생성한 유저가 방장 권한을 가지게 됩니다. 방장 여부는 joinRoom의 반환값 또는 updateRoomUser을 통해 수신됩니다. 방장은 바뀔 수 있음에 유의하세요. 예를 들어 원래 방장이 방에서 나가는 경우, 다른 랜덤한 유저에게 방장 권한이 넘어갑니다. 이 경우 updateRoomUser가 수신됩니다.

방장이 원하는 유저에게 권한을 넘길 수 있는 기능은 아직 구현되지 않았습니다.

게임

요약

게임 전

  1. 방장을 제외한 모든 유저가 ready를 보내 준비한다.
  2. 방장이 startRound를 보내 게임 시작을 요청한다.

라운드 1 - 단어 선택 시간

(서버는 방장의 게임 시작 요청이 성공했을 경우 진입) (클라이언트는 startRound를 수신받은 경우 진입)

  1. 모든 유저는 startRound를 수신받고, 게임 화면으로 전환된다. 해당 메세지에는 모든 유저의 역할이 포함되어 있다. 이들 중 특별히 drawer 역할을 배정받았다면, 캔버스에 그릴 수 있도록 준비한다.
  2. 만약 본인이 drawer라면, wordSet 메세지를 수신받게 되는데 여기에는 3가지 선택할 수 있는 단어가 포함되어 있다. 팝업을 띄워서 유저가 단어를 선택한다.
  3. 만약 본인이 drawer가 아니라면, 아직 라운드의 시간이 흘러가지 않고 대기한다.
  4. drawerchooseWord를 통해 자신이 선택하고자 하는 단어를 서버에 보낸다.

라운드 1 - 라운드 진행 (60초)

(서버는 drawerchooseWord를 보낸 경우 진입) (클라이언트는 wordChosen을 수신받은 경우 진입)

  1. 모든 유저는 wordChosen을 수신받는데, 여기에는 정답 단어의 글자 수 만이 포함되어 있다. 따라서 정답 단어를 밑줄 개수만으로 표시한다.
  2. 모든 유저는 라운드의 타이머가 시작되었다는 timer 메세지를 수신받는다. 이때 타이머를 동작하고 남은 시간을 동기화한다.
  3. guesser들은 단어를 채팅에 쳐서 맞춰볼 수 있다.
  4. drawer는 캔버스에 그림을 그릴 수 있다.
  5. 만약 guesser가 채팅으로 정답을 보냈다면 해당 유저는 answerAccepted 메세지를 수신받게 되고 여기에 이번 라운드의 정답이 포함되어 있다. 그리고 role을 통해 해당 유저의 역할이 winner로 변경된다.

라운드 1 - 라운드 종료 및 다음 라운드 시작 대기 (5초)

(서버는 남은 시간이 0으로 떨어지거나, drawer가 퇴장하거나, 모두가 답을 맞춰 남은 guesser가 0명이 된 경우 진입) (클라이언트는 finishRound를 수신받은 경우 진입)

  1. 모든 유저는 finishRound를 통해 이번 라운드의 정답을 알게 된다.

라운드 2 - 단어 선택 시간

(위와 동일하므로 생략)

...(생략)...

라운드 5 - 라운드 종료 (5초)

(위와 동일하므로 생략)

게임 종료

(서버는 다음 라운드가 없으면 진입, 게임 도중 인원이 2명 미만이 되는 경우 즉시 진입) (클라이언트는 finishGame을 수신받은 경우 진입)

  1. 방에 접속 중인 모든 유저는 finishGame를 수신받는다. 이 경우, 게임이 종료되었으므로 게임 화면에서 다시 준비 화면으로 전환된다.

준비

방장을 제외한 모든 플레이어는 준비를 해야 게임이 시작될 수 있습니다. 서버에 ready 메세지를 보내서 준비 상태를 설정할 수 있습니다. 준비 상태로 설정하려면 ready 속성을 참, 그렇지 않으면 거짓으로 담아 보내야 합니다. 누군가 ready를 하면 updateRoomUser를 통해 해당 유저의 준비 상태가 변경됩니다. 방장에게는 준비할 수 있는 버튼 대신에 게임을 시작할 수 있는 버튼이 주어집니다. 방에 2명 이상의 인원이 접속한 상태에서, 모든 플레이어가 준비해야만 버튼이 활성화 되어야 합니다. 방장이 게임 시작 버튼을 누르면 서버에 startGame가 전송됩니다. 만약 게임 시작에 실패하면 Response의 reason값으로 실패 사유가 전달되므로 이를 유저에게 보여줄 수도 있습니다. 성공적으로 게임이 시작되면 모든 유저에게 startRound가 전달됩니다.

라운드 진행

준비 화면에서 라운드가 시작되면 startRound가 수신됩니다. 즉, startRound를 수신하면 게임 화면으로 전환되어야 합니다. round는 현재 라운드 넘버 (1부터 시작), duration은 현재 라운드의 길이를 초 단위로 나타냅니다. roles는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 이제 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 서버에서 timer 메세지를 수신받고 타이머의 시간이 흐르게 됩니다. 서버는 클라이언트 타이머의 상태를 timer를 보내서 동기화합니다. statestarted이면 메세지를 수신한 즉시 타이머를 동작시키고, stopped이면 타이머를 일시 정지합니다. 이때 time에 남은 시간이 초 단위로 포함되므로, 항상 이 메세지를 수신할 때마다 타이머의 남은 시간을 time값으로 동기화해주세요. 일반적으로 이 메세지는 단어 선택이 완료되어 라운드의 시간이 흐르기 시작하는 시점과 라운드가 종료되는 시점에 전송됩니다. 라운드가 종료되면 state: stoppedtimer가 수신됩니다. 모든 플레이어가 단어를 맞추거나, 타이머의 시간이 0으로 떨어지면 라운드가 종료되면서 finishRound가 수신됩니다. 이 메세지는 이번 라운드의 정답을 포함하고 있습니다. 만약 진행할 라운드가 더 남았다면 몇 초 뒤에 다시 startRound가 수신될 것입니다. 그러나 이번 라운드가 마지막이었다면 finishGame가 수신됩니다. 이는 게임이 정상적으로 종료되었다는 의미이며, 다시 준비 화면으로 전환해주시면 됩니다.

예외적인 케이스로, 이전 라운드가 비정상적으로 종료되었을 때 finishRound를 수신받지 않고 startRound를 수신받게 될 수 있습니다. 이때 startRoundround 넘버가 이전 라운드와 동일한 값으로 수신받게 될 수도 있습니다. 예를 들면 drawer가 단어를 선택하지 않고 방에서 나가는 경우 해당 상황이 발생하게 됩니다. 또한 라운드를 진행 도중 누군가 퇴장하여 인원이 모자르게 된 경우, 즉시 finishGame을 수신받고 게임이 종료될 수 있습니다.

역할

가능한 역할은 drawer, guesser, winner, spectator로 구분됩니다. 이는 startRound와 함께 수신됩니다. 만약 라운드 진행 중에 역할이 바뀌게 된다면 role가 수신됩니다. 이는 단순히 플레이어 목록 UI를 업데이트 하기 위해서 사용되며, 따로 고려할 게임 로직은 없습니다.

  • drawer: 그림을 그리는 사람입니다.
  • guesser: 그림을 보고 단어를 맞추는 사람입니다. 아직 단어를 맞추지 못한 상태입니다.
  • winner: 그림을 보고 단어를 맞춘 사람입니다.
  • spectator: 중도 입장한 관전자입니다. 다음 라운드부터 참여됩니다. 이 값을 역할의 기본 값으로 취급할 수 있습니다.

drawer

drawer는 라운드가 시작된 뒤 바로 wordSet을 통해 선택 가능한 단어들을 수신받습니다. 수신받는 단어 수는 3개입니다. drawer는 이 중 하나를 선택해서 그림을 그릴 수 있습니다. 단어를 선택하면 해당 단어를 chooseWord에 담아 서버로 송신합니다. 서버는 이를 확인하고 타이머를 동작시킵니다. 이때 모든 플레이어는 오직 정답의 글자수만을 담고 있는 wordChosen과 타이머의 시작을 알리는 timer를 수신받습니다. 그림은 drawer의 브러시 움직임을 그대로 시뮬레이션하여 만들어집니다. drawer가 색깔, 굵기를 바꾸면 setBrush가 서버로 전송되고, 나머지 플레이어들은 이를 수신받게 됩니다. size는 브러시의 지름을 픽셀 단위로 나타내고, color는 브러시의 색상을 6자리 소문자 16진수로 나타냅니다. 이 메세지는 drawing 필드도 담고 있는데, 이는 마우스가 눌린 상태인지, 즉 브러시로 칠을 하는 상태인지를 나타냅니다. 중요한 것은 drawer가 캔버스 위에 마우스를 누르는 순간 drawingtrue로 설정된 메세지가, 캔버스에서 마우스를 떼는 순간 false로 설정된 메세지가 서버로 전송되어야 한다는 점입니다. 만약 drawer가 캔버스 위에서 그림을 그리는 중이라면 실시간으로 moveBrush가 서버로 전송되어야 합니다. xy는 캔버스 오른쪽 아래 지점을 (0, 0)로 설정했을 때의 마우스의 좌표를 픽셀 단위로 나타냅니다. 다른 플레이어들은 setBrush를 통해 drawingtrue로 설정된 시점부터, 다시 drawingfalse로 설정되는 시점까지, 마우스가 움직이는 모든 위치에 대해 점이 찍히게 됩니다. 정확히는 마우스의 좌표가 업데이트 될 때 이전 지점과 현재 지점을 선으로 이어 칠해주는 방식으로 보간을 해야 할 것입니다.

전체 과정을 정리하자면 다음과 같습니다.

  • 굵기, 색상을 변경하면 setBrush 전송.
  • 캔버스 위에서 마우스를 누르는 시점에 현재 마우스 위치를 moveBrush먼저 전송한 뒤에 drawingtrue로 한 setBrush 전송. (만약 Move가 먼저 전송되지 않는다면 마지막으로 브러시를 뗀 위치에서 현재 위치까지 불필요한 선이 그려질 것입니다.)
  • 캔버스 위에서 마우스를 움직이는 동안 moveBrush 전송.
  • 마우스를 떼는 시점에 drawingfalse로 한 setBrush 전송.

이 정보들을 가지고 캔버스를 칠하는 컴포넌트를 만들어서, 이를 drawer의 클라이언트에도 동일하게 사용하는 방식으로 구현하여 drawer와 다른 플레이어의 캔버스가 동일하게 보이도록 해야 할 것입니다.

캔버스의 크기: 512x384 (4:3) (추후 변경 가능) 브러시 사이즈: 1 ~ 64px

guesser

guesser는 정답을 채팅에 입력할 수 있습니다. 만약 정답이라면 채팅이 서버에서 무시되고 역할이 winner로 변경되는 role이 수신되고, 정답을 담고 있는 answerAccepted가 수신됩니다. 만약 답을 맞추지 못했다면 일반 채팅으로 전달됩니다. 시간이 지나 라운드가 종료되고 다음 라운드를 기다리는 도중 답을 채팅에 입력하면 이는 무시되어 정답 처리되지 않습니다.

winner, spectator

채팅만 이용할 수 있습니다.

중도 입장

게임 도중 입장한 유저에게는 다음과 같은 메세지들이 모두 전송됩니다.

  1. 준비 상태에서 자신이 방에 접속했을 때 전달 받는 모든 메세지들
  2. 현재 라운드 타이머와 동기화할 수 있는 timer
  3. 현재 라운드에 대한 정보를 담은 startRound
  4. 현재 라운드가 종료되었고, 다음 라운드를 기다리고 있는 중이라면 이번 라운드의 답을 담은 finishRound
  5. 마지막으로 서버상으로 기록된 브러시 정보를 담은 setBrush
  6. 마지막으로 서버상으로 기록된 브러시 위치를 담은 moveBrush // TODO: 중도 입장 유저에게는 비트맵을 전송하는 방식 고려해보기

다른 플레이어에게는 다음과 같은 메세지들이 모두 전송됩니다.

  1. 준비 상태에서 해당 유저가 방에 접속했을 때 전달 받는 모든 메세지들
  2. 신규 유저를 관전자로 설정하는 role 메세지