前章で、他のファイルに実体を持つ関数の呼び出しが出来るようになりました。
しかし、ファイル分割をしようとする人は最初に「どうしたらメインファイルで宣言した変数を他のファイルで参照できるか?」を疑問に思うことでしょう。
そして、なんとかメインファイルで宣言した変数を他のファイルから参照しようとすることでしょう。
これは、出来ますが、やめて下さい。(えっ龍神録プログラミングの館で言ってることと違う!と思った人はこちら)
グローバル変数をありとあらゆるファイルで参照可能にすると、
ソースコードが巨大になった時、いつどこで変更されているのかさっぱりわからなくなってしまうからです。
グローバル変数がもたらす悪影響は沢山ありますが、ここでは省略しましょう。簡単な例を出せば
「敵.cpp」というファイルがあったとして、敵に関する変数は全て「敵.cpp」にしか存在せず、このファイル内でしか変更出来ない仕組みだったとしたら、
何か敵に関する変数にバグが生じた時、原因探すの楽ですよね。
このように、「見せる必要のない他のモジュールからは変数を見せないようにする」ということをC++では「カプセル化」とか「隠蔽化」と言います。
C言語でも同じようにしっかりと隠蔽しましょう。
しかし変数参照出来ないのにどうやって処理をやり取りするの?
大丈夫です。関数さえ呼べればそれでいいのです。
必要な情報は関数の引数で与えてやり、取得したい時は関数の返り値で得ればいいのです。
(ポインタを引数に持たせて入れて返す仕組みでもok)
本サイトでは、「ファイル内のみで使用できるグローバル変数」を使ってこれを実現します。
変数の前に static と付けてグローバル変数を定義すると、そのファイル内でのみ使用出来るグローバル変数になります。
例えば先ほどの Player.cpp で static と付けたグローバル変数を定義すると、main.cpp からは参照したくても出来ません。
参照したくても出来ないようにすれば、意図しないタイミングで変数が書き換わることも無く、安全に使用できます。
では、この変数を使って、先ほどの Player.cpp にプレイヤーの機能を一部実装してみましょう。
プレイヤーや、敵、弾、などといったオブジェクトは基本的に大きく
・初期化
・計算
・描画
・終了処理
という4つの処理を持っています。(キーボードなどの描画が必要ないオブジェクトは除く)
main.cppからは以下のように呼び出します。
最初に初期化処理を呼び出し、メインループ部で計算・描画、終了時に終了処理を呼び出します。
当分は大きく分けてこのような処理だと思っていて差支えないでしょう。
では、これらをソースコードに落としてみます。
/* Player.h */
#ifndef DEF_PLAYER_H //二重include防止 #define DEF_PLAYER_H // 初期化をする void Player_Initialize(); // 動きを計算する void Player_Update(); // 描画する void Player_Draw(); // 終了処理をする void Player_Finalize(); #endif
まず、このように関数のプロトタイプ宣言を書き、main.cppから呼べるようにします。
先頭にあるプリプロセッサは、二重include防止文です。プログラムが大きくなると意図せず同じヘッダファイルincludeしてしまうことがよくあります。
そんな時、二度目に通った時はここを通さないように出来ます。
理屈は簡単です。一度目にこのヘッダを通った時は、DEF_PLAYER_Hが定義されていないので、そのまま中に入ります。
中に入ると #define によって DEF_PLAYER_H が定義され、プロトタイプ宣言を通ります。
もう一度ここを通ることがあれば、もう DEF_PLAYER_H は定義済みですから、#ifndef DEF_PLAYER_H (=もし DEF_PLAYER_Hが定義されていなかったら)
の
条件を満たさないので、最後の #endif まで処理が飛びます。こうして二重に宣言されることを防いでいるのです。
これからヘッダに何か書く時はこのように書きましょう。
定義している定義名は何でもいいですが、大文字でファイル名にするのが一般的です。本サイトでは大文字で「DEF_ファイル名」にしています。
/* Player.cpp */
#include "DxLib.h" #include "Player.h" // このファイル内でしか使えないグローバル変数 static int m_Image; //画像ハンドル static int m_y; //y座標 // 初期化をする void Player_Initialize(){ m_Image = LoadGraph("画像/キャラクタ01.png"); m_y = 0; } // 動きを計算する void Player_Update(){ m_y++; } // 描画する void Player_Draw(){ DrawGraph( 0, m_y, m_Image, TRUE ); } // 終了処理をする void Player_Finalize(){ DeleteGraph( m_Image ); }
変数を先頭で定義しています。前述の通り、staticを付けることで、このファイル内でしかアクセスできなくしています。
C言語で書いていますが、C++とほとんど同じ書き方をさせたいので、先頭に m_ を付けています。
これはクラスを使った時のC++の慣習なのですが、今は、「m_が付いていたらこのファイル内でのみ使えるグローバル変数という意味」だと思ってください。
上で説明した通り、Playerモジュールに4つの関数を定義しています。
これから色んなモジュールが出てきますが、ほとんどのモジュールは同じ構造をしています。
関数名が被るのを回避することと、Playerモジュールの関数であることを分かり易くすることから、
関数名は「モジュール名_関数名();」という命名規則にしています。
例えばPlayerモジュールの初期化関数なら「Player_Initialize();」です。パッと見てどこの何の関数なのか分かり易いですよね。
/* main.cpp */
#include "DxLib.h" #include "Player.h" //Playerモジュールの関数を使えるようにする int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){ ChangeWindowMode(TRUE),DxLib_Init(),SetDrawScreen( DX_SCREEN_BACK ); Player_Initialize(); // 初期化 while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 ){ Player_Update(); //計算 Player_Draw(); //描画 } Player_Finalize(); // 終了処理 DxLib_End(); return 0; }
呼び出している仕組みは図で紹介した通りと全く同じです。
実行結果
こうして、Playerモジュールのグローバル変数を main.cpp から参照することなしにキャラを動かすことが出来ました。
本章で紹介したプロジェクトファイルはこちらです。
C言語版 プロジェクトファイルはこちら
C++版 プロジェクトファイルはこちら
C言語版もC++を意識した書き方をしてみました。
C++とほとんど設計としては変わりありません。
ところで、遅くなりましたが、本サイトでは、「オブジェクト」単位でモジュールを分割します。
C++に「オブジェクト指向」という言葉があることをご存じでしょうか。
STGで言えば「弾」「敵」「コントローラー」と言った物体(オブジェクト)ごとに分割して設計する手法です。
オブジェクトごとに分割すると何が得なのかは、とりあえず置いておきましょう。
C言語を勉強している人のほとんどはいずれC++を学ぶことになると思います。
サンプルでは、C言語でありながら、C++風に書くことで、なるべくC++への抵抗を無くすよう試みています。
C++のコードも一緒に配布しているので、C++を学びたい方は合わせてご覧下さい。
さて、他のファイルの関数を呼ぶことは出来ましたが、他の関数で計算した結果を取得したい時はどうしたらいいでしょうか?
例えば、キーボード入力を司る「KeyBoard」モジュールがあったとすると、キーの入力状態を他のファイルにある関数からどうやって参照すればよいでしょうか。
こちらで実装した時は、同じファイル内だったので、グローバル変数が他から利用出来ましたが、
別ファイルにするとグローバル変数は参照出来ません。
では、次の章で、プレイヤーモジュールで表示している画像をキーボードによって動かせるようにしてみましょう。
※龍神録プログラミングの館との設計の違い
龍神録プログラミングの館では大量にグローバル変数を使っています。
「誰にでも簡単に作れる」というコンセプトで作った館であり、難しい話をしたくなかった為、使用しました。
しかし本来(どこからでも参照できる)グローバル変数は使うべきではありません。
きっちり設計をしたい人の為に、本サイトでちゃんとした設計の基礎をお伝えします。
- Remical Soft -