한 줄 소개 : Unity 기반의 캐주얼 멀티플레이어 대전 게임입니다.
- 개발 기간 : 2025.09 ~ 2026.01
- 개발 인원 : 4명
- 플랫폼 : PC
- Engine : Unity 2022.3.16f1
- Steam: https://store.steampowered.com/app/4249750/Thirteen_Kitty/
“Thirteen Kitty”는 12지신에 포함되지 못한 고양이가 13번째 십이지 동물이 되기 위해 경쟁한다는 세계관을 가진 실시간 1:1 대전 액션 게임입니다. 플레이어는 귀여운 고양이 캐릭터를 조작하여 상대를 쓰러뜨리고 최고의 자리에 올라야 합니다. 게임의 핵심은 전투 중 등장하는 12지신의 권능을 스킬 형태로 실시간 획득하여 활용하는 시스템입니다. 단순한 피지컬 싸움을 넘어, 전황에 따라 변화하는 스킬 조합과 다양한 맵 기믹을 전략적으로 활용해야 하며, 캐주얼한 비주얼 속에서 액션의 재미를 제공하고자 합니다.
| 이름 | 역할 |
|---|---|
| 허재승 | Programmer, Game Designer |
| 박민재 | Programmer |
| 문경서 | Graphic |
| 박민재 | Graphic |
| 이름 | 도움 주신 부분 |
|---|---|
| 손효민 | Acting |
| 김규진 | Graphic |
- 게임 규칙
- 다양한 스킬들을 활용한 대전
"Thirteen Kitty"에는 총 60종의 스킬이 있습니다. 게임 플레이 도중 상대방이 2포인트를 획득할 때마다 새로운 스킬을 획득할 수 있고 이를 활용하면 더욱 다이나믹 한 전투 플레이를 즐길 수 있습니다. 각 스킬들은 '소 - 대쉬 관련', '토끼 - 점프 관련', '호랑이 - 공격 관련'과 같이 각 동물들의 특징들을 컨셉으로 한 스킬들이 등장합니다.
- 12종의 맵 기믹 시스템
본 프로젝트는 서버리스(Serverless) 구조를 기반으로,
GameLift는 매칭만 담당하고 실제 게임 통신은 클라이언트 간 P2P로 이루어지도록 설계했습니다.
-
플레이어 접속 정보 등록
매칭 요청 전, 각 플레이어는 자신의IP / Port / Nickname정보를
AWS Lambda를 통해 DynamoDB에 저장합니다.
저장된 데이터는 TTL을 적용하여 일정 시간이 지나면 자동 삭제됩니다. -
GameLift 매칭 요청
각 클라이언트는 고유한PlayerId를 포함해 GameLift에 매칭을 요청합니다.
GameLift는 1:1 매칭이 성사될 때까지 티켓 기반으로 매칭 상태를 관리합니다. -
매칭 완료 이벤트 처리
매칭이 완료되면 GameLift는 SNS 이벤트를 발행하며,
이를 구독 중인 Lambda가 매칭된matchId와 각 플레이어의PlayerId를
DynamoDB에 저장합니다. -
상대 플레이어 정보 조회
클라이언트는 자신의 PlayerId만 전달하여 Lambda를 호출하고,
내부적으로 매칭 정보를 조회한 뒤 상대방의
IP / Port / Nickname정보를 전달받습니다. -
P2P 연결 시작
전달받은 네트워크 정보를 기반으로
클라이언트 간 직접 P2P 통신을 시작하여 실시간 대전을 진행합니다.
본 프로젝트의 실시간 대전은 UDP 기반 P2P 통신을 통해 구현되었습니다.
서버를 경유하지 않고 클라이언트 간 직접 통신을 사용함으로써,
입력 지연을 최소화하고 빠른 반응성을 확보하는 것을 목표로 했습니다.
실시간 액션 게임 특성상, 패킷의 완전한 신뢰성보다
지연 최소화와 지속적인 상태 동기화가 더 중요하다고 판단했습니다.
UDP는 패킷 손실 가능성이 존재하지만, 잦은 상태(State) 전송을 통해
자연스럽게 보정할 수 있어 실시간 대전에 적합하다고 판단했습니다.
UDP 통신 로직은 역할에 따라 명확히 분리되어 있습니다.
-
Sender
FixedUpdate주기로 플레이어의 상태(State)를 지속적으로 전송합니다.
위치, 방향, 입력, 애니메이션 상태 등 프레임 단위로 갱신되는 정보를 포함하며,
최초 연결 시에는 Hole Punching을 위한 초기 패킷을 송신합니다. -
Handler (Receiver)
UDP 소켓으로 수신되는 모든 패킷의 단일 진입점 역할을 하며,
수신된 메시지를 Prefix 기반으로 분류한 뒤, 등록된 각 Handler로 전달합니다. -
Dispatcher
게임 시작 전에P2PStateHandler,DamageHandler,SkillExecuteHandler등
메시지 타입별 핸들러를 등록/관리하고, 수신된 메시지를 순회하며
CanHandle(msg)가 true인 핸들러를 찾아Handle(msg)를 실행합니다.
이를 통해 메시지 타입이 추가되더라도 조건 분기 없이 확장할 수 있도록 구성했습니다.
대부분의 플레이어는 NAT(Network Address Translation) 환경에 위치해 있어,
외부에서 로컬 IP/Port로 직접 접근할 수 없는 구조를 가집니다.
이를 해결하기 위해 UDP Hole Punching 방식을 적용했습니다.
- 매칭 완료 후, 양측 플레이어는 서로의 공인 IP / Port 정보를 획득
- 양측이 거의 동시에 상대방에게 UDP 패킷을 송신
- NAT 장비에서 포트 매핑이 생성되며 양방향 통신 가능 상태가 됨
Hole Punching은 연결이 성립된 이후에는 더 이상 필요하지 않지만,
실제로는 로딩 완료 타이밍이 플레이어마다 다를 수 있기 때문에
양쪽 클라이언트가 서로의 패킷을 수신할 때까지 일정 시간 동안 Hole Punching 패킷을 반복 송신합니다.
상호 수신이 확인되면 Hole Punching 송신을 중단하고, 이후에는 FixedUpdate 기반 State 패킷만 전송합니다.
본 프로젝트에서는 별도의 KeepAlive / Heartbeat 패킷을 사용하지 않습니다.
대신,
FixedUpdate주기로 전송되는 State 패킷 자체를 연결 상태 판단 기준으로 활용- 일정 시간 동안 State 패킷 수신이 없을 경우 연결 이상으로 판단합니다.
이를 통해:
- 패킷 종류를 최소화하고
- 불필요한 네트워크 트래픽을 줄이며
- 실시간 게임 특성에 자연스럽게 부합하는 구조를 구현했습니다.
- UDP 기반 P2P 구조를 통한 지연 최소화
- Sender / Handler / Dispatcher 분리를 통한 명확한 책임 구조
- Prefix 기반 메시지 분기로 확장성과 가독성 확보
- Hole Punching을 통한 NAT 환경 대응
- State 패킷을 활용한 연결 유지 및 상태 동기화 일원화
-
스킬 애니메이션 시스템 (Template Method Pattern)
60종이 넘는 방대한 스킬 데이터를 효율적으로 관리하고, 카드 획득 시의 다양한 연출을 유연하게 처리하기 위해 데이터 기반 설계와 디자인 패턴을 결합했습니다.- Data-Driven Architecture: 스킬의 속성(아이콘, 쿨타임, 타입)을
ScriptableObject로 분리하여 코드 수정 없이 데이터만으로 콘텐츠 확장이 가능하도록 설계했습니다. - Smart Filtering & Acquisition: 런타임에 플레이어의 보유 스킬을 체크(
IsSkillOwned)하여 중복을 방지하고, 선택된 스킬을 즉시 인스턴스화(CreateSkillInstance)하여 메모리 낭비를 최소화했습니다. - Template Method Pattern (UI): 카드마다 제각각인 연출을 관리하기 위해 부모 클래스(
CardAnimationBase)가 생명주기(위치 저장/복구, 트윈 제거)를 관리하고, 자식 클래스가 구체적인 연출(ExecuteAnimation)을 담당하는 구조를 적용했습니다.
LoadingclassDiagram SkillCardController --> SkillCard_SO : 1. Load Data SkillCardController --> PlayerAbility : 2. Equip Skill SkillCardController ..> SkillCard_UI : 3. Control UI SkillCard_UI --> ICardAnimation : Strategy ICardAnimation <|.. CardAnimationBase : Implements CardAnimationBase <|-- CardAnimation_Num_1 : Inherits class SkillCardController{ +List~SkillCard_SO~ allSkills -IsSkillOwned() -CreateSkillInstance() } class CardAnimationBase{ +StartAnimation() +StopAnimation() #ExecuteAnimation()* -SaveOriginalPositions() } class PlayerAbility{ +SetSkill() +EquipPassive() } - Data-Driven Architecture: 스킬의 속성(아이콘, 쿨타임, 타입)을
-
전략 패턴을 활용한 맵 기믹 교체
12지신 컨셉에 맞춰 12종의 서로 다른 맵 패턴(기믹)을 구현해야 했습니다. 복잡한 if-else 분기 대신 전략 패턴(Strategy Pattern)을 활용하여 유지보수성과 확장성을 확보했습니다.
- Abstraction: 모든 기믹이 상속받는
AbstractMapGimic추상 클래스를 정의하여 공통 규격(Start, Update, End)을 통일했습니다. - Decoupling:
MapManager는 구체적인 기믹 내용(쥐, 소, 호랑이 등)을 알 필요 없이, 현재 설정된currentGimmick만 실행하면 되도록 설계했습니다. - OCP (Open-Closed Principle): 새로운 기믹을 추가할 때 기존 매니저 코드를 수정할 필요 없이, 단순히 새로운 기믹 클래스를 추가하기만 하면 되는 유연한 구조를 완성했습니다.
LoadingclassDiagram MapManager --> AbstractMapGimic : 1. Updates Current Gimmick AbstractMapGimic <|-- MapGimic_Rat : Inherits AbstractMapGimic <|-- MapGimic_Cow : Inherits AbstractMapGimic <|-- MapGimic_Dragon : Inherits class MapManager{ -List~AbstractMapGimic~ gimmicks +SetMapGimicIndex(index) -FixedUpdate() } class AbstractMapGimic{ +OnGimicStart() +OnGimmickUpdate() +OnGimicEnd() } - Abstraction: 모든 기믹이 상속받는






