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

ゲームの抽選ロジックにGenericsを使ってみたら開発が楽になった話

 ゲームの抽選ロジックにGenericsを使ってみたら開発が楽になった話

ゲーム開発ではランダムドロップやガチャといった抽選を必要とする機能が多く存在するため、Interfaceを利用し各抽選ロジックの共通化を行ったのでその設計について紹介します。

また、Go1.18でGenericsが導入された後は、上記ロジックにGenericsを適用することで開発効率が向上したため、Generics導入前後の事例についても共有します。

QualiArts

June 02, 2023
Tweet

More Decks by QualiArts

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  5. IDOLY PRIDEの
    抽選ロジック
    1

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. Interfaceを使った実装
    2

    View Slide

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

    View Slide

  14. 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を用意

    View Slide

  15. 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インターフェースを定義

    View Slide

  16. 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を実装

    View Slide

  17. 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.乱数をもとに抽選結果を決定する

    View Slide

  18. 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を受け取る

    View Slide

  19. 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
    }
    提供割合を取得し抽選

    View Slide

  20. 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で返却

    View Slide

  21. 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関数を実行

    View Slide

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

    View Slide

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

    View Slide

  24. Genericsを使った実装
    3

    View Slide

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

    View Slide

  26. 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インターフェース使う

    View Slide

  27. 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に変更

    View Slide

  28. // 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関数を実行

    View Slide

  29. // 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関数の戻り値からそ
    のまま値を取得

    View Slide

  30. // 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)
    }
    • 抽選ロジックの呼び出し元がシンプルになった。

    View Slide

  31. まとめ
    4

    View Slide

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

    View Slide

  33. ありがとうございました

    View Slide