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.ts
와 server/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">
이 다르게 정의되었다는 점에 유의하세요). sender
속성은 채팅을 보낸 유저의 닉네임입니다. 채팅 문자열의 양 끝 공백을 제거했을 때 문자열이 빈 문자열이거나, 채팅 문자열의 길이가 300을 초과하는 경우 요청이 실패 처리됩니다.
방을 나가고 싶으면 leaveRoom
을 보내면 됩니다.
방장
방을 생성한 유저가 방장 권한을 가지게 됩니다. 방장 여부는 joinRoom
의 반환값 또는 updateRoomUser
을 통해 수신됩니다. 방장은 바뀔 수 있음에 유의하세요. 예를 들어 원래 방장이 방에서 나가는 경우, 다른 랜덤한 유저에게 방장 권한이 넘어갑니다. 이 경우 updateRoomUser
가 수신됩니다.
방장이 원하는 유저에게 권한을 넘길 수 있는 기능은 아직 구현되지 않았습니다.
게임
요약
게임 전
- 방장을 제외한 모든 유저가
ready
를 보내 준비한다. - 방장이
startRound
를 보내 게임 시작을 요청한다.
라운드 1 - 단어 선택 시간
(서버는 방장의 게임 시작 요청이 성공했을 경우 진입)
(클라이언트는 startRound
를 수신받은 경우 진입)
- 모든 유저는
startRound
를 수신받고, 게임 화면으로 전환된다. 해당 메세지에는 모든 유저의 역할이 포함되어 있다. 이들 중 특별히drawer
역할을 배정받았다면, 캔버스에 그릴 수 있도록 준비한다. - 만약 본인이
drawer
라면,wordSet
메세지를 수신받게 되는데 여기에는 3가지 선택할 수 있는 단어가 포함되어 있다. 팝업을 띄워서 유저가 단어를 선택한다. - 만약 본인이
drawer
가 아니라면, 아직 라운드의 시간이 흘러가지 않고 대기한다. -
drawer
는chooseWord
를 통해 자신이 선택하고자 하는 단어를 서버에 보낸다.
라운드 1 - 라운드 진행 (60초)
(서버는 drawer
가 chooseWord
를 보낸 경우 진입)
(클라이언트는 wordChosen
을 수신받은 경우 진입)
- 모든 유저는
wordChosen
을 수신받는데, 여기에는 정답 단어의 글자 수 만이 포함되어 있다. 따라서 정답 단어를 밑줄 개수만으로 표시한다. - 모든 유저는 라운드의 타이머가 시작되었다는
timer
메세지를 수신받는다. 이때 타이머를 동작하고 남은 시간을 동기화한다. -
guesser
들은 단어를 채팅에 쳐서 맞춰볼 수 있다. -
drawer
는 캔버스에 그림을 그릴 수 있다. - 만약
guesser
가 채팅으로 정답을 보냈다면 해당 유저는answerAccepted
메세지를 수신받게 되고 여기에 이번 라운드의 정답이 포함되어 있다. 그리고role
을 통해 해당 유저의 역할이winner
로 변경된다.
라운드 1 - 라운드 종료 및 다음 라운드 시작 대기 (5초)
(서버는 남은 시간이 0으로 떨어지거나, drawer
가 퇴장하거나, 모두가 답을 맞춰 남은 guesser
가 0명이 된 경우 진입)
(클라이언트는 finishRound
를 수신받은 경우 진입)
- 모든 유저는
finishRound
를 통해 이번 라운드의 정답을 알게 된다.
라운드 2 - 단어 선택 시간
(위와 동일하므로 생략)
...(생략)...
라운드 5 - 라운드 종료 (5초)
(위와 동일하므로 생략)
게임 종료
(서버는 다음 라운드가 없으면 진입, 게임 도중 인원이 2명 미만이 되는 경우 즉시 진입)
(클라이언트는 finishGame
을 수신받은 경우 진입)
- 방에 접속 중인 모든 유저는
finishGame
를 수신받는다. 이 경우, 게임이 종료되었으므로 게임 화면에서 다시 준비 화면으로 전환된다.
준비
방장을 제외한 모든 플레이어는 준비를 해야 게임이 시작될 수 있습니다. 서버에 ready
메세지를 보내서 준비 상태를 설정할 수 있습니다. 준비 상태로 설정하려면 ready
속성을 참, 그렇지 않으면 거짓으로 담아 보내야 합니다. 누군가 ready
를 하면 updateRoomUser
를 통해 해당 유저의 준비 상태가 변경됩니다. 방장에게는 준비할 수 있는 버튼 대신에 게임을 시작할 수 있는 버튼이 주어집니다. 방에 2명 이상의 인원이 접속한 상태에서, 모든 플레이어가 준비해야만 버튼이 활성화 되어야 합니다. 방장이 게임 시작 버튼을 누르면 서버에 startGame
가 전송됩니다. 만약 게임 시작에 실패하면 Response의 reason
값으로 실패 사유가 전달되므로 이를 유저에게 보여줄 수도 있습니다. 성공적으로 게임이 시작되면 모든 유저에게 startRound
가 전달됩니다.
라운드 진행
준비 화면에서 라운드가 시작되면 startRound
가 수신됩니다. 즉, startRound
를 수신하면 게임 화면으로 전환되어야 합니다. round
는 현재 라운드 넘버 (1부터 시작), duration
은 현재 라운드의 길이를 초 단위로 나타냅니다. roles
는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 이제 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 서버에서 timer
메세지를 수신받고 타이머의 시간이 흐르게 됩니다.
서버는 클라이언트 타이머의 상태를 timer
를 보내서 동기화합니다. state
가 started
이면 메세지를 수신한 즉시 타이머를 동작시키고, stopped
이면 타이머를 일시 정지합니다. 이때 time
에 남은 시간이 초 단위로 포함되므로, 항상 이 메세지를 수신할 때마다 타이머의 남은 시간을 time
값으로 동기화해주세요. 일반적으로 이 메세지는 단어 선택이 완료되어 라운드의 시간이 흐르기 시작하는 시점과 라운드가 종료되는 시점에 전송됩니다. 라운드가 종료되면 state: stopped
인 timer
가 수신됩니다.
모든 플레이어가 단어를 맞추거나, 타이머의 시간이 0으로 떨어지면 라운드가 종료되면서 finishRound
가 수신됩니다. 이 메세지는 이번 라운드의 정답을 포함하고 있습니다. 만약 진행할 라운드가 더 남았다면 몇 초 뒤에 다시 startRound
가 수신될 것입니다. 그러나 이번 라운드가 마지막이었다면 finishGame
가 수신됩니다. 이는 게임이 정상적으로 종료되었다는 의미이며, 다시 준비 화면으로 전환해주시면 됩니다.
예외적인 케이스로, 이전 라운드가 비정상적으로 종료되었을 때 finishRound
를 수신받지 않고 startRound
를 수신받게 될 수 있습니다. 이때 startRound
의 round
넘버가 이전 라운드와 동일한 값으로 수신받게 될 수도 있습니다. 예를 들면 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
가 캔버스 위에 마우스를 누르는 순간 drawing
이 true
로 설정된 메세지가, 캔버스에서 마우스를 떼는 순간 false
로 설정된 메세지가 서버로 전송되어야 한다는 점입니다.
만약 drawer
가 캔버스 위에서 그림을 그리는 중이라면 실시간으로 moveBrush
가 서버로 전송되어야 합니다. x
와 y
는 캔버스 오른쪽 아래 지점을 (0, 0)로 설정했을 때의 마우스의 좌표를 픽셀 단위로 나타냅니다.
다른 플레이어들은 setBrush
를 통해 drawing
이 true
로 설정된 시점부터, 다시 drawing
이 false
로 설정되는 시점까지, 마우스가 움직이는 모든 위치에 대해 점이 찍히게 됩니다. 정확히는 마우스의 좌표가 업데이트 될 때 이전 지점과 현재 지점을 선으로 이어 칠해주는 방식으로 보간을 해야 할 것입니다.
전체 과정을 정리하자면 다음과 같습니다.
- 굵기, 색상을 변경하면
setBrush
전송. - 캔버스 위에서 마우스를 누르는 시점에 현재 마우스 위치를
moveBrush
로 먼저 전송한 뒤에drawing
을true
로 한setBrush
전송. (만약 Move가 먼저 전송되지 않는다면 마지막으로 브러시를 뗀 위치에서 현재 위치까지 불필요한 선이 그려질 것입니다.) - 캔버스 위에서 마우스를 움직이는 동안
moveBrush
전송. - 마우스를 떼는 시점에
drawing
을false
로 한setBrush
전송.
이 정보들을 가지고 캔버스를 칠하는 컴포넌트를 만들어서, 이를 drawer
의 클라이언트에도 동일하게 사용하는 방식으로 구현하여 drawer
와 다른 플레이어의 캔버스가 동일하게 보이도록 해야 할 것입니다.
캔버스의 크기: 512x384 (4:3) (추후 변경 가능) 브러시 사이즈: 1 ~ 64px
guesser
guesser
는 정답을 채팅에 입력할 수 있습니다. 만약 정답이라면 채팅이 서버에서 무시되고 역할이 winner
로 변경되는 role
이 수신되고, 정답을 담고 있는 answerAccepted
가 수신됩니다.
만약 답을 맞추지 못했다면 일반 채팅으로 전달됩니다.
시간이 지나 라운드가 종료되고 다음 라운드를 기다리는 도중 답을 채팅에 입력하면 이는 무시되어 정답 처리되지 않습니다.
winner, spectator
채팅만 이용할 수 있습니다.
중도 입장
게임 도중 입장한 유저에게는 다음과 같은 메세지들이 모두 전송됩니다.
- 준비 상태에서 자신이 방에 접속했을 때 전달 받는 모든 메세지들
- 현재 라운드 타이머와 동기화할 수 있는
timer
- 현재 라운드에 대한 정보를 담은
startRound
- 현재 라운드가 종료되었고, 다음 라운드를 기다리고 있는 중이라면 이번 라운드의 답을 담은
finishRound
- 마지막으로 서버상으로 기록된 브러시 정보를 담은
setBrush
- 마지막으로 서버상으로 기록된 브러시 위치를 담은
moveBrush
// TODO: 중도 입장 유저에게는 비트맵을 전송하는 방식 고려해보기
다른 플레이어에게는 다음과 같은 메세지들이 모두 전송됩니다.
- 준비 상태에서 해당 유저가 방에 접속했을 때 전달 받는 모든 메세지들
- 신규 유저를 관전자로 설정하는
role
메세지