Slide 1

Slide 1 text

ゲームの抽選ロジックに Genericsを使ってみたら 開発が楽になった話 株式会社QualiArts 朝倉信晴

Slide 2

Slide 2 text

朝倉 信晴 CyberAgent ゲーム・エンタメ事業部(SGE) 株式会社QualiArts バックエンドエンジニア スマートフォンゲーム「IDOLY PRIDE」のバックエン ドを担当 「SGE Go Tech Book Vol.03」執筆 技術書典14にて発売中

Slide 3

Slide 3 text

ゲーム・エンターテイメント事業部(SGE)について 子会社制をとっており、 ゲーム・エンターテイメント事業に 携わる10社の子会社が 所属しています。 ゲーム・エンターテイメント事業部(SGE)

Slide 4

Slide 4 text

Contents 1. IDOLY PRIDEの抽選ロジック 2. Interfaceを使った実装 3. Genericsを使った実装 4. まとめ

Slide 5

Slide 5 text

IDOLY PRIDEの 抽選ロジック 1

Slide 6

Slide 6 text

IDOLY PRIDE 「IDOLY PRIDE」は、アイドルをテーマとしたメディアミックス作品。 略称は「アイプラ」。 スマートフォンゲーム • アイドルマネジメントRPG • 2021年6月24日リリース。2周年。 • Goでバックエンドを開発。

Slide 7

Slide 7 text

IDOLY PRIDEの抽選ロジック スマートフォンゲームでは、ランダムでの アイテム獲得など様々な機能で、提供割合 に基づいてアイテム等を抽選する処理があ る。 IDOLY PRIDEでも、カード抽選、マーケッ ト、フォト効果...など様々な機能で抽選処 理を行っている。

Slide 8

Slide 8 text

1. 各アイテムの提供割合データを用意する 2. 提供割合の合計値を計算する 3. 合計値の範囲で乱数を生成する 4. 乱数をもとに抽選結果を決定する 抽選ロジックの流れ① ノーマルカード:6 レアカード:3 Sレアカード:1

Slide 9

Slide 9 text

1. 各アイテムの提供割合データを用意する 2. 提供割合の合計値を計算する 3. 合計値の範囲で乱数を生成する 4. 乱数をもとに抽選結果を決定する 抽選ロジックの流れ② ノーマルカード:6 レアカード:3 Sレアカード:1 6 9 10

Slide 10

Slide 10 text

1. 各アイテムの提供割合データを用意する 2. 提供割合の合計値を計算する 3. 合計値の範囲で乱数を生成する 4. 乱数をもとに抽選結果を決定する 抽選ロジックの流れ③ ノーマルカード:6 レアカード:3 Sレアカード:1 6 9 10 0以上、10未 満の乱数 「7」が生成

Slide 11

Slide 11 text

1. 各アイテムの提供割合データを用意する 2. 提供割合の合計値を計算する 3. 合計値の範囲で乱数を生成する 4. 乱数をもとに抽選結果を決定する 抽選ロジックの流れ④ ノーマルカード:6 レアカード:3 Sレアカード:1 6 9 10 0〜5:ノーマル 6〜8:レア 9:Sレア 7

Slide 12

Slide 12 text

Interfaceを使った実装 2

Slide 13

Slide 13 text

抽選ロジックの共通化 スマートフォンゲームでは、提供割合に基づいた抽選処理がたくさんある。 抽選ロジックの2〜4は同じ処理なのでコードを共通化したい。 1. 各アイテムの提供割合データを用意する 2. 提供割合の合計値を計算する 3. 合計値の範囲で乱数を生成する 4. 乱数をもとに抽選結果を決定する Interfaceを使って提供割合データを抽象化できると抽選ロジックを共通化できそう。

Slide 14

Slide 14 text

type Reward struct { CardID string Ratio int } // 1.各アイテムの提供割合データを用意する var Rewards = []*Reward{ {CardID: "Sレアカード", Ratio: 1}, {CardID: "レアカード", Ratio: 3}, {CardID: "ノーマルカード", Ratio: 6}, } type Drawable interface { GetRatio() int } func (e *Reward) GetRatio() int { return e.Ratio } 提供割合データRewardsを用意

Slide 15

Slide 15 text

type Reward struct { CardID string Ratio int } // 1.各アイテムの提供割合データを用意する var Rewards = []*Reward{ {CardID: "Sレアカード", Ratio: 1}, {CardID: "レアカード", Ratio: 3}, {CardID: "ノーマルカード", Ratio: 6}, } type Drawable interface { GetRatio() int } func (e *Reward) GetRatio() int { return e.Ratio } Drawableインターフェースを定義

Slide 16

Slide 16 text

type Reward struct { CardID string Ratio int } // 1.各アイテムの提供割合データを用意する var Rewards = []*Reward{ {CardID: "Sレアカード", Ratio: 1}, {CardID: "レアカード", Ratio: 3}, {CardID: "ノーマルカード", Ratio: 6}, } type Drawable interface { GetRatio() int } func (e *Reward) GetRatio() int { return e.Ratio } RewardはDrawableを実装

Slide 17

Slide 17 text

func draw(drawables []Drawable) Drawable { // 2.提供割合の合計値を計算する var total int for _, d := range drawables { total += d.GetRatio() } // 3.合計値の範囲で乱数を生成する random := rand.Intn(total) // 4.乱数をもとに抽選結果を決定する var temp int for _, d := range drawables { temp += d.GetRatio() if temp > random { return d } } return nil } draw関数に抽選ロジック2〜4実装 2.提供割合の合計値を計算する 3.合計値の範囲で乱数を生成する 4.乱数をもとに抽選結果を決定する

Slide 18

Slide 18 text

func draw(drawables []Drawable) Drawable { // 2.提供割合の合計値を計算する var total int for _, d := range drawables { total += d.GetRatio() } // 3.合計値の範囲で乱数を生成する random := rand.Intn(total) // 4.乱数をもとに抽選結果を決定する var temp int for _, d := range drawables { temp += d.GetRatio() if temp > random { return d } } return nil } []Drawableを受け取る

Slide 19

Slide 19 text

func draw(drawables []Drawable) Drawable { // 2.提供割合の合計値を計算する var total int for _, d := range drawables { total += d.GetRatio() } // 3.合計値の範囲で乱数を生成する random := rand.Intn(total) // 4.乱数をもとに抽選結果を決定する var temp int for _, d := range drawables { temp += d.GetRatio() if temp > random { return d } } return nil } 提供割合を取得し抽選

Slide 20

Slide 20 text

func draw(drawables []Drawable) Drawable { // 2.提供割合の合計値を計算する var total int for _, d := range drawables { total += d.GetRatio() } // 3.合計値の範囲で乱数を生成する random := rand.Intn(total) // 4.乱数をもとに抽選結果を決定する var temp int for _, d := range drawables { temp += d.GetRatio() if temp > random { return d } } return nil } 抽選結果をDrawableで返却

Slide 21

Slide 21 text

func main() { drawables := make([]Drawable, 0, len(Rewards)) for _, e := range Rewards { drawables = append(drawables, e) } fmt.Printf("抽選されたカード: %s", (draw(drawables).(*Reward)).CardID) } Rewardsを[]Drawableに詰め替えて draw関数を実行

Slide 22

Slide 22 text

func main() { drawables := make([]Drawable, 0, len(Rewards)) for _, e := range Rewards { drawables = append(drawables, e) } fmt.Printf("抽選されたカード: %s", (draw(drawables).(*Reward)).CardID) } draw関数の戻り値をキャストして値を取得

Slide 23

Slide 23 text

func main() { drawables := make([]Drawable, 0, len(Rewards)) for _, e := range Rewards { drawables = append(drawables, e) } fmt.Printf("抽選されたカード: %s", (draw(drawables).(*Reward)).CardID) } • 抽選ロジックを共通化できた。 • しかし、呼び出し元の処理が煩雑になった。

Slide 24

Slide 24 text

Genericsを使った実装 3

Slide 25

Slide 25 text

Generics 2022年03月にリリースされたGo1.18で、ジェネリクスが導入された。 IDOLY PRIDEのGo1.18アプデに合わせて、抽選ロジックをGenericsを使って改良し た。 (2021年06月リリースのためリリース時はInterfaceを使った抽選ロジックだった)

Slide 26

Slide 26 text

type Reward struct { CardID string Ratio int } // 1.各アイテムの提供割合データを用意する var Rewards = []*Reward{ {CardID: "Sレアカード", Ratio: 1}, {CardID: "レアカード", Ratio: 3}, {CardID: "ノーマルカード", Ratio: 6}, } type Drawable interface { GetRatio() int } func (e *Reward) GetRatio() int { return e.Ratio } Interface版から変更なし 引き続き、Drawableインターフェース使う

Slide 27

Slide 27 text

func draw[T Drawable](drawables []T) T { // 2.提供割合の合計値を計算する var total int for _, d := range drawables { total += d.GetRatio() } // 3.合計値の範囲で乱数を生成する random := rand.Intn(total) // 4.乱数をもとに抽選結果を決定する var temp int for _, d := range drawables { temp += d.GetRatio() if temp > random { return d } } // return nilだとコンパイルエラーになる var ret T return ret } Drawable型の型パラメータTを定義 引数、戻り値もTに変更

Slide 28

Slide 28 text

// Interfaceを利用した場合 func main() { drawables := make([]Drawable, 0, len(Rewards)) for _, e := range Rewards { drawables = append(drawables, e) } fmt.Printf("抽選されたカード: %s", (draw(drawables).(*Reward)).CardID) } // Genericsを利用した場合 func main() { fmt.Printf("抽選されたカード: %s", draw(Rewards).CardID) } Rewardsをそのまま引数に指 定し、draw関数を実行

Slide 29

Slide 29 text

// Interfaceを利用した場合 func main() { drawables := make([]Drawable, 0, len(Rewards)) for _, e := range Rewards { drawables = append(drawables, e) } fmt.Printf("抽選されたカード: %s", (draw(drawables).(*Reward)).CardID) } // Genericsを利用した場合 func main() { fmt.Printf("抽選されたカード: %s", draw(Rewards).CardID) } draw関数の戻り値からそ のまま値を取得

Slide 30

Slide 30 text

// Interfaceを利用した場合 func main() { drawables := make([]Drawable, 0, len(Rewards)) for _, e := range Rewards { drawables = append(drawables, e) } fmt.Printf("抽選されたカード: %s", (draw(drawables).(*Reward)).CardID) } // Genericsを利用した場合 func main() { fmt.Printf("抽選されたカード: %s", draw(Rewards).CardID) } • 抽選ロジックの呼び出し元がシンプルになった。

Slide 31

Slide 31 text

まとめ 4

Slide 32

Slide 32 text

まとめ 1. スマートフォンゲームでは、提供割合に基づいた抽選処理がたくさんある。 2. Interfaceを使って抽選ロジックの共通化を行った。 a. 呼び出し元の処理が煩雑になった。 3. Go1.18から導入されたGenericsを使って抽選ロジックを改良した。 a. 呼び出し元の処理もシンプルになり使いやすくなった。

Slide 33

Slide 33 text

ありがとうございました