【ご注意!(文頭で謝り文入れるなんて情けないっすーー;>俺)】
このページの管理者はC++超初心者であり、自身の勉強も兼ねて解析結果をアップしています。ですから質・内容共に充分につたないことをあらかじめご了承下さい。間違いもたくさんあると思われます。また、間違いを発見されましたら(そっと^^;)教えていただけるととても嬉しいです。
YaneSDK2ndを用いると、DirectXはおろか、Windowsプログラミングすら理解していなくても、本格的なゲームプログラミングが可能です(ブラボー!)。しかし、人間とは業の深い生き物。さらなる快楽を求めて「もっともっとカスタマイズしたいにょ〜。YaneSDK2ndフレームワークの挙動はどうなってるんだにょ。わかんないにょ〜っ!!!」ともだえ苦しむ時がいずれ必ず来るでしょう^^;。
てなわけで、今回は、プログラムが実行されてからウィンドウが閉じて終了するまで、どのように背後でYaneSDK2ndが動作しているかを追ってみましょう。
では、以下のサンプルプログラムの、起動から終了までを追って、YaneSDK2ndの内部構造に迫る事にしましょう(いつ終わるんだろう^^;)。
#include "stdafx.h"
#include "../yaneSDK/yaneSDK.h"
class CApp : public CAppFrame {
void MainThread(void) { // これが実行される
while(IsThreadValid()) { // これがValidの間、まわり続ける
InvalidateThread();
}
}
};
// これがmain windowのためのクラス。
class CAppMainWindow : public CAppBase { // アプリケーションクラスから派生
virtual void MainThread(void){ // これがワーカースレッド
CApp().Start(); // CApp app; app.Start();の意味ね
}
};
// 言わずと知れたWinMain
int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
CAppInitializer::Init(hInstance,hPrevInstance,lpCmdLine,nCmdShow); // 必ず書いてね
CAppMainWindow().Run(); // 上で定義したメインのウィンドゥを作成
return 0;
}
|
現存する最短のYaneSDK2ndソースファイルを言えるでしょう^^;。メインループの中で、InvalidateThread()を実行して「もうこのアプリは終わりにょ〜」と、YaneSDK2ndフレームワークに宣言します。するとIsThreadValid()がfalseを返すようになるので、無限ループが終わり、アプリケーションが終了します。これを、初めから解析していきましょう。
// 言わずと知れたWinMain
int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
CAppInitializer::Init(hInstance,hPrevInstance,lpCmdLine,nCmdShow); // 必ず書いてね
CAppMainWindow().Run(); // 上で定義したメインのウィンドゥを作成
return 0;
}
|
特に必要はありませんが、WinMainの引数は以下の意味を持ちます。
HINSTANCE hInstance
アプリケーションインスタンスハンドル。OSがアプリケーションに割り当てた識別番号です。各アプリケーション毎に唯一の番号が決められています。
HINSTANCE hPrevInstance
Win9Xでは常にNULL。同一メモリ上で複数アプリケーションが存在する場合にその総数が入りますが、現在は意味があり得ません。
LPSTR lpCmdLine
アプリケーションをコマンドラインから実行した場合、実行ファイル名に続けて入力した、オプションの文字列へのポインタです。
int nCmdShow
プログラムが実行される際のウィンドウの表示方法が入ります。
CAppInitializer::Init(hInstance,hPrevInstance,lpCmdLine,nCmdShow); // 必ず書いてね |
クラスCAppInitializerのメンバ関数Initを呼び出します。クラス宣言を直接呼び出しているように見えますが、Initは静的データメンバなので既に存在していて、直接呼び出す事が出来ます。
CAppInitializer::Initを見てみましょう。
void CAppInitializer::Init(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)
{
// パラメータをそのまま保存:p
m_hInstance = hInstance;
m_hPrevInstance = hPrevInstance;
m_lpCmdLine = lpCmdLine;
m_nCmdShow = nCmdShow;
// カレントディレクトリの設定
CFile::SetCurrentDir();
}
|
CAppInitializerのメンバ関数にWinMainに渡った引数をそのまま保存してます(それぞれの変数も静的データメンバです)。特にhInstanceはOSとなにかしらやりとりするために、これから多用します。
CFile::SetCurrentDir(); |
クラスCFileの静的メンバ関数SetCurrentDir()を使って、実行ファイルが存在しているフォルダを、カレントディレクトリとしてパスを保存します(SetCurrentDir()の解説は省略^^; CFileを解析するときにまとめてやります)。
CAppMainWindow().Run(); |
これは以下のプログラムと同じです。
CAppMainWindow App;
App.Run();
なんでこんな書き方が出来るのか分かりません(ごめんなさい^^; 誰か教えてください)。
CAppMainWindowは上にありましたね、YaneSDK2nd利用者が自分でCAppBaseから派生させるクラスです。サンプルをもう一度見てみましょう。見ての通り、CAppMainWindowは、MainThread(
)がオーバーロードされている以外、CAppBaseと同じです。
class CAppMainWindow : public CAppBase { // アプリケーションクラスから派生
virtual void MainThread(void){ // これがワーカースレッド
CApp().Start(); // CApp app; app.Start();の意味ね
}
};
|
Run()はCAppBaseのメンバであり、こんな感じになってます。
LRESULT CAppBase::Run(void){
// これが第一インスタンスならば、これを親ウィンドゥとみなす
if (m_lpMainApp==NULL) m_lpMainApp = this;
if (IsMainApp()) {
return JumpToThread();
}
// メインウィンドゥ以外ならば、それ専用にスレッドを作る↓
m_nThreadStatus = -1;
if (CreateThread()) return 1;
// ウィンドゥの完成まで待つ
while (true){
if (m_bMessage || m_nThreadStatus>=0) break;
::Sleep(100);
}
return 0;
}
|
CAppBaseはCWinHookとCThreadから派生していて、本当はそれらのコンストラクタを全部解説すべきですがやっぱり省略(よくわかってないって話しもあり^^;)
Run()は、要は初めて呼び出されたときはそのままThreadProc()を呼び出し、そうでなければ新しくスレッドThreadProc()を生成して、そちらの処理も開始します。
詳しく見ていきましょう。
if (m_lpMainApp==NULL) m_lpMainApp = this; |
m_lpMainAppは、CAppBaseのクラス宣言で以下のように宣言されています。
static CAppBase* m_lpMainApp; //
メインウィンドゥ(これの終了をプログラム終了とみなす)
また、YaneCAppBase.cpp内でNULLに初期化されています。初めてRunが呼ばれた場合(m_lpMainApp==NULLである場合)、このCAppBase(正確にはCAppBaseから派生したCAppMainWindowですね))がメインウィンドウであるとし、m_lpMainAppにこのクラスのthisポインタが入ります。
if (IsMainApp()) {
return JumpToThread();
}
|
IsMainApp()は自分自身がメインスレッド(初めから存在するスレッドていうのかな? 用語がよくわからん^^;)かどうかを返します。
bool IsMainApp(void) const { return this==m_lpMainApp; }
thisポインタとm_lpMainAppが同じ場合(つまり、このCAppBaseがメインウィンドウであれば)IsMainAppはtrueを返します。今はtrueを返すので、CAppBase::Run()はここでおしまい。JumpToThread()を返します。
JumpToThread()はCThreadのメンバ関数で、こうなってます。
LRESULT CThread::JumpToThread(void) {
m_bThreadExecute = true; // スレッドではないので自前でフラグ設定
m_bThreadValid = true;
ThreadProc(); // これを実行するのだが
m_bThreadExecute = false;
return 0;
}
|
JumpToThread()の役割は、メインスレッドを、他のこれから生成されるスレッドと同じように振る舞わせる事です。他のスレッドはCTread::CreateThread()を呼ばれた際に、いくつかのフラグを立て、スレッド関数ThreadProc()を動作させ、要がすんだらフラグを下ろして終了します。JumpToThread()はメインスレッドに対し、これらをシミュレートします。
まずフラグ立てから。CThread内で以下の2つのフラグが宣言されています。
volatile bool m_bThreadExecute; // Threadは実行中なのか? volatile bool m_bThreadValid; // Threadを停止させたいときはfalseにする |
volatileが何なのか良く知りませんが(こんなんばっかやな^^;)、外部のスレッドから参照できる識別子だと考えれば良いと思われます。
フラグを立てたらスレッド関数の実行。CThreadのThreadProc();は純粋仮想関数なので、
virtual void ThreadProc(void) = 0; // これをオーバーライドしてね! |
CAppBaseでオーバーライドされているThreadProc()を実行します。さあ、見ていきましょう(ちょと量多めですが、負けないでくださいにょ。みはえるは既に逃げ帰ってきましたにょ^^;)。
// これが作成されたメインスレッド
void CAppBase::ThreadProc(void){ // override from CThread
// ウィンドゥの作成とWorkThreadの作成とMessageLoop
if (OnInit()) return ;
CWindowOption opt;
if (OnPreCreate(opt)) return ; // ウィンドゥが作られる前に呼び出される
if (m_oWindow.Create(opt)) return ; // ウィンドゥの作成
if (OnCreate()) return ; // ウィンドゥが作られてから呼び出される
CAppManager::Add(this); // このCAppBaseの登録
CAppInitializer::Hook(this); // メッセージフック開始
m_bMessage = true; // やっとウィンドゥは完成した
MainThread(); // ユーザー側で用意された、メイン関数
m_bMessage = false; // ウィンドゥは破壊されるので...
OnDestroy(); // 終了直前
// Threadでappを判別しているのでHookしたThreadがDelしなくてはならない
::SendMessage(GetHWnd(),WM_DESTROY,0,0); // メッセージスレッドを停止させる
// WM_Destoryを処理しなくてはならないのでここでフック解除
CAppInitializer::Unhook(this); // メッセージフックの終了
CAppManager::Del(this); // このCAppBaseの削除
InnerStopThread(); // スレッドを停止
}
|
OnInit()、OnPreCreate()、OnCreate()、MainThread()、OnDestroy()は、CAppBaseで仮想関数として宣言されています。これらはどれも、デフォルトでは0を返すだけで、CAppMainWindowでこれらの関数をオーバーライドすれば、適切なタイミングで好きな処理を行う事が出来ます。
if (OnInit()) return ; |
OnInit()は初期設定が始まる前に呼ばれる仮想関数で、デフォルトでは0を返すだけです。必要に応じてオーバーライドできます。OnInit()がゼロを返さなければアプリケーションを終了します。
CWindowOption opt; |
CWindowOptionは、ウインドウの設定を保存するクラスで、こうなってます。
class CWindowOption {
public:
string caption; // キャプション
string classname; // クラス名(captionと同じでも良い)
int size_x; // 横方向のサイズ
int size_y; // 縦方向のサイズ
LONG style; // ウィンドゥスタイルの追加指定
CWindowOption(void) { style = WS_VISIBLE | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU; }
};
|
captionはウィンドウのタイトルバーに表示される文字列です。classnameはこのウィンドウクラスに割り当てる名前です。(ウィンドウクラスの「クラス」は、C++用語の「クラス」とは異なるので、windowsプログラミング初心者は注意(俺やん!(笑)))。size_x、size_yは、ウィンドウの縦横方向のサイズです。styleはウィンドウスタイルの追加設定(「閉じる」アイコンを持つのか? とか 水平スクロールバーを持つのか? とか)です。
コンストラクタで、ウィンドウスタイルの初期設定が以下のように設定されます。
WS_VISIBLE 初期状態で可視である
WS_CAPTION タイトルバーを持つ(暗黙にWS_BORDER(境界線を持つ)を持つ)
WS_MINIMIZEBOX 最小化ボタンを持つ
WS_SYSMENU タイトルバーにコントロールメニューボックス(左上のタイトルアイコンをクリックするとでる奴)を持つ
ただし、このクラスは直後のOnPreCreate(opt)で初期化します。
if (OnPreCreate(opt)) return ; // ウィンドゥが作られる前に呼び出される |
OnPreCreate(opt)は今初期化したCWindowOptionオブジェクトoptを引数に取り、それぞれの設定を初期化します。
LRESULT CAppBase::OnPreCreate(CWindowOption& opt){
opt.caption = "あぷりちゃん";
opt.classname = "YANEAPPLICATION";
opt.size_x = 640;
opt.size_y = 480;
opt.style = WS_MINIMIZEBOX | WS_CAPTION | WS_SYSMENU;
return 0;
}
|
ウィンドウの設定を初期化します。optを参照で受け取り、変更することが出来ます。
captionに入れた内容がウィンドウタイトルになります。「いつもタイトルが「あぷりちゃん」で恥ずかしいっ!」と言う人は、この関数をオーバーライドして「あぷりちゃんにょ!」に変更して下さい(笑)。
classnameはcaptionと同じで構わないそうですが、日本語は使わない方が無難ではないかと思います(よくしりません)。
size_x、size_yは変更しない方が無難でしょう。
何故かstyleが再定義されていますが気にしないで行きましょう(単にみはえるが解析間違いしてるのかも^^;)。WS_VISIBLEが無くなってるけどウィンドウは初期状態で可視です。なんででしょう?(やはり解析間違いしてるのかもーー;)
if (m_oWindow.Create(opt)) return ; // ウィンドゥの作成 |
m_oWindowは、CAppBaseが所有するCWindowオブジェクトです。
CWindow::Create(
)はoptを受け取ってウィンドウを生成、表示します。この関数はやたら長いので詳細は第2回にて。リンクはこちら>すみません、工事中^^;>(01/06/03)出来ました。こちら)。
無事にウィンドウが表示されたら、処理が戻ってきます。
if (OnCreate()) return ; // ウィンドゥが作られてから呼び出される |
OnCreate()はOnInit()と同じで、0を返すだけの仮想関数です。ウィンドウが作られてから呼び出されます。
CAppManager::Add(this); // このCAppBaseの登録 |
CAppManegerにCMainWindowのポインタを保管してもらいます。以後CAppBaseのメンバにアクセスしたくなったら、CAppManager::GetMyApp()とやればCAppBase*を得る事が出来ます。
CAppInitializer::Hook(this); // メッセージフック開始 |
メッセージフックを開始します。以後、メッセージポンプがメッセージを得るたびに、(CAppBaseが継承している)CWinHookのメンバWndProc()が呼ばれます。
m_bMessage = true; // やっとウィンドゥは完成した |
m_bMessageは外部に対しウィンドウが完全に作成されたのか、完全に終了したのかを知らせる為に設定される……のだと思います^^;。スレッド分かりません。さっぱり。
MainThread(); // ユーザー側で用意された、メイン関数 |
長かったっ!(笑) ようやくここまで来ました。CAppBaseのMainThread()は特に何もしません。
virtual void MainThread(void) { while (IsThreadValid()) ::Sleep(20); }
これを派生先(CAppMainWindow)でオーバーロードします。サンプルではこうなってますね。
virtual void MainThread(void){ // これがワーカースレッド
CApp().Start(); // CApp app; app.Start();の意味ね
}
|
CApp()は利用者が作成する関数で、CAppFrameを継承しなければなりません。Start()はCAppFrameのメンバ関数です。
void Start(void) { m_lpMyApp = CAppManager::GetMyApp(); MainThread();}
|
CAppFlame::MainThread( )は純粋仮想関数として宣言されています。
virtual void MainThread(void) = 0;
これをユーザーがオーバーロードすることで、自由にプログラムすることが出来ます。サンプルのMainThread( )はこうなってますよね。
void MainThread(void) { // これが実行される
while(IsThreadValid()) { // これがValidの間、まわり続ける
InvalidateThread();
}
}
|
……もっと長くしとけば良かったかな^^;。
IsThreadValid()がtrueを返す限り、関数MainThread( )内が周り続けます。この中に自分のゲームのプログラムを押し込む訳ですね。
CAppFrame::IsThreadValid( )は内部でCAppBase::IsThreadValid()を呼び出します。
bool IsThreadValid(void)const { return GetMyApp()->IsThreadValid(); }
CAppBase::IsThreadValid()はCThread::IsThreadValid()からオーバーロードしています。CThread::IsThreadValid()は、アプリケーションがまだ有効かどうかが設定されているメンバ変数を返すだけですが、
virtual bool IsThreadValid(void)const { return m_bThreadValid; }
CAppBase::IsThreadValid()は内部でメッセージポンプを形成します。
bool CAppBase::IsThreadValid(void) {
// このチェックのときにスレッドの正当性もチェックする
MSG msg;
while (::PeekMessage(&msg,GetHWnd(),0,0,PM_REMOVE)) {
// メッセージが存在する限り処理しつづける
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
CAppIntervalTimer::TimerCallBackAll(); // フックされているタイマにコールバックをかける
if (m_bWaitIfMinimized) {
// WM_QUITか最小化が解除されるのを待つ
while (GetMyWindow()->IsMinimized() && GetMessage(&msg,GetHWnd(),0,0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
return (m_bThreadValid);
}
|
PeekMessage()はキューにメッセージがあればそれをディスパッチします。GetMessage()と違い、キューが無ければすぐにNULLを返します。
メッセージが無くなったらCAppIntervalTimer::TimerCallBackAll();を呼び出し、全てのタイマフックにコールバックをかけます。
最後のループは、もしウィンドウが最小化されている場合は、最小化が解除されるか、WM_QUITが送られてくる(GetMessageがNULLを返す)かまで、メッセージループを回します(今度はGetMessageなので、最小化されたままの場合、OSの方に処理を渡します)。
IsThreadValid( )がtrueを返すとループ開始。サンプルではすぐにInvalidateThread( )を呼び出します。みなさんはここにソースを書いてくださいね^^;。InvalidateThread( )はメンバ変数を変更するだけ。
virtual void InvalidateThread(void) { m_bThreadValid = false; }
次のwhileの頭でfalseが返ってくるので、MainThread( )は終わり。ThreadProc()に戻ってきます。
m_bMessage = false; // ウィンドゥは破壊されるので... OnDestroy(); // 終了直前 |
OnDestroy()も0を返すだけの仮想関数です。アプリ終了する直前に呼ばれます。
// Threadでappを判別しているのでHookしたThreadがDelしなくてはならない ::SendMessage(GetHWnd(),WM_DESTROY,0,0); // メッセージスレッドを停止させる |
WM_DESTORYは、CAppBaseのWndProc()で受け取られ、PostQuitMessage()が実行されます。
// WM_Destoryを処理しなくてはならないのでここでフック解除 CAppInitializer::Unhook(this); // メッセージフックの終了 CAppManager::Del(this); // このCAppBaseの削除 InnerStopThread(); // スレッドを停止 } |
今まで登録した物を破棄してThreadProc()も終わり。JumpToThread()に戻ってきます。
m_bThreadExecute = false; return 0; } |
メインスレッドの存在フラグを下ろしてリターン。これでCAppMainWindow::Run()が終わり、めでたく関数WinMainが終了します(お疲れさまでした!)
終わった……。こんな大変だとは思ってなかったさ^^;。
いかがでしたでしょうか。よくわからないところは適当に誤魔化してしまいました^^;。多少なりとも参考になりましたら幸いです。