ゲーム開発でのコードジェネレーターの使われ方

プログラマーの尾関です。

今回はゲーム開発でのコードジェネレーターの使われ方について解説します。

コードジェネレーターとは

コードジェネレーターとは、ソースコードを生成するためのプログラムとなります。言い換えると、「プログラムからプログラムコードを生成する」ものです。

コードジェネレーターを作るメリット

コードジェネレーターを作るメリットとしては以下のとおりです

  • 一度仕組みを作ってしまえば、設定ファイルやデータが更新されても、自動的に正常に動作するコードとなる
  • データを入力する場所がわかりやすいので修正が楽

コードジェネレーターを作らずに作業を行うと、何かデータを追加するたびにソースコードを修正したり、それに付随する修正が行われることがあります。その手間を削減するのが、コードジェネレーターを作成する大きなメリットです。

また、ソースコードを直接修正するよりもわかりやすい入力方法になるので、作業分担しやすい、というメリットもあります。

もちろん、想定しない仕様変更があった場合や、機能に不備がある場合にはコードジェネレーターそのものを修正しなければなりませんが、仕様がある程度固まっていれば、データや設定情報の更新頻度の方が高いので、相対的に修正コストは抑えられる傾向があります。

デメリット

コードジェネレーターを作る際のデメリットとして考えられるのが、最初の作る手間が大きいことです。また通常は生成後のソースコードは修正しない(生成し直すと上書きされるため)ので、直接ソースコードを書くよりも修正コストがやや高くなり、動作確認も少し手間がかかります。

また仕様が固まっていない状態でコードジェネレーターを作り始めると、仕様変更があった場合に大きく作り直さなければならない可能性があるので、ある程度仕様が固まったら作り始めるのが良いかと思います。

コードジェネレーターの実例

ここではコードジェネレーターの実例として、

  1. サウンドデータからデータテーブルを自動生成する
  2. エフェクトのIDとパスを生成する
  3. ソースコードの雛形を生成する

という3つのパターンを見ていきます。

1. サウンドデータからデータテーブルを自動生成する

サウンドデータは、ゲーム開発ではかなりの頻度で更新されるデータの1つですが、それによりサウンドラベルの追加やサウンドアーカイブ(サウンドデータがカテゴリごとにまとめられたファイル)の追加などが頻繁に行われます。

その際、サウンドデータを参照するためのヘッダファイルやサウンドアーカイブのパスを自動生成するスクリプトを作成する(さらにサウンドデータを実行環境にコピーする)ようにしておくと、サウンドの更新が楽になります。

また自動生成ツールを作ることで、更新されたIDやファイルの差分が取りやすくなるメリットもあります。

例えば以下のようなテンプレートとなるヘッダファイルと.cppファイルを用意します

○ヘッダファイルのテンプレート

// ***************************************************************
// Copyright (C) 2020 LANCARSE,Ltd. All rights reserved.
// ***************************************************************
/*! @file  SoundTable.h
  @brief  !!編集禁止!! バッチ(TableCreate.py)で作成しています!!
      サウンドの定数とそれに対応するファイル名を定義するヘッダ
  @author  Y.Watanabe
  @date  $DATE
  @note
      
      History
      
      
*/
// ***************************************************************
#ifndef __SOUND_TABLE_H__
  #define __SOUND_TABLE_H__
// ------------------------------------------------------------------ include(s)
#include "LcLib/LcLibCommon.h"
namespace sound {
// ------------------------------------------------------ forward declaration(s)
// 開始・終了のインデックス番号.
$BEGIN_ENDS
// リソースにアクセスするための定数.
enum SoundId {
$ENUMS
  SoundId_Max,
};
// リソース文字列テーブル.
struct DATA {
  SoundId      Id;       // サウンドID.
  const CHAR*   pResName; // アーカイブ読み込み・再生・破棄時に指定するリソース名.
  const CHAR*    pPathAcb; // .acbファイルパス (appフォルダからの相対).
  const CHAR*    pPathAwb; // .awbファイルパス (appフォルダからの相対).
};
// ID指定でデータを取得する.
const DATA* SearchFromID(s32 id);
// 定数からリソース文字列を取得する.
const CHAR* GetResName(s32 id);
// イベントSE文字列からサウンドIDを取得する.
sound::SoundId GetSoundIdFromEventSeName(const CHAR* pName);
// -------------------------------------------------------------------- class(s)
}//! namespace sound
// ------------------------------------------------------------------- inline(s)
#endif //__SOUND_TABLE_H_

○cppファイルのテンプレート

// ***************************************************************
// Copyright (C) 2020 LANCARSE,Ltd. All rights reserved.
// ***************************************************************
/*! @file  SoundTable.cpp
  @brief  !!編集禁止!! バッチ(TableCreate.py)で作成しています!!
      サウンドの定数とそれに対応するファイル名を定義します
  @author  S.Ozeki
  @date  $DATE
  @note
      
      History
      
*/
// ***************************************************************
// ------------------------------------------------------------------ include(s)
#include "SoundTable.h"
namespace sound {
// ------------------------------------------------------ forward declaration(s)
// リソース文字列テーブル.
static const DATA g_Data[] = {
$PATH
};
// テーブル最大数.
static const s32 MAX = array_sizeof(g_Data);
// ID指定でデータを取得する.
const DATA* SearchFromID(s32 id)
{
  for( s32 i = 0; i < MAX; ++i ) {
    if( g_Data[i].Id == id ) {
      return &g_Data[i];
    }
  }
  
  INFO_BOARD_ERROR_SEND(0, "sound::SearchFromID() Not found id = %d", id );
  
  return nullptr;
}
// 定数からリソース文字列を取得する.
const CHAR* GetResName(s32 id)
{
  switch(id) {
$RES_NAME
  default:
    INFO_BOARD_ERROR_SEND(0, "sound::SearchFromID() Not found id = %d", id );
    break;
  }
  
  return "";
}
// イベントSE文字列からサウンドIDを取得する.
sound::SoundId GetSoundIdFromEventSeName(const CHAR* pName)
{
  struct EVENT_SE_INFO {
    const CHAR pName[16]; // イベントSE文字列.
    SoundId    SoundId;   // サウンドID.
  };
  EVENT_SE_INFO tbl[] = {
$EVENT_SE_TBL
  };
  const s32 SIZE = LC_ARRAY_SIZE(tbl);
  for(s32 i = 0; i < SIZE; i++) {
    if(LC_STRCMP(pName, tbl[i].pName) == 0) {
      // 見つかった.
      return tbl[i].SoundId;
    }
  }
  // 見つからなかったらひとまずシステムSEを返す.
  return SE_SYSTEM;
}
// -------------------------------------------------------------------- class(s)
}//! namespace sound

所々に「$PATH」「$RES_NAME」などの文字がありますが、ここにはデータのパスやリソース名が入ります。置き換え処理はスクリプト側で行うことで、コードを自動生成することができます。

2. エフェクトのIDとパスを自動生成する

例えばExcelで以下のようなエフェクト一覧を作成します

これをもとにエフェクトのID(enum)とエフェクトのパスを自動生成するツールを作ると、エフェクトの追加や管理が楽になります。

また画像をよく見ると、常駐設定があり、バトルのみで常駐するようにして読み込み処理を自動化する、ということもしています

3. ソースコードの雛形を自動生成する

とあるプロジェクトでは、戦闘中の演出の場合分けが多く、プログラムでゴリゴリ書くことが必要とされました。

そして分業のためには各演出をクラスとして実装する必要があったのですが、テンプレートとなるクラスを毎回コピーして作るのも不便だな、と思って雛形を自動生成するスクリプトを作成しました

#!/usr/bin/env python3
# coding: utf-8
# =========================================================
# AnimActのモジュールを生成するツール
# =========================================================
import time
import datetime
# --------------------------------------------------
# 置き換え文字をここに設定します
BRIEF      = "必殺技演出アニメーション"
AUTHOR     = "S.Ozeki"
CLASS_NAME = "AnimActSpecialAttack"
# --------------------------------------------------
def main():
  # 生成日を求める.
  dt_now = datetime.datetime.now()
  year  = dt_now.strftime("%Y")
  date  = dt_now.strftime("%Y/%m/%d")
  date2 = dt_now.strftime("%Y_%m_%d")
  # ヘッダファイルのテンプレート.
  HEADER = """/******************************************************************/
/* Copyright (C) {YEAR} LANCARSE,Ltd. All rights reserved.          */
/* ****************************************************************/
/*! @file  {CLASS_NAME}.h
  @brief  {BRIEF}.
  @author  {AUTHOR}
  @date  {DATE}
  @note  
      
      History
      
*/
/* *************************************************************** */
#ifndef ___{CLASS_NAME_UPPER}_H_{DATE_2}_
  #define ___{CLASS_NAME_UPPER}_H_{DATE_2}_
/* ------------------------------------------------------------------ include(s) */
#include "../AnimActBase.h"
// ------------------------------------------------------------------ literal(s)
class {CLASS_NAME} : public AnimActBase {
  LC_RTTI_DECLARE( {CLASS_NAME}, AnimActBase )
  // =================== public ====================== //
  public: // ------------------------------------ type(s)
  public: // ------------------------------------ enum(s)
    enum eState {
      eState_In,
      eState_Wait,
      eState_Out,
    };
  public: // ---------------------------------- struct(s)
  public: // ---------------------------------- method(s)
  // -------------------------------- override(s)
    // 初期化.
    void Init() LC_OVERRIDE;
  // ================== protected ==================== //
  protected: // --------------------------------- method(s)
    //更新コールバック.
    bool OnUpdate() LC_OVERRIDE;
    
  protected: // ------------------------------------ var(s)
  // =================== private ===================== //
  private: // -------------------------------- literal(s)
  private: // ----------------------------------- enum(s)
  private: // --------------------------------- struct(s)
  private: // --------------------------------- method(s)
  private: // ------------------------------------ var(s)
    StateObject<eState> m_State = {};
};
/* ------------------------------------------------------------------- public(s) */
//このクラスを生成する関数
{CLASS_NAME}* {CLASS_NAME}_Create(Behavior* parent, AnimActParam& p);
#endif // ___{CLASS_NAME_UPPER}_H_{DATE_2}_
"""
  # 各項目を置き換え.
  header = HEADER.replace("{BRIEF}", BRIEF)
  header = header.replace("{AUTHOR}", AUTHOR)
  header = header.replace("{CLASS_NAME}", CLASS_NAME)
  header = header.replace("{YEAR}", year)
  header = header.replace("{DATE}", date)
  header = header.replace("{DATE_2}", date2)
  header = header.replace("{CLASS_NAME_UPPER}", CLASS_NAME.upper())
  
  # ヘッダファイルとして保存する.
  f = open("%s.h"%CLASS_NAME, "w")
  f.write(header)
  f.close()
  
  # .cppファイルのテンプレート.
  CPP = """/******************************************************************/
/* Copyright (C) {YEAR} LANCARSE,Ltd. All rights reserved.          */
/* ****************************************************************/
/*! @file  {CLASS_NAME}.cpp
  @brief  {BRIEF}.
  @author  {AUTHOR}
  @date  {DATE}
  @note  
      
      History
      
*/
/* *************************************************************** */
/* ------------------------------------------------------------------ include(s) */
#include "{CLASS_NAME}.h"
// ------------------------------------------------------------------ literal(s)
// -----------------------------------------------------------------------------
// public method(s).
// -----------------------------------------------------------------------------
// 初期化.
void {CLASS_NAME}::Init()
{
}
// -----------------------------------------------------------------------------
// protected method(s).
// -----------------------------------------------------------------------------
//更新コールバック.
bool {CLASS_NAME}::OnUpdate()
{
  return false;
}
// -----------------------------------------------------------------------------
// private method(s).
// -----------------------------------------------------------------------------
/* ------------------------------------------------------------------- public(s) */
//このクラスを生成する関数
{CLASS_NAME}* {CLASS_NAME}_Create(Behavior* parent, AnimActParam& p)
{
  return AppGameObject_NewChild(parent, {CLASS_NAME});
}
"""
  # 各項目の置き換え
  cpp = CPP.replace("{BRIEF}", BRIEF)
  cpp = cpp.replace("{AUTHOR}", AUTHOR)
  cpp = cpp.replace("{CLASS_NAME}", CLASS_NAME)
  cpp = cpp.replace("{YEAR}", year)
  cpp = cpp.replace("{DATE}", date)
  cpp = cpp.replace("{DATE_2}", date2)
  cpp = cpp.replace("{CLASS_NAME_UPPER}", CLASS_NAME.upper())
  
  # .cppファイルとして保存.
  f = open("%s.cpp"%CLASS_NAME, "w")
  f.write(cpp)
  f.close()
  
  print("出力完了")
if __name__ == "__main__":
  main()

このスクリプトの先頭に定義してある

  • BRIEF: 説明文
  • AUTHER: 作成者
  • CLASS_NAME: クラス名

を設定すればそれによりソースコードの雛形を作ることができます。

以下、上記スクリプトで生成したコードです。

○ヘッダファイル (AnimActSpecialAttack.h)

/******************************************************************/
/* Copyright (C) 2022 LANCARSE,Ltd. All rights reserved.          */
/* ****************************************************************/
/*! @file  AnimActSpecialAttack.h
  @brief  必殺技演出アニメーション.
  @author  S.Ozeki
  @date  2022/06/14
  @note  
      
      History
      
*/
/* *************************************************************** */
#ifndef ___ANIMACTSPECIALATTACK_H_2022_06_14_
  #define ___ANIMACTSPECIALATTACK_H_2022_06_14_
/* ------------------------------------------------------------------ include(s) */
#include "../AnimActBase.h"
// ------------------------------------------------------------------ literal(s)
class AnimActSpecialAttack : public AnimActBase {
  LC_RTTI_DECLARE( AnimActSpecialAttack, AnimActBase )
  // =================== public ====================== //
  public: // ------------------------------------ type(s)
  public: // ------------------------------------ enum(s)
    enum eState {
      eState_In,
      eState_Wait,
      eState_Out,
    };
  public: // ---------------------------------- struct(s)
  public: // ---------------------------------- method(s)
  // -------------------------------- override(s)
    // 初期化.
    void Init() LC_OVERRIDE;
  // ================== protected ==================== //
  protected: // --------------------------------- method(s)
    //更新コールバック.
    bool OnUpdate() LC_OVERRIDE;
    
  protected: // ------------------------------------ var(s)
  // =================== private ===================== //
  private: // -------------------------------- literal(s)
  private: // ----------------------------------- enum(s)
  private: // --------------------------------- struct(s)
  private: // --------------------------------- method(s)
  private: // ------------------------------------ var(s)
    StateObject<eState> m_State = {};
};
/* ------------------------------------------------------------------- public(s) */
//このクラスを生成する関数
AnimActSpecialAttack* AnimActSpecialAttack_Create(Behavior* parent, AnimActParam& p);
#endif // ___ANIMACTSPECIALATTACK_H_2022_06_14_

○cppファイル (AnimActSpecialAttack.cpp)

/******************************************************************/
/* Copyright (C) 2022 LANCARSE,Ltd. All rights reserved.          */
/* ****************************************************************/
/*! @file  AnimActSpecialAttack.cpp
  @brief  必殺技演出アニメーション.
  @author  S.Ozeki
  @date  2022/06/14
  @note  
      
      History
      
*/
/* *************************************************************** */
/* ------------------------------------------------------------------ include(s) */
#include "AnimActSpecialAttack.h"
// ------------------------------------------------------------------ literal(s)
// -----------------------------------------------------------------------------
// public method(s).
// -----------------------------------------------------------------------------
// 初期化.
void AnimActSpecialAttack::Init()
{
}
// -----------------------------------------------------------------------------
// protected method(s).
// -----------------------------------------------------------------------------
//更新コールバック.
bool AnimActSpecialAttack::OnUpdate()
{
  return false;
}
// -----------------------------------------------------------------------------
// private method(s).
// -----------------------------------------------------------------------------
/* ------------------------------------------------------------------- public(s) */
//このクラスを生成する関数
AnimActSpecialAttack* AnimActSpecialAttack_Create(Behavior* parent, AnimActParam& p)
{
  return AppGameObject_NewChild(parent, AnimActSpecialAttack);
}

スクリプトとしては200行程度のものですが、これでクラスが大量生産できたので、かけた手間に見合ったリターンが得られたのではないかと思っています。

そして念のためとして、クラスの仕様変更がありそうな部分に関しては、外部にパラメータを定義できるようにしたり、クラス継承をして継承元を修正できるなどの工夫をしておいて、仕様変更に柔軟に対応できる作りにしておきました。

生成後のコードを修正してよいか、修正すべきではないか

コードジェネレーターにより生成したコードを修正してよいか、または修正すべきではないか、ということについて考えてみます。

基本的にはコードジェネレーターで作られたコードは修正すべきではないと思っています。特にコードジェネレーターの例として上げた「サウンドテーブルの自動生成」「エフェクトのIDとパスを自動生成」するコードは、”もととなるデータや設定を頻繁に修正して再出力する” ということを繰り返すため、生成後のコードを修正しても、元のデータに変更が入ったら再生成時にその修正は上書きされます。

そのため生成後のコードの先頭にあるコメントに「このコードをは自動生成されました。変更禁止」などと書いておくのが良いと思います。

ただ「ソースコードの雛形ジェネレーター」のように修正して使用するのが前提である場合、修正しても問題ありません

まとめ

  • コードジェネレーターを活用することで、頻繁に更新される部分に伴う修正作業を自動化・簡素化できる
  • ジェネレートされるコードの元データが存在する場合は、出力後のコードを変更しない
  • ただし、ソースコードを量産する場合にはその限りではない

以上、ゲーム開発に使われるコードジェネレーターについて解説しました

Follow me!