어느덧 나만무 4주가 지나갔다. 이제는 MVP 개발이 끝나고 Nice To Have 기능들을 구현하는 단계가 들어왔다. 4주 차의 회고를 시작하겠다.
업적 기능
우리 서비스는 1대1 코딩 배틀 게임이기 때문에 초창기 피드백에서 게임적 요소가 있어야 할 것 같다는 피드백을 받았던 적이 있었고 당시에는 일단 MVP 구현에 바빠서 게임적 요소를 뒤로 미뤄두었던 상황에서 이제 MVP 구현이 되었다고 판단이 되어 업적 기능을 구현하게 되었다. 스팀의 다양한 게임들과 온라인 게임에서 유저들의 지속적인 플레이와 수집욕구를 불러일으키는 것 중에 하나가 업적 시스템이라고 생각한다. 나만 해도 문명 6의 업적을 수백 개씩 획득하기도 하고 업적을 획득하기 위한 플레이를 하기도 하니 말이다. 그래서 업적 시스템을 도입하는 것을 진행하게 되었다.
그렇게 업적 기능을 구현하기 위해 어떤 방식으로 접근해야 할지 고민하며 여러 자료를 찾아 봤고 그중 한 블로그에서 아이디어를 얻었다.
뱃지 시스템을 기획하고, 디자인하고, 개발하기
그라운드 플립의 새로운 기능인 뱃지 시스템을 기획부터 디자인, 프론트엔드, 백엔드 까지 다 해본 경험을 소개합니다.
velog.io
해당 블로그에서 DB 모델을 정의하면서 DB 테이블 내부에 자기참조를자기 참조를 통해 누적형 업적을 구현했다고 한다. 누적형 업적도 좋지만 대부분 게임의 결과에 따라 승리 횟수, 문제를 풀어낸 횟수 등 자연스럽게 증가하는 형식의 업적을 구현할 생각을 했기 때문에 사실 자기 참조를 하지 않아도 구현이 가능할 것이라고 생각했다.
나는 누적형 업적 보다 조합형 업적을 구현하는 것을 목표로 했다.
조합형이란 이게 진짜 있는 것인지 모르겠지만 예를 들어 이런 것이다.
25년 7월 17일은 제헌절이다. 즉 기념일이다. 이 날 로그인을 한 사용자에게 특정 날짜에 로그인했다는 업적을 줄 수 있다. 그런데 여기에 그치지 않고 이날 로그인 해서 제헌절 로그인 업적을 가진 사람 중에 우리 게임 도중에 상대방이 부정행위를 저지르는 것을 신고 한 경우 이때 트리거가 되어 법대로 합시다!라는 업적을 얻을 수 있다.
이게 우리가 구현한 예시이다. 이 외에도 승리와 패배를 경험한자 라는 업적이 있는데 이 경우도 서로 반대되는 업적을 연결한 조합형 업적이다.
사실 테이블 자기참조를 통한 누적 업적이나 내가 마음대로 정의한 조합형 업적모두 해당 업적을 획득하기 전에 선행 조건이 만족되었는지 확인해야 하는 업적이다.
업적 시스템 설계 소개
1. 전체 구조 개요
- 업적 시스템은 유저 활동의 트래킹, 보상 지급, 업적 달성 조건 평가, 선행 조건 관리, 시각적 분류(카테고리) 등을 포괄적으로 다루는 기능이다.
- 주요 구성:
- Achievement: 업적의 정의
- UserAchievement: 사용자의 업적 달성 현황
- AchievementCategory: 업적 분류
- AchievementPrerequisite: 업적 간 선행 조건
- 트리거 타입, 보상 타입 등 Enum 정의
2. 핵심 모델 상세
Achievement
- 업적 자체를 정의하는 테이블
- 주요 필드:
- title, description: 업적 설명
- reward_type, reward_amount: 보상 정보 (배지, 포인트 등)
- trigger_type: 어떤 이벤트로 달성되는지 정의 (Enum)
- parameter: 트리거 조건 값 (예: 5연승 → parameter=5)
- start_at, end_at: 기간 한정 업적 지원
- unlocked_count: 누적 달성 횟수
UserAchievement
- 사용자의 업적 달성 현황
- 주요 필드:
- current_value: 진행도 (ex. 누적 문제 풀이 수)
- obtained_at: 달성 시각
- is_reward_received: 보상 수령 여부
AchievementTriggerType (Enum)
- 업적이 어떤 이벤트로 트리거되는지 정의
- 예시:
- FIRST_WIN: 첫 승리
- TOTAL_WIN: 누적 승리 수
- CONSECUTIVE_LOGIN: 연속 로그인
- TOTAL_REPORTS_MADE: 부정행위 신고 수
RewardType (Enum)
- 업적 보상 종류 정의
- BADGE, POINT, GIFT 등
3. 업적 카테고리
AchievementCategory
- 업적을 시각적으로/기능적으로 묶는 분류
- 예: "게임 플레이", "문제 해결", "커뮤니티 기여"
- 필드: name, description, image_url
4. 업적 간 선행 조건
AchievementPrerequisite
- 어떤 업적이 달성되기 위해 선행 업적을 필요로 하는 경우 사용
- Composite PK로 achievement_id, prerequisite_achievement_id를 사용
- self-referencing 관계 설정
5. 업적 등록 예시
{
"title": "마스터 코더",
"description": "브론즈 승리 업적과 실버 문제 해결 업적을 모두 달성",
"achievement_category_id": 1,
"trigger_type": "TOTAL_WIN",
"parameter": 100,
"reward_type": "BADGE",
"reward_amount": 1,
"prerequisite_achievement_ids": [
101, // "브론즈 승리 업적"의 ID (예시)
102 // "실버 문제 해결 업적"의 ID (예시)
]
}
업적을 위와 같은 형식으로 저장하게 설계했다.
async def get_user_data(db: Session, input_id: int) -> schemas.UserDto:
logger.info(f"Attempting to retrieve user data for ID: {input_id}")
user = crud.get_user_by_id(db, input_id=input_id)
if not user:
logger.warning(f"User with ID {input_id} not found.")
raise HTTPException(status_code=404, detail="User not found")
# 로그인 정보 업데이트 및 업적 확인
today = datetime.now(timezone.utc).date()
last_login_date = user.last_login_at.date() if user.last_login_at else None
if last_login_date == today:
# 같은 날 재로그인 시 연속 로그인 유지
pass
elif last_login_date == today - timedelta(days=1):
# 어제 로그인했으면 연속 로그인 증가
user.consecutive_login_days += 1
else:
# 연속 로그인 끊김
user.consecutive_login_days = 1
user.last_login_at = datetime.now(timezone.utc)
crud.update_user_login_info(db, user, user.last_login_at, user.consecutive_login_days)
# 업적 확인
await achievement_service.handle_achievement_event(
db, user.user_id, AchievementTriggerType.CONSECUTIVE_LOGIN, current_value=user.consecutive_login_days
)
await achievement_service.handle_achievement_event(
db,
user.user_id,
AchievementTriggerType.LOGIN_ON_DAY_OF_WEEK,
current_value=datetime.now(timezone.utc).weekday(), # 월요일=0, 일요일=6
)
logger.info(f"Successfully retrieved user data for ID: {input_id}")
return schemas.UserDto.model_validate(user)
구현 소감
업적이라는 생각하는 것 보다 어려웠다. 특히 업적의 유형을 추가할 때마다 특정 조건에 맞는 코드에 업적 달성을 했는지 확인하는 코드를 추가해야 했고 그리고 특정 조건을 만족하는지 확인하기 위해 기존의 DB 테이블에 추가적인 속성을 추가해서 이를 확인하려고 하니 기존의 DB에 속성을 추가하는 명령을 보내서 문제없이 속성을 추가해야 하며 기존 로직이 망가졌는지 확인도 했어야 했다.
'크래프톤 정글' 카테고리의 다른 글
[크래프톤 정글 8기] 나만무 2~3 주차 회고 (3) | 2025.07.11 |
---|---|
[크래프톤 정글 8기] 나만무(나만의 무기 갖기) 1주차 회고 (6) | 2025.06.26 |
[pintos] Week4~5: Virtual Memory - Part.9 Copy-On-Write (3) | 2025.06.08 |
[pintos] Week4~5: Virtual Memory - Part.7 페이지 교체 및 구현 완료를 위한 수정 (0) | 2025.06.07 |
[pintos] Week4~5: Virtual Memory - Part.6 Swap In/Out (2) | 2025.06.07 |