스킬 쿨다운과 스킬 트리 데이터베이스 스키마 구성

스킬 쿨다운과 스킬 트리 데이터베이스 스키마 구성

스킬 쿨다운을 일관되게 처리하는 기준과 스킬 트리를 데이터 중심으로 설계하는 방법을 정리합니다. Unity 같은 클라이언트에서의 런타임 상태와 서버·저장소의 영속 데이터를 분리해 보겠습니다.

먼저 분리할 것: 정의 데이터와 런타임 상태

스킬 시스템에서 가장 흔한 설계 문제는 스킬의 정의와 플레이 중 바뀌는 상태를 한곳에 넣는 것이다. 피해량, 마나 소모량, 기본 쿨다운처럼 모든 캐릭터가 공유하는 값은 정의 데이터다. 반면 마지막 사용 시각, 남은 쿨다운, 현재 레벨처럼 캐릭터마다 달라지는 값은 런타임 상태 또는 플레이어 보유 데이터다.

이 둘을 분리하면 밸런스 수정은 스킬 정의만 바꾸면 되고 저장 데이터에는 필요한 개인 상태만 남는다. 또한 서버 권한 게임에서는 쿨다운 판정의 기준을 서버 시간으로 통일하기 쉬워진다.

스킬 정의 데이터와 플레이어별 런타임 상태를 분리한 구조

쿨다운은 남은 시간이 아니라 종료 시각으로 저장하기

클라이언트 UI는 보통 남은 시간을 보여 준다. 하지만 데이터 저장과 판정에는 쿨다운 종료 시각이 더 안전하다.

현재 시각을 now, 쿨다운 종료 시각을 cooldownEndAt이라고 하면 남은 시간은 다음처럼 계산한다.

remaining=max(0,cooldownEndAtnow)remaining = \max(0, cooldownEndAt - now)

스킬 사용 가능 여부도 단순하다.

canUse=nowcooldownEndAtcanUse = now \ge cooldownEndAt

남은 시간을 매 프레임 저장하면 게임을 종료했을 때의 경과 시간을 따로 보정해야 한다. 반대로 종료 시각을 저장하면 다음 접속 시 현재 시각과 비교하는 것만으로 자연스럽게 처리된다. 서버가 있는 게임이라면 클라이언트 시간이 아니라 서버 시간을 사용해야 시간 조작에 덜 취약하다.

재사용 대기시간을 시작하는 시점

스킬에 따라 쿨다운 시작 시점은 다를 수 있다.

  • 즉시 발동 스킬: 사용이 승인된 직후 시작
  • 시전형 스킬: 시전 완료 후 시작
  • 채널링 스킬: 채널링 종료 또는 취소 규칙에 따라 시작
  • 충전형 스킬: 마지막 충전 소모 뒤 충전 회복 타이머 시작

중요한 점은 이 규칙을 코드에 흩어 놓지 않는 것이다. 스킬 정의에 쿨다운 정책을 명시하면 디자이너와 프로그래머가 같은 규칙을 볼 수 있다.

public enum CooldownStartPolicy
{
    OnCastStart,
    OnCastComplete,
    OnChannelEnd
}

public sealed class SkillDefinition
{
    public int Id { get; init; }
    public string Key { get; init; } = string.Empty;
    public float CooldownSeconds { get; init; }
    public CooldownStartPolicy CooldownStartPolicy { get; init; }
}

public sealed class SkillRuntimeState
{
    public double CooldownEndTime { get; private set; }

    public bool IsReady(double now) => now >= CooldownEndTime;

    public double RemainingCooldown(double now)
    {
        return Math.Max(0.0, CooldownEndTime - now);
    }

    public void StartCooldown(SkillDefinition skill, double now)
    {
        CooldownEndTime = now + skill.CooldownSeconds;
    }
}

Time.time은 게임 진행 중인 시간에 적합하지만 저장 후 재접속까지 포함하는 쿨다운에는 적합하지 않다. 싱글플레이 게임은 UTC 시각을 저장할 수 있고 온라인 게임은 서버가 판정한 UTC 시각이나 단조 증가하는 서버 타임스탬프를 쓰는 편이 낫다.

스킬 트리는 그래프지만 저장은 관계로 한다

스킬 트리는 보통 노드와 선으로 보이므로 그래프 자료구조처럼 생각하기 쉽다. 데이터베이스에서는 이를 스킬, 선행 조건, 플레이어 보유 스킬 관계로 나누어 저장하면 조회와 검증이 편해진다.

flowchart LR
    S[skills\n스킬 정의] --> P[skill_prerequisites\n선행 스킬 관계]
    S --> R[skill_rank_values\n레벨별 수치]
    U[player_skills\n플레이어 보유 상태] --> S
    U --> C[player_skill_cooldowns\n개별 쿨다운 상태]

여기서 skills는 스킬의 공통 정의다. skill_prerequisites는 어떤 스킬을 배우기 위해 필요한 선행 스킬을 표현한다. 한 스킬에 여러 선행 조건이 있을 수 있으므로 prerequisite_skill_idskills 테이블에 직접 넣는 것보다 별도 테이블이 유연하다.

player_skills는 플레이어가 습득한 스킬과 현재 레벨을 보관한다. 쿨다운 상태는 꼭 영속화할 필요는 없지만 재접속 뒤에도 쿨다운을 유지해야 한다면 player_skill_cooldowns처럼 별도 테이블에 둔다.

관계형 데이터베이스 예시

다음 예시는 PostgreSQL 문법에 가깝다. 다른 관계형 데이터베이스에서도 핵심 구조는 같다.

CREATE TABLE skills (
    id                INTEGER PRIMARY KEY,
    skill_key         VARCHAR(64) NOT NULL UNIQUE,
    display_name      VARCHAR(100) NOT NULL,
    max_rank          SMALLINT NOT NULL CHECK (max_rank > 0),
    mana_cost         INTEGER NOT NULL CHECK (mana_cost >= 0),
    base_cooldown_ms  INTEGER NOT NULL CHECK (base_cooldown_ms >= 0),
    cooldown_policy   VARCHAR(32) NOT NULL,
    is_active         BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE TABLE skill_prerequisites (
    skill_id                INTEGER NOT NULL REFERENCES skills(id),
    prerequisite_skill_id   INTEGER NOT NULL REFERENCES skills(id),
    required_rank           SMALLINT NOT NULL DEFAULT 1 CHECK (required_rank > 0),
    PRIMARY KEY (skill_id, prerequisite_skill_id),
    CHECK (skill_id <> prerequisite_skill_id)
);

CREATE TABLE skill_rank_values (
    skill_id          INTEGER NOT NULL REFERENCES skills(id),
    rank              SMALLINT NOT NULL CHECK (rank > 0),
    damage            INTEGER NOT NULL CHECK (damage >= 0),
    cooldown_delta_ms INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY (skill_id, rank)
);

CREATE TABLE player_skills (
    player_id         BIGINT NOT NULL,
    skill_id          INTEGER NOT NULL REFERENCES skills(id),
    current_rank      SMALLINT NOT NULL CHECK (current_rank > 0),
    unlocked_at       TIMESTAMP WITH TIME ZONE NOT NULL,
    PRIMARY KEY (player_id, skill_id)
);

CREATE TABLE player_skill_cooldowns (
    player_id         BIGINT NOT NULL,
    skill_id          INTEGER NOT NULL REFERENCES skills(id),
    cooldown_end_at   TIMESTAMP WITH TIME ZONE NOT NULL,
    PRIMARY KEY (player_id, skill_id)
);

base_cooldown_ms처럼 시간 단위를 컬럼 이름에 포함하면 초와 밀리초가 섞이는 실수를 줄일 수 있다. 코드에서는 TimeSpan이나 명확한 시간 타입을 사용하고 DB 경계에서만 정수 밀리초로 변환하는 방식도 좋다.

선행 조건 검증 쿼리의 의미

스킬을 해금할 때는 선행 조건을 모두 만족하는지 검사해야 한다. 예를 들어 플레이어가 배우려는 스킬의 모든 선행 스킬이 요구 레벨 이상인지 확인한다.

SELECT NOT EXISTS (
    SELECT 1
    FROM skill_prerequisites prerequisite
    LEFT JOIN player_skills owned
        ON owned.player_id = :player_id
       AND owned.skill_id = prerequisite.prerequisite_skill_id
    WHERE prerequisite.skill_id = :target_skill_id
      AND COALESCE(owned.current_rank, 0) < prerequisite.required_rank
) AS can_unlock;

이 검증과 실제 해금 처리는 같은 트랜잭션 안에서 수행해야 한다. 그렇지 않으면 짧은 시간에 해금 요청이 여러 번 들어왔을 때 스킬 포인트가 중복 차감되거나 조건이 어긋날 수 있다.

레벨별 수치와 예외 규칙

레벨별 피해량이나 쿨다운을 어떤 방식으로 저장할지는 규칙의 복잡도에 따라 결정한다.

수치가 단순한 선형 증가라면 기본값과 증가량만 저장하고 런타임에 계산할 수 있다. 반면 레벨마다 계수가 크게 바뀌거나 특정 레벨에 부가 효과가 붙는다면 skill_rank_values처럼 행 단위로 명시하는 편이 읽기 쉽고 밸런스 조정도 안전하다.

쿨다운 감소 효과도 기본 쿨다운 값을 직접 덮어쓰지 않는 편이 좋다. 장비, 버프, 특성에서 얻는 보정값을 분리하고 최종 값만 계산한다. 예를 들어 감소율을 합산하는 규칙이라면 다음과 같이 상한을 둘 수 있다.

finalCooldown=baseCooldown×max(1reductionRate,minimumMultiplier)finalCooldown = baseCooldown \times \max(1 - reductionRate, minimumMultiplier)

게임마다 감소율 합산, 곱연산, 고정 시간 감소의 우선순위가 다르다. 따라서 계산 순서를 문서와 테스트 코드로 고정해야 패치마다 의도하지 않은 변화가 생기지 않는다.

구현 시 확인할 항목

  • 쿨다운 판정의 기준 시간을 하나로 정한다. 온라인 게임에서는 서버 시간이 기준이다.
  • 스킬 정의 데이터와 플레이어별 상태를 분리한다.
  • 선행 조건 관계에 순환이 없는지 콘텐츠 등록 시 검사한다. A가 B를 요구하고 B가 A를 요구하면 어느 쪽도 해금할 수 없다.
  • 스킬 최대 레벨보다 높은 player_skills.current_rank가 저장되지 않도록 애플리케이션 검증을 둔다. 일반적인 외래 키만으로는 다른 테이블의 max_rank를 직접 제한하기 어렵다.
  • 쿨다운 적용, 자원 차감, 효과 실행의 성공·실패 기준을 정한다. 효과 적용에 실패했을 때 쿨다운을 소비할지 여부도 스킬 규칙의 일부다.

마무리

좋은 스킬 시스템은 화려한 트리 UI보다 데이터 경계가 먼저 명확하다. 공통 정의는 skills 계열 테이블에 플레이어의 습득 상태는 player_skills에, 시간에 따라 바뀌는 쿨다운은 종료 시각으로 관리하면 확장과 저장이 쉬워진다. 이후 특성, 장비 보정, 충전형 스킬, 서버 검증을 더하더라도 같은 분리 원칙을 유지하면 구조가 크게 흔들리지 않는다.

#게임 개발#스킬 시스템#데이터베이스#Unity#C#

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs