Documentation index for AI agents

GS2-Enhance

강화 기능

이 기능은 개발자에 따라서는 전혀 이해가 안 될 수도 있는 기능일 것입니다.

하지만 일본을 비롯한 모바일 게임의 사실상 표준 게임 시스템에는 반드시 존재하는 기능입니다. 이 게임 메커니즘 자체는 Game as a Service를 실현하는 데 유용한 지식이 될 것이므로, 이해가 어려운 개발자를 위해 게임 메커니즘에 대한 해설을 추가합니다.

강화 기능의 게임 메커니즘에 대해 충분한 지식이 있는 경우에는 이어지는 해설 섹션은 건너뛰어도 무방합니다.

강화 기능의 게임 메커니즘

강화 기능 자체는 매우 단순한 게임 시스템으로, 소재 아이템을 소비함으로써 대상의 경험치를 가산할 수 있는 구조입니다. 배틀에 참가해 경험치를 얻는 방법뿐만 아니라, 다양한 방법으로 캐릭터나 장비를 육성할 수 있게 되어 있습니다. (장비에도 레벨이 있습니다)

그럼 이 게임 시스템이 왜 Game as a Service를 실현하는 데 한몫하고 있는지 설명해 보겠습니다.

한마디로 말하면, 플레이 시간을 늘리는 데 활약하고 있습니다.

개발 속도보다 게임 플레이 속도가 압도적으로 빠르다는 것은 여러분도 충분히 이해하고 계실 것입니다. 우리 게임 개발자가 3년에 걸쳐 개발한 게임을 플레이어는 10시간 만에 끝내버리는 것입니다. 하지만 이 격차를 메우지 않으면 Game as a Service는 성립하지 않습니다. 즉, 플레이어의 콘텐츠 소비 속도에 디버프를 걸 필요가 있습니다. 그 마법이 바로 강화 게임 시스템입니다.

일반적인 Game as a Service 게임은 매달 한 번은 이벤트를 실시합니다. 그 이벤트에는 어떤 형태로든 보스가 존재하며, 플레이어는 그 보스를 이벤트 기간 동안 반복해서 토벌하게 됩니다. 보스는 플레이어의 성장 단계에 맞춰 여러 난이도를 준비해 두고, 토벌함으로써 캐릭터나 장비의 강화 소재를 얻을 수 있습니다.

모은 강화 소재를 사용해 캐릭터나 장비의 경험치로 전환해 성장시킵니다. 캐릭터나 장비를 성장시키면 더 높은 난이도의 보스에 도전할 수 있게 됩니다.

이렇게 함으로써 모든 플레이어가 이벤트에 참여하면서, 자신의 캐릭터 성장 단계에 맞는 강화 소재를 입수하고, 캐릭터나 장비를 성장시키는 재미를 즐길 수 있습니다.

실제로는 강화 소재를 보스가 직접 드롭하는 경우는 적을 수도 있습니다. 대신 이벤트 기간에만 도전할 수 있는 가챠가 있어, 그 가챠를 뽑기 위한 포인트를 모아 가챠를 통해 소재를 얻거나 이벤트 기간에만 개점하는 상점에서 보스를 토벌해 얻을 수 있는 게임 내 통화를 모아 상점에서 강화 소재를 구매하는 등 플레이어의 판단으로 어디를 우선해서 강화할지 결정할 수 있도록 하는 장치는 있지만, 최종적으로는 플레이어가 캐릭터 육성에 필요한 시간을 늘려 플레이 시간을 부풀리게 됩니다.

이벤트를 통해 일관되게 변하지 않는 것은 플레이어의 캐릭터가 강해진다는 점입니다. 이벤트가 끝나면 강해진 캐릭터를 사용해 더 높은 난이도의 상시 콘텐츠를 즐기는 등, 다음 재미로 이어집니다.

graph TD
  BossBattle["Boss Battle"] -- Acquire Event Point --> Shop
  Shop -- Buy Enhance Materials --> Enhance["Enhance Character"]
  Enhance -- More Formidable --> BossBattle

아키텍처

강화는 소재가 되는 아이템을 관리하는 GS2-Inventory와, 강화 대상인 캐릭터나 장비를 관리하는 GS2-Inventory 또는 GS2-Dictionary. 그리고 그 캐릭터나 장비의 경험치·레벨을 관리하는 GS2-Experience를 GS2-Enhance를 통해 조작함으로써 실현하고 있습니다.

현재 GS2에서는 1회의 API 요청으로 소비와 경험치 가산을 완결하는 DirectEnhance를 이용한 강화를 권장하고 있습니다. 이전에는 트랜잭션 자동 실행 기능이 없었기 때문에, 대성공이 나올 때까지 통신을 차단하고 다시 시도하는 행위를 방지할 목적으로, 준비·실행·완료 보고 단계로 나누어진 강화 프로세스(Progress)를 권장했지만, 지금은 트랜잭션 자동 실행이나 원자적 커밋(atomic commit) 구조가 갖추어져 그 우려는 해소되었습니다.

actor Player
participant "GS2-Enhance#Namespace"
participant "GS2-Inventory#ItemSet(Material)"
participant "GS2-Experience#Status"
Player -> "GS2-Enhance#Namespace" : Direct Enhance(Materials/Target Character)
"GS2-Enhance#Namespace" -> "GS2-Inventory#ItemSet(Material)" : Get experience value from metadata
"GS2-Enhance#Namespace" -> "GS2-Inventory#ItemSet(Material)" : Consume
"GS2-Enhance#Namespace" -> "GS2-Experience#Status": Add experience(Key: Target Character/Equipment Id)
"GS2-Enhance#Namespace" -> Player : Enhance result

GS2-Enhance에 “강화 대상”, “강화에 사용할 소재"를 파라미터로 하여 강화 실행 API(DirectEnhance)를 호출합니다.

그러면 GS2-Enhance는 소재가 되는 아이템의 마스터 데이터를 GS2-Inventory에서 가져와, 메타데이터 내에 기록되어 있는 소재로 사용했을 때의 경험치량을 취득합니다. 경험치량이 확정되면 아이템을 소비하고 경험치 가산 처리를 GS2-Experience에서 실행합니다.

강화 대상을 지정하고는 있지만, GS2-Experience의 경험치를 관리하는 키로 사용될 뿐이며, 강화 대상의 정보를 직접 사용하는 일은 없습니다.

DirectEnhance와 Progress의 차이

GS2-Enhance에서는 두 가지 강화 흐름을 제공하고 있습니다.

항목DirectEnhanceProgress (Start / End)
API 호출 횟수1회2회(시작·종료)
권장 용도모든 강화 처리대성공 등 추첨 결과를 연출에 반영하기 위해, 추첨 결과를 먼저 취득해 두고 싶은 경우
원자성요청 내에서 소비·경험치 가산이 완결서버에서 추첨한 결과를 유지하고, 이후 End에서 확정
통신 차단에 의한 부정 이용발생하지 않음진행 중인 Progress는 동시에 1건만 유지할 수 있어, DeleteProgress를 통해서만 재시도 가능

특별한 요건이 없는 한 DirectEnhance 이용을 권장합니다.

sequenceDiagram
  participant Player
  participant Enhance as GS2-Enhance
  participant Inventory as GS2-Inventory
  participant Experience as GS2-Experience
  Player ->> Enhance: DirectEnhance(rateName, targetItemSetId, materials)
  Enhance ->> Inventory: 메타데이터에서 경험치량 취득
  Enhance ->> Inventory: 소재 아이템 소비
  Enhance ->> Experience: 경험치 가산
  Enhance -->> Player: 가산 경험치·대성공 배율

강화 레이트

강화에 사용할 수 있는 소재나 강화 대상을 한정하기 위해 강화 레이트를 마스터 데이터로 설정해야 합니다.

마스터 데이터에는 소재로 사용할 수 있는 아이템의 GS2-Inventory 네임스페이스 이름이나 인벤토리 이름, 강화 대상으로 사용할 수 있는 아이템의 GS2-Inventory 네임스페이스 이름이나 인벤토리 이름과 같은 정보를 기록합니다.

마스터 데이터는 JSON 형식으로 관리합니다.

여기서는 ItemModel의 메타데이터에 { “experience”: 50 } 와 같은 JSON 형식으로 설정하는 예를 보여드립니다.

RateModel의 acquireExperienceHierarchy에서 JSON의 키(위의 예에서는 experience)를 정의합니다. 또한 acquireExperienceHierarchy에는 계층 구조를 정의할 수 있습니다. 예를 들어 { “aaa”: { “bbb”: { “experienceValue”: 100 } } } 와 같은 구조의 데이터를 메타데이터에 설정하고 싶은 경우에는, acquireExperienceHierarchy에 [ “aaa”, “bbb”, “experienceValue” ] 와 같이 지정합니다.

RateModel의 주요 설정 항목

항목설명
name강화 레이트 이름
targetInventoryModelId강화 대상 아이템이 저장되어 있는 GS2-Inventory의 인벤토리 모델 ID
materialInventoryModelId강화 소재 아이템이 저장되어 있는 GS2-Inventory의 인벤토리 모델 ID
acquireExperienceHierarchy소재 아이템의 메타데이터에서 경험치량을 꺼내기 위한 JSON 경로
acquireExperienceSuffix경험치 모델 이름 키에 부여하는 접미사(예: :level)
experienceModelId경험치를 가산할 GS2-Experience의 경험치 모델 ID
bonusRates대성공 시의 배율과 추첨 가중치

GS2-Enhance RateModel 마스터 데이터 메타데이터에 경험치를 설정하는 예:


{
  "version": "2020-08-22",
  "rateModels": [
    {
      "name": "enhanceRate",
      "description": "",
      "metadata": "",
      "targetInventoryModelId": "grn:gs2:ap-northeast-1:YourOwnerId:inventory:enhance-inventory:model:character",
      "acquireExperienceSuffix": ":level",
      "materialInventoryModelId": "grn:gs2:ap-northeast-1:YourOwnerId:inventory:enhance-inventory:model:material",
      "acquireExperienceHierarchy": [
        "experience"
      ],
      "experienceModelId": "grn:gs2:ap-northeast-1:YourOwnerId:experience:enhance-experience:model:character",
      "bonusRates": [
        {
          "rate": 2,
          "weight": 1
        },
        {
          "rate": 1,
          "weight": 1
        }
      ]
    }
  ]
}

bonusRates에는 여러 엔트리를 설정할 수 있으며, 각 엔트리는 추첨 가중치(weight)와 배율(rate)을 가집니다. 위의 예에서는 “2배 경험치"와 “등배 경험치"가 동일한 가중치로 추첨되어, 대략 50%의 확률로 대성공(2배)이 발생합니다.

GS2-Experience ExperienceModel 마스터 데이터 예:

{
  "version": "2019-01-11",
  "experienceModels": [
    {
      "name": "character",
      "metadata": "CHARACTER",
      "defaultExperience": 0,
      "defaultRankCap": 50,
      "maxRankCap": 80,
      "rankThreshold": {
        "metadata": "RANK_THRESHOLD",
        "values": [
          100,
          300,
          500,
          1000
        ]
      }
    }
  ]
}

GS2-Inventory ItemModel 마스터 데이터 메타데이터에 경험치를 설정하는 예:

{
  "version": "2019-02-05",
  "inventoryModels": [
    {
      "name": "character",
      "initialCapacity": 1,
      "maxCapacity": 1,
      "protectReferencedItem": false,
      "itemModels": [
        {
          "name": "character-0001",
          "stackingLimit": 1,
          "allowMultipleStacks": false,
          "sortValue": 0
        }
      ]
    },
    {
      "name": "material",
      "metadata": "",
      "initialCapacity": 10,
      "maxCapacity": 10,
      "protectReferencedItem": false,
      "itemModels": [
        {
          "name": "material-0001",
          "metadata": "{\"experience\":50}",
          "stackingLimit": 99,
          "allowMultipleStacks": false,
          "sortValue": 0
        }
      ]
    }
  ]
}

한계 돌파 (Unleash)

같은 종류의 아이템 등을 소재로 소비해, 대상의 레벨 상한을 끌어올리는 “한계 돌파” 기능도 제공하고 있습니다. GS2-Experience가 관리하는 rankCap(랭크 상한)을 한 단계 끌어올리는 구조로, UnleashRateModel의 마스터 데이터로 다음을 설정합니다.

  • 한계 돌파 대상 인벤토리 모델
  • 연결된 그레이드 모델(GS2-Grade)
  • 각 그레이드에 도달하기 위해 필요한 소재 아이템과 그 수량

이를 통해 “같은 캐릭터를 4명 모으면 한계 돌파를 할 수 있다”, “특정 소재 아이템을 N개 소비하면 레벨 상한이 해제된다” 같은 게임 메커니즘을 구현할 수 있습니다.

스크립트 트리거

네임스페이스에 enhanceScript를 설정하면 강화 실행 전후로 커스텀 스크립트를 호출할 수 있습니다. 처리의 허가·거부나 입수 경험치의 오버라이드가 가능하며, 실행 방식은 동기·비동기를 선택할 수 있습니다. 비동기에서는 GS2-Script나 Amazon EventBridge를 이용한 외부 연동에도 대응합니다.

설정할 수 있는 주요 이벤트 트리거와 스크립트 설정 이름은 다음과 같습니다.

  • enhanceScript(완료 통지: enhanceDone): 강화 실행 전후

스크립트 내에서는 강화 대상·소재 아이템·추첨되는 배율을 참조할 수 있어, 게임 밸런스 조정이나 이벤트 기간 중 부스트 배율 변경과 같은 용도로 활용할 수 있습니다.

마스터 데이터 운용

마스터 데이터를 등록함으로써 마이크로서비스에서 이용 가능한 데이터나 동작을 설정할 수 있습니다.

마스터 데이터의 종류에는 다음이 있습니다.

  • RateModel: 강화 소재나 대상의 정의
  • UnleashRateModel: 상한 해제용 레이트 정의

마스터 데이터의 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub에서 데이터를 반영하거나 GS2-Deploy를 사용해 CI에서 등록하는 워크플로우를 구성할 수 있습니다.

트랜잭션 액션

GS2-Enhance에서는 다음 트랜잭션 액션을 제공하고 있습니다.

  • 소비 액션: 실행 중인 강화 진행 정보(Progress)의 삭제
  • 입수 액션: 즉시 강화(DirectEnhance)의 실행, 한계 돌파(Unleash)의 실행, 강화(Progress)의 시작

“즉시 강화(DirectEnhance)의 실행"을 입수 액션으로 이용함으로써, 특정 퀘스트 클리어 보상이나 상점에서의 아이템 구매 시 보상으로, 직접 캐릭터나 장비에 경험치를 부여하는 처리가 가능해집니다. 또한 “한계 돌파(Unleash)“를 보상으로 설정함으로써, 특정 미션 달성 시 자동으로 레벨 상한을 끌어올리는 운용도 가능합니다.

구현 예제

강화 레이트 목록 취득

    var items = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).RateModelsAsync(
    ).ToListAsync();
    const auto It = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->RateModels();
    TArray<Gs2::UE5::Enhance::Model::FEzRateModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
var iterator = ez.enhance.namespace_(
        "namespace-0001"
    ).rate_models(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 오류를 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

강화 실행(DirectEnhance)

materials에는 소비할 소재 아이템의 ItemSetId와 수량을 지정합니다. 강화 대상으로 targetItemSetId에 강화하고 싶은 캐릭터·장비의 ItemSetId를 전달합니다.

    var result = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Enhance(
    ).EnhanceAsync(
        rateName: "rate-0001",
        targetItemSetId: "item-set-0001",
        materials: new [] {
            new Gs2.Unity.Gs2Enhance.Model.EzMaterial
            {
                MaterialItemSetId = "material-0001",
                Count = 1,
            },
        }
    );

    var transaction = result;
    await transaction.WaitAsync();
    const auto Domain = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Enhance(
    );
    const auto Future = Domain->Enhance(
        "rate-0001",
        "item-set-0001",
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Enhance::Model::FMaterial>>>();
            v->Add(MakeShared<Gs2::Enhance::Model::FMaterial>()
                ->WithMaterialItemSetId(TOptional<FString>("material-0001"))
                ->WithCount(TOptional<int32>(1)));
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).enhance(
    )

var async_result = await domain.enhance(
    "rate-0001", # rate_name
    "item-set-0001", # target_item_set_id
    [
        Gs2EnhanceEzMaterial.new()
            .with_material_item_set_id("material-0001")
            .with_count(1),
    ], # materials
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

강화(Progress)의 시작

추첨 결과를 먼저 취득해 두고 싶은 경우의 흐름입니다. 진행 중인 Progress는 1명의 사용자당 1건만 유지할 수 있습니다.

    var result = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).StartAsync(
        rateName: "rate-0001",
        targetItemSetId: "item-set-0001",
        materials: new [] {
            new Gs2.Unity.Gs2Enhance.Model.EzMaterial
            {
                MaterialItemSetId = "material-0001",
                Count = 1,
            },
        }
    );
    const auto Future = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    )->Start(
        "rate-0001", // rateName
        "item-set-0001", // targetItemSetId
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Enhance::Model::FMaterial>>>();
            v->Add(MakeShared<Gs2::Enhance::Model::FMaterial>()
                ->WithMaterialItemSetId(TOptional<FString>("material-0001"))
                ->WithCount(TOptional<int32>(1)));
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.start(
    "character-level", # rate_name
    "item-set-0001", # target_item_set_id
    [
        Gs2EnhanceEzMaterial.new()
            .with_material_item_set_id("material-0001")
            .with_count(1),
    ], # materials
    null, # force
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

진행 중인 강화 정보 취득

추첨이 완료된 배율(대성공 플래그)이나 입수 경험치량을 취득해, 강화 연출에 활용할 수 있습니다.

    var item = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).ModelAsync();

    var experienceValue = item.ExperienceValue;
    var rate = item.Rate;
    const auto Domain = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Result = Future->GetTask().Result();
    const auto ExperienceValue = Result->GetExperienceValue();
    const auto Rate = Result->GetRate();
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

강화(Progress)의 완료

Start로 시작한 강화를 End로 확정하여, 소비·경험치 가산을 실행합니다.

    var result = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).EndAsync();

    await result.WaitAsync();
    const auto Future = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    )->End();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.end(
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

진행 중인 강화 정보 파기

통신이 끊겨 완료 보고를 할 수 없게 된 경우 등의 복구에 사용합니다.

    await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).DeleteProgressAsync();
    const auto Future = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    )->DeleteProgress();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.delete_progress(
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

기타 기능

강화의 대성공

강화 사양으로서, 일정 확률로 “대성공"이 발생해 입수할 수 있는 경험치량이 1.5배나 2배가 되는 사양이 있는 경우가 있습니다. GS2-Enhance에서는 강화 레이트로서 대성공이 발생할 확률과 그 경우의 경험치 획득량 배율을 설정할 수 있습니다.

bonusRatesweight 합계값에 대한 각 엔트리의 weight 비율이 그 배율의 발생 확률이 됩니다.

Config를 사용한 스크립트로의 파라미터 전달

EnhanceAsync / StartAsync에는 config 파라미터를 지정할 수 있어, 스크립트 트리거 실행 시 임의의 키와 값의 쌍을 전달할 수 있습니다. 게임 내 이벤트 부스트 상태나 플레이어가 선택한 강화 모드와 같은 정보를 스크립트에 전달할 수 있습니다.

투기적 실행(Speculative Execute)

speculativeExecute 인수(기본값 true)를 활성화하면, API 요청의 응답을 기다리기 전에 클라이언트 측 캐시를 갱신하여, UI 상에서는 즉시 경험치 가산 후의 상태를 표시할 수 있습니다. 통신 지연의 영향을 덜 받는 쾌적한 조작감을 실현할 수 있습니다.

상세 레퍼런스