Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

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

QualiArts

April 23, 2022
Tweet

More Decks by QualiArts

Other Decks in Programming

Transcript

  1. 大規模ゲーム開発で問題になる点① 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
  2. 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
  3. 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
  4. 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
  5. Data Change リクエスト内で変更されたトランザクションデータをクライアントに返す仕組み 処理の流れ • Insert / Update / Delete

    されたデータをContext内に保持 • API処理の最後にMiddleware内の処理で更新データを一括でフォーマットする • 更新されたデータをクライアント側で受け取る 利点 • Infra層でのDB更新結果をHandler層まで伝搬できる 11
  6. 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
  7. 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
  8. 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
  9. まとめ 大規模ゲームを開発する上でContextを以下のように利用している • Game Context … 認証情報やユーザー情報 • Query Cache

    … DBアクセスした内容をキャッシュしておく仕組み • Data Change … トランザクションデータを伝搬する仕組み (その他、ステータス管理や課金情報の管理にもContextを利用している) 15