GS2-Exchange
This is one of the most widely used microservices provided by GS2. It is responsible for converting the resources of any microservice into the resources of a completely different microservice.
There are many game specifications related to resource exchange, and GS2-Exchange comes into play each time.
Examples of resource exchange
Conversion of enhancement materials
Exchange 10 ★1 enhancement materials managed by GS2-Inventory for 1 ★2 enhancement material also managed by GS2-Inventory.
Obtain an item once every 6 hours
Exchange 1 stamina, which recovers once every 6 hours, managed by GS2-Stamina, for an item managed by GS2-Inventory.
Sell items
Exchange items managed by GS2-Inventory for in-game currency also managed by GS2-Inventory.
Types of exchange
GS2-Exchange has three modes: direct exchange, where exchange happens immediately at the rate defined in master data; asynchronous exchange, where the exchange result is obtained after a certain real-world time has passed after executing the exchange; and cost-increasing exchange, where the cost rises with each purchase.
flowchart LR Verify["Verify cost<br/>(verifyActions)"] --> Consume["Consume cost<br/>(consumeActions)"] Consume -->|timingType=direct| Acquire["Grant reward<br/>(acquireActions)"] Consume -->|timingType=await| Await["Create Await object"] Await -. lockTime elapses .-> Acquire2["AcquireAsync receives reward"]
The exchange behavior can be enabled per mode through the namespace settings enableDirectExchange / enableAwaitExchange.
By switching the timingType of a rate model to direct or await, you can choose whether each individual rate operates as an immediate exchange or as an asynchronous exchange.
Behavior of asynchronous exchange
When an asynchronous exchange is used, the cost is consumed at the time the exchange is executed, and an Await object is created instead of the reward being granted.
Once the exchange wait time defined in master data (lockTime seconds) elapses from the time the Await object was created, the reward can be received.
| State | Field |
|---|---|
| Time the exchange was executed | exchangedAt |
| Time at which the reward becomes acquirable | acquirableAt |
| Quantity consumed as cost | count |
| Seconds shortened by skipping | skipSeconds |
Strengthening bases
In a city-building game, after consuming resources to grow a base, the base’s experience is actually added after 8 hours have elapsed.
Expeditions
After organizing a party and going on an adventure, the reward of the adventure can be received 3 hours later.
Skipping asynchronous exchange
You can also allow the wait of asynchronous exchanges to be skipped by paying additional cost. This kind of specification is typically implemented as a monetization feature, but by consuming in-game currency managed by GS2-Money that has been purchased with real money, you can implement a specification where wait times can be shortened.
Forced acquisition and deletion of pending rewards
Through the management console or API (transaction actions), it is possible to forcibly acquire rewards ignoring the wait time, or to cancel an in-progress asynchronous exchange (Await object) by deleting it.
Examples of cost-increasing exchange
Enhancement (inflationary game)
The amount of gold consumed to enhance increases each time you enhance.
Stamina recovery cost increases
The amount of paid currency required to purchase stamina increases each time stamina is purchased. The purchase count is reset daily.
Calculation of cost increase
There are three modes for the amount of cost increase, specified by calculateType of IncrementalRateModel.
linear
baseValue + (coefficientValue * number of exchanges)
Example
baseValue = 100, coefficientValue = 50
| Number of exchanges | Cost |
|---|---|
| 0 | 100 |
| 1 | 150 |
| 2 | 200 |
| 3 | 250 |
| 4 | 300 |
power
coefficientValue * (number of exchanges + 1) ^ 2
Example
coefficientValue = 50
| Number of exchanges | Cost |
|---|---|
| 0 | 50 |
| 1 | 200 |
| 2 | 450 |
| 3 | 800 |
| 4 | 1250 |
gs2_script
Calculated based on the result of executing a GS2-Script. Useful when you want to calculate the cost based on complex conditions.
Example
currentExchangeCount = args.currentExchangeCount
quantity = quantity
cost = 100
for i = 1 , quantity do
cost = cost + (i + currentExchangeCount - 1) * 50
end
result = {
cost=cost
}| Number of exchanges | Cost |
|---|---|
| 0 | 100 |
| 1 | 150 |
| 2 | 200 |
| 3 | 250 |
| 4 | 300 |
Managing the number of exchanges
In IncrementalRateModel, by specifying exchangeCountId and maximumExchangeCount, you can integrate with GS2-Limit’s count restriction model to track the number of exchanges executed, or set an exchange limit within a specific period.
If you specify a GS2-Limit Counter model as exchangeCountId, the exchange count is tracked in GS2-Limit and can be reset daily, weekly, etc., according to GS2-Limit’s reset specification.
Exchanges that exceed maximumExchangeCount are rejected, which makes it possible to implement gachas with a per-period purchase limit, or item shops where you can purchase only N times per day.
Master data operations
Registering master data allows you to configure data and behavior that can be used by the microservice.
Available master data types:
RateModel: Rate definitions for immediate or asynchronous exchangesIncrementalRateModel: Rate definitions where cost increases with the number of exchanges
Main fields of RateModel
| Field | Description |
|---|---|
name |
Rate name (unique) |
verifyActions |
Verify actions performed before executing the exchange |
consumeActions |
Actions consumed as the cost |
acquireActions |
Actions granted as the reward |
timingType |
direct (immediate exchange) / await (asynchronous exchange) |
lockTime |
Wait seconds of asynchronous exchange |
Main fields of IncrementalRateModel
| Field | Description |
|---|---|
name |
Rate name (unique) |
consumeAction |
Action consumed as cost (quantity is determined by the cost calculation) |
acquireActions |
Actions granted as reward |
calculateType |
Cost calculation method (linear / power / gs2_script) |
baseValue / coefficientValue |
Coefficients used in cost calculation |
calculateScriptId |
Script invoked when using gs2_script |
exchangeCountId |
GS2-Limit counter that manages the exchange count |
maximumExchangeCount |
Upper limit on the number of exchanges |
JSON example of master data
{
"version": "2019-08-19",
"rateModels": [
{
"name": "material_n_to_r",
"metadata": "N -> R enhancement material exchange",
"consumeActions": [
{
"action": "Gs2Inventory:ConsumeItemSetByUserId",
"request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"n-material\",\"userId\":\"#{userId}\",\"consumeCount\":10}"
}
],
"timingType": "await",
"lockTime": 3600,
"acquireActions": [
{
"action": "Gs2Inventory:AcquireItemSetByUserId",
"request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"r-material\",\"userId\":\"#{userId}\",\"acquireCount\":1}"
}
]
}
]
}Master data can be registered via the management console, imported from GitHub, or registered from CI using GS2-Deploy or various language CDKs.
Script Triggers
By adding the following script settings to a namespace, you can run custom scripts before and after exchange processing or pending reward retrieval.
Main event triggers and script setting names are:
exchangeScript(completion notification:exchangeDone): before and after exchange processingincrementalExchangeScript(completion notification:incrementalExchangeDone): before and after cost-increasing exchangeacquireAwaitScript(completion notification:acquireAwaitDone): before and after acquiring a pending reward
These scripts can be executed synchronously or asynchronously, with asynchronous execution also supporting external integration via GS2-Script or Amazon EventBridge.
When calculateType gs2_script is selected on an IncrementalRateModel, the calculateScriptId configured per rate is invoked to calculate the cost.
Correction by Buff
When linked with GS2-Buff, you can dynamically override lockTime, acquireActions, verifyActions, and consumeActions of a RateModel, and acquireActions, consumeAction, and maximumExchangeCount of an IncrementalRateModel via the context stack. This allows flexible adjustment of rewards, wait times, and exchange limits for events or campaigns.
For example, you can implement measures such as “halving the wait time for enhancement material exchanges for a limited period” or “raising the daily gacha purchase limit” by simply applying a buff, without rewriting master data.
Implementation Example
Executing an exchange (direct)
The config parameter accepts context values used in the transactions of the cost/reward actions associated with the exchange.
var result = await gs2.Exchange.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Exchange(
).ExchangeAsync(
rateName: "rate-0001",
count: 1
); const auto Domain = Gs2->Exchange->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Exchange(
);
const auto Future = Domain->Exchange(
"rate-0001", // rateName
1, // count
nullptr // config
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;Starting an asynchronous exchange
When ExchangeAsync is called on a rate with timingType=await, the cost is consumed immediately and the reward is held in an Await object.
var result = await gs2.Exchange.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Exchange(
).ExchangeAsync(
rateName: "material_n_to_r",
count: 1
); const auto Domain = Gs2->Exchange->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Exchange(
);
const auto Future = Domain->Exchange(
"material_n_to_r", // rateName
1, // count
nullptr // config
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;Get a list of Awaits
var items = await gs2.Exchange.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).AwaitsAsync(
).ToListAsync(); const auto Domain = Gs2->Exchange->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto It = Domain->Awaits(
);
TArray<Gs2::UE5::Exchange::Model::FEzAwaitPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}Receiving the reward of an Await after the wait time has elapsed
Receive the reward from an Await whose wait time has elapsed.
The receive process issues a transaction, and the reward-granting processes defined in acquireActions are executed against other microservices.
var result = await gs2.Exchange.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Await(
rateName: "material_n_to_r",
awaitName: "await-0001"
).AcquireAsync(
); const auto Domain = Gs2->Exchange->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Await(
"await-0001", // awaitName
"material_n_to_r" // rateName
);
const auto Future = Domain->Acquire(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;Deleting an Await (cancellation)
Discards a pending Await and cancels the exchange. Note that resources consumed as cost are not returned. If a refund is necessary, compensate via a server-side script using transaction actions.
var result = await gs2.Exchange.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Await(
rateName: "material_n_to_r",
awaitName: "await-0001"
).DeleteAwaitAsync(
); const auto Domain = Gs2->Exchange->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Await(
"await-0001", // awaitName
"material_n_to_r" // rateName
);
const auto Future = Domain->DeleteAwait(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;Skipping wait time
To skip the wait time by paying an additional cost, configure the cost required to skip via skipByConfig, then call the server-side API (via GS2-JobQueue or GS2-Script).
The skip process can also be used as a transaction action, so it can be combined with shop designs such as skip items or skip tickets.
Executing a cost-increasing exchange
var result = await gs2.Exchange.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Exchange(
).IncrementalExchangeAsync(
rateName: "rate-0001",
count: 1
); const auto Domain = Gs2->Exchange->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Exchange(
);
const auto Future = Domain->IncrementalExchange(
"rate-0001", // rateName
1, // count
nullptr // config
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;Transaction Actions
GS2-Exchange provides the following transaction actions:
- Consume action: Delete an in-progress exchange wait (Await)
- Acquire action: Immediately execute an exchange (Exchange), execute a cost-increasing exchange (IncrementalExchange), create an exchange wait (Await), forcibly acquire a pending reward, skip wait time
By using “Forcibly acquire pending reward” as an acquire action, you can immediately complete an in-progress construction or expedition (asynchronous exchange) as a reward for acquiring a specific item or achieving a mission. Also, by using “Delete exchange wait” as a consume action, you can interrupt (cancel) an in-progress process.