project/wargame

포탑의 공격 우선순위와 병종 시스템 개선 (feat. Kafka 메시지 유실)

downfa11 2025. 1. 17. 16:24

 

적진 않았지만 구현한 내용 : Redis로 구현한 스핀락 >> Redisson을 이용한 분산 락

그냥 길게 적을 내용 아닌거 같아서 뻈다. 나중에 배포하면서 노드 수가 늘어나면 다시 포스팅 주제로 할애하겠음

 

 

nlohmann/json 라이브러리 도입을 통해 JSON 타입 처리 개선

 

GitHub - nlohmann/json: JSON for Modern C++

JSON for Modern C++. Contribute to nlohmann/json development by creating an account on GitHub.

github.com

 

난 그동안 뭘했던걸까... 그냥 라이브러리 쓰면 편한데...

 

 

Kafka 메시지 유실에 대비한 후처리로 사용자 경험 개선

사실 난관은 '메시지의 유실을 어떻게 인지하는가?'이다.

 

다행히 게임이라는 도메인에서는 매칭이 완료되면 클라이언트에서 게임 서버로 접속하기 때문에, 서버-클라이언트 관계의 인가 과정으로 인지할 수 있었다.

 

게임 서버 입장에서는 매칭 결과를 받지 못했기 때문에 '지금 접속한 클라이언트는 내가 모르는 녀석인데 뭐지? 내가 안받은 매칭 결과가 있나?' 라고 판단할 수 있도록 인가 과정을 추가했다. 

 

백엔드단에서는 이미 매칭 결과로 생성된 방의 번호를 사용자에게 부여한 상태로, 이를 통해 게임중인지 판단한다.

 

게임서버에 접속한 클라이언트의 인가 과정에서 전달받은 매칭 결과를 조회한 뒤, 비인가 상태로 판단해 접속을 끊는다.

 

그럼 백엔드에서도 메시지 유실 등의 장애 발생으로 문제가 생겼고 사용자가 실제 게임중이 아니라고 알려야 다시 매칭을 시도할 수 있다.

 

당연하다면 당연한 소리지만 현재 게임중인지 검증을 거친 뒤에야 매칭을 진행할 수 있도록 로직을 짰기 때문이다.

 

 

게임서버가 Kafka를 통해 백엔드단으로 '검증되지 않은 클라이언트의 접속' 상황을 전달하면 비인가 상황을 판단할 수 있다.

 

매칭 결과의 유실로 인한 상황이라면, 사용자가 게임중이 아니라고 표시하도록 후처리할 수 있었다.

 

이를 통해 클라이언트는 closesocket 당하면서 자연스럽게 로비로 돌아오게된다.

 

백엔드에서도 정상적으로 게임중이 아니라고 처리했기에 다시 매칭을 시작해서 게임을 진행할 수 있다.

 

 

상태 이상 구현(슬로우, 스턴, 도트 데미지)

게임 서버 단에서 타이머를 구현해두니 비교적 편하게 상태 이상을 처리할 수 있었다.

 

클라이언트 수준에서 로그 찍지 말고 상태 이상을 표시하면 끝난다. 

 

예를 들어서 슬로우면 느릿느릿한 이펙트라거나.. 출혈 혹은 독과 같은 도트 데미지에 맞는 이펙트, 스턴시 혼란스러운 효과 등..

 

 

이런 식으로 도트 데미지와 슬로우, 스턴 표시를 중앙 체력바 위에 표시했다.

 

병종의 재생산 수단 고민

서비스적인 부분이라 오래 고민한거 같은데 마땅한 해답이 보이지 않는다.

 

지금까지 정해진 틀은 귀환시에 죽은 병종들이 복구되어야한다.

 

일단 귀환 기능부터 만들었다.

 

1. 클라이언트에서 B키를 통해 귀환 요청

2. 게임 서버 수준에서 타이머를 통해 8초 대기

 

여기서도 귀환을 위한 채널링이 끊기는지 판단하는 방법에 생각보다 처리해야할 상황이 많았다.

 

1. 일단 어떠한 상태 이상에 당해선 안된다.

2. 어떠한 형태로도 피격되어선 안된다.

3. 채널링중인 사용자는 어떠한 형태로도 움직여선 안된다.

 

채널링하는 8초동안 서버에서 계속 검증 과정을 연산하고 있는건 너무 리소스 낭비라고 판단했다.

 

8초의 채널링 시간을 전후로 상태 이상 여부, 좌표값, 현재 체력 등의 데이터값을 비교해서 수월하게 구현할 수 있었다.

 

클라이언트 단에서 위 3가지 과정에 대해서 채널링 게이지 바만 보이지 않도록 사용자 경험을 개선해주는데 오히려 더 시간을 썼다.

대충(...) 만든 귀환 중임을 표시하는 채널링 게이지 바.

 

8초 뒤에 아군 진영의 Structure::Nexus 위치로 자동 이동하도록 설정해서 구현을 마쳤다.

 

 

 

std::function<void()>

C++ 14 이후부터 자동으로 타입을 추론하여 선언함

 

[] : 람다 표현식의 캡처 리스트

(람다 함수가 외부 변수를 어떻게 사용할지 지정, [=]는 외부 스코프의 모든 변수를 캡처)

 

 

 

1차적인 기본 골자 - 귀환시 소모된 병종과 병사 수를 회복

넥서스 인근에서는 능력치 회복과 함께 병종의 재생산이 이뤄지도록 한다.

 

currentUnitCount보다 현재 Unit 수가 작은 경우, 유닛 생성→ currentUnitCount는 Map 형태로 unitKind와 unitCount를 기록해야한다.

 

이런 방식으로 게임 서버 내에서 해당 사용자의 가용 병종과 병사 수가 기록시켰다.

 

귀환을 통해 아군 진영의 본진으로 이동할때마다, 해당 병사 수를 넘는 병력을 가지면 초과분만큼 삭제하고 부족하면 채우는 방식으로 병력 보충이 이뤄진다.

 

병종 시스템 개선(병사의 포탑 피격 판정), Turret의 공격 우선순위 선정

기본적으로 Unit과 Client 구분 없이 가장 포탑의 공격 범위에 가까운 대상을 먼저 공격한다.

 

하지만 그 공격 범위 안에 존재하는 아군 사용자(포탑과 동일한 팀)가 상대 클라이언트로부터 공격받으면 공격 대상을 바꿔야한다.

 

포탑의 공격 범위 내에서 아군 클라이언트의 피격을 어떻게 알지….?

 

 

현재 구현 방법을 고민하고 있으며, 아직 포탑의 공격 범위 안에서 가장 가까운 녀석을 공격한다.

 

 

 

잘 보면 현재 포탑에게 피격당하고 있는 병종을 검은색 원으로 표시한 전후 이미지이다.

 

아직 매 초마다 가장 가까운 녀석을 연산하고 있는 상황이라, 범위 안에서 한번 정해진 피격 대상이 거리가 멀어짐에도 바뀌지 않도록 구현해야한다.

 

 


MySQL C++ Driver 오류

백엔드가 아니라 C++ 게임 서버쪽 문제를 해결한건데, champion_unit_type이 전부 ChampUnitType::Brigade로 나오는 오류를 겪었다.

 

각 챔프는 여단형(Brigade)와 사단형(Regiment)로 구분해서 병종의 양질을 결정하는 요소를 가진다. 

 

champion.unit_type = (row[18]=="Regiment" ? ChampUnitType::Regiment : ChampUnitType::Brigade);

 

로직상 문제인줄 알았는데, 사실 데이터베이스 초기화부터 잘못 읽어서 죄다 여단형(Brigade) 형태로 가져오고 있었던거다.

 

MySQL C++ Driver에서 제공하는 타입인 MYSQL_ROW는 std::string와 달라서 문자열을 바로 비교할 수 없었다.

 

std::string unit_type = row[18];
champion.unit_type = (unit_type =="Regiment" ? ChampUnitType::Regiment : ChampUnitType::Brigade);

 

 

std::string 으로 캐스팅해서 다시 비교하니 잘된다. 모르면 당하는거여