ゲームプログラミング/RPG/リファクタリング
本ページの注意点
編集本ページは、RPGゲームを主にC言語で作る際の、リファクタリングについての考え方の参考を述べた教科書です。
なお、「リファクタリング」とは、動作しているプログラムの内部構造を、結果を保ちながら、整理するためにプログラムを書き換えることです。
ゲーム以外のリファクタリングについては一切、想定してないです。もしゲーム以外の話題について本文中で言及していたとしても、派生的なものにすぎませんので、誤解しないでください。
ページの題名をよくお読みください。『ゲームプログラミング/RPG/リファクタリング』とあるように、あくまで本書はゲームをC言語などのプログラミングで開発する際の、 もし作ろうとするジャンルがRPGの場合での、 何千行や何万行にも及ぶコードをどう管理しやすくメンテナンスするかという目的でしか、書かれていません。
特に断らないかぎり、読者としては、ゲームプログラミング初心者を想定しています。中上級者は想定していません。そのため、厳密には間違った内容もあるかもしれません。正確性よりも分かりやすさを本ページでは優先しています。
- 完全な結果保持はゲームでは不可能
なお、ゲームでは処理速度が求められますので、現実には処理速度まで考えると、コード改修前と改修後とで完全に同じ結果を保つというのは(わずかに処理速度が違うので、結果の完全保持は)不可能です。
ただし本書では説明の都合上、処理速度の問題については、ある程度は目をつぶるとします。初心者レベルのターン制RPGなら、ある程度は処理速度の問題に目をつぶれます。
ページ題名にもあるとおり本ページは「RPG」の教科書であり、特にことわらないかぎり本ページでは、ジャンルとして、さらに、(プログラミング初心者が作るであろう)ターン制RPGを想定しています。
一方、アクションRPGをプログラミングするのは、RPGプログラミングとアクションゲームのプログラミングの両方の知識が必要なので、初心者には、なかなか敷居が高いです。
リファクタリング
編集リファクタリングすべき時
編集リファクタリングとは、動くようになったプログラムの内部構造を結果を保ちながら整理することです。
リファクタリングの必要がないコードを初期から書くことが理想的ですが、ほとんどの場合はそれについて考えながら書くのはコストが高すぎるので、はじめは「とりあえず」の形で 叩き台(たたきだい)的なコードが書かれます(「テストファースト」と言います)。そしてテストファーストのテストコードを書くだけでなく、軽くでいいのでテストプレイをします。そして、とりあえず大きなバグの無いことを確認したら、必要に応じてコードを整理する事もあります。つまり、もしリファクタリングを行うとすれば、テストファーストによる初期テストの終わった段階ですので、つまりリファクタリングが行われるのは中期、あるいは動作が安定した後期です。
- ※ なお「叩き台」(たたきだい)とは、あとから直すことを見越して、とりあえず何かの提案を、批判などの来ることを覚悟した上で(※広辞苑に「批判」とあります)、提案や原案を出したりすることです。プログラミングに限らず、一般の国語として「叩き台」という言葉は使われ、仲間内での建設的な議論などの際に用いられたりします。
リファクタリングは、テストと一体です。なぜなら、そのリファクタリングの適用後が、本当に以前の機能を維持できているかを確認する必要があるからです。この事を「等価性検証」または「振る舞い保持の検証」などと言います。
wikisource s:プログラマが知るべき97のこと/リファクタリングの際に注意すべきことにも、
「すべてをゼロから書き直したい衝動に駆られることもありますが、その誘惑に打ち克たなければなりません。既存のコードをできるかぎり活かすべきです。いかに醜悪なコードであっても、そのコードはテストやレビューを通っているものなのです。既存のコード(特に、すでにリリースされていたシステムのコード)をすべて破棄するというのは、それまでの何ヶ月(何年)という時間を捨ててしまうということを意味します。大変な作業を経て、曲がりなりにも形にしてきたコードです。その過程では無数に発生した問題の回避策を講じ、数えきれないほどのバグ修正もしてきたでしょう。仮にコードを新たにゼロから書き直したとすると、同じようなことをまた繰り返すことになりますし、既存のコードでは発見/修正できたバグを、今度は見逃してしまうかもしれません。これでは時間と労力の無駄だし、過去の作業で得た知識も無駄になってしまいます。 」
(以上、引用) とあります。
ときどきネットでは、等価性検証などのテストの手間を無視して、闇雲にコードを短くしようとしたり、あるいは抽象論を並べ立てる浅知恵の自称プログラマーの人がいますが、しかし彼らは知ったかぶりのニワカの人なので、無視しましょう。
もしリファクタリングをするなら、けっして無闇にコードを短くすればよいのではく、等価性検証やコード可読性などの質も考えた上でのリファクタリングをする必要があります。
けっして、透過性検証の手間も考えずに性急にリファクタリングをしてはなりません。
下記のような格言もあります。
性急な最適化は、諸悪の根源である。
―リファクタリングについて、ドナルド・クヌースリファクタリング
第一段階としては、// TODO: リファクタリング
と書き残しておくことです。そうすることで、周囲 (あるいは未来の自分) にリファクタリングしなければならないことを動作の変更なしに伝えることができます。
第二段階は、そのリファクタリングが論理的に正しいと確定できる道筋を考え、それを記録として残しておくことです。ソースコードレベルで保証できることが一番ですが、大抵のプログラミング言語はそれについて十分な能力を持ちません。
また、書籍『Game Programing Patterns』P18(amazonのweb立ち読み版)によると、「低レベルの究極な最適化は、コードに前提条件をもちこんでしまうので、できるだけ後回しにすべき」とあります。
リファクタリングすべきでない時
編集バグがあるときは、そのバグを修正することを優先しましょう。ただし例外として、先にリファクタリングしたほうがバグも修正しやすくなるなら、リファクタリングしても構いません。
リファクタリングしやすいコードを書く習慣をつける
編集コメントをプログラム中に書く習慣を付けておくと、後々楽です。
// 武器の強さ
// 短剣の強さ
int tanken_tuyosa = 10;
// 長剣の強さ
int tyouken_tuyosa = 25;
修正データの追加と項目並び換えのタイミング
編集さて、たとえばゲームを作っていると、たとえばRPGなら下記のように、忘れていた装備品データを追加する必要が生じたりします。
たとえば、「どうのつるぎ」という武器データを書き忘れたまま、武器データベースを構築してしまったとしましょう。 追加前データは
- 1:こんぼう
- 2:てつのつるぎ
- 3:ミスリルのけん
- 4:ダイヤのつるぎ
- 5:クリスタルのつるぎ
という順序だったとします。
これから追加したい「どうのつるぎ」は、1:こんぼう より強いが 2:てつのつるぎ より弱いと仮定しましょう。
この場合、追加後は、もし追加した時点がすでに開発の後半段階なら、
- 1:こんぼう
- 2:てつのつるぎ
- 3:ミスリルのけん
- 4:ダイヤのつるぎ
- 5:クリスタルのつるぎ
- 6:どうのつるぎ ※ ここだけ変わってる
みたいに、最後の番号の次に追加していく方式のほうが、安全です。
なぜこうするのが安全かというと、もし、「2:てつのつるぎ」と「3:ミスリルのけん」の間に「どうのつるぎ」を挿入してしまうと、「3:ミスリルのつるぎ」以降のデータがすべて1個ずつズレてしまうからです。 つまり、
- 1:こんぼう
- 2:てつのつるぎ
- 3:どうのつるぎ ←※※ ここに挿入 ※※
- 4:ミスリルのけん ※ 番号が変わった
- 5:ダイヤのつるぎ ※ 番号が変わった
- 6:クリスタルのつるぎ ※ 番号が変わった
のように、挿入した「どうのつるぎ」以降、すべての番号が変わってしまいます。
番号が変わってしまうと、デバッグやテストプレイなどは、やりなおしです。開発の後半の段階でもし、このように番号を途中に挿入してしまうと、挿入箇所の以降の項目すべてに再テストが必要になってしまいます。
なので、上記のように、追加したいデータは最後の番号の次につけたすのが合理的な場合が多くあります。
このような工夫は、ゲームのデータ追加だけでなく、一般の仕事などで図面のような指示書にデータ追加する場合にも、同じようなテクニックが使えます。
たとえば、金属機械の設計に必要な部品リスト(製造業では『部品表』という)などの書類で、記載し忘れてた部品を追加する場合にも使えるテクニックです。最後の番号の次に、忘れてた部品を記載すれば、書類の検証の手間は最後の追加部分だけで済むので、検証の手間が大幅に減るのです。
さて、ゲームの話に戻ると、番号を武器の強さの順番どおりに並び変えをする段階がいつかというと、次回作を作る場合に並べ替えればいいのです。なぜなら、新作を作る際にデバッグやテストプレイをやり直しするからので、その直前に一緒に並び換えをしておけば、次回作のテストプレイ時にまとめて検証できるからです。
実際、金属機械の部品リストでも、並び換えをする場合には、次回に似たような新機械を設計する場合に、過去の修正項目を反映した並び換えをするのが一般的です。この理由も、新機械を設計する時には検証しなおすので、その時にまとめて部品リストの項目の並び換えをする亊で、検証をまとめて行えるからです。
ライブラリ依存部分などの機能の関数の分離
編集リファクタリングにおいて共通関数を作る際には、DXライブラリ依存部分などライブラリ依存部分を共通関数側にまとめておくと、移植性も高まるので一石二鳥です。
具体的には、たとえばカーソル表示コードやウィンドウ表示コードなどといった、自作ゲームで使い回すマルチメディア関連のコードを共通化して共通関数をつくる際に、ついでに追加ライブラリ(たとえばDXライブラリなど)や追加プラグインの提供する関数を共通関数側にまとめておけば、もし将来的に別OSなどの別の開発環境に移った場合にも、共通関数側だけをイジれば移植がし終わるようになるので、移植をラクにできます。
- ※ ウィンドウ表示に限らず、画像表示や効果音などマルチメディア関連の機能を使う場合、ほとんどの場合、開発環境に依存する命令文を使うことになりますので(標準C言語の国際規格ではサポートされてませんので)、こういったマルチメディア関連機能はライブラリ依存または(ライブラリを使わないなら)OSなど開発環境依存になります。
ただし共通関数側にマルチメディア機能を分離した場合、短所として、第三者(たとえば後輩など)がゲームのストーリー側のプログラムの部分を見ても、第三者の見慣れない作者自作の関数が共通操作の呼出しで使われているので、可読性(分かりやすさ)が下がります。なので、もし集団制作をする場合、なんらかの書類、またはコード中の冒頭のほうのコメントなどで、自作の共通関数の仕様などをまとめて説明しておきましょう。
教訓的に言うなら、リファクタリングで関数などの共通化をする際は、けっしてやみくもに共通部分を片端から共通化をするのではなく、上記のようにライブラリ構成やプラグイン構成なども考えて、開発全体の工程を考えてスタイリッシュに関数などを共通化をすると、良い感じになるかもしれません。
ときには行を増やすリファクタリングも
編集行数を減らすだけがリファクタリングではありません。時には、バグの発生予防をするために、命令を追加して行を追加したほうが良い場合もあります。
一例として、たとえば既存コードのコピーで似たような処理を作っているときに、本来ならコピー先に応じてコード中の「4」とかの直接に書かれた数値を、 適する数値に変えないといけない場合に、書き換え忘れる場合があります。
この場合、行数は増えますが、いっそコード中の定数だった数値をローカル変数に変えて、
int localConst =4;
または
const int localConst =4;
とでもして、コピー先の直前に配置すると、 その変数が目立つので、変更のし忘れがなくなります。
この書き換えの際、コピー元のほうもリファクタリングして、行数を追加することもあります。
「関数を使ってリファクタリングすればいいのでは?」と思うかもしれませんが、関数にすると、コード冒頭のmainブロックの外側に書かないといけなくなるので、コピー先の場所から行が大幅に離れて読みづらくなり、
そのせいでプログラマーにバグ発生させてしまいかねない場合もあります。
あえて関数化しないノウハウの出典としては、書籍では「A Philosophy of Software Design」( John Ousterhout 著)に似たような事が書いてあるようです[1](wiki編集者が未読。読んだ人が編集に協力して上書きしれくれると、うれしい)。
もし、なんでもかんでも関数にしてしまうと、視線移動が多くなってしまうとの事です。そして視線移動をしないと分かりづらいので、なるべくドキュメントを残す必要があると著者ジョン(John)先生が言っているとの事らしいです[2]。
これは裏を返せば、ドキュメント化しづらい処理は、あまり関数にせず、代わりに(「関」数ではなく)変数などの、あまり視線移動をしなくて済むような別の方法で、プログラミングを切り抜ける必要があるかと思います。
なので、変数だけ書き換えるようにしてコードをコピーすると、便利なこともあるのです。
このように変数による処理だと、書き換え忘れなどを予防しやすいため、最終的にはコード中の色々な数値は変数に置き換える必要が生じるのですが、 しかし最初から変数を増やしすぎると、コードの見通しが悪くなります。
また、行が増えるとその直後にはデバッグの手間が1個増えるので、開発の序盤では数値で直接的に指定することも多くあります。
なので、最初は数値をコード中に直接的に書いていくなどで様子をみて、開発の進行とともに実際に書き換えミスなどを発生させた場合などに、必要に応じて、書き換え忘れ予防のための変数を増やして行を増やしましょう。
リファクタリング前のコードは定期的に消す
編集リファクタリングしても、修正箇所の修正前のコードは消さずに、コメントアウトして修正前コードを無効化するだけにしておいて、 しばらく同じソースコード内に保管しておきます。
この理由は、リファクタリングでは、ときどきミスで、改善をしたつもりで誤ってバグのあるコードを書いてしまうことがあるからです。バグにすぐに気づくとは限りません。何週間もあとに、ようやくリファクタリングのミスによるバグが見つかることもあります。
このため、もしバグのあるコードを書いてしまった場合、変更前(リファクタリング前)の状態に戻すのを容易にする必要があるので、リファクタリング前のコードも、いつでも見れる状態にしてコメントアウトだけして保管しておく必要があります。
そして開発中、定期的に、コメントアウトしてあるリファクタリング前の古いコードを消します。(書籍「ルール・オブ・プログラミング」では、未使用コードは消せと指導しているらしいです(未入手のため、未確認)。しかし本wikiでは、ルールオブ以下略そのままの指導ではなく、世間にはリファクタリング前のコードを残す流儀もあるので、折衷案を書いている)
この消去の際は、たとえ最近コメントアウトしたばかりの未使用コードでも、消します。もしかしたら間違ったリファクタリングによるバグの発生もあるかもしれませんが、かといって、コメントアウトしたコードを見ながら、いちいち「このコメントアウトは残すべきか? 消すべきか?」を考えるのは時間の無駄です。
どうしても古いコードを残す必要のある場合でも、別ファイルとして例えば「game_2024_03_15」みたいに日付でもつけてコピーして残すなどして、現在開発中のコードとは別の場所に保管すれば済みます。
異なる関数どうしでのパラメータ共有
編集- おおまかな内容
グローバル変数やポインタなどを使って、別々の関数どうしでパラメータを共有できることを紹介。
普通のC言語の知識があれば、それで足りる知識の紹介なので、すでにC言語をある程度 知っている人は読まなくてもいい。
処理速度などに関する方針
編集書籍『Game Programing Patterns』によると、処理速度の向上にとりかかる工程の時期は、後回しです。その書籍の著者の経験上、そうだということです。書籍のP15に「私の経験から述べると、面白いゲームを早くすることのほうが、早いゲームをも白くするより簡単です。」とあります。
また、書籍『Game Programing Patterns』P17によると処理速度の面からでも、あまり抽象化レイヤーは増やさずに、なるべく問題を直接的に解決するコードを書くほうが、結果的に処理速度も速くなるとの事です。そうすることで関数オーバーヘッドなどの処理速度を低下させる要因の発生を減らせるなどの利点もあり、結果的に処理速度も向上するとあります。
他の書籍でもそうです。洋ゲー「ゴーストオブツシマ」の開発者の一人による書籍『ルールズ・オブ・プログラミング ―より良いコードを書くための21のルール』( 2023/8/28、Chris Zimmerman (著), 久富木 隆一 (翻訳))でも、著者が新人によくする指導法として「問題の例が三つ集まるまで一般性のある解法を書くことを許さない」というのがあります(amazon見本を参照)。このChrisの会社でも、大学を出たばかりの新人は、新人が1つの問題をみつけると、他の問題も解ける一般的なプログラミングを書こうとするのですが、Chrisは「そういうでかい問題は解かなくていい」と指導して、目の前の問題だけをまず解くことに集中させるように指導しています。そういうでかい問題を解こうとするプログラムは、往々にして、バグを潜みやすかたり、理解のための学習コストが高くなりがちなので、ゲーム開発では避けるべきだということを Chris はこの著書で述べています。 ともかく、目の前に無い、でかい問題を一般化して解こうとすると、メンテ性の悪いプログラムイが出来上がりやすいのです。
ゲーム以外の分野でも、googleが似たような見解を出しています。『「コードに早まってDRY原則を適用しないこと」とGoogleが呼びかけ』2024年06月05日 08時00分
GIGAZINEが、下記のようにgoogleの見解を解説しています。
DRYは「Don't Repeat Yourself」の略称で、コードを重複させないことを重視する考え方です。重複するコードが存在していると、特定の機能を変更しようとした時に同じ機能を持つ部分を全て探して同時に変更する必要があり、見落としやミスが発生する危険性が高まります。一方、コードの重複を防げていれば一カ所だけを変更すればOKというわけ。 一見DRYを厳しく適用するとコードの保守性が向上して良さそうに見えますが、GoogleのCode Healthグループはブログに「DRY原則の適用を厳格にしすぎると抽象化が早まってしまい、将来の変更が必要以上に複雑になる」と時期尚早なDRYの適用を避けるべきと述べました。
- (中略)
コードの要件は時間とともに変化することが多いため、開発の初期段階や小規模な開発では多少の重複を許容しておき、DRYを適用するべきだとはっきり判別できる十分な共通パターンが出現してから抽象化を始める方が開発を簡単に進められるとのことです。
本ページのこういった分野を考察する学問についての名称ですが、「ソフトウェア工学」で良いだろうと思われます。書籍『ルールズ・オブ・ゲームプログラミング』の冒頭の『本書への賛辞』に
『ルールズ・オブ・ゲームプログラミング』には、どんなソフトウェアエンジニアでも自己のレベルを次のレベルに引き上げるために使える、実用的経験則としてのルールが満載だ。
とあります。エンジニアとは技術者という意味です。エンジニアリングを日本語に訳すと「工学」です。なのでソフトウェアエンジニアリングなら「ソフトウェア工学」です。