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

Goの標準機能で簡易システムを低コストで作成するテクニック

 Goの標準機能で簡易システムを低コストで作成するテクニック

Goによるゲーム開発のプロジェクトにて、Go標準の機能を複数活用し、実装コストの低いデバッグツール開発基盤を作成しました。 本資料のデバッグツールとは、動作確認やデバッグのため、ユーザーのレベル操作や所持アイテムの増減等、データの状態を操作・参照するAPI群、及びwebツールを指します。

デバッグツールの拡充は、ゲームに限らず様々な分野で成果物の品質に直結しますが、ドメイン固有のロジック実装に集中するためにはより低コストで開発できることが望ましいです。 本資料では、低コストで開発を行えるデバッグツール基盤をどのように実現したのかを紹介します。

8192a3d22a844b926f2e1bd7aef5fb25?s=128

QualiArts

April 23, 2022
Tweet

More Decks by QualiArts

Other Decks in Programming

Transcript

  1. Goの標準機能で簡易システムを 低コストで作成するテクニック Shimada Yuji

  2. 自己紹介 島田 裕司 / Shimada Yuji 株式会社CyberAgent -> QualiArts スマートフォン向けゲームの開発を担当

    Goは今のプロジェクトから使い始めました (2年程度)
  3. ゲーム開発におけるデバッグツール • テストプレイやデバッグのため、特定の状態を作りたいケースが多い ◦ ユーザーのレベルを50に変更 ◦ 回復アイテムを100個付与 ◦ クエストをステージ10までクリア •

    状態操作を簡単に行えるツールが必要 ⇒ 低コストで追加開発できるデバッグツール基盤の事例を紹介
  4. 用語説明 • 弊社ではデバッグツールのことをデバッグコマンド(デバコマ)と呼称 • ソースコード中での表現 ◦ Command ▪ 「ゲームユーザーのレベルを操作する」といった単一の操作、およびそれに関する情報 ▪

    ツール画面上のメニューにあたる ◦ CommandGroup, Group ▪ 「カード」や「クエスト」等の区分で、複数のCommandをまとめた情報 ▪ ツール画面上のカテゴリにあたる
  5. ツールイメージ type UpdateRarityRequest struct { UserID string `label:"ユーザーID"` CardID string

    `master:"card" type:"select"` Rarity int32 `enum:"rarity" type:"radios"` } func (h *handlerImpl) GetBaseCommandGroup() *Group { return &Group{ Name: "カード", Commands: command.Commands{ { HandlerFunc: h.UpdateRarity, URL: "/updateCardRarity", Name: "レアリティ操作", Model: UpdateRarityRequest{}, }, }, } }
  6. 3. フォーム一覧をリクエスト 4. フォーム一覧情報を返却 5. デバコマ実行をリクエスト 6. 実行結果を返却 処理の流れ 2.

    起動時にフォーム情報を生成 1. フォーム定義を記述
  7. 3. フォーム一覧をリクエスト 4. フォーム一覧情報を返却 5. デバコマ実行をリクエスト 6. 実行結果を返却 処理の流れ 2.

    起動時にフォーム情報を生成 1. フォーム定義を記述
  8. ポイント 1. 共通インターフェースとStructタグでフォーム定義を記述 2. リフレクションを用いてStructタグからフォーム情報を生成 3. フォームの一覧情報を返却するAPIを定義

  9. ポイント 1. 共通インターフェースとStructタグでフォーム定義を記述 2. リフレクションを用いてStructタグからフォーム情報を生成 3. フォームの一覧情報を返却するAPIを定義

  10. 共通インターフェースの実装(1) type CardHandler interface { BaseHandler UpdateRarity(c echo.Context) error }

    type handlerImpl struct{} type BaseHandler interface { GetBaseCommandGroup() *Group } • ハンドラのインターフェースに、 ベースインターフェースを埋め込む • ハンドラは、ベースインターフェー スのメソッドを実装 • ハンドラ = APIの受け口となるメ ソッド
  11. 共通インターフェースの実装(2) func (h *handlerImpl) GetBaseCommandGroup() *CommandGroup { return &CommandGroup{ Name:

    "カード", Commands: []*Command{ { HandlerFunc: h.UpdateRarity, URL: "/updateCardRarity", Name: "レアリティ操作 ", Model: UpdateRarityRequest{}, }, }, } } • CommandGroupを定義 (≒ カテゴリ) ◦ カテゴリ名 ◦ Commandの一覧 • Commandを定義 (≒ 各メニュー) ◦ ハンドラメソッド ◦ URL ◦ デバコマ機能名 ◦ フォームの基になる構造体
  12. Structタグによるフォームの設定 type UpdateRarityRequest struct { UserID string `label:"ユーザーID"` CardID string

    `master:"card" type:"select"` Rarity int32 `enum:"rarity" type:"radios"` } func (h *handlerImpl) UpdateRarity(c echo.Context) error { var r UpdateRarityRequest err := c.Bind(&r) // 実際の処理… } • リクエストパラメータをバインドす る構造体にタグを設定 • Structのフィールドに対してメタ情 報を付加できる ◦ jsonタグが身近
  13. ポイント 1. 共通インターフェースとStructタグでフォーム定義を記述 2. リフレクションを用いてStructタグからフォーム情報を生成 3. フォームの一覧情報を返却するAPIを定義

  14. リフレクションでStructタグを取得 func CreateSchema(c *Command) *Schema { // 構造体の型情報を取得 rt :=

    reflect.TypeOf(c.Model) var schema Schema for i := 0; i < rt.NumField(); i++ { // フィールド毎にタグ情報を取得 f := rt.Field(i) schema.Label = f.Tag.Get("label") // 各タグを処理… } return &schema } • リフレクションによりタグ情報を取 得しフォーム情報を生成 ◦ type:テキスト、セレクトボック ス、ラジオボタン... ◦ label:フォーム名 ◦ required:必須チェック ◦ enum, master:次スライドで説明 • 型ごとにデフォルト値も設定してあ るのでタグは指定しなくてもOK
  15. セレクトボックスの選択肢を作成 func CreateValues(f *reflect.StructField) Values { // Enum指定 if e

    := f.Tag.Get("enum"); e != "" { return CreateEnumValues(e) } // マスタデータの指定 if m := f.Tag.Get("master"); m != "" { return CreateMasterValues(m) } return Values{} } • セレクトボックスやラジオボタンの 選択肢もタグを基に生成 • enumタグ ◦ 指定Enumからリストを作成 ◦ (例)アイテムタイプ定義のEnum • masterタグ ◦ 指定マスタデータからリストを作成 ◦ マスタデータ = レベルやクエストと いったゲームパラメータ ◦ (例)クエスト一覧
  16. ポイント 1. 共通インターフェースとStructタグでフォーム定義を記述 2. リフレクションを用いてStructタグからフォーム情報を生成 3. フォームの一覧情報を返却するAPIを定義

  17. Commandの初期化とルーティング handlers := []BaseHandler{card.New(), …} groups := make(CommandGroups, 0, len(handlers))

    for _, h := range handlers { group := h.GetBaseCommandGroup() // フォーム情報の生成とルーティング InitCommand(group) for _, c := range group.Commands { e.POST(command.URL, c.HandlerFunc) } groups = append(groups, group) } // フォーム情報の一覧を返す API e.GET("/api/list", func(c echo.Context) error { return c.JSON(http.StatusOK, groups) }) • サーバー起動時に各Commandの初期 化を実行 ◦ フォーム情報の生成 • 各デバコマをルーティング • フォームの一覧を返すAPIを登録
  18. まとめ • StructタグやInterfaceを活用し、追加開発しやすい基盤を実装 ◦ 共通インターフェースとStructタグでフォーム定義を記述 ◦ リフレクションを用いてStructタグからフォーム情報を生成 ◦ フォームの一覧情報を返却するAPIを定義 •

    補完が効かない・記述ミスに気づきにくいデメリット • 開発するシステムの要件に合わせて実装方法を検討
  19. ご静聴ありがとうございました!