전체 글 38

뱀서라이크 던전 돌파 게임 만들기 (19)

게임 서버, C++로 처음부터 만들어보기 — 재보니 네 방 뒤였다서버 로드맵의 마지막 편이다.14편부터 18편까지, "나중에 재고 고친다"고 미뤄둔 게 넷 쌓였다.미뤄둔 것언제몬스터를 몇 마리까지 넣을 수 있나17편인원이 늘면 난이도가 제곱으로 쉬워짐18편RTT가 네트워크 지연이 아니라 ack 지연15편인플라이트 상한에 밀려 유실 판정도 못 받는 패킷16편전부 "그럴 것 같다"로만 적어둔 것들이다.재보니 둘은 맞았고, 하나는 생각보다 훨씬 가까웠고, 하나는 이름만 고치면 되는 문제였다.먼저 자를 만들어야 한다지금까지 테스트는 PowerShell 스크립트로 했다. 30Hz로 입력을 보내도록 짰는데, 세어보니:클라가 보낸 Input 패킷: 122개 (20.3/초)21개만 보냈다. 목표의 70%다.측정 도구가..

뱀서라이크 던전 돌파 게임 만들기 (18)

게임 서버, C++로 처음부터 만들어보기 — 실패해도 되게 만들기지금까지 만든 던전은 끝나지 않았다. 방이 무한히 이어지고, 몬스터가 닿아도 아무 일이 없었다.이번 편에서 끝나는 길이 생긴다. 맞으면 쓰러지고, 동료가 일으켜 세우고, 전원이 쓰러지면 실패.그리고 끝났다는 걸 로비에 알려야 한다. 13편에서 서버를 둘로 나누며 "나중에 붙인다"고 했던 그 나중이다.쓰러진 사람을 쫓으면 안 된다몬스터가 가장 가까운 플레이어를 쫓는다. 그대로 두면 쓰러진 사람 위에 앉아 있는다.구하러 가면 그 떼를 전부 상대해야 하고, 구조가 사실상 불가능해진다.for (const Player& p : players_) { if (!p.is_alive()) continue; // 쓰러진 사람은 쫓지 않는다 ....

뱀서라이크 던전 돌파 게임 만들기 (17)

게임 서버, C++로 처음부터 만들어보기 — 조건이 네 개면 이미 틀렸다지금까지 만든 서버에는 게임의 흐름이 없었다. 몬스터가 있고, 다가오고, 죽는다. 그게 전부였다.이번 편에서 흐름이 생긴다. 몇 마리를 언제 내보낼지, 다 죽이면 뭐가 되는지, 언제 다음 방으로 넘어갈지.같은 열거형, 다른 성격로비 서버에도 RoomPhase라는 게 있었다. Waiting, Fighting, Cleared.여기서도 비슷한 걸 만든다. 그런데 결정적으로 다른 게 있다.로비: 누가 공격 메시지를 보낸다 → 체력이 깎인다 → 0이 되면 Cleared배틀: 아무도 아무것도 안 보낸다 → 그래도 웨이브가 끝나고 방이 넘어간다로비에서는 메시지가 상태를 바꿨고, 여기서는 틱이 바꾼다.10편에서 경매 타이머를 만들며 "아무도 부..

뱀서라이크 던전 돌파 게임 만들기 (16)

게임 서버, C++로 처음부터 만들어보기 — 언제 나눌지는 코드가 알려준다지난 편에서 이렇게 적었다.Player를 Connection 안에 둔 이유: 지금 플레이어와 연결은 1:1이다. 분리할 이유가 없다.이번 편에서 몬스터를 만든다. 그 말이 거짓이 되는 순간이다.몬스터는 연결이 없다. ConnectionMap에 넣을 수 없고, 넣는다 해도 endpoint가 뭐가 되나. 포트 번호가 없는 클라이언트는 없다.미리 만들지 않아서 좋았던 점Step 15에서 World를 미리 만들 수도 있었다. 어차피 몬스터를 넣을 걸 알고 있었으니까.안 만들었다. 그랬으면 플레이어만 든 빈 껍데기가 하나 생겼을 것이다. 그리고 더 중요한 건, 그 모양이 맞는지 판단할 근거가 없었다는 것이다.지금은 안다. 몬스터를 넣어보니 ..

뱀서라이크 던전 돌파 게임 만들기 (15)

게임 서버, C++로 처음부터 만들어보기 — 서버가 먼저 말을 건다Step 1부터 14까지, 이 서버의 모든 코드는 같은 모양이었다.패킷이 온다 → 처리한다 → 응답한다전부 반응이다. 누가 뭔가를 보내지 않으면 서버는 아무것도 하지 않는다.이번 편에서 그게 바뀐다. 아무도 아무것도 안 보내도 초당 30번 세계가 흘러가야 한다.1초에 30번이 아니라, 33.333ms마다틱 타이머 자체는 로비에서 이미 만들었다. 경매 마감을 검사하는 1초짜리였다.그걸 그대로 33ms로 바꾸면 안 된다.tick_timer_.expires_after(std::chrono::milliseconds(33));expires_after는 "지금부터 33ms 뒤"다. 틱 처리에 2ms가 걸렸다면 실제 간격은 35ms가 된다. 그게 매번..

뱀서라이크 던전 돌파 게임 만들기 (14)

게임 서버, C++로 처음부터 만들어보기 — 똑같이 만들면 TCP를 쓰면 되니까지난 편에서 UDP 서버의 연결 계층을 만들었다. 헤더에 ack와 ack_bits라는 필드를 넣어두고 값은 0으로 비워뒀다.이번엔 그걸 채운다. TCP가 공짜로 해주던 걸 손으로 만드는 작업이다.그런데 TCP와 똑같이 만들면 안 된다. 똑같이 만들 거면 TCP를 쓰면 되니까.안 보내도 되는 것들TCP는 모든 바이트를 책임진다. 유실되면 재전송하고, 순서가 틀리면 맞출 때까지 기다린다.게임에서는 그게 손해다.초당 30번 플레이어 위치를 보낸다고 하자. 0.5초 전 위치 패킷이 유실됐다. TCP는 그걸 기어코 다시 보내고, 도착할 때까지 그 뒤 패킷을 전부 막는다. 최신 위치가 이미 도착해서 버퍼에 쌓여 있는데도 꺼내 쓸 수가 없..

뱀서라이크 던전 돌파 게임 만들기 (13)

게임 서버, C++로 처음부터 만들어보기 — TCP가 해주던 걸 손으로지금까지 만든 건 로비 서버다. 로그인, 방, 채팅, 매칭, 경매, 친구. TCP로 잘 돌아간다.전투는 거기서 못 한다. TCP가 너무 잘해줘서 못 한다.보장이 독이 되는 순간TCP는 도착과 순서를 보장한다. 패킷이 유실되면 재전송하고, 그동안 그 뒤에 온 데이터를 붙잡아둔다. head-of-line blocking이라고 부른다.초당 30번 위치를 보내는 상황을 생각해보자.t=0.00 위치 A → 유실t=0.03 위치 B → 도착t=0.06 위치 C → 도착TCP는 A를 재전송할 때까지 B와 C를 애플리케이션에 안 준다. 0.03초 전 위치를 기다리느라 지금 위치를 못 받는 거다.그런데 게임에서 A는 버려도 되는 데이터다. ..

뱀서라이크 던전 돌파 게임 만들기 (12)

게임 서버, C++로 처음부터 만들어보기 — 서버는 창을 닫으면 안 된다12편 만에 서버를 밖으로 내보낸다. 라즈베리파이에 올려서 24시간 돌린다.그런데 배포를 준비하다 보니, 지금까지 미뤄둔 것들이 한꺼번에 청구되기 시작했다.문제개발 중엔배포하면io.run()이 반환 안 함창을 닫으면 그만docker stop이 10초 기다렸다 강제 종료로그가 버퍼에 갇힘불편한 정도크래시하면 직전 로그가 통째로 사라짐포트가 코드에 박힘재컴파일하면 됨이미지를 다시 만들어야 함죽으면 끝다시 띄우면 됨새벽에 죽으면 아침까지 멈춰 있음전부 "나중에"라고 넘겼던 것들이다. 나중이 왔다.왜 도커인가원래 계획은 systemd 유닛 파일이었다. Restart=always를 걸고 apt로 의존성을 깔면 된다.도커로 바꾼 이유는 의존성이..

뱀서라이크 던전 돌파 게임 만들기 (11)

게임 서버, C++로 처음부터 만들어보기 — 지금 접속 안 한 사람지금까지 이 서버가 "사람들"을 담아둔 곳은 셋이었다.std::vector members; // 같은 방 사람들std::vector match_queue_; // 매칭 대기 중std::unordered_map> sessions_; // 접속 중인 연결전부 "지금 이 순간"의 관계다. 방에서 나가면 남이고, 매칭되면 큐에서 빠지고, 연결이 끊기면 사라진다.친구는 다르다. 세 가지가 전부 새롭다.접속하지 않은 사람과도 유지된다상대가 수락해야 성립한다 — 한쪽만 신청한 중간 상태가 존재한다양방향이다 — 내 목록에 쟤가 있으면 쟤 목록에도 내가 있어야 한다세 번째가 특히 함정이다.각자 목록을 들고 있으면제일 먼저 떠오르는 방식은 이거다..

뱀서라이크 던전 돌파 게임 만들기 (10)

게임 서버, C++로 처음부터 만들어보기 — 아무도 부르지 않는 코드Step 1부터 9까지, 이 서버의 모든 코드는 같은 모양이었다.메시지가 온다 → 처리한다 → 응답한다연결이 끊긴다 → 정리한다전부 반응이다. 누가 뭔가를 하지 않으면 서버는 아무것도 하지 않는다.경매는 다르다. 마감 시각이 있고, 그때가 되면 아무도 아무것도 안 보내도 스스로 낙찰돼야 한다.서버에 시간이라는 개념을 들일 차례다.1초마다 깨어나는 것void start_tick_timer() { tick_timer_.expires_after(std::chrono::seconds(1)); tick_timer_.async_wait([this](boost::system::error_code ec) { if (ec) ret..