$30 off During Our Annual Pro Sale. View Details »

大規模ゲーム開発におけるContext活用パターン

QualiArts
April 23, 2022

 大規模ゲーム開発におけるContext活用パターン

GoのContextではリクエストスコープの値を伝播する事ができます。 主な利用例として認証トークンを伝搬させる手法がありますが、ゲーム開発においては他にも様々なContextの利用方法があります。

この資料では大規模ゲーム開発で利用しているContextの活用パターンについてお話しします。 Contextを活用することで、DBアクセス頻度を少なくしたり、レスポンスサイズを小さくする工夫ができたため、その実装手法を共有します。

QualiArts

April 23, 2022
Tweet

More Decks by QualiArts

Other Decks in Programming

Transcript

  1. 大規模ゲーム開発における
    Context活用パターン
    Go Conference 2022 Spring
    @8kka

    View Slide

  2. 自己紹介
    8kka(はっか)
    ● 株式会社QualiArts所属
    ● Goでのゲーム開発歴は2年ぐらい
    ● GCPやk8sなどのインフラ構築も担当
    ● 最近技術書店でGoの本出しました
    2

    View Slide

  3. 大規模ゲーム開発で問題になる点①
    type interactor struct {
    mCache mcache.Cache
    userTxManager database.UserTxManager
    actionLogger action.Logger
    conditionComponent condition.Component
    missionComponent missioncomponent.Component
    idConverterComponent idconverter.Component
    logComponent log.Component
    liveFacade live.Facade
    questFacade questfacade.Facade
    gvgFacade gvg.Facade
    hierarchyFacade hierarchy.Facade
    questLiveService questservice.LiveService
    questRewardService questservice.RewardService
    questListService questservice.ListService
    deckService deckservice.Service
    photoService photoservice.Service
    rewardService rewardservice.Service
    consumptionService consumption.Service
    userService userservice.Service
    giftService giftservice.Service
    pvpService pvpservice.Service
    messageService message.Service
    activityLessonService activitylesson.Service
    hierarchyService hierarchyservice.Service
    tutorialService tutorial.Service
    divisionService division.Service
    }
    同じデータへのアクセスが様々な箇所で発生する
    ある処理を実装するInteractorでアイテム情報を
    扱うServiceやComponentはこれだけある
    ● conditionComponent
    ● questRewardService
    ● rewardService
    ● consumptionService
    ● giftService
    3

    View Slide

  4. 大規模ゲーム開発で問題になる点②
    APIの処理が複雑で、データの伝搬が困難
    更新データをクライアントに返す場合、伝搬させ
    る階層が多く煩雑になりやすい
    Handler
    Interactor
    Facade
    Service
    Component
    Infra
    4

    View Slide

  5. コンテキスト利用例一覧
    ● Game Context
    ● Query Cache
    ● Data Change
    5

    View Slide

  6. Game Context
    認証情報、リクエスト情報、ユーザー情報など各所に伝搬させたい情報を保持する
    ● 一般的にGoのContextで利用されるような情報を定義する
    ● ここで扱う情報はイミュータブルな値で、変更はされない
    6

    View Slide

  7. Game Context
    type contextKey struct{} // Contextに保持する際のkey
    type GameContext struct {
    Auth *Auth
    Request *Request
    Log *Log
    Now time.Time
    }
    // Contextへの設定と取得
    func Set(ctx context.Context, gctx *GameContext) context.Context {
    return context.WithValue(ctx, contextKey{}, gctx)
    }
    func Extract(ctx context.Context) *GameContext {
    value := ctx.Value(contextKey{})
    if value == nil {
    return nil
    }
    return value.(*GameContext)
    }
    Auth:userID, authToken etc...
    ● ユーザーの認証に必要な情報
    Request:ip, os, api, illegalData etc...
    ● リクエストに含まれる情報
    ● クライアントで検知した不正判定なども含まれる
    Log:logID etc...
    ● ログ出力に利用する情報
    ● 同じAPIから出力されたログはlogIDで判定する
    Now:
    ● API共通で利用する現在時刻の情報
    > gamecontext.go
    7

    View Slide

  8. Query Cache
    DBから取得したトランザクションデータをContext内でキャッシュする仕組み
    処理の流れ
    ● リクエスト単位で初めて叩かれるクエリはDBから取得してContextに保持
    ● リクエスト単位で同一のクエリが叩かれる場合はContextから取得
    ● データが更新された場合はContext内のデータも更新する
    ○ (トランザクション処理はリクエストの最後に一括で行う)
    利点
    ● 各所で同じクエリを叩いてもDBアクセスが走らない
    ● リクエスト単位で状態を保持したレコードを扱える
    8

    View Slide

  9. Query Cache
    type QueryCache interface {
    Set(key string, value interface{})
    Get(key string) interface{}
    Exists(key string) bool
    GetAndExists(key string) (interface{}, bool)
    Clear()
    }
    type queryCache struct {
    sync.RWMutex
    // テーブル名をkeyとするクエリ結果情報群
    // keyの例) user, usercard, useritem etc...
    cacheMap map[string]interface{}
    }
    // cacheMapのvalueに当たる構造(テーブル毎に自動生成)
    type cacher struct {
    sync.RWMutex
    queries []*query
    caches map[string]*cache // PKをkeyとするクエリ結果
    }
    // クエリ結果の情報(userテーブルの場合)
    type cache struct {
    value *user.User
    }
    // キャッシュに利用したクエリ情報(conditionはカラムや条件の情報)
    type query struct {
    conditions []*condition
    }
    > querycache.go > usercache.go
    9

    View Slide

  10. Query Cache (condition)
    type query struct {
    conditions []*condition
    }
    type condition struct {
    column string
    operator ConditionOperator
    value interface{}
    }
    // conditionの例(SELECT * FROM user WHERE UserID = "foo")
    // {
    // column: "UserID",
    // operator: ConditionOperatorEq,
    // value: "foo",
    // },
    > usercache.go
    user_itemの取得
    1. WHERE UserID IN (“userA”, “userB”)
    -> userAとuserBのuser_itemをDBから取得
    -> 以下のconditionを設定する
     {column:”UserID”, operator:”IN”, value”userA”}
     {column:”UserID”, operator:”IN”, value”userB”}
    2. WHERE UserID = “userA”
    -> userAのuser_itemをContextから取得
    10

    View Slide

  11. Data Change
    リクエスト内で変更されたトランザクションデータをクライアントに返す仕組み
    処理の流れ
    ● Insert / Update / Delete されたデータをContext内に保持
    ● API処理の最後にMiddleware内の処理で更新データを一括でフォーマットする
    ● 更新されたデータをクライアント側で受け取る
    利点
    ● Infra層でのDB更新結果をHandler層まで伝搬できる
    11

    View Slide

  12. Data Change
    type DataChange interface {
    AddUpdatedData(userID string, data interface{}) error
    AddDeletedData(userID string, data interface{}) error
    GetUpdated() *UpdatedData
    GetDeleted() *DeletedData
    Clear()
    }
    type dataChange struct {
    updated *UpdatedData
    deleted *DeletedData
    }
    // UserItems など更新されるトランザクションデータ毎にmapを保持
    type UpdatedData struct {
    sync.RWMutex
    User *user.User
    UserItems map[string]*useritem.UserItem
    UserCards map[string]*usercard.UserCard
    UserCharacters map[string]*usercharacter.UserCharacter
    }
    // UserItemPKs など、削除されるトランザクションデータ毎にPKを保持
    type DeletedData struct {
    sync.RWMutex
    UserItemPKs map[string]*useritem.PK
    UserDeckPKs map[string]*userdeck.PK
    UserPointPKs map[string]*userpoint.PK
    }
    > datachange.go > datachange.go
    12

    View Slide

  13. Data Change
    func (d *dataChange) AddUpdatedData(userID string, data interface{}) error {
    if reflect.TypeOf(data).Kind() != reflect.Ptr {
    return fmt.Errorf("attempt to add a non-pointer")
    }
    updated := d.updated
    switch castdata := data.(type) {
    case *useritem.UserItem:
    // UserItemが更新されている場合、castdataを詰める
    if updated.UserItems == nil {
    updated.UserItems = make(map[string]*useritem.UserItem)
    }
    updatedData.UserItems[generateKey(castdata.ItemID)] = castdata
    }
    // case *usercard.UserCard: のように、他のトランザクションデータも記述する
    return nil
    }
    > datachange.go
    13

    View Slide

  14. Data Change
    // データ更新情報を乗せるレスポンスかチェック
    dcRes, ok := res.(dataChangeResponse)
    if !ok {
    return res, nil
    }
    // 更新,削除データを入れる変数を用意する
    updated := &pb.UpdatedData{}
    deleted := &pb.DeletedData{}
    dataChange := datachange.Extract(ctx)
    if dataChange == nil {
    return res, nil
    }
    updatedData := dataChange.GetUpdated()
    deletedData := dataChange.GetDeleted()
    for _, item := range updatedData.UserItems {
    pbitem := converter.ToProtoUserItem(item)
    updated.Items = append(updated.Items, pbitem)
    }
    for _, deck := range updatedData.UserDecks {
    pbdeck := converter.ToProtoUserDeck(deck)
    updated.Decks = append(updated.Decks, pbdeck)
    }
    for _, point := range updatedData.UserPoints {
    pbpoint := converter.ToProtoUserPoint(point)
    updated.Points = append(updated.Points, pbpoint)
    }
    dcRes.SetCommonResponse(&pb.CommonResponse{
    UpdatedData: updated,
    DeletedData: deleted,
    })
    > middleware.go > middleware.go
    14

    View Slide

  15. まとめ
    大規模ゲームを開発する上でContextを以下のように利用している
    ● Game Context … 認証情報やユーザー情報
    ● Query Cache … DBアクセスした内容をキャッシュしておく仕組み
    ● Data Change … トランザクションデータを伝搬する仕組み
    (その他、ステータス管理や課金情報の管理にもContextを利用している)
    15

    View Slide