ゲームプログラミング/RPG

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

はじめに[編集]

本ページのRPGは、ターン制の戦闘システムを採用した[注 1]RPG(Turn-based RPG)を中心にとり扱う。アクションRPGやシミュレーションRPG、アクティブバトルなどを語る場合には、注記する。

以下のものは取り扱わない。

  • テーブルトークRPG。TRPGや、テーブルトップRPGとも呼ばれる。

なお、本ページでは説明のためにC言語を用いている。C言語の理解を前提としている箇所が存在するため、もし読者が知識をお持ちでない場合は、適切な媒体で調べながら読み進めていただきたい。 本ページではC言語を説明のために用いているが、開発環境がそれ以外の言語である場合は、適宜読み替えていただきたい。

設計の方針[編集]

アイテム[編集]

アイテム設定[編集]

RPGの企画を考えるとき、大量の武器や防具の「攻撃力」や「防御力」を、ノートに書き起こしたりする場合がある。

例えば

  • 竹やり: 攻撃力=1, 値段 = 50G,
  • 鉄のナイフ: 攻撃力=3, 値段 = 100G,
  • 鉄の剣: 攻撃力=10, 値段 = 700G,

(中略)

  • ミスリルのよろい: 防御力 = 40, 値段= 13000G,
  • ミスリルのたて: 防御力 = 70, 値段= 10000G,

(後略)

・・・・みたいに、何十個や何百個もの武器や防具のデータを、ノートに書き起こしたくなる人もいるだろう。

なぜなら、もし、武器や防具のプログラムを修正するとき、いままでプログラムした武器データ・防具データすべてを修正する場合がありうるからである。なお、データとプログラム自体を分離しておけば、データを変更するたびにソースコードを再コンパイルする必要がなくなるというメリットもある(ただし、デバッグなどのために、可能なら、最終的には全部コンパイルしなおしたバージョンを通しプレイするほうが安全である)。

もし、既にソースコードに大量のデータを入力してしまった人は、それらのデータを、ソースコードとは別のテキストファイルまたはExcelなどの表計算ファイルに移動しておくのが便利である。

特に、武器や防具などのデータは、表計算ソフトで

           

のような空白の表に、

名称 攻撃力 値段
竹やり 1 50
鉄のナイフ 3 100
鉄の剣 10 700

のように入力しておくと、後々で編集しやすく、また、必要に応じてテキストファイルなどに変換できるので便利である。

アルゴリズムとデータの分離

大規模なプログラムを開発するときは、データとソースコードを分離します。このことを、「ロジックとデータの分離」と言います。このように分離するべき理由として、データを変更してもソースコードは変更されず、ソースコードを変更してもデータが変更されないため、結果として結合度を下げることができることがあげられます。

CSVファイル

プログラムで読み込むための大量の数値データを取り扱うとき、CSVファイルで格納することがある。 簡単に説明すると、コンマ (,) でデータを区切り、改行でレコードを区切る方式である。

内部設計[編集]

C言語には構造体が存在する。構造体の解説については、リンク先を参照されたい。

さて、ゲーム制作の最初では構造体を使わなくても良いのですが、将来的に構造体を使うことになるので、説明しておきます。 たとえばRPGなら、武器データベースや防具データベースなどの読み書きのコードは、構造体でまとめる事になると思います。 たとえば、武器名と、武器の攻撃力は、同じ武器グループなので、buki構造体にまとめる・・・ようなことになるでしょう。構造体の配列 ( array of struct ) を作ることで、アイテムを複数管理することができます。


さて、防具構造体(たとえばbouguKouzou)と武器構造体(bukiKouzou)とを流用したいと思われるかもしれませんが、しかし、それはなかなかC言語では困難です。なぜなら、C言語では、要素に 異なる型 をもつ構造体どうしを、コピーしたり代入する事が不可能または、コンパイラが動作未保障だからです。

もちろん、構造体の要素の型をすべて同じにしてしまえば、構造体をコピーする事は可能です。

たとえば、使用品(薬草)や武器や防具や特殊アイテムなどをすべてまとめて扱う構造体としてアイテム総合構造体 sougou を作ったとします。 変数 sougou.itemType

  • 1 なら使用品・道具[注 2]
  • 2 なら武器、
  • 3 なら盾、
  • 4 なら(かぶと)
  • 5 なら(よろい)

・・・・のような方式です。

ですが、それはコードの「パッと見」では、その構造体が何を表すのか、一見しただけでは分かりません。また、もしアイテム関連のバグが発生した際、デバッグのたびに、sougou.itemType がズレてないか調べなければいけないので、デバッグの手間が増えます。

ログラム内部的には所持しているアイテムの種類を数値などで判別する必要があるので、アイテムの種類に番号をつける必要自体は、けっして無駄にならないでしょう。

アイテム種類番号を使う方式とは、たとえば、「勇者ゴンザレスが『毒消し』と『銅の剣』を所持している」場合なら、毒消しがアイテム種類1番でID番号2番、銅の剣がアイテム種類2番でID番号4番、などなら、勇者ゴンザレスの所持している所持アイテムの構造体配列 syojiItemは

『毒消し』についてはsyojiItem[1].Type=1 かつ syojiItem[1].bangou=2
『銅の剣』についてはsyojiItem[2].Type=2 かつ syojiItem[2].bangou=4

のように管理する事で、異なる種類のアイテムを一人のアイテム所持欄でまとめて扱うことができます。

アイテム管理番号を振り直す場合、テストプレイを念の為もう一度行ったほうが良いですが、プロジェクトの規模に比例して大変なことになります。

コード例
#include <stdio.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。
#include <string.h>

// アイテム例
struct ItemSougou {
    int Grouptype;
    int subID;
    char ItemName[20];
};

int main(void) {
    struct ItemSougou ItemYouso[5][7]; // 構造体配列の宣言

    // 1品目
    int GroupID;
    GroupID = 0;
    strcpy_s(ItemYouso[GroupID][0].ItemName, 10, "薬草");
    ItemYouso[GroupID][0].Grouptype = 0;
    ItemYouso[GroupID][0].subID = 0;

    strcpy_s(ItemYouso[GroupID][1].ItemName, 10, "毒消し");
    ItemYouso[GroupID][1].Grouptype = 0;
    ItemYouso[GroupID][1].subID = 1;

    // 2品目
    GroupID = 1;
    ItemYouso[GroupID][1].Grouptype = 1;
    ItemYouso[GroupID][1].subID = 1;
    strcpy_s(ItemYouso[GroupID][1].ItemName, 10, "鉄の剣");

    int i = 0;

    //ドラクエ式の持ち物の構造体
    struct motimono {
        int Grouptype;
        int subID;
    };

    struct motimono hinmoku[5]; // 構造体配列の宣言

    // 持ち物の定義
    // 1品目の定義
    // strcpy_s(  hinmoku[0].ItemName, 10 ,"毒消し");
    hinmoku[0].Grouptype = 0;
    hinmoku[0].subID = 1;

    // 2品目の定義
    // strcpy_s( ItemYouso[1][1].ItemName, 10 ,"鉄の剣");
    hinmoku[1].Grouptype = 1;
    hinmoku[1].subID = 1;

    // 表示コード
    for (int temp = 0; temp <= 1; temp = temp + 1) {
        printf(
            "アイテム: %s\n",
            ItemYouso[hinmoku[temp].Grouptype][hinmoku[temp].subID].ItemName);
    }

    system("pause"); // 「続行するには何かキーを押してください . . .」の待機命令
    return 0;
}


実行結果
アイテム: 毒消し
アイテム: 鉄の剣
※ なお、上記コード例は、MS-DOSプロンプトでの確認用である。windows版gccで確認してある。
実際にデスクトップアプリとしてゲーム用にする場合、strcpy_s 関数を lstrcpy_s 関数に変更するなどの、若干の変更の必要がある。

持ち物だけでなく、ゲーム中の武器防具屋などの品揃えにある商品リストなどでも、上記の例と同様に、異なるアイテム種類の商品を統一的に扱う必要もあるだろう。(たとえば武器防具屋は、武器と兜・鎧・盾という、異なる種類のデータベースを1つの商品リストに同時掲載している。)このため、ドラクエ式の持ち物管理のゲームでなくとも、武器防具屋などで上記コードのようなテクニックを使う可能性も考えられる。


さて別手法として、上記の(構造体変数としての)アイテム種類番号のテクニックとは異なる手法もあります。「構造体のネスト」という、構造体の中に別の新規の構造体を定義するというテクニックもありますが(※ wikibooksのC言語教の科書で解説済み)、たとえソレを使っても、内側の構造体は共通化しなければいけないので、上記のsougou構造体の例と似た制約があります。また、C言語は可変長配列を認めていないので、たとえば兜・帽子の種類が少ない場合などは上記と同様、メモリに多くの無駄が出ます。


なにより、構造体ネストの方式であってもデバッグが面倒になりますし、アイテム種類の番号の整合性を維持しなければならないのでデータベースを後から変更するのは、なかなか困難です(もしアイテム種類番号の仕様を変更したら、テストプレイ等は、やり直し)。

異なる装備品の連番方式はあまりオススメできない[編集]

なお、鎧の数に比べて極端に兜・帽子の数が少ない作品が多い場合など、データベース管理方式について上述とは他の方式として、あまりオススメできない方式ですが、たとえば、鎧も兜も、同じ防具データベースで、「防具IDの20番までは鎧、21番から25番までは兜・帽子」のような連番にしてしまう方式でも、原理的には鎧・兜・盾などの統一的な扱いが可能です。

しかし、この連番方式の場合、もし将来的に仕様変更で「鎧の数を23種類まで増やしたい」のように仕様変更になったとき、すでに21~23番が兜に使われているので、追加の鎧はまだ未使用の番号に追加しなければならず、ソースコードおよびデータベース構造が複雑化します。あるいは仕様書(設計書)が複雑になります。なので、連番方式はあまりオススメできません。連番方式なら、メモリの負担だけなら少ないですが、しかしソースコードの管理および仕様書の管理が、とても面倒になるでしょう。

インターフェース[編集]

※ここでいうインターフェースは、Javaとかにあるinterfaceではなく、GUIのことである。

RPGの場合、最終的にはインターフェースを完成させるべきですが、製作途中の段階ではプロトタイプモデルを採用し、簡素なインターフェースでも良いでしょう。しかし、簡素なインターフェースは、自分が知っているが、第三者が知る機会のない「暗黙の知識」に依存しがちであるため、簡素なインターフェースは第三者からのフィードバックをもとにブラッシュアップすると良いでしょう。

ともかく、ゲーム全体を先に作るのが先決です。ニュアンスは違いますがアトラス社いわく(ゲーム開発では)「ゲーム全体に全体に値を回すのが先」という格言があります[1]

プレイヤーが見たいのは、ゲーム全体のストーリーやテンポといったゲームの全体像です。カーソルの動きとかウィンドウの動きとかは、あくまで補助的であり、そういったUIに対するプレイヤーの興味も、あくまでオマケの範囲でしかないのです。

実装しようとしているものを明確にする[編集]

アナログをデジタルで完全に実装するのは、大概困難ですが、アナログの「エミュレート」の実装は、それほど困難ではありません。

これはゲーム制作でも同様です。

たとえば、RPGの作中の武器屋や道具屋などでの買い物のシステムは、現実のスーパーマーケットやコンビニでの買い物とは異なり、まとめ買いはサポートされていないことが有ります。

ユーザーレベルの目線では、サーバー側で異なるアイテムを連続して買う行動が一連のトランザクションとして処理されていたとしても、同様のことがありえます。

プレイヤーにとっては操作がやや面倒でも、デバッグの都合やメンテナンス等の都合で、あえてデジタル的な操作をしているゲームもあります。

たとえば昔のドラクエ3では、酒場で仲間をパーティに加入させる際、1人ずつ逐次的にコマンド入力をして加入させる必要がありました。(「3人まとめて加入」とかしたくでも、できない仕様。)ウィザードリィなどでも同様で、仲間のパーティ加入の編成などは、一人ずつ逐次的に操作する必要がありました。

このような、「まとめて〇〇を編集・編成」みたいな処理は、その分状態―取り得るケースが増えるため、それに比例してデバッグにかかるコストが増える場合が有ります。

CGアルバム機能

アナログとはやや違いますが、例として、たとえばゲーム中に、CG閲覧機能として、プレイヤーがゲーム中でクリアしたイベントに関するCG閲覧機能などがある場合で、「アルバム機能」などの名前が付いている場合を考えましょう(2000年以降のノベルゲームなどで、よくある機能です)。

「アルバム」等の名前から、ついつい仕様の設計時には、実際の写真アルバム(物理)の操作性を真似したくなるかもしれません(つまり、「本の手触り」的な)。ですが、ゲーム設計では、そういうことは無視して、単にCGの閲覧と検索のしやすさだけを考えたほうが、操作性とメンテナンス性もよい仕様が、無難に設計できるでしょう。

ゲームはゲーム以外の操作性を取り入れられるか?
家電とはゲームは操作性が異なる

上記のケーススタディの教訓としては、つまりデジタル家電(地デジ対応テレビやMP3プレイヤーなど)のような操作仕様ですら、ゲームには不適切な場合があります。(家電のハナシ)現実世界で機能に直接対応したボタンのある家電と、一方でゲーム中の選択肢だけしか(十字キーなどを介して選択してからでしか)押せないゲームとでは、操作の前提条件が異なるのです。

アナログ家電(アナログ放送テレビや銀塩カメラやラジカセなど)と、(デジタルコンテンツである)ゲームとでは操作性が異なるのはもちろんですが、デジタル家電ともゲームは操作性が異なることに、気をつける必要があります。

似た機能を複製する場合[編集]

まず、それらの機能の共通点を探します。次に、その共通点を抽象化します。抽象化のコストと、それを再テストするコストの合計が、そのままにしておくコストよりも高く付くと考えられるならば、抽象化は後回しにしても構いません。

テキスト比較ツール

テキスト比較ツールを使うと、効率的に違う部分を強調して表示することができます。

GNUのdiffutils、Gitなどのバージョン管理システム、あるいは「ベクター」や「窓の杜」からインストールすることができます。

リファクタリングすべき時[編集]

リファクタリングとは、動くようになったプログラムの内部構造を結果を保ちながら整理することです。

リファクタリングの必要がないコードを初期から書くことが理想的ですが、ほとんどの場合はそれについて考えながら各コストが高すぎるので、はじめは「とりあえず」の形でコードが書かれます。つまり、リファクタリングを行うとすれば中期、あるいは動作が安定した後期です。時間が経つに連れて、どの辺りが「におう」のか、より明確になります。

性急な最適化は、諸悪の根源である。

―リファクタリングについて、ドナルド・クヌースリファクタリング

第一段階としては、// TODO: リファクタリング と書き残しておくことです。そうすることで、周囲 (あるいは未来の自分) にリファクタリングしなければならないことを動作の変更なしに伝えることができます。

第二段階は、そのリファクタリングが論理的に正しいと確定できる道筋を考え、それを記録として残しておくことです。ソースコードレベルで保証できることが一番ですが、大抵のプログラミング言語はそれについて十分な能力を持ちません。

リファクタリングすべきでない時[編集]

バグがあるときは、そのバグを修正することを優先しましょう。そのバグをリファクタリングする前に修正するコストが、リファクタリングした後に修正するコストよりも高く付くのであれば、リファクタリングした後にバグを修正しても構いません。

リファクタリングしやすいコードを書く習慣をつける[編集]

コメントをプログラム中に書く習慣を付けておくと、後々楽です。

    // 武器の強さ
    // 短剣の強さ
    int tanken_tuyosa = 10; 
    // 長剣の強さ
    int tyouken_tuyosa = 25;

異なる関数どうしでのパラメータ共有[編集]

おおまかな内容

グローバル変数やポインタなどを使って、別々の関数どうしでパラメータを共有できることを紹介。

普通のC言語の知識があれば、それで足りる知識の紹介なので、すでにC言語をある程度 知っている人は読まなくてもいい。

モードの管理手法について[編集]

基礎[編集]

ターン制RPGの場合、モードの切り替え方法は、 たとえば、モード番号 mode_number みたいな変数を用意して、

マップ移動モード: 1000番
会話モード: 2000番
メニューモード: 3000番
戦闘モード: 4000番

のように、番号で管理したほうがラクでしょう。

マップ移動フラグ map_flag (trueならマップ表示) や会話中フラグ talk_flag (trueなら会話ウィンドウ表示) みたいな変数を用意しないほうがラクでしょう。

その理由は、

たとえば戦闘モードへの突入のためのモードの切り替え時に、いちいちmap_flagをオフ(0にセット)にして、talk_flagをオフにして、menu_flagをオフにして、・・・などのプログラム作業を、モード番号管理の方式ならば1回の作業で省略できます。

また、もし、管理するフラグの個数が増えると、フラグの切り替えのし忘れなどのバグも発生しやすくなります。

このように、モード番号方式にすると、自然と、各モードへの遷移時に他モードを排他することができ、モードの混在を防げるので、ゲームシステムをモジュール化できます。

よって、モード番号の方式で管理するのが無難でしょう。

これはつまり、何かのオン/オフのフラグは、なるべく特別なイベント(例: 中ボスを倒した。伝説の武器を手に入れた。)などの例外的な処理の管理のみで活用するのが無難ということです。


なお、1番、2番、3番、・・・などのように1つおきではなく、1000番、2000番、のように間を置くのは、途中で追加するモードを挿入しやすくするためです。

たとえば、マップ移動でも、通常の歩行での移動を1000番として、ダッシュ移動を1010番、船による海上の移動を1020番のように、モードを追加したくなるかもしれません。なので、ある程度の間を、事前に基本モード間に空けとくと便利です。

特に、メニューモードや戦闘モードなど、コマンドをいろいろとプレイヤーが選ぶことになるので、それぞれのコマンドに対応したモードを多く追加することになるでしょう。

メニューモードの内部にコマンド(たとえば「装備」コマンド)に対応した中モードがあって、さらにコマンド(武器を装備? それとも防具を装備?)があって、選んだコマンドに対応した小モードに遷移する・・・みたいな3段階のモードの存在する可能性もあるので、基本のモード番号は3桁以上(つまり100番以上、できれば1000番ほど)ないと、不便でしょう。

このため、最低でも

マップ移動モード: 100番
会話モード: 200番
メニューモード: 300番
戦闘モード: 400番

くらいの3桁以上で管理するべきです。

変数を媒介としてモード番号の数値の定義[編集]

モード番号は、開発中に番号の数値の仕様を変更したくなる可能性があるので(例えば、オープニング画面のモード番号を追加したくなったとか)、後から番号を変更しやすいように、変数を媒介して定義するとラクです。

まず、単純な方法で、

// モード番号の定義
int mode_map1 = 100; // マップ画面のモード番号
int mode_talk1 = 200; // 会話画面のモード番号
int mode_menu1 = 300 // メニュー画面のモード番号
int mode_battle1 = 400 // 戦闘画面のモード番号

みたいに定義する方法があります。

こうすれば、もしモード番号の数値を変更するときには、対応する宣言文 int mode_〇〇 = △△の1個の文だけを変更するだけで済みます。

なお、C言語には定数値の宣言の修飾子 const がありますので、

// モード番号の定義
const int mode_map1 = 100; // マップ画面のモード番号
const int mode_talk1 = 200; // 会話画面のモード番号
const int mode_menu1 = 300 // メニュー画面のモード番号
const int mode_battle1 = 400 // 戦闘画面のモード番号

と書くと、よりいっそう、意味が明確になります。


ほかにも、defineマクロを宣言する方法もあります。

// モード番号の定義
#define MODE_MAP1 100 // マップ画面のモード番号
#define MODE_TALK1 200 // 会話画面のモード番号
#define MODE_MENU1 300 // メニュー画面のモード番号
#define MODE_BATTLE1 400 // 戦闘画面のモード番号

マクロ名が大文字になってることに、特に深い理由は無く、単なるC言語業界での慣習です。

数字を「1」と後につけている理由は、のちのち、それぞれのモードのサブメニュー画面が追加されていくから、それとの区別のためです。


あらかじめ「1」とか「main」とかをつけておかないと、あとでサブ画面との区別のために、変数名をコードエディタの置換機能で一括変換する際に面倒なことになります。 たとえば

MODE_MAP
MODE_MAP_sub

でMODE_MAPだけを名前変更したくて「MODE_MAP_main」にしようとしても、 「MODE_MAP_sub」も一緒に「MODE_MAP_main_sub」になってしまう。

なので、あらかじめ、「1」でも「main」でもいいので、モード名の定義のさい、定義名称のうしろにサブ画面モードとの区別のための追加文字を付けて起きましょう。

列挙型

モード番号を使う代わりに、C言語の列挙型を使うことでも、上記のような排他的な制御は出来ます。

モード番号方式の限界[編集]

ただし、上記の一連のノウハウは、あくまでターン制RPGの場合だけでしょう。

もしアクションRPGの場合なら、マップ移動中でも戦闘したり会話したりする可能性もあるので、モード番号方式ではなくフラグ方式にせざるを得ないでしょう。

このことは、ターン制RPGとアクションRPGとでは、コードを共有する事が、なかなか難しい事にも、つながるでしょう。

2Dマップ[編集]

方針だけ述べる。具体的なコードは、上手い人のコードを参考にしよう(コードがけっこう長くなり、紹介がメンドウくさい)。

2Dマップがあると、俄然、RPGっぽく見えるようになって、ヤル気が出るので、さあ作ろう。

マップのデータは、ふつう、2次元配列で書く。

まず、グローバル領域で、たとえば

static int maptable[10][10] = {
	{ 0,0,0,0,0,0,0,0,0,0 }, //0 y
	{ 0,0,0,0,0,0,0,0,0,0 }, //1
	{ 0,0,0,0,0,0,0,0,0,0 }, //2
	{ 0,0,0,0,0,0,0,0,0,0 }, //3
	{ 0,0,0,0,0,0,0,0,0,0 }, //4
	{ 0,0,0,0,0,0,0,0,0,0 }, //5
	{ 0,0,0,0,0,0,0,0,0,0 }  //6
};

のように、とりあえず配列を確保しよう。


ここで重要なのは、 確保した配列のタテとヨコは、ヨコの並びをx方向として、タテの並びをy方向としたとき、

maptable[y][x]

のように確保されていることに注意する。


さて、各配列に「0」を入れても、はたして0番が何を意味するか、まだ何も決めていない。

ゲームにもよるが、とりあえず、何もマップチップを上書きしない領域だと定義しよう。

背景色と同じ色のベタ塗りのマップチップを作っておけば、それで上書きすれば、あたかも何も上書きしてないように見える。

いちいちif文などで、何も上書きしないように場合わけをするのもメンドウであるので、どっちの方式にするか、決めておこう。


さて、マップ用の配列は、作成したいマップで最低限必要なマス目よりも、やや大きめの領域を確保しておく必要がある。

たとえば、5×5のマップを作りたいなら、配列ではもっと大目に、たとえば 8×7 とか確保しておく必要がある。


もし、なんらかのバグで確保されていない領域を呼び出してしまうと、プログラムがエラーで異常停止してしまう。


たとえば、一番左端をもしx=0とした場合、もしこの場所に主人公がいると、さらにプレイヤーが「左端に行こう」と考えて、左ボタンを押したときに、エラーで異常停止してしまう。

こういう停止はメンドウくさいので、だったら念のため、最初から、マップを移動可能な場所よりも何マスか大目に確保しておくと安全である。


さて、まだ配列を確保しただけなので、「0」が何かとか、「1」が何かとか、まったく定義していない。

とりあえず、

0番は、進入不可能の暗闇。
1番は、床とか、草原とか平地とか、とにかく歩ける場所

としよう。 だと思ってればイイ。


たとえば、もし配列 maptable の宣言が、

static int maptable[10][10] = {
	{ 0,0,0,0,0,0,0,0,0,0 }, //0 y
	{ 0,0,1,1,1,1,1,1,0,0 }, //1
	{ 0,0,1,1,1,1,1,1,0,0 }, //2
	{ 0,0,1,1,1,1,1,1,0,0 }, //3
	{ 0,0,1,1,1,1,1,1,0,0 }, //4
	{ 0,0,0,0,0,0,0,0,0,0 }, //5
	{ 0,0,0,0,0,0,0,0,0,0 }  //6
};

のような宣言だったら、このマップでは真ん中のほうに4マス×5マスの移動可能な地帯がある。 その周囲は移動不可能になっている。

ともかく、このように、プログラマーは、まず、何番のマップチップが何を現すかということを、脳内で設計しておく必要がある。


配列の範囲外の読み込みエラー防止のためも兼ねて、0番は、すべてのゲームキャラが進入禁止としておくのは安全だろう。


あとはもう、win32 API などの機能を使って、マップチップの画像を配列にもとづいて各マスごとに表示すればいい。


前提として、まずマップチップ画像の読み込みを行う必要がある。あるいは、画像をあらかじめ実行ファイルに組み込んでおく必要がある。

画像の実行ファイルへの組み込みは、Visual C++ にそういう機能があるので、それを使えばいい。

画像を読み込みたいなら、ゲーム起動時にでも読み込んでおけばいい。


イメージ的にコードの雰囲気(ふんいき)を書くと、たとえば case WM_PAINT: の節に、下記のように for文などを使って画像ハンドらに画像を代入するコードを裏画面に書いていき、最後にまとめて本画面に描画することになる。(「裏画面」とか「ダブルバッファリング」と言われるテクニックを使います。詳しくはネット検索して調べてください。)


(イメージ)
(※ あくまでイメージです。このままでは、動きません。)
       int iTemp;

	for (x_map = 0; x_map <= 9; ++x_map)
	{
		for (y_map = 0; y_map <= 6; ++y_map)
		{			
			iTemp = maptable[y_map][x_map]; // 配列の文字が長いので、いったんiに置き換え
			hbmp = hbmp_mapchip_list[iTemp].hbmp_mapchip;

			SelectObject(hMdc, hbmp);
			BitBlt(hbackDC, 225 + x_map * 32, 140 + y_map * 32, 32, 32, hMdc, 0, 0, SRCCOPY);

			// DeleteDC(hMdc); // これを入れると、マップが表示されない。
		}
	}


	// 裏画面から本画面に転送
	BitBlt(hdc, 0, 0, 700, 500, hbackDC, 0, 0, SRCCOPY);

	hbmp = NULL; // 初期化。


	// 中間ハンドルからhbmpを解除
	SelectObject(hMdc, NULL);
	SelectObject(hbackDC, NULL);

	// 不要になった中間物を削除	
	DeleteDC(hbackDC);
	DeleteDC(hMdc);

	DeleteObject(hbmp);
}



SelectObject や BitBlt は、Win32 API の専用の関数なので、分からなければ読者で調べてください。


なお、マップチップ画像そのものの作成は、単にWindowsアクセサリ『ペイント』などの画像作成アプリでビットマップ画像を用意すればいい。

ツクールやウディタなどだと、いくつかのマップチップをまとめた「タイルセット」などを取り扱うが、実はプログラミング的には、わざわざタイルセットを作らなくても、マップチップ1個ずつを読みとりすることも可能である(単にwin32APIのビットマップ画像の読みとりの機能を使えばいい)。

(win32 API の初期設定のままではビットマップしか表示できないハズ。PNGなどを使いたいなら、GDI+の設定を行うこと。GDI+については説明を省略。)。


なお、マップチップのサイズの規格は、よくある規格は16ピクセルの倍数で、「px」をピクセル単位の意味として16px×16pxまたは32px×32pxまたは64px×64pxのマップチップ規格がよくある(ツクールやウディタのマップチップのサイズ規格も、この系統)。

とりあえず、私たちにとって作るのがラクなのは、小さいほうが作成がラクなので、とりあえず16×16のマップチップを作ろう。


プログラム技術としてはマップ表示は単なる二次元配列なので難しいことは無い。しかし、その他の準備が忙しく、たとえばマップチップ用のビットマップを用意したりとか、あるいはキーボード操作のプログラムを作ったりとか、そういう準備が難しい。


これだけだと、まだマップを表示しただけなので、さらに主人公キャラのキャラチップとか、主人公がx=何マス目、y=何マス目にいるかの変数とかも宣言する必要がある。

戦闘中のアルゴリズム[編集]

素早さ順行動のアルゴリズム[編集]

ターン制RPGでは、戦闘中、敵と味方が入り乱れて、素早さ順に行動するシステムが、一般的です。

このアルゴリズムが、けっこう難しいです。

ドラクエ1のように、敵と味方が1対1の戦いなら簡単ですが、ドラクエ2以降や『ファイナルファンタジー』シリーズのように、味方が複数人、敵も複数匹の場合、それらの行動準の決定アルゴリズムは、けっこう難しいです。

さて、まずアナタのすべき事として、ドラクエ1のように敵と主人公が1対1のバトルのプログラムを、あなたの自作ゲームのソースコードに書いてください。それが今後の作業の出発点です。そして、その1対1のバトルのプログラムを実際に自作ゲームに組み込んで、とりあえずプレイ可能な状態まで持っていってください。

それが出来たら、次に、その1対1のプログラムを拡張して 多数 対 多数 のプログラムに改造していきましょう。

では、そのような多数対多数での素早さ順行動アルゴリズムのための考えかたを述べます。

#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct {
  const char * name;
  uint8_t speed;
  bool is_enemy;
} character;

int compare_by_speed(const void* ca, const void* cb) {
  return *((*character) cb->speed) - *((*character) ca->speed);
}

void sort_by_speed(character characters[], size_t len) {
  qsort(characters, len, sizeof(character), compare_by_speed);
}

character[] get_characters() {
  return {
    {.name = "勇者", .speed = 20, .is_enemy = false},
    {.name = "僧侶", .speed = 25, .is_enemy = false},
    {.name = "戦士", .speed = 15, .is_enemy = false},
    {.name = "魔法使い", .speed = 17, .is_enemy = false},
    {.name = "ゴブリン", .speed = 21, .is_enemy = true},
    {.name = "スライム", .speed = 5, .is_enemy = true}
  };
}

void process() {
  character charas = get_characters();
  sort_by_speed(charas, 6);
  for (size_t i = 0; i < len; i++) {
    // 素早さが高いほうが先に来ているため、ここでいろいろやる
  }
}

戦闘中の各ターンの進行の自動化[編集]

戦闘で各ターンのコマンド入力が終わったら、今度は、自動的にそのターンの戦闘が始まらなければいけません。

そして、1~2秒くらいおきに、たとえば

「戦士の攻撃!ゴブリンに5のダメージを与えた!」 (0~2秒のあいだ表示)
「ゴブリンの攻撃!魔法使いは2のダメージを受けた!」(2~4秒のあいだ表示)
「魔法使いの攻撃!ゴブリンは4のダメージを受けた!」(4~6秒のあいだ表示)

みたいに秒の経過によってメッセージが自動進行する必要があります。

※ 説明の単純化のため、全員、通常攻撃で肉弾攻撃だとします。

このような自動進行の処理のため、タイマーを呼び出す必要があります。

タイマーは、オペレーティングシステムが管理しています。

オペレーテイングシステムの種類によって、タイマーの呼び出し方は異なります。

Windowsの場合、Windows API にある SetTimer 関数でタイマーを呼び出せます。

SetTimer 関数で指定した時間が経過するごとに、case WM_TIMER ラベルが呼び出され、ラベル内のプログラムを実行します。

この case WM_TIMER ラベルをWindows API の、WM_PAINT などのある位置の前後に作る必要があります。


さて、case WM_TIMER ラベルが書けたら、


今度はそこに、さきほどの素早さの処理で作成した行動順配列の順番に基づき、戦闘に参加している各キャラが行動する必要があります。

コードは、書きのようなイメージになります。(イメージですので、下記のままでは動きません。)

case WM_TIMER:

if (mode_scene == MODE_BATTLE_NOW ) {			
			TimeCount++; // バトル時以外はカウントしない. 戦闘開始期はゼロになっている

				if (monster_alive == 1 && TimeCount >= 2 && timerFlag == 0)  // 自動進行の開始時に timerFlagをゼロにセットしておく
				{
					timerFlag = 1;
					ID = 0; // 戦闘参加キャラの通し番号. TimerFlag-1でも代用できるが、意味を考えて別の変数を用意した

					// 行動者が味方側の場合
					if (junjoHairetu[0] < partyNinzu) {
						
						heroside_attack(hWnd);

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

					// 行動者が敵側の場合					
					if (junjoHairetu[0] >= partyNinzu) {
						
						if (encount_mons_alive == 1) {
							enemy_attack(hWnd);
						}
						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}
				}

				if (monster_alive == 1 &&  TimeCount >= 4 && timerFlag == 1) 
				{
					timerFlag = 2;
					ID = 1;

					// 行動者が味方側の場合
					if (junjoHairetu[1] < partyNinzu) {

						heroside_attack(hWnd);

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

					// 行動者が敵側の場合					
					if (junjoHairetu[1] >= partyNinzu) {

						if (encount_mons_alive == 1) {
							enemy_attack(hWnd);
						}

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}
				}
	


				if (monster_alive == 1 && TimeCount >= 6 && timerFlag == 2) 
				{					
					timerFlag = 3;
					ID = 2;

					// 行動者が味方側の場合
					if (junjoHairetu[2] < partyNinzu) {

						heroside_attack(hWnd);

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

					// 行動者が敵側の場合					
					if (junjoHairetu[2] >= partyNinzu) {

						if (encount_mons_alive == 1) {
							enemy_attack(hWnd);
						}

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

	                        }
				if ( TimeCount >= 9) {
					mode_scene = MODE_BATTLE_COMMAND; // コマンド画面に復帰するためのモード設定
					TimeCount = 0;
					timerFlag = 0;
					ID = 0;
				}
				InvalidateRect(hWnd, NULL, FALSE);
		} 	
		break;


説明の簡単のため、for文を使わないでコードを書いています。また、味方の死亡を無視しています。

なのに敵の死亡だけ上記コードで考慮してあるのは、戦闘を有限ターンで終了させるためです。本当は上記コードにさらに敵の死亡判定のためのコードが加わったり、敵全滅時の自動進行のコードが加わりますが、説明の単純化のために除去してあります。

そのほか、クリティカルヒットやミスなどの判定も無視しています。敵のAIも無視して、たとえば、敵の(味方への)攻撃対象は、あらかじめパーティ最前列の戦士だけを攻撃するように決めてあるとします。


さて上記コード例で「行動者が味方側の場合」と「行動者が敵側の場合」とでif文や関数を別々に分けている理由は、

参照先のデータベース(または構造体)が、それぞれ異なっているからです。

味方キャラのデータベースを参照するか、それともモンスターのデータベースを参照するかの違いがあるからです。

このように、文章だけなら一見すると同じ動作であっても、参照データベース(あるいは参照先の構造体など)が異なっていれば、実質的に別々の命令文になります。


上記コードの重要ポイントは、たとえば TimerFlag という変数を用意して、指定時間の経過ごとに case を呼び出した時に、このTimerFlag フラグと同じ番号(行動順配列での通し番号)のキャラだけが動くように設計している事です。通し番号の異なる別番号のキャラのIf文ブロックはマスク(遮蔽)されており、別キャラは行動しないようになっています。

そして、このTimerFlagが、行動順配列の各キャラの行動ごとに、TimerFlagの値が順番どおりに1アップしていくようにしておけば、よいのです(説明が難しい。要するに、こういう感じでRPG戦闘シーンでの自動処理が実装できる。実際に Visual C++ でやればワカル)。

アイテムの上詰めの自動化[編集]

原理[編集]

たとえば中世ファンタジー風RPGのアイテム欄で、「薬草」や「毒消し」などといったアイテムが、アイテム欄で、

薬草          5個
高級な傷薬    1個
毒消し        5個
気付け薬      3個
テント        1個

などと、道具欄にアイテム名と個数と併記して表示されているシステムだとしましょう。

さて、もし1個しか持ってなかった『高級な傷薬』を大怪我の治療のために1個使用して、『高級な傷薬』を使いきって0個になり、

薬草          5個
毒消し        5個
気付け薬      3個
テント        1個

みたいに、自動的に上詰めが実行される(たとえばドラクエ方式のアイテム表示の)システムのゲームの場合、

つまり、使いきった道具(『高級な傷薬』)の以降に並んでいた別道具(「毒消し」「気付け薬」「テント」」)が上につめられるような仕様のゲームの場合、

はたしてコレをどうやって実装すればいいのでしょうか?


答えは、C言語で言う『配列』を使う事です。

まず、アイテムの定義の配列を用意します(仮に itemTeigi[要素数] とする)。(アイテム名などや分類などの定義が必要です。構造体配列などでも書けますが、当面は単なる配列でも充分です。)

さらに、

どのアイテムを何個持っているかという配列(仮に itemKosuu[要素数])も必要です。

※ この定義配列と個数配列とは、自動上詰めをしないシステムのゲームの場合でも必要になります。


さらに、アイテムの定義や個数の配列とは別に、

所持アイテムの並び順の配列(仮に itemNarabi[要素数] とする)を用意します。


つまり、最低でも3種類の配列(アイテム定義配列、アイテム個数配列、アイテム並び配列)が必要でしょう。


もしかしたら配列を使わなくても出来る方法もあるのかもしれませんが、しかし、もっとも直感的で分かりやすい方法が、このアイテム自動上詰めシステムの実装の場合なら「配列を使う方法」だと思います。

なので、将来的な集団作業のことも想定して、配列で実装するのがオススメでしょう。(仕事は集団作業なので、他の人でも分かりやすいコードを書く必要があります。なので、わざわざ配列を使わない、分かりづらい方法を使う必要が無い。)


なお、原理だけなら簡単ですが、実はメンテナンス上の理由により、なるべく上詰めの実装は、後回しにしたほうが、ゲーム全体の実装がラクです。

なぜなら上詰めなどのように配列の中身を並べ替えてゆく機能は、メンテナンス性が悪いからです。

なので、たとい最終的に、上詰めなどの自動並べ替えを実装する場合であっても、例えば実はデバッグモードでは自動並べ替えをしない方式でゲームをプレイできるように、二層構造で設計していくのがラクでしょう。

まず先にデバッグモードでの自動並べ替えをしない方式で設計・実装できてから、あとから一般プレイヤー用に自動並べ替えをするインタフェースを追加するという二層構造で実装すればいいのです。


実際の手順[編集]

さて、もし実際に、上述のような上詰めシステムを実装する場合、設計の順序としては、実は先に、上詰めしないシステムから作るほうがラクです。

なぜなら、いきなり上詰めをするシステムから作るのは、けっこう難しいのです。なので、まず先に、下記のように、0個のアイテムを表示するシステムから作りましょう。

手順 1   0個のアイテムも表示

つまり、たとえば、まず先に、0個のアイテムでも、アイテム欄に表示してしまうように作ります。

薬草          5個
高級な傷薬    0個
毒消し        5個
気付け薬      3個
テント        1個


手順 2   カーソル操作やアイテム選択の実装

上記のような表示のできる状態で、まず、カーソル操作によってアイテム選択できるまで作ります。そして、選択したアイテムをとりあえず使用できて効果を発揮できるように作ります。(たとえば傷薬をつかったら、主人公のHPが回復するなどのシステムを、先に実装します。 上詰めシステムの設計は、後回しです。)


手順 3   0個アイテムの非表示化

カーソル選択やアイテム選択の実装まで出来たら、次は、0個のアイテムを非表示にするシステムを作ります。

(※ 商業ゲームでは、たとえばイース3(スーパーファミコン版)というアクションRPGのゲームがこういう方式(未入手アイテムが道具欄の空欄になる方式)だった。なお、イース3は入手アイテムが(名称でなく)アイコン画像で表示される方式。こういう未入手アイテムの非表示方式でも、面白いゲームは作れるので、初心者は気にせず下記のプログラミングを実行すればいい。)
薬草          5個

毒消し        5個
気付け薬      3個
テント        1個

単にif文などを使って、アイテム個数が0個のアイテムについては、表示しないように分岐すればいいだけです。


これが出来たら、次に、

手順 4   表示だけ上詰め自動化
薬草          5個
毒消し        5個
気付け薬      3個
テント        1個

のように、ようやく、上詰めをするような手順で作るという順序で設計していくとラクです。

※ この手順のいいところは、万が一スキル不足やなんらかの事情で、ドラクエ方式の上詰めが不可能になったとしても、イース3方式の空欄方式にもどせば、そのままゲーム製作を続行できる事である。


手順 5   カーソル表示とアイテム効果ズレのバグなどの修正

なお、上詰めを実装する際、よくあるバグで、表示内容のアイテム名と、実際のアイテム効果とは、よくズレます。(毒を消そうと「毒消し」をつかったのに、前にその位置に表示されてた『高級な傷薬』の効果が出たりするバグが、よくある。)

ですが、いきなり、このバグの無いプログラムを書こうと思っても難解なので、まず先に、とりあえず上詰めした状態の表示だけできるプログラムを作っておきましょう。

※ 前提として、カーソル選択などのできるまでにプログラムを書いておく必要があります。

そして、とりあえず表示だけ上詰め自動化をできたら、あとから、アイテム効果選択の位置ズレのバグを直していけばいいのです。


まとめ

このように、段階的に設計していくと、プログラムを思いつきやすくなります。

けっして、いきなり高度なプログラムを作るべきでは、ないのです。

そうではなく、まず先に、やや仕様が最終目的と違ってもいいので、まずは単純な仕組みの とりあえず動くプログラムを作ります。

とりあえずのプログラムが出来たら、そのあとに最終目的のより高度なプログラムに近づけて、機能を追加していくように設計していきましょう。


留意事項: 並べ替えの処理時間について[編集]

アイテム処理プログラムにかぎった事ではないのですが、並べ替え(ソート)のような処理をコンピュータにやらさると、並べ替え対象が多い場合には、時間が掛かることがあります。

よく表計算ソフトなどで、たとえば価格表なら、価格順に並べ替えたり、あるいは在庫数ごとに並べ替えたり、のような場合です。

お仕事の表計算ソフトなら、1秒くらいの時間が掛かっても気にしませんが、ゲームによっては気になる場合があります。

ゲームでも実際、『信長の野望』や『三国志』などのスーパーファミコンの時代ごろのバージョンのをプレイすると、もし武将の人数が多い国などでの操作で、能力値順(たとえば「武力」順に武将を替えたり、あるいは「政治」順に武将を並び替えたり、)に名簿を入れ替えたり、あるいは名簿の最後のほうの武将のページを見ようとすると、読み込み時間が数秒ほどですが掛かります。(名簿の最初のほうの表示なら時間の掛からない場合もありますが、おそらく、他の待ち時間のあいだに名簿の並び替えをするなどして、プレイヤーに気づかせない工夫などをしてるのだと思われます。シミュレーションゲームなので考える時間が掛かるので、そのあいだにカーソルの位置などから、今後の操作を予想して、先に読み込みなどをしているのでしょう。)

アイテム倉庫など[編集]

よくRPGには、持ちきれないアイテムなどを倉庫に預けたりなどのシステムがあります。いわゆる「預かり所」です。

たとえば、薬草を何十個もプレイヤーがもっていて、持ちすぎでこれ以上は持ちきれないので、預かってもらえる、というような施設です。


これはプログラミングでは、どうやって実装するのかというと、原理的には、アイテム所持配列を2つぶん用意して、片方はパーティの所持中のアイテムのための配列、もう片方は倉庫用の配列とすればイイだけです。


さて、プログラミング的に思いつくのが難しいのは、倉庫にアイテムを出し入れするプログラムです。

これは答えを言うと、預けたいものをコピーして倍増させたあと、古い方(渡す側)の所持数を減らせばいいのです。


たとえば、薬草を主人公パーティが手持ちで最初は10個もっているとします。

これから手持ちの薬草を3個預ける場合、コピー方式でのプログラム実装では、まず倉庫の薬草の個数を+3個するのです。この時点で、

手持ちの薬草: 10個
倉庫の薬草: 3個

です。この時点では、薬草の合計数が、一時的に10個を超えて、合計13個になっていますが、これで合ってるのです。

そのあと、即座に、パーティ側のそのアイテムを、預けた分の個数だけ消します。

手持ちの薬草: 7個
倉庫の薬草: 3個

です。つまり、薬草の個数を、預けた後にすぐにマイナス3個する、という事です。

こうすれば、薬草の個数は合計10個ですし、たしかに倉庫に3個の薬草があります。


このようなコピー方式の受け渡しプログラムは、人間のアナログ的な直感には反しているので(一時的だが、預けたハズのアイテムが、手持ち側にも存在しているので)、直感ではなかなか思いつかないです。

ゲームにかぎらず、プログラム中の何かのオブジェクトの所属先グループなどを変える際、このようなプログラム手法が有効でしょう。

このコピー的なプログラム手法はもしバグがあった場合のリスクとして、データが重複して倍増したりします。

ですが、一方でもしコピーしない別手法の場合に、バグで情報の消失するリスクの大きさと比べたら、コピーによる情報の重複のほうが遥かに安全でマシなバグです。

名探偵シャーロック・ホームズの格言か何かですが(創作上の人物ですが)、本来あるべきハズのものが無い場合には、違和感に気づくには予備知識として「こういう場所には、こういうモノがあるはず」という知識が必要なので、普通の人間はなかなか、無いことに気づきません。一方、無いハズのものが存在している場合には、比較的に容易に人は気づくのだと、ホームズは述べています。

さて、上記のコピー的手法の別の利点として、このコピー的手法なプログラミングは、すでにあるアルゴリズムの使いまわしで済むので、作るのが比較的にラクです。


説明の都合上、「薬草」などのアイテムで説明しましたが、アイテムにかぎらず、たとえば日本の戦国時代ゲームで織田信長が国を移動したり(尾張の国から美濃の国に移動したり)とかの所属先を変えたりするような場合も同様です。とにかく、なにかのオブジェクト(「織田信長」みたいな人物オブジェクトでもいい)の所属先が受け渡される場合、一時的にコピーをする方式が簡単です。

コピー方式の受け渡しの長所は、バグ発生時の被害が小さいという長所であり、

たとえばバグが起きても織田信長が2人になるコピーバグと、
一方でコピーしない別方式で織田信長が死んでもないのに0人になるバグとを比べたら、

どちらがマシかと言われれば、マシなのは織田信長が2人になるほうが発見しやすくマシだという長所です。


またなお、別の観点としては、上記の「薬草」の例の説明では、説明の簡単化のため「薬草」3個を倉庫に預けても7個残るようにしたので、どのような方式で実装しても比較的に簡単ですが、もし1個しかもってないアイテムを倉庫に預ける場合を考えると、明らかにコピー方式のほうが簡単です。

もし、先に所持数を減らしてしまう方式だと、もし主人公が薬草を1個しかもって無い場合、その薬草を預かリ所に預ける実装において先に薬草の所持数を0個に減らしてしまうと、倉庫サイドでは主人公の0個所持の「薬草」を参照する事になってしまい、0個のアイテムを参照するのは実装が面倒だし、プログラマ-のアナログ的な直感にも大きく反します。(実装できない事は無いですが、実装が面倒になってしまう方式なので、わざわざ先に所持数を減らす方式を採用するほどの価値が無いのです。)


セーブ時に、預かり所の中身もセーブする

あと、「預かり所」機能のあるゲームにセーブ機能を作る際、預かり所に保管してあるアイテムの一覧とその個数も、セーブする必要があります。

ファミコンのドラクエ3にも「預かり所」があるので、つまりドラクエのセーブデータは、預かり所のアイテムのIDとその個数も保存している事になり、ファミコン時代の当時としては、けっこうサイズの大きいセーブデータだという事になります。

ともかくプログラミング時に、預かり所の保管品もセーブするようにプログラミングしないと、ロード後に預かり所に保管したアイテムが消失したりして、大きなバグです。歴史シミュレーションゲームの武将などの地域移動(尾張→美濃 みたいな)も同様であり、どの地域にどの武将がいるかをすべてセーブ時に記録する必要もあります。記録しないとロード後に武将が消失して(ロストして)しまいます。

冒険者ギルドのシステム[編集]

ドラクエでいう「ルイーダの酒場」や、ウィザードディでいう「ギルガメッシュの酒場」のような、パーティの仲間の出し入れの出来る、2000年以降のRPGでいう「冒険者ギルド」のシステムは、実は上述の「預かり所」システムとほぼ同じです。

むしろ、C言語でRPGを自作する場合、先に「冒険者ギルド」のシステムを作り始めるかもしれません。

説明の都合上、キャラクターメイキング機能などの説明は省略して、すでに作成されているキャラクターの、パーティの加入や、パーティから離脱してギルドの控えメンバーになる処理などの話に限定します。


これは上述の織田信長の例の「尾張 → 美濃」の移動の例と同じです。


たとえばパーティキャラの仲間の一人である「戦士ガーライン」というキャラが、パーティから離脱してギルドの控えに戻る場合のアルゴリズムの原理を考えて見ましょう。

戦士ガーラインをパーティから外す処理のプログラムは、たとえば戦士ガーラインがパーティの2番目の位置にいるなら、プログラムの処理内容はおおむね、

  1. パーティの2番目にいるキャラ(戦士ガーライン)のキャラIDを読み取る、
  2. そのキャラID(この例では「戦士ガーライン」)のパーティ/ギルド処理用フラグ変数(たとえば、パーティ加入中なら1番、ギルド控え中なら2番)を、「ギルド控え中」(つまり「2」番)にセットして更新する、
  3. ギルド控え中のキャラ合計人数の変数を +1 する、
  4. パーティ加入中メンバーの合計人数の変数を -1 する、

のような処理になるわけです。

というか、そもそも普通、C言語での自主RPG制作では、まず武将システムは作らないので、RPGでは先にギルド・パーティの機能を作ります。(※ 本wikiでは説明の簡単化の都合のため、現実世界に対応しやすい歴史シミュレーションゲーム風のたとえを先に説明しました。)


ギルド処理は原理的には「預かり所」システムとも同じですが、しかし変数名などをもしアイテム処理用の変数や配列と流用するとプログラミング時やデバッグ時に混乱を招きます。なので、プログラミングの最初は手間が掛かりますが、アイテム用の変数とは別に、ギルド処理用の変数や配列を別途で作成して、そのギルド処理専用の変数・配列でギルド機能を実装しましょう。

武器・防具の装備システム[編集]

武器・防具の装備システムも、預かり所システムと同じです。 ファンタジーRPGにおける、「銅の剣」を装備したり「棍棒」を装備したりなどの装備システムのコードの実装方法の原理も、上記の「預かり所」の手法と同じです。

というか、ゲームの歴史的には、装備システムのほうが「預かり所」よりも歴史が古いです。

本wikiでは単に説明の都合で、現実世界とも対応しやすい「預かり所」とか「織田信長の移動」とかの例で説明しました。(説明文でいきなり「勇者ゴメスの装備」とか説明しても、初心者には分かりづらいから。読者には「勇者ゴメスって誰だよ? だいたい、どういう装備システムのRPGかよ?」って疑問に思われかねないので。「ゴメス」は分からなくても「織田信長」なら義務教育で習うので。)


で、文章で上記のように「装備システムの原理は、預かり所システムと同じ」説明するだけなら簡単そうですが、初心者が実際にコードを書くとまず、バグが発生し、「アイテム無限増殖バグ」または「アイテム消失バグ」のいずれかが発生します。

ツクール製やウディタ製のフリーゲームRPGでも、作者が独自の装備システムなどを考案しているRPGでは、ベータ版などでときどき、アイテム無限増殖バグなどが発生している場合もあります。


アイテム無限増殖バグの原因は色々と考えられますが、やりがちなミスのひとつとして、

本来なら装備から外した武器IDの武器の所持数を+1だけ増やさなければならないのに、間違えて選択中武器の武器IDの武器を+1所持数してしまう、

のようなミスをしがちです。

また、この種のプログラミングやデバッグをするので、バグ予防としては、所持武器の配列の構造体を作成する際に、あらかじめ作成しておく構造体変数として、ID番号も保管できるように構造体変数を用意しておくとラクです。

さらに言うなら、武器の作成時に、すべての武器にIDを割り当てておくべきです。原理的にはID番号が無くても、一時変数などで代用できますが、しかし、かなり面倒です。


ともかく、装備システムのコードは、かなり難しいので、答えを書いておきます。手本にしてください。

int tempID; // 作業用の一時ID
tempID = (weapon_have_list[heros_def_list[partyNarabijyun[whomTargetID]].heros_weapon1]).have_weapon_id   ; // 一時IDに、選択中キャラの右手武器の装備品IDを代入している
int tempEquip = heros_def_list[partyNarabijyun[whomTargetID]].heros_weapon1;

// 外した装備の個数が1増える。
weapon_have_list[tempEquip].have_kosuu = weapon_have_list[tempEquip].have_kosuu + 1;
// 上記で [tempID - 1]としている理由は単に、仕様表では武器IDが1番から数えてるけど、プログラム内部ではゼロからカウントしているので1個ズレしてるから


// 装備したものは個数が1減る。 変数「sentakutyuu2」は装備画面での装備可能品リストで選択中のものが何行目かを表す数値。
weapon_have_list[itemHairetu[sentakutyuu2]].kosuu = weapon_have_list[itemHairetu[sentakutyuu2]].kosuu - 1;


// 上記で先に所持武器の個数を更新してから、あとから装備内容の更新。こうしないとロストする。
heros_def_list[partyNarabijyun[whomTargetID]].heros_weapon1 = itemHairetu[sentakutyuu2];

のようになります。

なお、上記コードでの、あらかじめ作成しておいた構造体変数や配列の意味は、それぞれ、

・所持武器の構造体変数: weapon_have_list
その所持武器の所持数: .kosuu
所持武器のID用メンバ変数: .have_weapon_id
・パーティキャラ定義の構造体変数: heros_def_list
そのパーティキャラの右手武器を表す構造体メンバ: .heros_weapon1
・パーティ並び順を保管した配列: partyNarabijyun[]

という意味です。

もしドラクエ1のように勇者1人だけのパーティのゲームなら、もう少し構造体などが減らせて、簡単になるのかもしれません。しかし、現代の一般的なRPGではパーティ内に多くの人数がいるので、そういった事も構造体変数や配列などでプログラミングする必要があるので、そのため、装備システムのプログラミングはけっこう難しくなります。


ともかく、文章だけで「預かり所のシステムと同じ」だと、読めば簡単そうですが、しかし実際のゲーム制作では、RPGの装備システムの実装は、諸々の事情により、かなり難しいです。

RPGプログラミングのもうひとつの山場の「戦闘モードにおける、各キャラの素早さ順に並べた行動順配列をつくるためのソート」のテクニックと同様に、上記の装備品システムでの、データ消失なき各・装備品の個数増減のプログラミングも、RPG制作プログラミングにおける山場のひとつでしょう。


なお、この装備システムを作る前の準備として、実務的にはセーブシステムが必要であり、現在の装備品やアイテム所持数(薬草などの消耗品だけでなく武器・防具などの装備可能品の所持数も含む)などをセーブできるようにしておく必要があります。なぜなら、テストプレイの際に、何度も、コード改修するたびにロードを繰り返して検証するからです。

このように、装備システムの実装には、前準備(各種の構造体配列の作成や、セーブやロードにおける所持アイテムや装備中アイテムなどのセーブ・ロード機能の対応)が多く必要です。

この前準備の多さのため、装備システムの実装は、けっこう大変です。山場です。もしC言語で自作RPGを作りたい人は、がんばりましょう。

後述の、「アイテム上詰め」システムなどとも関連するので、装備システムの実装は、かなり難しくなります。

上詰め表示の実装は、後回しに[編集]

説明の都合上、先にアイテム表示の自動上詰めを紹介しましたが、実は上詰め方式はメンテナンス性を悪くしますので、上詰め機能の実装は、後回しにすべき項目です。

なぜメンテナンス性を悪くするかというと、理由は、上詰めによる置き換わり先の位置が、一定ではないからです。位置が一定でないという事はつまり、配列(あらかじめ「アイテム配列」などを用意する)などのそれぞれの要素の値も一定ではないという事なので、デバッグの際、いくつも要素数のある配列の中身をつねに追跡しつづける手間が生じますが、このことはつまり、配列の要素数が増えるとメンテナンスの手間が増え、(不可能ではないですが)そこそこ煩雑になります。

単に所持アイテムだけを上詰めするのは、まだ簡単なほうですが、アイテム倉庫なども実装するとなると、倉庫アイテム配列と主人公所持アイテム配列との受け渡しになってしまい、それを両方とも上詰めすると、2種類ある配列の中身を追跡するのが、かなり煩雑になってしまいます。

しかも、その2種類のアイテム配列どうしを、矛盾のないようにアイテムの受け渡しをするように設計しなければいけないので、中身の追跡の手間が増えるのは、かなり面倒です。

なのでプログラミングの際、上詰めなどの並べ替えは極力、後回しにすべきであり、ゲームシステム実装の最後のほうの段階になってから行うようにすると、メンテナンスへの影響が減らせます。

たといアイテムの種類が何十個や何百個もあるゲームであっても、プログラミングの始めの段階では、上詰めを行わないほうが簡単です。


プレイヤー視点だと、アイテムなどの種類の多いゲームの場合は、普通はそういうゲームはアイテムが上詰めで紹介されるので、ついつい自分がプログラミングする場合もそのように早めに上詰めを実装したくなりますが、しかし実はメンテナンス上の理由により、上詰めのような自動並べ替え機能の実装は、後回しにすべきです。

上詰め表示の実装は、後回しに2[編集]

アイテムの上詰めシステムを導入する場合、所持アイテムの種類が合計で何種類か、などの変数が必要です。

なぜなら、上詰めによって空白になった何も無い(下部の)場所に、カーソルが移動したら、オカシイからです。

このため合計で何種類のアイテムを所持しているかの変数が必要です。


問題点・難所は、どうやってその、合計種類数を追跡を行うか、という事です。


方法は主に2種類あります。

  • たとえば『薬草』を使い切るなどして『薬草』が0個になった時など、アイテム数が0になった場合に種類数をマイナス1する方式。逆に、今まで0個だったアイテムを、そのあと入手した場合には、入手直後にプラス1していく。(仮に「0個カウント方式」と言いましょう。)
  • アイテム配列をもとに、実際に配列の中身をfor文などで走査的に調べてカウントしていく方式。(仮に「走査方式」と言いましょう。)


このうち、圧倒的にラクな方式は、アイテム数が0個になった場合にマイナス1していく方式です。初心者のうちは、まず0個のアイテムをマイナス1していく方式がラクです。

原理的には、アイテム配列をもとに毎回、配列の中身を走査していくほうが正確なのですが、しかし大きな問題点として、コードが複雑化するので、デバッグ(バグの修正)をしづらい、という問題があります。

なので初心者のうちは、配列の中身を走査していく方式は、避けるほうが安全です。

たとい正確な走査方式でバグの発生を防げる可能性が増えても、万が一にバグが発生した場合に、初心者レベルではバグの修正をしづらい、と言う問題点が、走査方式にはあります。

どうしても走査方式を試みるなら、せめて中級レベルのプログラミング力を獲得してから、走査方式を試みるべきです。

また、アイテム種類数が多いゲームだと、アイテム配列をもとに毎回走査するのは、計算量が増えていくので(指数関数ではなく一次関数的に増加する計算量ですが)、それなりに時間が掛かる可能性があります。


ハイブリッド方式はしない

また、アイテム種類カウントでは、上述の両方式のハイブリッド的なプログラムを試してイイトコどりしようとするのは、初心者のうちは、あまりしないほうが安全です。

なぜなら、ハイブリッド方式を初心者が試みた場合によく起きるバグとして、下記のようなバグが、よくあります。

アイテム種類数の変化も計算を、あやまって重複して、2倍の変化量で計算してしまうという、計算ミスがよくある。
0個カウント方式と走査方式の両方に書かないといけないコードを、あやまって片方にだけ書いてしまい、もう片方との食い違いによって、バグになる事がよくある。

道具屋の商品メニュー表示のキャンセルなど[編集]

基本[編集]

RPGでは、道具屋に行くと、「買う」コマンドを選択後に商品メニューが出て

薬草         10 G
毒消し草     4 G 
ワープ石     10 G

とか商品メニューが出てきます。

駄目な例[編集]

さて、もしも、この商品メニューの最後に、コマンドを追加して

薬草         10 G
毒消し草     4 G 
ワープ石     10 G
店を出る
所持品を売る

とか追加するのは、どうでしょうか?

こういうゲームは、あまり見かけませんね。

普通の多くのRPG作品では、キャンセルボタン(PCゲームならXボタン、十字コントローラーならBボタンなど)を押すことで、

「買う」「売る」「店を出る」コマンドなどの選択画面に戻るという仕組みになっています。


答えを言うと、商品リストの末尾に「店を出る」コマンドを追加してしまうとデバッグの手間が増えるので、よってコマンド追加はやめたほうが良いです。

この末尾コマンド追加方式をしなくても、もっと安全に、プレイヤーに操作方法をわかりやすく伝える代替策がありますので、その代替策を実装したほうが安全です。(代替策については後述。)


では、なぜ末尾コマンド追加方式にしたらデバッグの負担が増えるかというと、理由は、

・商品メニューの種類数によって、末尾コマンドの位置が変わるので、まずそれのテストプレイ検証などが手間です
・しかも、商品メニューの一覧のためのプログラム命令文と、コマンド一覧のためのプログラム命令文とを、相互に関連しあって不整合の無い様にコード記述しなければならず、だいぶ手間が増えます。

たとえば、いくつか前の節でも説明しましたが、まず道具屋での商品リスト掲載の前提となるアイテムの上詰め処理の実装だけでも、けっこうデバッグの負担を増やします(しかし、この上詰めは仕方無い)。

その上、さらに道具屋の商品メニュー末尾に別のコマンドを末尾に追加するというのは、つまり、購入モードにおいて 商品/コマンド の選択時に、いちいち、「プレイヤーは、いま何行目を選択しているか?」をもとに条件分岐(if文など)で「商品選択中」または「コマンド選択中」などとフラグ分岐の内部処理をプログラムに追加する必要が生じるので、だいぶ設計の手間が増え、しかもバグの発生しやすい高リスクな設計になります。

そして、手間が掛かる割りには、目立ちません。しかも、他のゲームでも使わないので、教育的な応用も利かなそうです。デバッグ防止という工学的な理由もあるので、ゲーム以外の一般的なIT産業にも、上記のような「何かのデータベース表示の末尾にコマンド追加方式」が採用される確率は低いので、あまり他産業にも応用が利かないでしょう。(一般のIT産業でも、何かのメニューコマンド欄と、データベース表示欄とは、分離する事になろだろうと思われます。)

特に商業ゲーム作品では、バグ発生が品質問題として大幅に嫌われるので、コマンド追加方式はさけたほうが安全です。

代替策[編集]

それでも、どうしても初心者への操作の説明の分かりやすさを考えている設計をしたいなら、たとえば

「Xボタン で 買う/売る コマンド画面に もどります。」

などの説明を画面のどこかに表示するだけにして、末尾コマンドは追加しないようにすると良いでしょう。


プレイヤーにとっては、どうせ(このwikiを読んでるアナタ以外の)他の作家のRPGのプレイでも、キャンセルボタンのプレイ技術は習得する必要があるので(デバッグ的な理由もあるので、他のゲームでもキャンセルボタン方式が採用されている確率が高い)、プレイヤーへの教育も兼ねて、そういうキャンセルボタン方式でコマンド画面に戻れる設計にしましょう。


RPGツクールやウディタなどでも、道具屋や武器屋・防具屋などからの退出は、ボタン方式です。もっとも、ツクールの制作会社がクリエイターのデバッグの負担まで考えたか分かりませんが(単にウィザードリィ(昔のツクールの開発元のアスキーがウィズの日本移植してた)やドラクエなど市販の有名ゲームの多くを統計的に参考にしたのだろう)、経緯はともかく結果的にツクールのデフォルト(標準設定時)での機能は、バグの発生しづらい設計になっていますので、そういうプログラマー的な視点でツクールなどのデフォルト設定も参考にしなおしてみると良いでしょう。


なお、この工学発想を一般のIT産業に応用するなら、「データベースの表示およびアクセス手段と、メニューの表示とアクセス手段は、なるべく分離しろ」という原則になるでしょう。分離することでバグの発生を抑えられる利点があるし、テスト(バグの無いことを確認するための試験)などの検証もモジュールごとに分割して行いやすくなるので、デバッグの負担が減ります。

マウス操作のゲームや、タッチパネルのゲームなど、ゲームの操作デバイスによっては、ツクールやウディタのデフォルト設定のようにはいかないかもしれませんが、設計の際には上述のような工学的なモジュール分離の発想を参考に、うまくモジュール分割してバグを抑える設計をすると良いでしょう。

今後もメンテナンスしやすい設計を[編集]

もし、むりやり末尾コマンド追加方式で道具屋システムを作ろうと思えば、とりあえず一時的には試作品を作れてしまうでしょう。しかし、問題点は、その後のメンテナンスの手間と費用です。改修やブラッシュアップなどのメンテナンスのたびに、いちいち上記のバグ混入リスクのような点に注意するのは、だいぶ非効率です。

しかも、もしゲームが集団制作の場合には、共同プログラマーどうしで、いちいち上記のバグ混入リスクの高い部分の仕様のすり合わせをしなければならず、だいぶ手間が増えます。

つまり、集団制作な大作ほど、手間が増えてしまいます。

そして、手間が大幅に増える割に、しかし末尾コマンド追加方式は、プレイヤーにとってプレイで得られるメリットが少ないのです。


とにかく、RPG作家は、道具屋の商品メニューなどでのコマンド追加方式はさけるのが安全です。


画像関係[編集]

Windowsによる画面消去[編集]

半透明レイヤー合成に限らず、Windowsではプログラマーが画面クリアの命令を書いてなくても、OSがなんらかの理由で強制的にいったん画面クリアする場合があります。

典型的なのが、アプリをウィンドウ右上の最小化ボタンを押してタスクバーに格納すると、いったん画面が消去されてから、WM_PAINTを実行するという再描画が始まります。


モードの管理方式だと、この際、前のモードの描画はすべて消えてしまいます。


たとえば、(ドラクエ1みたいに)戦闘モード中にマップ画面を背景で表示しようと思って、マップモードの画像を残していても、しかしWindowsの場合、なんらかの理由でWindowsが強制的に画面クリアすることがあり、そのあとにはマップモード時に描画された画像は、このままでは消去されてしまい再描画されません。


こういったWindowsによる強制消去の対策としては、戦闘画面モード側にもマップ背景の描画をコード記述するしか、方法はありません。(他の方法があるかどうかは、一般には知られていません。)


結局、それぞれのモードで、それぞれ別個に、同じ背景画面の描画を指定することになります。

なお、関数などを使えば、背景などの描画はコードの使いまわしが出来ます。


たとえば、戦闘画面モードで、マップ画面描画の関数を呼び出せば、追加する必要のあるコードはその呼出し命令だけで済むという事です。


方法1(非推奨)[編集]

バグが発生しやすいので非推奨な方法ですが、もし異なる関数どうしで、裏画面(ダブルバッファ)用の画像を受け渡しする場合、下記のような方法で画像の受け渡しが一応は出来ます。しかし、他の画像関連の関数の描画時に原因不明のバグ画像が発生する事が多く、あまりオススメできないです。

参考前に、下記の方法を紹介しておきます。

たとえば、戦闘画面の背景にマップ画面を書く場合、コード例として下記

// マップ描画側の関数
void map2_Draw(HDC hdc,HDC hbackDC) {
   // hbackDC上で 画像の作成のコードを個々に書いておく

   // ここにマップ描画の関数を書くが、本画面(hdc)に転送 BitBlt しないでおく 
   // BitBlt(hdc, 0, 0, 700, 500, hbackDC, 0, 0, SRCCOPY); // なのでコレはコメントアウト
}


void battle_Draw(HDC hdc) {
	filterFlag = 1; // 背景にフィルターをかぶせるのでフラグのセット

	static HDC hbackDC = CreateCompatibleDC(hdc); // 裏画面用のハンドル	
    map2_Draw(hdc, hbackDC);

	BitBlt(hdc, 0, 0, 700, 500, hbackDC, 0, 0, SRCCOPY);
	
    // ここまでマップ画面の記述
    
    // 後略


のようにすれば、一応は、異なる関数どうしで裏画面のデバイスコンテキストの受け渡しが出来ます。

つまり、呼び出しの戦闘関数側で、引数をhdcだけでなく、裏画面用の hbackDC も引数として含めて、

map2_Draw(hdc, hbackDC);

のような2引数以上の関数にして呼び出す必要があります。

また、このため、呼び出されるマップ側にも、引数として

void map2_Draw(HDC hdc,HDC hbackDC) {

のように引数を増やす必要があります。


このため、既存のマップ描画関数では引数がhdcの一つだけなので、そのままでは使いまわし出来ないです。よって、戦闘画面の背景用に書き換える必要があるので、なので既存の map1_Draw(HDC hdc) をコピーペーストして新しく map2_Draw(HDC hdc,HDC hbackDC) 関数を作らなければなりません。

このため、この方式では、あまりコード行数は減らないし、むしろ行数が増えます。


しかも、せっかく作ったコードなのに、実際にテストプレイしてみると、街頭の戦闘シーンだけは画像は正常に表示されるのですが、上述のコード例の場合、なぜか戦闘後のマップ移動画面がバグりました。

このwikiを書いた著者の戦闘画面の描画プログラムが単に欠陥なのか、それともwindowsの仕様上の問題があるのかは当wiki著者には分かりません。

ですが、どちらにしろ、上記の方式では、たとえ異なる関数どうしの間で裏画面の受け渡しが出来たとしても、デバッグやメンテナンスがかなり難しくなると予想されるので、なるべくこの方式は避けるほうが安全でしょう。


ネットでさらっと検索して調べてみても、この問題の対処法は全然見当たりません。ネット上にいるゲームプログラマー志望の皆さん、どうもここまで実験してないようです。

なので、前の節で述べたように、マップ側で本画面に転写するなど、裏画面を作成した関数の側で、本画面にも転写してしまうのが、バグが発生しないで安全です。


windowsでは出来ない事[編集]

上述のような問題が発生する背景として、windowsでは下記のような、機能不足の仕様があるという理由があります。


グローバル変数では画像を保管できない

グローバル変数などで画像を保有する方法は、試みても、、うまくいかないです。(標準c言語の文法どおりの仕様には、Windows APIの仕様は従っていないのです。)

たとえば、あらかじめグローバル変数として(コード冒頭の部分の)グローバル領域で

HDC hbackDC; 

など、裏画面用の画面変数を宣言しておいて、ためしにローカル関数サイドでグローバル変数のhbitmapやHDCなどに代入コピーしようとしてイコール記号「=」でプログラム中で代入コピーしても、(コンパイルはできるのですが、)なぜかモード変更時にはコピー内容を残せない仕様になっています。(おそらく、メモリのオーバーブローを防ぐための仕様として、Winodowsが自動で勝手に、代入命令の意味をメモリ内容の複製ではなく(コピーではなく)、参照リンクにしていると思われます。)

(なお、HDCは型名である。Windowsではデバイスコンテキストの型名がこう決まっている。 hbackDCは単なる変数名なので、自由に命名できるし、別の変数名でも構わない。)

どうやらWindowsAPIにおけるhbitmapやHDCといった型のグローバル変数宣言は、実態は単に、各ローカル関数ごとに宣言をする手間を減らすために、一括でグローバル領域で宣言できるという仕様でしかないようです。

もし、上述の説明とは別のうまい方法があったとしても、つまり複数のローカル関数どうしでhbitmapやHDCの内容を共有できる方法があったとしても、その方法はけっして、上述のような直感的な方法ではないので(上述のような直感的な(標準C言語のような)方法では、共有不可能です)、メンテナンス上の理由であまり用いないほうが安全でしょう。

このようなWindowsAPIの非直感的な仕様のため、どうしても背景を複数のモードで使いまわしたい場合にはWindowsの場合、やむを得ず、CPUに負担は掛かってしまうのですが、結局は前の節で説明したように、関数などを用いて各モードごとに同じ描画をする方法しか、IT業界ですら、この関数の方法しか世間的にはよく知られていません。


static でも保存できない

静的変数 static としてグローバル変数として、

static HDC hbackDC; 

とグローバル領域で宣言しても、同様の結果です(共有できない)。なお、これとは別件で、ローカル関数側で static HDC と宣言しても、何も表示されなくなりますので、ローカルで static 宣言しても役立ちません。


ポインタ変数でも保存できない

画像の描画の際、グローバル変数でアドレス変数やポインタ変数を作成しても、通常変数の場合と同様に、異なるモード間では一行に画像を共有できないです。

たとえば、下記のようにポインタなどを使ったコードを書いてもコンパイルは出来るのですが、しかし画像がモード遷移時に消去され、複数のモード間では画像を共有できないです。

例えばグローバル変数としてグローバル領域で

HDC* point_dc;

と宣言してみて、

描画時に

point_dc = &hbackDC;

などと宣言をしてみて、

BitBlt(hdc, 0, 0, 600, 400, *point_dc, 0, 0, SRCCOPY);

としても、単に普通に通常変数で描画した場合と同様のことが起きるだけで(つまり、宣言したモード内だけで描画が出来るだけ)、異なるモード間では何も画像は共有されません。

また、

HBITMAP*

で試しても、同様の結果です。


画像や音声の埋め込み方法あれこれ[編集]

ツクールやウディタやその他の各種の日本産のゲーム制作ツールなどのゲームをプレイすると、実行ファイルかせいぜいあと1つのデータファイルしかなく、画像ファイルや音声ファイルなどのアイコンはゲームフォルダの中には見つかりません。

windowsの Visual Studio には、一応、実行ファイルに画像ファイルや音声ファイルなどの埋め込みをする機能がありますので、Visual Studio でゲーム制作する場合はそれを使えば済みます。


なので、この説明でこの章を終わりしてもいいのですが、しかしツクールやウディタが、C言語コンパイラの機能もないのに、どうやって何十枚や何百枚もある画像や、あるいは合計で何曲もある音声ファイルなどを埋め込んでいるのか、それはそれで読者は気になると思います。

なので、ツクールの方法かは分かりませんんが(ツクールの製作元企業の企業秘密なので)、画像などをファイルに埋め込む方法を調べてみて、まとめます。

調べた結果、下記のように、Visual Stduio 以外にも、幾つかの方法があるようです。

方法1. 実行ファイルを機械語で編集して、実行ファイルの末尾に画像ファイルなどの機械語(機械語は簡単に読み取れる)をつけたし、使用時にはC言語のfopenなどを使って読み込む。
方法2. windowsの実行ファイルexe形式のPEフォーマットという仕様を解析して、正攻法で書く。(しかし難度は激高)
方法3. 独自の画像フォーマットや音声フォーマットを作り、それを使用時に変換(エンコード)する方法。(難度は激高。しかも、エンコードの手間が掛かる

などの方式があるようです。

難易度的には、機械語を用いる方法が簡単なようです。しかし、そのような動作はマイクロソフトはそのような使い方を未保証にしていま。しかし、他のPEフォーマット方式や、独自フォーマットはとても難しいというとか、ほとんどOS開発に近いので、あまりオススメできないです。総合的に考えて、機械語が無難かと思います。

RPGのほかアクションゲームなどでも、無料のゲーム制作ツールを作って配布している人達は日本に数人ほどいますが、それらの無料ゲームエンジンはどれも、画像や音声などの埋め込み機能があります。

さすがに、そんなに多くの人達が、音声ファイルの独自フォーマットやら、PEフォーマットの仕様に精通しているとは思えませんので、おそらくは、ほとんどの無料ゲームエンジンの場合、画像などリソースの埋め込みは、機械語の方式だと思います。


ただし、暗号化のつもりなので機械語化などをしても、それでも第三者によって復号されるツールなどは出回っています。


かといって、たとえば画像ファイルを、プレイヤー視点では画像ファイルだとばれないように、ファイル冒頭の機械語にあるファイル形式の情報を書き換えると(たとえばビットマップファイルには冒頭にファイルヘッダを示す文字列「BM」がある)、windowsの画像用API関数では処理できなくなってしまうでしょう(処理時にヘッダファイルを読み取る必要があるので)。また仮に、ヘッダファイルを偽装したまま読み取らせる事がもし出来たとしても今度は、最悪の場合、外部のセキュリティセフトによってウイルス判定されるかもしれません。なぜならファイル偽装は、コンピュータウイルスによくある手口だからです。


あるいは、もし使用の瞬間にだけ画像ファイルを機械語リソース集などから取り出してファイル出力などで作成して読み込み終わったら消す方式にしたりする方式にすると、今度は頻繁に画像を作成・消去をする事になるので、処理速度が低下して、あまり好ましくありません。特に、SSDフラッシュメモリの普及している現代、頻繁にファイルの作成・消去することはSSDを消耗するので製品寿命を損なってしまいます。

実際にハードディスクに書き込まずにメモリ上で仮想のファイルやフォルダを作る方法については、この版のwikibooks著者が知りません。また、そのようなメモリ上の仮想化は、仮に出来たとしても、膨大な画像ファイルや音声ファイルをメモリ上に常駐させる事になるので、だいぶメモリに負担になるだろうし、処理速度なども低下させるのだろうから、あまり好ましくないと思います。

もしかしたら、リソースファイル集の機械語の文字列をC言語プログラムで1文字ずつ、各コンテンツの最後まで読み取って、プログラム側で、各種のメディア読み取り用のAPI関数でエンコードする事で、むりやりに画像や音声などをメモリに読み込ませる事が出来るのかもしれません(未確認)。ですが、そのためには、読み取り元のデータベース集ファイル側に、どこからどこまでが1つの画像コンテンツ単位(または1つの音楽コンテンツ単位)だとかの目次的な情報を、冒頭あたりに記録しておく必要があります。そのためには、各ファイルのサイズ数(つまりバイト文字数)なども正確に目次部分で管理する必要がったりと面倒そうです。目次の仕様も、自身で策定しなければならないでしょう。


なお、偶然に目次の情報の機械語と、画像コンテンツ側の機械語が一致してしまうと、読み取りミスになってしまいますので、そういう現象の発生しないように、充分に長い機械語文字列を使って、目次の位置を表示したりなどの工夫が必要かと思われます。


用語

なお、windowsには、「tempフォルダ」がありますが、これはべつにメモリ上の仮想(ハードディスク上にはない「仮想」)のフォルダではないはずです(メモリ上ファイルだとは、聞いた事が無いし、ネット検索しても出て来ません)。単に、ウィンドウズが一時的に自動作成したファイルであるので、ガッツリとハードディスクなどのストレージに書き込まれているはずです。(そもそも、なるべくメモリの負担を減らす必要があります。)

なお、「仮想メモリ」は、上記とは意味が異なります。仮想メモリとは、ハードディスクなどストレージ領域をメモリとして使用する機能です。このため、「仮想メモリ」を使うと、頻繁にハードディスクに書き込まれることになります。

「仮想ディスクドライブ」というメモリ上の開き領域をドライブ化する技術やソフトウェアが存在しますが、それをどうゲームに組み込めるのか、知りません。また、当然ですが、残りのメモリ容量が圧迫されます。なお、「仮想フォルダ」とは、単に「マイコンピュータ」や「ごみ箱」のような、システム内でデータの実在する場所とは別の階層位置に表示されるアイコンやそのリンクの機能のことであり、「仮想ディスクドライブ」とは意味が異なります。

(※: 未確認)戦闘中のモンスター隊列[編集]

さて、戦闘中のモンスター隊列の構成を定義するための配列(または同等の内容の数列)も、必要です。

まず、画面に敵を、表示したい順に表示するためも必要です。

また、上述の「素早さ順」での行動処理のためにも、前提として、敵パーティの構成を表すための配列が必要になります。


説明の単純化のため、敵は横一列に、左から右に、並んでいるとしましょう(ドラクエ2~3みたいな方式だとしましょう)。


たとえば敵が左から順に

ホブゴブリンが1匹、毒々スライムが1匹、ゾンビ兵士が1匹

出現したとしましょう。(説明の単純化のため、モンスターは各種類ごとに1体だけとする。)


そして、ホブゴブリンのモンスター用データベース中でのIDが13番だとして、 毒々スライムのIDが8番、 ゾンビ兵士のIDが25番、 だとしましょう。

すると、この敵パーティを表すための配列として、

{13,8,25,-99}

のような配列が必要です。(もしくは、配列に格納できるような数値の列「13,8,25,-99」)

末尾の-99は、これはその配列の読み終わりとして使用するための数字です。この数字は、別に「-10」でも「-99」でもいいですが、モンスターのidとけっして重ならないようにする必要があります。

通常、なんらかのデータベースのidの値は0以上の正の整数ですので、マイナスの整数を「終了」の意味で使用しておけば、安全でしょう。


このように、敵側にも、配列が必要になります。

しかも、この敵側の配列は、そのゲーム中で出現する敵パーティの全パターンを用意する必要があります。


たとい1体だけしか出現しない敵でも(たとえばボス敵や強敵)、その1体分の敵パーティの配列が必要です。

たとえば、その1体だけで出現する、あるボス敵「暗黒大王」のモンスター用データベース中でのIDが205番だとしたら、

{205,-99}

のような、敵1体だけの配列を用意する必要があります。


余談: マイナスを含む並べ替え[編集]

なお、もしマイナスの数を含む数をふくめて並べ替えをしたいのでしたら、 上記の「-99」など終了処理を負数で区別する方法は、そのままでは使えないです。

たとえば、方向でもし東向きをプラス、西向きをマイナスとした場合、終了コードのつもりの「-99」は、誤解で「西に99マス」などと誤解される恐れがあります。

たとえば、

-3 , 5 ,2, -6, -2 , 0 , 1

の並べ替えをしたい場合を考えましょう。


解決策としては、いろいろあります。

解決策1[編集]

一番ラクな方法は、要素数を新たな変数として追加し、それと組み合わせて配列を使うことでしょう。


たとえば、

-3 , 5 ,2, -6, -2 , 0 , 1

は7個の数が並んでいるので、

youso = 7;

みたいに新たに変数を用意します。

そして、たとえば最初の「-3」から順番意読み取りの際に、数えおわった個数を+1していき、+7になったら読み取りを終了することです。

この方法は、拡張性が高いのでオススメですが、ただし、要素を書く前に個数を宣言する必要があります。

実際の配列データは

-3 , 5 ,2, -6, -2 , 0 , 1, 0 ,0 ,0 , 0 ,0 ,0 , (以下略)

のようになっているので、けっして数え終わって欲しいところでキリよく要素が終わるわけではないので、なので、要素数をあらかじめ宣言する必要があるのです。

さもないと、8番目の「0」が、読み取りたい数値としてゼロなのか、それとも、単にデータなしのためゼロなのか、不明になります。


終了コードなどとして負数を使用するのは、マイナスを含む並べ替えでは、あきらめましょう。それが安全でしょう。


解決策2[編集]

最初の解決策1(要素数を新たな変数として追加)がもっとも安全かつ簡単ですが、比較のため、別の解決策も紹介しておきます。


次の解決策2は、数学的にはエレガントですが、しかしプログラミング的には、煩雑になり、また、バグ時などのエラーの波及をしやすい欠点があります。


さて、並べ替えのもうひとつの解決策として、

一例は、符号と絶対値を分離して、その組み合わせとして処理することです。


たとえば、

-3 , 5 ,2, -6, -2 , 0 , 1

の並べ替えをしたい場合、


プレイヤーからは表面的には「-6」は1つの数に見えまずが、これをあえて、「フラグhugouが状態2(マイナスに相当)である」「絶対値 zetai が6である」というように、2つの変数hugou と zetaiに分けます


符号がプラスなら、hugou = 1 にでもしておきましょう。また、ゼロは便宜上、hugou = 1 にしておきましょう。


すると、上記の数の並びは、(hugou, zetai)のベクトルで考えると、

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1)

という組み合わせに変換するので、マイナスが使われない形になります。


なので、終了処理に、「-99」などの負数を割り当てても、並べ替え対象の数値と重なる心配がなくなります。: (2, 6,) , (2, 3) , (2, 2) , (1,0) , (1,1) ,(1,2), (1,5)

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1),(-99,0)

などのように、終了コード「(-99,0)」を末尾に追加しても、なんの混同の心配もなくなります。


さて、いったん上記のように、数を、符号と絶対値の組み合わせに分解して、同じ符号どうしで並びかえたなら、

あとは、符号が同じものどうしで、並べ替えをするだけですみます。


たとえば、

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1)


の並べ替えをしたい場合、ベクトルの第一引数に注目し、※ (第一引数、第二引数)

まず、

(2,3) , (2,6), (2,2) のグループ
(1,5) ,(1,2), (1,0) , (1,1)のグループ

に分けます。


そして、絶対値(例では第二引数)だけに注目すれば、

マイナス数の絶対値は 3,6,2 → 2,3,6 と並べ替え → 逆順にして6,3,2 → マイナス符号をつけて -6,-3,-2
プラス数の絶対値は 5,2,0,1, → 0,1,2,5 と並べ替え

となるので、簡単に並べ替えできます。


あとは、これを合成し、

-6, -3 , -2 , 0 , 1 ,2, 5

と、並べ替えできます。


なお、説明の都合上でベクトルを蒸気の解説で使ったが、しかしC言語にベクトルの機能は無いので、もし上記の機能を実装するなら配列や構造体配列を使って、ベクトルと似たような処理を実装することになる。

たとえば配列で実装するなら、符号用の配列 hugou[id] と、絶対値用の配列 zetai[id] のようなものを用意したりすることになるだろう。

もし1番目の最初の数が「-3」なら、符号フラグは2、絶対値は3だからベクトル表現では

(2,3)

であるが、これは配列で表現するなら、たとえば

hugou[0]=2; zetai[0]=3;

のように記述できる(C言語では配列の番号は0から数える)。


欠点

この方法は、一見すると数学的にエレガントに見えるかもしれませんが、しかし、もしバグやタイプミスなどによって数え間違えると、数え落としや重複があると、以降の符号が1個ずつズレてしまうなどの大きな影響があります。

たとえば、説明のため上記では

-3 , 5 ,2, -6, -2 , 0 , 1

の分解を

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1)

とマトメて書きましたが、 実際には

hugou 2,1,1,2,2,1,1
zatai 3,5,2,6,2,0,1

のように別々の変数に分かれて保管されるのです。

もし、たとえば hugou の最初に、バグなどで「1」が加わると、

hugou 1,2,1,1,2,2,1,1
zatai 3,5,2,6,2,0,1,0

のようになってしまい、すべての符号が1個分、ズレてしまい、

3, -5, 2, 6 ,-2 , 0, 1 ,0

となってしまいます。

正しい元々の数字の並びの

-3 , 5 ,2, -6, -2 , 0 , 1

と比べると、符号がいくつも間違っており(1個目と2個目と4個目が違う)、解決策2ではエラーの波及が大きいことが分かります。

脚注[編集]

  1. ^ いわゆるドラクエ シリーズやや昔のFF(ファイナルファンタジー)1~3あたり
  2. ^ 『薬草』、『毒消し』

参考文献など[編集]