ゲームのメインループと時間待ち

[戻る]
anne 2004/07/05(月) 03:02:23
二回目の投稿です。
(2003/11/12のみずきです。同じHNの方がいらっしゃいましたので変更しました。)
相談したいことがあります。

開発環境はWindowsXP、VC++.NET、Win32API SDK、C言語を使用しています。
ウィンドウを出したり、画像を表示したりと、
Windowsプログラミングの基本的なことが少しはわかってきたので、
ゲームアプリケーションを作りたいと思いました。
(右上に×ボタンなどがあるウィンドウのゲームです。)


そこで、いざゲームを作ろうとしたのですが、
ゲームのメインループとメッセージループの関係がよくわからなくて苦戦しています。
ゲームのメインループはなんとかなるのですが、
「時間待ち」をどのようにして入れようか悩みます。
例えば、3秒待ちたいときはどうするのかなど。。

タイマーを使ったやり方と、
SleepとGetTickCountを使って待ち時間を作る方法と、
スレッドを作ってメッセージループを分ける方法も試してみました。
(タイマーだと遅くて、スレッドは間引きされているような原因不明のバグが出まし
た。)

皆さんはシューティングゲームのような比較的画面書き替えの多いゲームを
作る時はどのような構造にしているのでしょうか。
ネットで調べてみたのですが、なかなかソースを公開なさっているところが少なくて…
よろしければ簡単なゲームの雛形のソースを公開されているホームページや、
解説しているホームページなど教えてくだされば嬉しいです。

たいちう 2004/07/05(月) 09:44:46
> ゲームのメインループとメッセージループの関係がよくわからなくて苦戦しています。
> ゲームのメインループはなんとかなるのですが、
> 「時間待ち」をどのようにして入れようか悩みます。
> 例えば、3秒待ちたいときはどうするのかなど。。
>
> 皆さんはシューティングゲームのような比較的画面書き替えの多いゲームを
> 作る時はどのような構造にしているのでしょうか。

質問をもう少し具体的にしてくれないと答えにくいと思います。

雛形として、自機を矢印キーで上下左右に動かすだけのプログラムを
考えてください。これはできますよね?
これに機能(弾を撃つ、敵を出す、当り判定、背景のスクロール等など無尽蔵に)
を追加して、シューティングゲームが出来上がると思われますが、
3秒待ちは、どこででてくるのですか?
雛形からの追加を最小限にして説明してみてください。
anneさんが問題にしていることがうまく伝わるのではないでしょうか。

PATIO 2004/07/05(月) 10:39:50
多分、背景と敵機の描画に使いたいのではないかと思いますが、
タイマーであるとか、マルチスレッドで処理するとか以前に
WindowsのGDIを使った描画では、そもそも描画スピードが遅くて
書き換え回数が多いシューティングやアクションゲームは無理だと思います。
一般的にはDirectXなどの高速描画ライブラリを使う事が多いと思いますけれど。

背景の描画そのものをタイマーイベントで行っているのか、
別スレッドで行っているのかはわかりませんが、
一回の書き換え時間をかなり短く抑えないとうまく動いているように
見せるのは難しいと思いますよ。

ゲームを本格的にやりたいのであれば、DirectXの使い方なんかを
勉強した方が良いと思います。そのための本も結構出ていますし。
ゲームそのものが目的と言うわけではないのであれば、そこまでする必要は
無いと思いますけれど。

くたくた 2004/07/05(月) 12:03:21
"待つ"の意味が、"ある時からの経過時間(60[fps]なら180回のフレーム)を計る"
という前提でお話します。

>タイマーを使ったやり方
使わない方がいいです。何故なら、
o タイマー資源を消費する(資源を使わずに済む方法があるならそれを採用すべき)
o 環境依存である
o 他のソフトの起動状況により、挙動が変わる
からです。

>Sleep
使いません。使うと、ゲームを実行しているソフトそのもの(=現スレッド)を停止
させる事になりますよ。

>GetTickCount
この系統(clock()やQueryPerformanceCounter()等)を単体で使うのがいいと思います。
最も単純で、一般に使いこまれているであろう為です。

>ゲームの雛形のソースを公開されているホームページ
DirectX SDKをインストールし、D3Dのサンプルソースを参照するといいと思います。
( もし自力で解決できなければ諦めた方がいいでしょう )
あとはここ → http://i-saint.abz.jp/pg/index.html (要OpenGLの知識)

anne 2004/07/05(月) 19:07:08
皆様アドバイスどうもありがとうございます。

今作っているゲームは、インタプリタ言語みたいなもので、
テキストファイルの文字を判断してゲームを動作させるというものです。
DirectXは使わずに作っています。
簡単なシューティングなどを作ることができるまで完成していますが、
現在のプログラムでは、どうも待ち時間を作るところがスマートにいかないのです。


↓WinMain関数のメッセージループ

int Wait = 0;

dwTime = GetTickCount();
for (;;) {                       // メッセージループ
    if (PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE)) {
        if (!GetMessage (&msg, NULL, 0, 0)) break;
        TranslateMessage (&msg);
        DispatchMessage (&msg);
    } else {
        // 待ち時間がある場合
        if (Wait > 0) {
            // 時間待ち
            if (GetTickCount() - dwTime > unsigned(Wait)) {
                dwTime = GetTickCount();
                Wait = 0;
            }
            Sleep (5);
        }else{
            // ゲーム処理関数
            if (GameMain (hwnd, hInstance, iCmdShow)) break;
        }
    }
}


上のように、GetTickCount関数とSleep関数を使ってCPU使用率を抑えています。
Waitの変数はグローバル変数です。GameMain関数で値を変えています。
このWaitの変数の値が0より大きい場合はその分だけ時間待ちをします。

一応ちゃんとバグも出ずに動いていますが、どうスマートにいかないのかというと、
ゲームのメイン関数を抜け出ないと時間待ちができないです。
上の方法でいいのでしょうか。
例えばGameWaitという関数を作ってこの中でメッセージループをさせるというのは可能
でしょうか。
また、そのような方法は一般的に使われている方法なのでしょうか。
他の方はどうやって作っているのか知りたいです。

くたくた 2004/07/05(月) 19:32:57
>ゲームのメイン関数を抜け出ないと時間待ちができない
メッセージポンピングをしながらの時間待ちをしたい場合、これは避けられないと思いま
す。

>GameWaitという関数を作ってこの中でメッセージループをさせるというのは可能
でしょうか
可能です。が、可能な限りWinMain()内に唯一ある状態にするのが良いと思います。
開発時間の浪費(デバッグ時間延長等)を避ける為です。

どうしても GameMain()を抜けずにメッセージループを動かしたいなら、
anneさんがお考えになった方法を取るしかないと思います。

>そのような方法は一般的に使われている方法なのでしょうか。
>他の方はどうやって作っているのか知りたいです。
これはちょっと分かりません。すいません。
このような待機の必要があるコードは、初めて見た為です。
差し支えなければ、その必要性を詳しく伺いたいです。

こじま 2004/07/05(月) 19:35:33
少なくとも、これだと、
>>Sleep
>使いません。使うと、ゲームを実行しているソフトそのもの(=現スレッド)を停止
>させる事になりますよ。
くたくたさんのおっしゃるとおり、
メインのメッセージループを停止してしまうので、あまりいい方法とは思えません。
基本的に、メッセージループを回すスレッドでSleepするのはやめるべきです。

> 例えばGameWaitという関数を作ってこの中でメッセージループを
> させるというのは可能でしょうか。
おそらく、これでいいような。。
void GameWait(DWORD Wait)
{
  DWORD dwTime = GetTickCount();
  while(GetTickCount() - dwTime > unsigned(Wait)) {
    if (PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE)) {
      GetMessage (&msg, NULL, 0, 0)) break;
      TranslateMessage (&msg);
      DispatchMessage (&msg);
    }
  }
}

> スレッドを作ってメッセージループを分ける方法も試してみました。
> (タイマーだと遅くて、スレッドは間引きされているような原因不明のバグが出まし
>  た。)
普通は、GameMainを別スレッドにすることを考えると思うのですが何が問題でしたか?

サスライの旅人 [E-Mail] 2004/07/06(火) 00:49:42
久々登場!テスト前ですが、休憩でチラッとこのサイトに入ってきました。←どうでもい
い!
私ならこうします。
WinMain関数の最後の方・・・

WinMain(..
{
  MSG msg;
  DWORD time;
.....
....
  while(1)
  {
    if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ))
    {
      if(!GetMessage( &msg, NULL, 0, 0 )) break;
      TranslateMessage( &msg );
      DispatchMessage( &msg );
    }
    else
    {
      if( GetTickCount() - time >= 100 ) // 1/10秒毎に・・・
      {
        time = GetTickCount();
        Loop(); //ゲームの主な処理をするための関数、名前はご自由に
        InvalidateRect( ~省略 );
      }
    }
}

ここではGetTickCount()関数を使っていますが、シューティングなどより精確さを問われ
る場合はtimeGetTime()関数の方がより高精度なので、こちらを使うべきかもしれませ
ん。しかし、この関数はWin95系とWinNT系では、使用の仕方が微妙に違うので注意が必要
です。ちなみに、WinXPはどちらかというと、WinNT系だったような気がする・・・記憶が
曖昧。後、DrectXを使わずにWinAPIでゲームを作るときは、GetDC()関数を使わずに、
WM_PAINTメッセージで再画することをお勧めします。DrectXを模様したポインタを使った
再画の方法など、説明しようと思いましたが、・・・・・・・・さぁ、勉強開始!さよう
なら。

サスライの旅人 2004/07/06(火) 00:52:55
ていうか、私が書いた内容。書くだけ無駄みたいです。上のチャット読んでいませんでし
た。・・・ガックシ

PATIO 2004/07/06(火) 10:05:15
うーん、基本的にWindowsのアプリはワンイベント、ワンスルーが基本です。
ですから、回数が極端に多いループ処理は好ましくありません。
なにせ、0.1秒ルールと言うのがあるくらいですから。
Windowsの場合、定期的にメッセージループを動作させることでイベントドリブンの
仕組みを成立させていますので一つの処理が終わったら速やかに関数を終了させて
メッセージループに戻る必要があります。

要するにメッセージループを戻らずに処理を続けること事態に問題があるのです。
ですからコンソールアプリのようなつくりと言うのはよろしくありません。

既に話が出ていますが、GameMain部を別スレッドにしてWait等も中で処理します。
WinMainはメッセージループの処理に専念させ、キーイベント等はスレッドと
同期を取る部分で情報のやり取りをするようにした方がWindowsのアプリ的には
すっきりすると思います。
インタープリター部とWindowsのインターフェイス処理部をきっぱり分けてしまった方が
よいと思いますね。

anne 2004/07/06(火) 16:37:41
まだマルチスレッドについて勉強不足ということもあり、
わからないことだらけなのですが(^^;
ゲームのメイン関数の処理が飛び飛びになって、時々実行されなくなります。
原因を突き止めることができないので上に書きましたプログラムに戻して動かしていま
す。

GameMain関数のインタプリタ処理では
スクリプトファイルから一行抜き出し、書かれているものが何なのか解析し、
時間待ちの命令だった場合だけ、returnしてメッセージループに戻るようにしていま
す。


>メインのメッセージループを停止してしまうので、あまりいい方法とは思えません。
>基本的に、メッセージループを回すスレッドでSleepするのはやめるべきです。
貴重な情報ありがとうございます。
Sleepを使っていた理由は、CPUの使用率を軽減するために使っていました。
CPUの負荷軽減は大切なことですよね。
Sleepの引数を4〜10くらいにすれば、終了ボタンやウィンドウを動かすことにはあまり
影響せず、効果的に抑えられると思ったからです。
メッセージループで少しでもSleepを使ってしまうとメッセージが
捕らえられないことがあるなどの弊害が生じるからなんでしょうか。
↓この場合でもダメなんですか?

while (GetMessage (&msg, NULL, 0, 0)) {  // メッセージループ
    TranslateMessage (&msg);
    DispatchMessage (&msg);
    Sleep (1);
}


いろいろな方からアドバイスしていただいて大変参考になりました。
私の方法(2004/07/05(月)19:07:08のプログラム)より、別スレッドでメッセージルー
プとゲームのメインループを分ける方法がさらに良い方法ということですね?

PATIO 2004/07/07(水) 09:59:51
もともと、メッセージループと言うのはメッセージ待ちに入れば、
システムウエイトがかかるのでSleepは必要ないと思います。
逆に他のところでSleepが入るとメッセージループの進行が滞るので
そっちの方が問題です。

ソースを見て予想してましたが、インタープリター部は一旦入ったら
Waitがかかるまで抜けてきませんよね。
そうなるとその間メッセージループは止まったままになります。
これではウインドウの再描画を含めた色々なウインドウメッセージは
メッセージキューに溜まりっぱなしになります。

すでに書いていますが、イベントドリブン型のシステムの基本は、
ワンイベント・ワンスルーです。
イベントにかかわり無く常に動くような部分はワーカースレッドに
追い出さないとこの辺の原則が崩れてしまいます。
もし、メインの部分で何とかしたいのであれば、
一つの処理が終わるごとにタイマーをかけて一旦関数から抜けるような
配慮が必要です。

インタープリター部の作りをすっきりしたいのであれば、
この部分は別スレッドにしてウインドウ周りはメインスレッドに任せ、
メインスレッドとインタープリター部の間でデータをやり取りする仕組みを
確立させた方がよいと私は思います。

少なくともソフトが動き始めてから終わるまで回りっぱなしでいいのは、
メインスレッド内ならメッセージループだけです。
ワーカースレッドであれば、メッセージループに影響は無いので
終わるまで回りっぱなしでもかまいません。
但し、メインスレッドの動作との兼ね合いで特定のタイミングで
Sleep(0)と入れる等の配慮をしないとメインスレッド側がスムーズに
動いてくれなかったりすることは考えられます。
これは、マルチスレッドを使っていく上で勉強する必要がある部分なので
スレッド間の同期やデータのやり取りの手法とあわせて勉強してください。

必要ならマルチスレッドを扱った本を購入してじっくり勉強した方が
今後のためにもよろしいと思います。

anne 2004/07/07(水) 23:25:53
すみません勘違いしていました(^^;

while (GetMessage (&msg, NULL, 0, 0)) {  // メッセージループ
    TranslateMessage (&msg);
    DispatchMessage (&msg);
    Sleep (1);
}

↑こうではなくて、↓こうでした。

while (TRUE){  // メッセージループ
    if (PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE)) {
        if (!GetMessage (&msg, NULL, 0, 0)) break;
        TranslateMessage (&msg);
        DispatchMessage (&msg);
    } else {
        Sleep (1);
    }
}


SleepをなくしてしまうとCPU使用率はほぼ100%になります。
実際にはこんなプログラムはしないと思いますが(^^;)このプログラムでは何かマズいこ
とがあるのでしょうか。
これが聞きたかったんです。こちらの不手際すみませんでした。



>ソースを見て予想してましたが、インタープリター部は一旦入ったら
>Waitがかかるまで抜けてきませんよね。
>そうなるとその間メッセージループは止まったままになります。
>これではウインドウの再描画を含めた色々なウインドウメッセージは
>メッセージキューに溜まりっぱなしになります。
それはたぶん問題ないと思います。
インタプリタのスクリプトファイルでは適当な場所に必ず待ち時間を入れています。
例えば、シューティングゲームであれば

    メインループのラベル
        キー入力の判断
        衝突判定
        キャラクターの描画
        「メインループのラベル」に戻る


このようなぐるぐる回る構成でシューティングゲームを動作させていますが、
これでは待ち時間(つまりメッセージ取得のことですね)がないので
フリーズしてしまいます。なので、

    メインループのラベル
        キー入力の判断
        衝突判定
        キャラクターの描画
        ★Wait
        「メインループのラベル」に戻る

このように間にWaitを入れているので大丈夫です。
Wait以外の処理も少ない時間でできるので「0.1秒ルール」を満たしています
…と思っているのですが、私が作ってきた方法は
ゲームに向かない方法なのかなぁ^^;

PATIO 2004/07/08(木) 10:05:11
> while (TRUE){  // メッセージループ
>     if (PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE)) {
>         if (!GetMessage (&msg, NULL, 0, 0)) break;
>         TranslateMessage (&msg);
>         DispatchMessage (&msg);
>     } else {
>         Sleep (1);
>     }
> }

確かにPeekMessageを使うならSleepがいるでしょうね。
駄目とは言いませんけれど、どうなんでしょうねぇ。
メインのメッセージループ自体を書き換えてしまうようなことはしたことが無いので
実際にこうやってしまったときにどうなるのかはよくわかりません。
ただ、本来のメッセージループよりもCPUに負担は確実に大きいと思います。
本来ならメッセージが無ければ、GetMessageでシステムウエイトがかかるはずなので
これによってシステム全体の負担を軽減しているはずです。
ところが、これだとあろうが無かろうが、PeekMessageを連発しているので
それだけCPUを使用することになります。

> このように間にWaitを入れているので大丈夫です。
> Wait以外の処理も少ない時間でできるので「0.1秒ルール」を満たしています
> …と思っているのですが、私が作ってきた方法は
> ゲームに向かない方法なのかなぁ^^;

いや、それだとスクリプトファ
イルの方に必ず待ち時間を入れる制約が出てしまいますよね。
本来ならスクリプトを書く側にそれを意識させるのは仕組み的にまずいと思いますよ。
スクリプトを書く側は本来のゲームの動きだけに注目してスクリプトを書けるような
環境を用意するのが本筋だと思います。

私が書いている方法だとスレッド間の同期処理が必要になりますが、
スクリプトを書く側はWindowsの都合を考慮しないで済みます。
考慮するのはゲームを動かす上で必要なタイミング調整だけです。
まあ、この辺になると多分に趣味の問題が入ってきますが、
スクリプトの目的がゲームを記述することなら記述する人はゲームに直接関係無い
部分には気を使わないで済むようにしてあげるのがベストだと思っています。

あとは、anneさん自身がそのアプリをどういう位置づけで考えているかですね。
自分以外の人にも使ってもらいたいという考えがあるなら利用者の利便性を
考えてあげるべきだと思いますし、逆に自分が使うためだけなら割り切ってしまって
今のままで良いと考えるのもありでしょう。
力の入れ所は、そのアプリの位置付けによっても変わってくると思いますよ。

anne 2004/07/08(木) 19:22:26
[[解決]]
>ただ、本来のメッセージループよりもCPUに負担は確実に大きいと思います。
>本来ならメッセージが無ければ、GetMessageでシステムウエイトがかかるはずなので
>これによってシステム全体の負担を軽減しているはずです。
>ところが、これだとあろうが無かろうが、PeekMessageを連発しているので
>それだけCPUを使用することになります。
わかりました。
自分でももっと調べてみようと思います。


>本来ならスクリプトを書く側にそれを意識させるのは仕組み的にまずいと思いますよ。
>スクリプトを書く側は本来のゲームの動きだけに注目してスクリプトを書けるような
>環境を用意するのが本筋だと思います。
少しでもインタプリタの実行速度を速くしようとして、
スクリプトファイルのほうでwaitを入れる仕組みにしていました。
確かに仰るとおり、ユーザーにはそのような制約はプログラムの妨げになりますね。
根本的に間違った考えでした(>_<)すごく賛成です。


私の勉強不足ということもあって、まだ納得いかない感がありますが、
PATIOさんの方法の、スレッドを使った方法を勉強してみようと思います。
少し質問の範囲が広かったり、皆様に迷惑をおかけしてすみませんでした。
皆様の意見、すごく勉強になりました。
また、答えてくださった方ありがとうございます。

古いスレッドには追加発言できません。