RenderParam 解説
 
 

■ RenderParam 概略

 3D 描画機能を利用した簡単なシューティングゲームです。
 ジョイパッドで操作することが出来ます。
 キーボードで操作する場合には、方向キーと Z キーを使用します。

 

■ PrepareRenderParam 関数

 RenderParam では、画像の描画に3Dの機能を用いることが出来る PrepareRenderParam 関数、及び RenderPolygon 関数を利用しています。
 PrepareRenderParam 関数に渡す描画パラメータの指定方法は2Dの描画関数である PrepareDraw 関数及び、パラメータを計算するための補助関数 eglGetRevolvedAxes 関数と良く似ていますが、これらの関数よりもより高度な描画が簡単に出来ます。
 EEnemyItem::Draw 関数を見てください。

void EEnemyItem::Draw( HEGL_RENDER_POLYGON hRender )
{
    E3D_RENDER_PARAM rp ;
    E3D_REV_MATRIX matrix ;
    ::eslFillMemory( &rp, 0, sizeof(rp) ) ;
    EGL_POINT ptHotSpot = m_app->m_image[m_iImage].GetHotSpot( ) ;
    //
    rp.dwFlags = EGL_WITH_Z_ORDER
        | E3DRP_RENDER_REV_IMAGE | E3DSAF_TEXTURE_SMOOTH ;
    rp.rgbaColor = m_rgbaColor ;
    rp.pSrcImage = m_app->m_image[m_iImage] ;
    rp.rev.vRenderPos = m_vBase ;
    rp.rev.vRevCenter.x = (REAL32) ptHotSpot.x ;
    rp.rev.vRevCenter.y = (REAL32) ptHotSpot.y ;
    rp.rev.pRevMatrix = &matrix ;
    //
    double r ;
    matrix.InitializeMatrix
        ( E3DVector( (REAL32) m_rEnlarge,
            (REAL32) m_rEnlarge, (REAL32) m_rEnlarge ) ) ;
    r = m_rRevY * rRadPIby180 ;
    matrix.RevolveOnY( (REAL32) sin(r), (REAL32) cos(r) ) ;
    r = m_rRevX * rRadPIby180 ;
    matrix.RevolveOnX( (REAL32) sin(r), (REAL32) cos(r) ) ;
    r = m_rRevZ * rRadPIby180 ;
    matrix.RevolveOnZ( (REAL32) sin(r), (REAL32) cos(r) ) ;
    //
    if ( !hRender->PrepareRenderParam( &rp ) )
    {
        hRender->RenderPolygon( ) ;
    }
}

 この関数では、敵の画像を描画していますが、この例は PrepareRenderParam 関数の典型的な使用例です。
 黄色の部分では、描画する画像や、合成する色、描画のための表示座標を設定しています。
 橙色の部分では、画像をどのように回転変形するかを指定する行列を生成しています。
 緑色の部分で実際に描画を実行しています。
 各構造体のメンバの意味については、「EGL-ref.doc」を参照してください。

 

■ プログラムの構造

 RenderParam では、GLS3 を利用してプログラミングする際の典型的なプログラミングスタイルを提示しています。

 セカンダリスレッドは ERenderParamApp クラスを EGLSThread クラスから派生させて、ThreadProc 関数をオーバーライドして実現しています。
 プライマリスレッドでは、初期化、メッセージループという流れがあるだけです。

     HANDLE hObjects[1] ;
     hObjects[0] = itfMain.Handle( ) ;
     while ( ::MsgWaitForMultipleObjects
          ( 1, hObjects, FALSE, 5, QS_ALLINPUT ) != WAIT_OBJECT_0 )
     {
          MSG  msg ;
          while ( ::PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
          {
               ::TranslateMessage( &msg ) ;
               ::DispatchMessage( &msg ) ;
          }
     }

 このサンプルで実装しているメッセージループの形は少し珍しいかもしれませんが、このメッセージループでは、セカンダリスレッドが終了するとメッセージループも終了するように出来ています。
 次に、ThreadProc 関数を見てください。

DWORD ERenderParamApp::ThreadProc( void )
{
    //
    // ゲーム初期化
    //
    if ( InitGame( ) )
    {
        return 1 ;
    }
    //
    // ウィンドウが作成されるまで待機
    //
    ::WaitForSingleObject( m_hReadyEvent, INFINITE ) ;
    m_dwFrameTime = GetThreadTime( ) ;
    //
    // ゲーム処理ループ
    //
    while ( !TitleMode() )
    {
        if ( GameMode() )
            break ;
    }
    //
    return 0 ;
}

 見てのとおりですが、ここではゲームのフロー(流れ)をそのまま実現します。
 但し、後で説明するように、プライマリスレッドとの同期には十分に注意してください。
 この例ではスプライトなどは利用していないので、特に問題になるような現象は発生しませんが、スプライトのような知的な構造を持つオブジェクトを2つ以上のスレッドで共有する(操作する)場合、同期を取らなければなりません。

 

■スレッドの同期と画面の描画

 RenderParam では、スレッドの同期を取るためにクリティカルセクションオブジェクトを用いています。
 ERenderParamApp::OnPaint 関数や、ERenderParamApp::GameMode 関数などを見てください。
 画面を描画する前に Lock 関数を、終わったところで Unlock 関数を呼び出しています。
 Lock 関数や Unlock 関数内部ではクリティカルセクションを使った同期関数を呼び出しています。(ソースを見てください)
 こうすると、Lock 関数と Unlock 関数で囲まれた部分は2つ以上のスレッドで同時に実行されることはなくなります。

 ところで、セカンダリスレッドでゲームフローを実行する際のポイントとして、プライマリスレッドにウィンドウメッセージを処理が出来るように、スレッドを開放することを意識してください。
 スレッドの処理を開放するには、スレッドの同期関数などを用います。
 最も簡単な方法は Sleep 関数です。この関数にスレッドを開放する時間を渡すと、一定期間、この関数を呼び出したスレッドには処理が割り当てられなくなります。
 プライマリスレッドに処理が渡されないと、ユーザーの入力が受け付けられにくくなり、操作性の低下に繋がります。
 ここで、ERenderParamApp::DrawScreen 関数を見てください。

void ERenderParamApp::DrawScreen( void )
{
    EWindow * pWnd = GetWindow() ;
    HDC hdc = pWnd->GetDC( ) ;
    m_imgScreen.DrawToDC( hdc, VIEW_LEFT, VIEW_TOP, NULL, NULL ) ;
    pWnd->ReleaseDC( hdc ) ;
    //
    pWnd->SendMessage( WM_USER, 0, 0 ) ;
}

 この関数では、バックバッファからDC(デバイスコンテキスト)へ画像を描画した後、メインウィンドウに WM_USER メッセージを送信しています。
 まず、画面の描画ですが、これは上記のように直接セカンダリスレッドから描画しても良いですし、InvalidateRect 関数で更新領域を設定し、UpdateWindow 関数で更新を待つのでもかまいません。(GLS3 では後者の方法を推奨しています)
 次に、WM_USER メッセージの送信ですが、これは、プライマリスレッドに一定のメッセージ処理を促しています。

    case WM_USER:
        for ( i = 0; i < 0x20; i ++ )
        {
            if ( ::PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
            {
                ::TranslateMessage( &msg ) ;
                ::DispatchMessage( &msg ) ;
            }
            else
            {
                break ;
            }
        }
        return 0 ;

 上記コードは、WindowProc 関数内部で実装されているコードです。
 WM_USER メッセージを受信したら最大32個のウィンドウメッセージを処理するようになっています。
 WM_USER と言うメッセージは実際には何でもかまいませんが(WM_USER+n)、プライマリスレッドとセカンダリスレッドで用途を一致させておかなければなりません。
 セカンダリスレッドから SendMessage 関数でこのメッセージが渡されているので、プライマリスレッドがこのメッセージを受け取り、メッセージループを抜けるまで、セカンダリスレッドは待機状態になることも重要なポイントです。(単に PostMessage 関数でメッセージをポストするだけでは、意味がありません)

 

■ ステージデータ

 サンプルのステージデータは MASM のマクロ機能を利用してデータを作成し、モジュールの中に直接格納しています。
 StageData.asm がそのソースファイルですが、サンプルをコンパイルする場合、MASM を持っていない場合には StageData.obj をリンクしてください。
 このソースでは、ウェイトや、敵の出現を指示する命令を生成しています。
 1つの命令のデータフォーマットはかなり単純で、ソースにあるとおり、以下のようになっています。
; OP-CODE : オペレーションコード
; PARAM-LEN : パラメータ数
; PARAM-LIST : パラメータ列(任意数)
 1つのデータはDWORDで指定され、OP-CODEでその命令がどのような意味かを指定します。

 RenderParam サンプルでは、MASM でデータを作成していますが、実際には EDescription オブジェクトなどを利用してステージデータをコンパイルするプログラムを作成し、そのステージデータを読み込んで使用するような方式にする方が良いでしょう。

 

■ 応用のポイント