> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-Mission

미션·업적 기능




게임 내에 축적된 행동을 기반으로 플레이어에게 보상을 지급하기 위한 구조입니다.
일반적으로 업적·트로피·미션이라고 불리는 기능을 구현하기 위한 기능입니다.

```mermaid
graph TD
  Action["게임 내 행동<br/>(퀘스트 클리어 / 가챠 실행 등)"] -- "카운터 상승<br/>(IncreaseCounterByUserId)" --> Counter["미션 카운터<br/>(Counter / Scope)"]
  Counter -- "목표값 도달" --> Task["미션 태스크<br/>(MissionTaskModel)"]
  Task -- "ReceiveRewards" --> Reward["completeAcquireActions"]
  Reward --> Distributor["GS2-Distributor<br/>보상 배포"]
  Task --> Group["미션 그룹<br/>(MissionGroupModel)"]
  Group -- "resetType 에 기반해<br/>수령 플래그를 리셋" --> Task
```

## 미션 카운터

플레이어의 행동에 기반한 횟수를 세기 위한 엔티티입니다.
"퀘스트를 클리어한 횟수", "캐릭터를 강화한 횟수", "가챠를 뽑은 횟수"와 같은 게임 내 행동 횟수를 카운트하기 위한 카운터를 준비합니다.

카운터에는 스코프를 설정할 수 있습니다.
스코프에는 다음 값을 설정할 수 있습니다.

| 스코프 종류                 | 스코프 내용                         |
|-------------------------|---------------------------------|
| 리셋하지 않음                 | 게임 플레이 시작부터의 합계                   |
| 매일 X시에 리셋               | 당일 실행한 횟수                        |
| 매주 X요일 X시에 리셋            | 이번 주 실행한 횟수                        |
| 매월 X일 X시에 리셋             | 이번 달 실행한 횟수                        |
| 일정 일수마다 리셋             | 기준 시각부터 지정 일수마다 리셋               |
| 검증 액션에 매치했을 때만 카운트업 | verifyAction 의 실행 결과에 기반해 카운터를 상승 |

카운터 값은 각 스코프별로 관리되며, 미션의 달성 조건에는 각 스코프의 값을 사용할 수 있습니다.

```mermaid
graph LR
  Up["카운터 상승 요청"] --> Counter["Counter"]
  Counter --> Scope1["스코프1<br/>리셋하지 않음"]
  Counter --> Scope2["스코프2<br/>매일 리셋"]
  Counter --> Scope3["스코프3<br/>매주 리셋"]
  Scope1 -- ScopedValue --> Mission1["누계 100회 미션"]
  Scope2 -- ScopedValue --> Mission2["데일리 10회 미션"]
  Scope3 -- ScopedValue --> Mission3["위클리 50회 미션"]
```

### 검증 액션에 의한 스코프 제어

scopeType=verifyAction 을 사용하면 conditionName 과 condition 으로 임의의 조건을 정의하고,
다른 마이크로서비스의 검증 결과에 따라 카운터를 상승시킬지 제어할 수 있습니다.
이를 통해 특정 이벤트 기간에만 카운터를 상승시키는 스코프나, 특정 캐릭터를 편성한 상태에서만 상승하는 스코프를 만들 수 있습니다.

### 챌린지 기간

`CounterModel` 에는 `challengePeriodEventId` 를 설정할 수 있으며, GS2-Schedule 의 이벤트 GRN 을 지정하여 카운터 조작 기간을 제한할 수 있습니다.
지정된 기간 외에는 카운터를 갱신할 수 없으므로, 기간 한정 이벤트 등에서 사용할 때 유용합니다.

## 미션 태스크

플레이어에게 제시할 목표를 정의하는 마스터 데이터입니다.

"미션 카운터"와 "스코프"와 "목표값", 그리고 그 목표를 달성했을 때 얻을 수 있는 "보상"을 설정합니다.
예를 들어 다음과 같은 설정이 가능합니다.

| 미션 카운터 | 스코프 종류 | 목표값 | 보상 |
| ----------------- | ----------- | ----- | ---- |
| 퀘스트를 클리어한 횟수 | 매일 X시에 리셋 | 10 | 아이템A |
| 퀘스트를 클리어한 횟수 | 매주 X요일 X시에 리셋 | 50 | 아이템B |
| 캐릭터를 강화한 횟수 | 매일 X시에 리셋 | 5 | 아이템C |

### 태스크 의존 관계

`premiseMissionTaskName` 을 지정하면 선행 태스크를 달성한 이후에만 수령 가능한 미션을 정의할 수 있습니다.
"태스크A를 달성하면 태스크B를 수령할 수 있게 된다"와 같은 단계적인 달성 흐름을 구축할 수 있습니다.

### 검증 액션을 사용한 달성 판정

`verifyCompleteType` 에 `verifyActions` 를 지정하면 다른 마이크로서비스의 검증 액션을 충족하는지에 따라 태스크의 달성 여부를 판정할 수 있습니다.
이 기능을 사용하는 경우, GS2-Mission 이 제공하는 `Complete` 오브젝트에 의한 달성 판정(서버 사이드에서의 자동 판정)은 이루어지지 않습니다. 따라서 달성 상태는 클라이언트에서 계산한 뒤 수령 UI를 제어하고, 보상 수령 액션을 실행해야 합니다.

서버 사이드에서 달성 가능 여부를 재평가하고 싶은 경우에는 `EvaluateComplete` API를 호출하여 `Complete` 의 상태를 최신화할 수 있습니다.

### 챌린지 기간

`MissionTaskModel` 의 `challengePeriodEventId` 로 달성 가능 기간을 GS2-Schedule 의 이벤트로 제한할 수 있습니다.
달성 완료된 태스크는 기간이 지나도 수령이 가능하지만, 미션 태스크에 리셋 간격이 설정되어 있는 경우에는 리셋 시점을 맞이하면 수령할 수 없게 됩니다.

## 미션 그룹

여러 미션 태스크를 묶는 엔티티입니다.
미션 그룹에는 보상 수령 플래그의 리셋 주기를 설정할 수 있습니다.

| 미션 카운터 | 스코프 종류 | 목표값 | 보상 |
| ----------------- | ----------- | ----- | ---- |
| 퀘스트를 클리어한 횟수 | 매일 X시에 리셋 | 10 | 아이템A |
| 캐릭터를 강화한 횟수 | 매일 X시에 리셋 | 5 | 아이템C |

이러한 미션 태스크를 하나의 미션 그룹에 연결하고, 미션 그룹의 보상 수령 플래그 리셋 주기에도 "매일 X시에 리셋"을 설정하면
미션 태스크를 매일 달성할 경우 매일 보상을 받을 수 있게 됩니다.

### 리셋 종류

`resetType` 에는 다음을 지정할 수 있습니다.

| `resetType` | 설명 |
| --- | --- |
| `notReset` | 리셋하지 않음(업적·트로피 용도로 사용) |
| `daily` | 매일 `resetHour` 시에 리셋 |
| `weekly` | 매주 `resetDayOfWeek` 의 `resetHour` 시에 리셋 |
| `monthly` | 매월 `resetDayOfMonth` 의 `resetHour` 시에 리셋 |
| `days` | `anchorTimestamp` 를 기점으로 `days` 일마다 리셋 |

### 임의 일수로 리셋

`resetType` 에 `days` 를 지정하면 `anchorTimestamp` 로 지정한 기점 시각부터 지정한 일수마다 보상 수령 플래그를 리셋할 수 있습니다.
예를 들어 이벤트 시작부터 3일마다 리셋하는 등의 운영이 가능합니다.

## 스크립트 트리거

네임스페이스에 `missionCompleteScript`·`counterIncrementScript`·`receiveRewardsScript` 를 설정하면 미션 달성·카운터 상승·보상 수령 등의 시점에 커스텀 스크립트를 호출할 수 있습니다.

설정 가능한 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.

- `missionCompleteScript`: 미션 달성
- `counterIncrementScript`: 카운터 상승
- `receiveRewardsScript`: 보상 수령

이를 활용하면 달성 시 BI 연동, 부정한 카운터 조작 감지, 보상 수령 시의 특수 처리 등을 커스터마이즈할 수 있습니다.

## 푸시 알림
설정 가능한 주요 푸시 알림과 설정명은 다음과 같습니다.

- `completeNotification`: 미션 태스크 달성 시 알림

오프라인 단말에 대한 모바일 푸시 전송에도 대응하며, 보상 수령을 유도할 수 있습니다.

## 마스터 데이터 운용
마스터 데이터를 등록하면 마이크로서비스에서 사용 가능한 데이터나 동작을 설정할 수 있습니다.

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

- `CounterModel`: 카운트 대상과 리셋 주기를 정의하는 `CounterScopeModel` 목록
- `MissionGroupModel`: 그룹 단위의 리셋 설정과 소속된 태스크
- `MissionTaskModel`: 달성 조건(카운터와 목표값)과 달성 보상

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

다음은 마스터 데이터의 JSON 예입니다.

```json
{
  "version": "2019-04-12",
  "counters": [
    {
      "name": "quest_complete",
      "metadata": "퀘스트 클리어 횟수",
      "scopes": [
        {
          "scopeType": "resetTimingScope",
          "resetType": "daily",
          "resetHour": 5
        },
        {
          "scopeType": "resetTimingScope",
          "resetType": "weekly",
          "resetDayOfWeek": "monday",
          "resetHour": 5
        }
      ]
    }
  ],
  "missionGroups": [
    {
      "name": "daily-mission",
      "metadata": "데일리 미션",
      "resetType": "daily",
      "resetHour": 5,
      "tasks": [
        {
          "name": "mission-task-0001",
          "metadata": "퀘스트 10회 클리어",
          "counterName": "quest_complete",
          "targetResetType": "daily",
          "targetValue": 10,
          "completeAcquireActions": []
        }
      ]
    }
  ]
}
```

## GS2-Buff 연동

GS2-Buff 와 연동하면 미션 태스크 모델의 `completeAcquireActions` 를 버프로 보정하여, 이벤트 등에 따라 보상량을 유연하게 조정할 수 있습니다.

## 트랜잭션 액션

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

- 검증 액션: 미션 달성 상황 검증, 카운터 값 검증
- 소비 액션: 보상 수령(일괄 포함), 카운터 감산·리셋
- 획득 액션: 카운터 가산·설정, 보상 수령 상태 복원(미수령화)

"카운터의 가산"을 획득 액션으로 활용하면, 상점에서 상품을 구매했을 때나 퀘스트를 클리어했을 때의 보상으로 직접 미션의 진행도를 진행시키는 처리가 가능해집니다. 또한 "카운터의 감산"을 소비 액션으로 활용하면, 특정 특전을 받기 위한 비용(포인트 소비형 미션 등)으로 미션 카운터를 소비하는 운용도 트랜잭션 내에서 안전하게 실현할 수 있습니다.

## 구현 예제

### 미션 카운터 상승

미션 카운터의 상승은 게임 엔진용 SDK로는 처리할 수 없습니다.

GS2-Quest 의 클리어 보상이나 GS2-Lottery 의 추첨 보상으로 카운터를 상승시키는 방법으로 구현하십시오. 클라이언트에서 직접 카운터를 상승시키고 싶은 경우에는 서버 측에서 트랜잭션을 발행하는 형태로 GS2-Distributor 를 경유한 획득 액션으로 실행합니다.

### 미션 태스크의 달성 상황·보상 수령 정보 취득



**Unity**
```csharp

    var item = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Complete(
        missionGroupName: "mission-group-0001"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Complete(
        "mission-group-0001" // missionGroupName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Result = Future->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.mission.namespace_(
        "namespace-0001"
    ).me(game_session).complete(
        "mission-group-0001"
    )

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

var result = async_result.result

```


### 미션 달성 보상 수령



**Unity**
```csharp

    var result = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Complete(
        missionGroupName: "mission-group-0001"
    ).ReceiveRewardsAsync(
        missionTaskName: "mission-task-0001"
    );

    await result.WaitAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Complete(
        "mission-group-0001" // missionGroupName
    );
    const auto Future = Domain->ReceiveRewards(
        "mission-task-0001"
    );
    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;
```
**Godot**
```gdscript

var domain = ez.mission.namespace_(
        "namespace-0001"
    ).me(game_session).complete(
        "mission-group-0001"
    )

var async_result = await domain.receive_rewards(
    "mission-task-0001", # mission_task_name
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 달성 완료 보상 일괄 수령

달성 완료되었지만 아직 미수령인 태스크를 한 번에 수령하고 싶은 경우, `BatchReceiveRewards` 를 사용할 수 있습니다.
여러 개의 `missionTaskName` 을 지정하여 하나의 트랜잭션으로 보상을 수령합니다.



**Unity**
```csharp

    var result = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Complete(
        missionGroupName: "mission-group-0001"
    ).BatchReceiveRewardsAsync(
        missionTaskNames: new [] {
            "mission-task-0001",
            "mission-task-0002",
        }
    );

    await result.WaitAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Complete(
        "mission-group-0001" // missionGroupName
    );
    const auto Future = Domain->BatchReceiveRewards(
        []
        {
            auto v = MakeShared<TArray<FString>>();
            v->Add("mission-task-0001");
            v->Add("mission-task-0002");
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.mission.namespace_(
        "namespace-0001"
    ).me(game_session).complete(
        "mission-group-0001"
    )

var async_result = await domain.batch_receive_rewards(
    [
        "mission-task-0001",
        "mission-task-0002",
    ], # mission_task_names
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 검증 액션을 이용한 태스크 달성 판정 재평가

`verifyCompleteType` 에 `verifyActions` 를 지정한 태스크의 달성 상황을 서버 측에서 재평가하고 싶은 경우 호출합니다.



**Unity**
```csharp

    var result = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Complete(
        missionGroupName: "mission-group-0001"
    ).EvaluateCompleteAsync(
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Complete(
        "mission-group-0001" // missionGroupName
    );
    const auto Future = Domain->EvaluateComplete();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.mission.namespace_(
        "namespace-0001"
    ).me(game_session).complete(
        "mission-group-0001"
    )

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

var result = async_result.result

```


### 미션 카운터의 값 취득



**Unity**
```csharp

    var item = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Counter(
        counterName: "quest_complete"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Counter(
        "quest_complete" // counterName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Result = Future->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.mission.namespace_(
        "namespace-0001"
    ).me(game_session).counter(
        "quest_complete"
    )

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

var result = async_result.result

```


### 미션 카운터 리셋

특정 스코프의 카운터 값을 수동으로 리셋할 수 있습니다.
이벤트 종료 시 이벤트 관련 카운터를 초기화하는 등의 용도로 사용합니다.



**Unity**
```csharp

    var result = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Counter(
        counterName: "quest_complete"
    ).ResetCounterAsync(
        scopes: new [] {
            new Gs2.Unity.Gs2Mission.Model.EzScopedValue() {
                ResetType = "daily",
                Value = 0,
            },
        }
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Counter(
        "quest_complete" // counterName
    );
    const auto Future = Domain->ResetCounter(
        Scopes
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.mission.namespace_(
        "namespace-0001"
    ).me(game_session).counter(
        "counter-0001"
    )

var async_result = await domain.reset_counter(
    [
        Gs2MissionEzScopedValue.new()
            .with_reset_type("daily"),
        Gs2MissionEzScopedValue.new()
            .with_reset_type("weekly"),
    ] # scopes
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 미션 목표값 취득



**Unity**
```csharp

    var items = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).MissionGroupModel(
        missionGroupName: "mission-group-0001"
    ).MissionTaskModelsAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->MissionGroupModel(
        "mission-group-0001" // missionGroupName
    );
    const auto It = Domain->MissionTaskModels(
    );
    TArray<Gs2::UE5::Mission::Model::FEzMissionTaskModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.mission.namespace_(
        "namespace-0001"
    ).mission_group_model(
        "mission-group-0001"
    ).mission_task_models(
    )

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

var items = async_result.result

```


## 상세 레퍼런스

[GS2-Mission API 레퍼런스](../../api_reference/mission)



