マスターデータ更新だけで完結!拡張性に強いスキル説明文生成システム

マスターデータ更新だけで完結!拡張性に強いスキル説明文生成システム

マスターデータ更新だけで完結!拡張性に強いスキル説明文生成システム

はじめに

株式会社QualiArtsでサーバーサイドエンジニアをしている鈴木です。

ゲーム開発において、アイテムやスキルの説明文はユーザー体験に影響する重要な要素です。一方で、能力値や効果の組み合わせが増えるにつれ、説明文の管理は難しくなりがちです。 例えば次のような説明文を、数百〜数千件分手動で管理するのは現実的ではありません。

攻撃力が10上昇し、3ターンの間、防御力が5%増加する

この運用では、表記ゆれが発生しやすく、数値変更時の更新漏れや、実際の効果値と説明文の不整合といった問題が起こる可能性があります。

本記事では、こうした課題を解決する「説明文を文字列として管理しない」テンプレートベースの自動生成システムを紹介します。

システムの全体像

このシステムでは、説明文を1つの文字列として扱いません。 説明文は実際には、効果・効果名・数値・継続時間といった複数の部品からなる階層構造を持っています。

説明文を『効果名・効果値・継続時間』に分解した階層構造の図

例えば、

攻撃力が10上昇

という一文は、

  • 効果名:「攻撃力」
  • 効果値:「10」

という複数の部品から成り立っています。

この考え方を説明文生成の仕組みとして取り入れます。

説明文の構造はそのままテンプレートとして表現します。 部品をテンプレートに代入することで説明文を生成します。

  • 「{効果名}が{効果値}上昇」

テンプレート同士を再帰的に参照することで、再利用性と拡張性の両立も実現します。 表示内容はマスターデータで管理します。 プログラムはその「組み立て方」だけを担います。

これにより、プランナーはマスターデータを編集するだけで、説明文を変更できるようになります。

説明文生成の基本フロー

説明文生成は、次の流れで行われます。

  1. テンプレートを取得
  2. テンプレート内の変数と展開処理を紐づけ
  3. テンプレートを解析して変数を展開
  4. 完成した説明文を返却

このとき、変数は値ではなく関数として扱います。

func (c *component) generateItemDescription(
    masterData MasterData,
    itemID string,
) (string, error) {
    // 1. テンプレート種別からテンプレート文字列を取得
    template := masterData.GetTemplate(TemplateType_Item)

    // 2. 変数と展開関数の紐づけ + 3. テンプレート解析
    return c.parseTemplate(
        masterData,
        template.Text,
        map[string]func() (string, error){
            "effect": func() (string, error) {
                return c.replaceItemEffect(masterData, itemID)
            },
            "duration": func() (string, error) {
                return c.replaceItemDuration(masterData, itemID)
            },
        },
    )
}

変数を関数で渡す

テンプレート内の変数は、値ではなく関数として登録しています。

テンプレートごとに必要な変数は異なり、さらに変数の展開処理の中で 別のテンプレート解析を行うケースもあります。 そのため、あらかじめすべての値を計算するのではなく、 実際に参照されたものだけを評価する遅延評価を採用しています。

この仕組みにより、テンプレート構成が変わっても、 生成ロジックを変更せずに対応できるようになります。

テンプレートの再帰的参照

変数の展開処理の中では、さらに別のテンプレート解析を行います。

func (c *component) replaceItemEffect(
    masterData MasterData,
    itemID string,
) (string, error) {

    item := masterData.GetItemByID(itemID)
    template := masterData.GetTemplate(item.EffectType)

    return c.parseTemplate(
        masterData,
        template.Text,
        map[string]func() (string, error){
            "effect_name": func() (string, error) {
                return c.replaceEffectName(masterData, item.EffectID)
            },
            "effect_value": func() (string, error) {
                return c.replaceEffectValue(masterData, item.EffectID)
            },
        },
    )
}

この構造により、

  • 表現の共通化
  • 変更箇所の局所化
  • テンプレートの柔軟な組み合わせ

を同時に実現しています。

効果タイプの拡張性

テンプレートベースの設計には、拡張性の面でも大きなメリットがあります。

例えば、効果タイプごとに説明文生成ロジックを分岐させる方法では、新しい効果タイプが追加されるたびにコード修正が必要になってしまいます。

// 従来のアプローチ(非推奨)
func getEffectDescription(effectType EffectType, value int) string {
    switch effectType {
    case EffectType_AttackUp:
        return fmt.Sprintf("攻撃力が%d上昇", value)
    case EffectType_DefenseUp:
        return fmt.Sprintf("防御力が%d上昇", value)
    case EffectType_HPRecover:
        return fmt.Sprintf("HPを%d回復", value)
    // 新しい効果タイプを追加するたびにここを修正...
    default:
        return "不明な効果"
    }
}

本システムでは、

  • 効果タイプとテンプレートの対応
  • 表示形式の違い

をすべてマスターデータで管理しています。 その結果、新しい効果タイプの追加や表現変更はマスターデータ修正だけで完結します。

条件によるテンプレート切り替え

同じ効果タイプでも、条件によって異なる説明文にしたいケースがあります。例えば:

  • 効果値が小さい(1-10)なら「上昇」、大きい(11以上)なら「大幅に上昇」
  • 対象が単体なら「〇〇が上昇」、全体なら「全員の〇〇が上昇」
  • 確率発動なら「{percent}%の確率で」を先頭に追加
  • 特定のトリガー条件があれば「〇〇時に」を追加

これらをすべてプログラムで分岐させると、コードが複雑になり保守性が下がります。この課題を解決するため、テンプレートマスターに複数の検索キーを持たせています。

// テンプレートマスターの構造
type TemplateEffect struct {
    // 検索キー(複合プライマリキー)
    EffectType     EffectType  // 効果タイプ
    TargetType     TargetType  // 対象タイプ(単体/全体/ランダム)
    EffectValueMin string      // 効果値の最小値(範囲指定)
    EffectValueMax string      // 効果値の最大値(範囲指定)
    TriggerType    TriggerType // 発動条件タイプ

    // テンプレート本体
    Template string
}

説明文を生成する際、効果の実際の値を使ってテンプレートを検索します。

func (c *component) getEffectTemplate(
    masterData MasterData,
    effect *Effect,
) (*TemplateEffect, error) {
    // 効果の実際の値でテンプレートを検索
    template, err := masterData.GetTemplateEffectMap().GetByKeys(
        effect.EffectType,
        effect.TargetType,
        effect.EffectValue,  // 範囲チェックに使用
        effect.TriggerType,
    )
    if err != nil {
        return nil, err
    }
    return template, nil
}

すべての条件を個別に定義するとテンプレート数が爆発的に増えてしまうため、ワイルドカード(*または空文字)による部分一致をサポートしています。

効果タイプ対象効果値トリガーテンプレート
ATTACK_UP***{effect_name}が{effect_value}上昇
ATTACK_UP*50以上*{effect_name}が{effect_value}大幅上昇
ATTACK_UPALL**全員の{effect_name}が{effect_value}上昇
ATTACK_UP**PROBABILITY{trigger_percent}%の確率で{effect_name}が{effect_value}上昇

この仕組みにより、プログラムにif文やswitch文を追加せずに済み、新しい条件パターンもマスターデータの追加だけで対応できます。

まとめ

本設計の本質は、説明文を単なる「表示用文字列」ではなく「再利用可能な構造データ」として扱った点にあります。

テンプレートベースの説明文自動生成システムにより、運用面・拡張面で以下の効果を得られました。

課題解決策
説明文の一貫性テンプレートによる構造の統一
更新コスト数値変更の自動反映
新機能追加時の工数マスターデータ追加のみで対応可能
条件による表現の違い複合キーによるテンプレート切り替え

このアーキテクチャのポイントは、テンプレートと変数置換の分離にあります。 これにより、説明文の「構造」と「内容」を独立して管理でき、プランナーとエンジニアの責務を明確に分けられます。

同様の課題を抱えているプロジェクトの参考になれば幸いです。

著者

鈴木 稜太朗のプロフィール画像

(Suzuki Ryotaro)

2024年にサイバーエージェントに新卒入社。QualiArtsにて運用プロジェクトのバックエンドエンジニアとして各種ゲーム機能の開発に携わる。

この記事をシェア

目次