Showing
2 changed files
with
200 additions
and
0 deletions
... | @@ -27,3 +27,62 @@ | ... | @@ -27,3 +27,62 @@ |
27 | 유저가 채팅을 입력했다면 `RoomChatMessage`를 보내면 됩니다. 이 경우 오직 `message` 필드만 채워 보내면 됩니다. 다른 사람이 채팅을 입력한 경우, `RoomChatMessage`가 수신됩니다. 자신이 보낸 채팅에 대해서는 `RoomChatMessage`가 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다. | 27 | 유저가 채팅을 입력했다면 `RoomChatMessage`를 보내면 됩니다. 이 경우 오직 `message` 필드만 채워 보내면 됩니다. 다른 사람이 채팅을 입력한 경우, `RoomChatMessage`가 수신됩니다. 자신이 보낸 채팅에 대해서는 `RoomChatMessage`가 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다. |
28 | 28 | ||
29 | 방을 나가고 싶으면 `RoomLeaveMessage`를 보내면 됩니다. | 29 | 방을 나가고 싶으면 `RoomLeaveMessage`를 보내면 됩니다. |
30 | + | ||
31 | +## 게임 | ||
32 | + | ||
33 | +### 루프 | ||
34 | + | ||
35 | +준비 화면에서 라운드가 시작되면 `RoundStartMessage`가 수신됩니다. `round`는 현재 라운드 넘버 (1부터 시작), `duration`은 현재 라운드의 길이를 초 단위로 나타냅니다. `roles`는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 항상 라운드가 시작되면 타이머를 라운드의 길이로 맞춘 뒤 타이머를 정지해주세요. 이때 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 타이머의 시간이 흐르게 됩니다. | ||
36 | +서버는 클라이언트 타이머의 상태를 `RoundTimerMessage`를 보내서 동기화합니다. `state`가 `started`이면 메세지를 수신한 즉시 타이머를 동작시키고, `stopped`이면 타이머를 일시 정지합니다. 이때 `time`에 남은 시간이 초 단위로 포함되므로, 항상 이 메세지를 수신할 때마다 타이머의 남은 시간을 `time`값으로 동기화해주세요. 일반적으로 이 메세지는 단어 선택이 완료되어 라운드의 시간이 흐르기 시작하는 시점에 한 번만 전송됩니다. 라운드가 종료되면 `state: stopped`인 `RoundTimerMessage`가 수신됩니다. | ||
37 | +모든 플레이어가 단어를 맞추거나, 타이머의 시간이 0으로 떨어지면 라운드가 종료되면서 `RoundFinishMessage`가 수신됩니다. 이 메세지는 이번 라운드의 정답을 포함하고 있습니다. 만약 진행할 라운드가 더 남았다면 몇 초 뒤에 다시 `RoundStartMessage`가 수신될 것입니다. 그러나 이번 라운드가 마지막이었다면 `GameFinishMessage`가 수신됩니다. 이는 게임이 정상적으로 종료되었다는 의미이며, 다시 준비 화면으로 전환해주시면 됩니다. | ||
38 | + | ||
39 | +### 역할 | ||
40 | + | ||
41 | +가능한 역할은 `drawer`, `guesser`, `winner`, `spectator`로 구분됩니다. 이는 `RoundStartMessage`와 함께 수신됩니다. 만약 라운드 진행 중에 역할이 바뀌게 된다면 `RoundRoleMessage`가 수신됩니다. 이는 단순히 플레이어 목록 UI를 업데이트 하기 위해서 사용되며, 따로 고려할 게임 로직은 없습니다. | ||
42 | + | ||
43 | +- `drawer`: 그림을 그리는 사람입니다. | ||
44 | +- `guesser`: 그림을 보고 단어를 맞추는 사람입니다. 아직 단어를 맞추지 못한 상태입니다. | ||
45 | +- `winner`: 그림을 보고 단어를 맞춘 사람입니다. | ||
46 | +- `spectator`: 중도 입장한 관전자입니다. 다음 라운드부터 참여됩니다. 이 값을 역할의 기본 값으로 취급할 수 있습니다. | ||
47 | + | ||
48 | +### drawer | ||
49 | + | ||
50 | +`drawer`는 라운드가 시작된 뒤 바로 `RoundWordSetMessage`를 통해 선택 가능한 단어들을 수신받습니다. 수신받는 단어 수는 3개입니다. `drawer`는 이 중 하나를 선택해서 그림을 그릴 수 있습니다. 단어를 선택하면 해당 단어를 `RoundChooseWordMessage`에 담아 서버로 송신합니다. 서버는 이를 확인하고 타이머를 동작시킵니다. 나머지 참가자들은 오직 정답의 글자수만을 담고 있는 `RoundWordChosenMessage`를 수신받게 됩니다. | ||
51 | +그림은 `drawer`의 브러시 움직임을 그대로 시뮬레이션하여 만들어집니다. `drawer`가 색깔, 굵기를 바꾸면 `PaintBrushMessage`가 서버로 전송되고, 나머지 플레이어들은 이를 수신받게 됩니다. `size`는 브러시의 지름을 픽셀 단위로 나타내고, `color`는 브러시의 색상을 6자리 소문자 16진수로 나타냅니다. 이 메세지는 `drawing` 필드도 담고 있는데, 이는 마우스가 눌린 상태인지, 즉 브러시로 칠을 하는 상태인지를 나타냅니다. 중요한 것은 `drawer`가 캔버스 위에 마우스를 누르는 순간 `drawing`이 `true`로 설정된 메세지가, 캔버스에서 마우스를 떼는 순간 `false`로 설정된 메세지가 서버로 전송되어야 한다는 점입니다. | ||
52 | +만약 `drawer`가 캔버스 위에서 그림을 그리는 중이라면 실시간으로 `PaintMoveMessage`가 서버로 전송되어야 합니다. `x`와 `y`는 캔버스 오른쪽 아래 지점을 (0, 0)로 설정했을 때의 마우스의 좌표를 픽셀 단위로 나타냅니다. | ||
53 | +다른 플레이어들은 `PaintBrushMessage`를 통해 `drawing`이 `true`로 설정된 시점부터, 다시 `drawing`이 `false`로 설정되는 시점까지, 마우스가 움직이는 모든 위치에 대해 점이 찍히게 됩니다. 정확히는 마우스의 좌표가 업데이트 될 때 이전 지점과 현재 지점을 선으로 이어 칠해주는 방식으로 보간을 해야 할 것입니다. | ||
54 | + | ||
55 | +전체 과정을 정리하자면 다음과 같습니다. | ||
56 | + | ||
57 | +- 굵기, 색상을 변경하면 `PaintBrushMessage` 전송. | ||
58 | +- 캔버스 위에서 마우스를 누르는 시점에 현재 마우스 위치를 `PaintMoveMessage`로 **먼저** 전송한 뒤에 `drawing`을 `true`로 한 `PaintBrushMessage` 전송. (만약 Move가 먼저 전송되지 않는다면 마지막으로 브러시를 뗀 위치에서 현재 위치까지 불필요한 선이 그려질 것입니다.) | ||
59 | +- 캔버스 위에서 마우스를 움직이는 동안 `PaintMoveMessage` 전송. | ||
60 | +- 마우스를 떼는 시점에 `drawing`을 `false`로 한 `PaintBrushMessage` 전송. | ||
61 | + | ||
62 | +이 정보들을 가지고 캔버스를 칠하는 컴포넌트를 만들어서, 이를 `drawer`의 클라이언트에도 동일하게 사용하는 방식으로 구현하여 `drawer`와 다른 플레이어의 캔버스가 동일하게 보이도록 해야 할 것입니다. | ||
63 | + | ||
64 | +캔버스의 크기: 512x384 (4:3) (추후 변경 가능) | ||
65 | + | ||
66 | +### guesser | ||
67 | + | ||
68 | +`guesser`는 정답을 채팅에 입력할 수 있습니다. 만약 정답이라면 채팅이 서버에서 무시되고 역할이 `winner`로 변경되는 `RoundRoleMessage`가 수신되고, 정답을 담고 있는 `AnswerAcceptedMessage`가 수신됩니다. | ||
69 | +만약 답을 맞추지 못했다면 일반 채팅으로 전달됩니다. | ||
70 | + | ||
71 | +### winner, spectator | ||
72 | + | ||
73 | +채팅만 이용할 수 있습니다. | ||
74 | + | ||
75 | +### 중도 입장 | ||
76 | + | ||
77 | +게임 도중 입장한 유저에게는 다음과 같은 메세지들이 모두 전송됩니다. | ||
78 | + | ||
79 | +1. 준비 상태에서 자신이 방에 접속했을 때 전달 받는 모든 메세지들 | ||
80 | +2. 현재 라운드에 대한 정보를 담은 `RoundStartMessage` | ||
81 | +3. 현재 라운드 타이머와 동기화할 수 있는 `RoundTimerMessage` | ||
82 | +4. 마지막으로 서버상으로 기록된 브러시 정보를 담은 `PaintBrushMessage` | ||
83 | +5. 마지막으로 서버상으로 기록된 브러시 위치를 담은 `PaintMoveMessage` | ||
84 | + | ||
85 | +다른 플레이어에게는 다음과 같은 메세지들이 모두 전송됩니다. | ||
86 | + | ||
87 | +1. 준비 상태에서 해당 유저가 방에 접속했을 때 전달 받는 모든 메세지들 | ||
88 | +2. 신규 유저를 관전자로 설정하는 `RoundRoleMessage` 메세지 | ... | ... |
... | @@ -85,6 +85,136 @@ export class RoomChatMessage implements Message { | ... | @@ -85,6 +85,136 @@ export class RoomChatMessage implements Message { |
85 | constructor(public message: string, public sender?: string) {} | 85 | constructor(public message: string, public sender?: string) {} |
86 | } | 86 | } |
87 | 87 | ||
88 | +/** | ||
89 | + * 클라 <- 서버 | ||
90 | + * 라운드가 시작되었음을 알립니다. | ||
91 | + * @param round 현재 라운드 넘버입니다. (1부터 시작) | ||
92 | + * @param duration 초 단위의 라운드 시간입니다. | ||
93 | + * @param roles 모든 방 접속 인원의 역할입니다. | ||
94 | + */ | ||
95 | +export class RoundStartMessage implements Message { | ||
96 | + readonly type = MessageType.ROUND_START; | ||
97 | + constructor( | ||
98 | + public round: number, | ||
99 | + public duration: number, | ||
100 | + public roles: { | ||
101 | + username: string; | ||
102 | + role: "drawer" | "guesser" | "winner" | "spectator"; | ||
103 | + }[] | ||
104 | + ) {} | ||
105 | +} | ||
106 | + | ||
107 | +/** | ||
108 | + * 클라 <- 서버 | ||
109 | + * 라운드 시작시에 오직 drawer에게만 전송되는 메세지로, drawer가 선택할 수 있는 단어들입니다. | ||
110 | + */ | ||
111 | +export class RoundWordSetMessage implements Message { | ||
112 | + readonly type = MessageType.ROUND_WORD_SET; | ||
113 | + constructor(public words: string[]) {} | ||
114 | +} | ||
115 | + | ||
116 | +/** | ||
117 | + * 클라 -> 서버 | ||
118 | + * drawer가 단어를 선택하면 해당 메세지가 서버로 전송됩니다. | ||
119 | + * @param word RoundWordSetMessage에서 수신받은 단어 중 drawer가 선택한 단어입니다. | ||
120 | + */ | ||
121 | +export class RoundChooseWordMessage implements Message { | ||
122 | + readonly type = MessageType.ROUND_CHOOSE_WORD; | ||
123 | + constructor(public word: string) {} | ||
124 | +} | ||
125 | + | ||
126 | +/** | ||
127 | + * 클라 <- 서버 | ||
128 | + * drawer가 단어를 선택하였음을 알립니다. | ||
129 | + * @param length 정답 단어의 길이입니다. | ||
130 | + */ | ||
131 | +export class RoundWordChosenMessage implements Message { | ||
132 | + readonly type = MessageType.ROUND_WORD_CHOSEN; | ||
133 | + constructor(public length: number) {} | ||
134 | +} | ||
135 | + | ||
136 | +/** | ||
137 | + * 클라 <- 서버 | ||
138 | + * 서버가 클라이언트의 타이머를 동기화하기 위해 사용됩니다. 라운드가 시작하면 타이머는 초기화되며 기본 상태는 stopped입니다. | ||
139 | + * @param state 타이머의 동작, 정지 여부입니다. | ||
140 | + * @param time 타이머의 남은 시간입니다. 초 단위로 주어집니다. | ||
141 | + */ | ||
142 | +export class RoundTimerMessage implements Message { | ||
143 | + readonly type = MessageType.ROUND_TIMER; | ||
144 | + constructor(public state: "started" | "stopped", public time: number) {} | ||
145 | +} | ||
146 | + | ||
147 | +/** | ||
148 | + * 클라 <- 서버 | ||
149 | + * 라운드가 종료되었음을 알립니다. | ||
150 | + * @param answer 이번 라운드의 정답입니다. | ||
151 | + */ | ||
152 | +export class RoundFinishMessage implements Message { | ||
153 | + readonly type = MessageType.ROUND_FINISH; | ||
154 | + constructor(public answer: string) {} | ||
155 | +} | ||
156 | + | ||
157 | +/** | ||
158 | + * 클라 <- 서버 | ||
159 | + * 플레이어의 역할이 바뀌었음을 알립니다. | ||
160 | + * @param username 대상 유저의 username입니다. | ||
161 | + * @param role 대상 유저의 새로운 역할입니다. | ||
162 | + */ | ||
163 | +export class RoundRoleMessage implements Message { | ||
164 | + readonly type = MessageType.ROUND_ROLE; | ||
165 | + constructor( | ||
166 | + public username: string, | ||
167 | + public role: "drawer" | "guesser" | "winner" | "spectator" | ||
168 | + ) {} | ||
169 | +} | ||
170 | + | ||
171 | +/** | ||
172 | + * 클라 <- 서버 | ||
173 | + * 플레이어가 정답을 맞췄음을 알립니다. | ||
174 | + * @param answer 이번 라운드의 정답입니다. | ||
175 | + */ | ||
176 | +export class AnswerAcceptedMessage implements Message { | ||
177 | + readonly type = MessageType.ANSWER_ACCEPTED; | ||
178 | + constructor(public answer: string) {} | ||
179 | +} | ||
180 | + | ||
181 | +/** | ||
182 | + * 클라 <- 서버 | ||
183 | + * 게임이 종료되었음을 알립니다. 다시 준비 화면으로 돌아갑니다. | ||
184 | + */ | ||
185 | +export class GameFinishMessage implements Message { | ||
186 | + readonly type = MessageType.GAME_FINISH; | ||
187 | + constructor() {} | ||
188 | +} | ||
189 | + | ||
190 | +/** | ||
191 | + * 클라 <-> 서버 | ||
192 | + * 브러시 설정을 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다. | ||
193 | + * @param size 픽셀 단위의 브러시 지름입니다. | ||
194 | + * @param color 6자리 소문자 16진수로 표현된 브러시 색상입니다. | ||
195 | + * @param drawing 현재 브러시가 캔버스에 닿은 상태인지를 나타냅니다. | ||
196 | + */ | ||
197 | +export class PaintBrushMessage implements Message { | ||
198 | + readonly type = MessageType.PAINT_BRUSH; | ||
199 | + constructor( | ||
200 | + public size: number, | ||
201 | + public color: string, | ||
202 | + public drawing: boolean | ||
203 | + ) {} | ||
204 | +} | ||
205 | + | ||
206 | +/** | ||
207 | + * 클라 <-> 서버 | ||
208 | + * 브러시 위치를 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다. | ||
209 | + * 왼쪽 하단이 원점입니다. | ||
210 | + * @param x 픽셀 단위의 가로 위치입니다. | ||
211 | + * @param y 픽셀 단위의 세로 위치입니다. | ||
212 | + */ | ||
213 | +export class PaintMoveMessage implements Message { | ||
214 | + readonly type = MessageType.PAINT_MOVE; | ||
215 | + constructor(public x: number, public y: number) {} | ||
216 | +} | ||
217 | + | ||
88 | export class MessageType { | 218 | export class MessageType { |
89 | static readonly LOGIN = "login"; | 219 | static readonly LOGIN = "login"; |
90 | static readonly ROOM_LIST_REQUEST = "room_list_request"; | 220 | static readonly ROOM_LIST_REQUEST = "room_list_request"; |
... | @@ -92,4 +222,15 @@ export class MessageType { | ... | @@ -92,4 +222,15 @@ export class MessageType { |
92 | static readonly ROOM_LEAVE = "room_leave"; | 222 | static readonly ROOM_LEAVE = "room_leave"; |
93 | static readonly ROOM_USER_UPDATE = "room_user_update"; | 223 | static readonly ROOM_USER_UPDATE = "room_user_update"; |
94 | static readonly ROOM_CHAT = "room_chat"; | 224 | static readonly ROOM_CHAT = "room_chat"; |
225 | + static readonly ROUND_START = "round_start"; | ||
226 | + static readonly ROUND_TIMER = "round_timer"; | ||
227 | + static readonly ROUND_FINISH = "round_finish"; | ||
228 | + static readonly ROUND_ROLE = "round_role"; | ||
229 | + static readonly ANSWER_ACCEPTED = "answer_accepted"; | ||
230 | + static readonly GAME_FINISH = "game_finish"; | ||
231 | + static readonly ROUND_WORD_SET = "round_word_set"; | ||
232 | + static readonly ROUND_CHOOSE_WORD = "round_choose_word"; | ||
233 | + static readonly ROUND_WORD_CHOSEN = "round_word_chosen"; | ||
234 | + static readonly PAINT_BRUSH = "paint_brush"; | ||
235 | + static readonly PAINT_MOVE = "paint_move"; | ||
95 | } | 236 | } | ... | ... |
-
Please register or login to post a comment