ネット対戦ゲームを作成する

Direct Play を組み込んで「対戦型の三山くずしゲーム」を作成します。
対戦ゲームが始めての時は、Host 側と Client 側を同時に組み込むと混乱するので、 当初は別々に作成して、動き出してから統合する方法がお勧めです。

前田稔(Maeda Minoru)の超初心者のプログラム入門

プロジェクトの作成

  1. 新規プロジェクト(Miyama) を作成して下さい。
    プロジェクトのフォルダーに次のファイルを格納して下さい。
    ファイル名 説明
    miyama.lib Audio Class を組み込んだライブラリ
    miyama.h ライブラリ・ヘッダ
    Main.ico 三山くずしゲームのアイコン
    Small.ico 三山くずしゲームのアイコン
    Jewel.bmp ゲームで使用する画像
    037_klar.mid ゲームで使用する MIDI
    Lose.wav ゲームで使用する WAV
    Poku.wav ゲームで使用する WAV
    Start.wav ゲームで使用する WAV
    Win.wav ゲームで使用する WAV
    Latch.wav ゲームで使用する WAV
  2. [プロジェクト][リソースの追加][Dialog][新規作成] で DirectPlay の接続用 DialogBox を作成して下さい。
    キャプションとIDを次のように設定して下さい。
    BOX ID キャプション
    DialogBox IDD_DIALOG1 Direct Play
    StaticBox IDD_STATIC Player Name
    EditBox IDC_EDIT1
    Button IDNEW NewSession
    Button IDCONNECT Connect
    Button IDCLOSE Close
  3. 次のメニューを追加して下さい。
    ID キャプション
    ファイル(&F)
    IDM_RESULT 対戦成績(&R)
    IDM_SET 再設定(&S)
    IDM_START 成績クリア(&C)
    IDM_EXIT アプリケーションの終了(&X)
    ヘルプ(&H)
    IDM_RULE ルール説明(&R)
    IDM_ABOUT バージョン情報(&A)
  4. 次のファイルを[プロジェクト][既存項目の追加] でプロジェクトに追加します。
    ファイル名 説明
    miyama.h ライブラリ・ヘッダ
  5. 次のライブラリを[プロジェクト][プロパティ][リンカ][入力] から追加します。
    ファイル名 説明
    miyama.lib Audio Class を組み込んだライブラリ
    dxguid.lib DirectX ライブラリ
    dplay.lib DirectPlayライブラリ

ソースコードの修正

  1. Miyama.cpp を次の説明に従って作成して下さい。
    *App が「三山くずしゲーム」の Class で、AUDIO が「音楽」と「効果音」の Class です。
    DEMO はデモプレイのフラグで「0:ゲーム中、1:デモ人間・2:デモComputer」になっています。
    ComPlay() はデモで COMP がプレイする関数です。
        #include   "Miyama.h"
        #define     ID_TIMER 32767
    
        Audio       AUDIO;
        Miyama     *App= NULL;          // Miyama Object Class
        LPSTR       BmpFile= { "jewel.bmp" };
        POINT       pt;                 // CLICK の位置を記録
        short       NUM;                // T[] の次の記録位置
        short       DEMO;               // Demo Mode Flag
        char        Rule[]=
        { "三個の山に石が積まれています。\n"
          "この石を私とあなたが交互に取り除きます。\n"
          "一度に一つの山からであれば、幾つでも取り除くことができます。\n"
          "最後の石を取らされた方が負けです。"
        };
    
        LRESULT             ComPlay(HWND hwnd);     //+++
        
  2. InitInstance() で Window のサイズを設定します。
        hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
               100,100,600,420,NULL,NULL,hInstance,NULL);    //+++
        
  3. WndProc() の追加・修正です。
    WM_CREATE: でバックミュージックとゲーム開始の音声を鳴らします。
    DEMO を1(人間側のプレイ)にセットして、タイマ割り込みを設定します。
        char    buf[64];
        long    nVolume= 2000;	    //+20db
    
        switch( message ) 
        {
            case WM_CREATE:             //+++
                AUDIO.Load2("037_klar.mid");
                AUDIO.Play2(200);
                Sleep(3000);
                App= new Miyama(hWnd,BmpFile);
                App->Start();
                NUM=DEMO= 1;        //Start Demo Mode
                AUDIO.Load("Start.wav");
                AUDIO.SetVol(&nVolume);
                AUDIO.Play();
                Sleep(2000);
                SetTimer(hWnd,ID_TIMER,0,NULL);
                break;
        
  4. メニューの処理です。
    IDM_START でゲームを開始します。
    IDM_RESULT で対戦成績を表示します。
    IDM_RULE でゲームのルールを説明します。
        case IDM_START:     //+++ゲーム開始
             KillTimer(App->hParent,ID_TIMER);
             App->down=App->Win_c=App->Lose_c= 0;
             DEMO= 0;
             App->Start();
             NUM= 1;
             break;
         case IDM_RESULT:
             wsprintf(buf,"%d勝   %d敗",App->Lose_c,App->Win_c);
             MessageBox(NULL,buf,"Miyama",MB_OK);
             break;
         case IDM_RULE:
             MessageBox(NULL,Rule,"Miyama",MB_OK);
             break;
        
  5. WM_LBUTTONDOWN: でマウスがクリックされた座標を調べて石を取り除きます。
    (App->down!=0 || (NUM&1)==0) はアニメーションの終了確認と手番の判定です。
    コンピュータ側のプレイは ComPlay(hWnd) で処理します。
        case WM_LBUTTONDOWN:
            if (App->down!=0 || (NUM&1)==0)  break;
            pt.x= LOWORD(lParam);
            pt.y= HIWORD(lParam);
            if (App->ManPlay(&pt)==0)
            {   MessageBox(hWnd,"エラーです","確認", MB_OK);
                return TRUE;
            }
            AUDIO.Load("Latch.wav");
            AUDIO.Play();
            App->Log(NUM);
            // コンピュータがプレイする
            ComPlay(hWnd);
            return TRUE;
        
  6. WM_TIMER: でデモプレイと石を取り除くアニメーションを制御します。
    App->down が取り除く石の個数で、アニメーションしながら一個ずつ取り除きます。
    DEMO==0 のときは、通常のゲームモードです。
    ComPlay(hWnd) でプレイした局面をログに記録して ScrollBar を設定します。
    山の石が無くなったときはコンピュータの負けです。
    負けをカウントして次のゲームを始めます。
    DEMO==1 のときは、デモプレイで人間側の手番です。
    人間側の手番で石が無ければ、最後の石を取らされたコンピュータの負けです。
    App->DemoPlay() で人間に代わってプレイします。
    DEMO==2 のときは、デモプレイでコンピュータ側の手番です。
    コンピュータ側のプレイは ComPlay(hWnd) で処理します。
        case WM_TIMER:
            KillTimer(hWnd,ID_TIMER);
            if (App->down)
            {   //アニメーション処理
                AUDIO.Load("Poku.wav");     //石を取り除く音
                AUDIO.Play();
                App->CntDown();
                SetTimer(hWnd,ID_TIMER,500,NULL);
                return TRUE;
            }
            switch(DEMO)
            {   case 0:     //Game Mode
                    NUM= App->Log();
                    if (App->Stone()==0)
                    {   AUDIO.Load("Lose.wav");
                        AUDIO.Play();
                        MessageBox(hWnd,"あなたの勝ちです","Miyama", MB_OK);
                        App->Lose_c++;
                        App->Start();
                        NUM= 1;
                    }
                    return TRUE;
                case 1:     //挑戦者のプレイ
                    if (App->Stone()==0)
                    {   AUDIO.Load("Lose.wav");
                        AUDIO.Play();
                        Sleep(2000);
                        App->Lose_c++;
                        App->Start();       //Next Game
                        DEMO=NUM= 1;
                        SetTimer(hWnd,ID_TIMER,0,NULL);
                        return TRUE;
                    }
                    Sleep(1500);
                    App->DemoPlay();
                    AUDIO.Load("Latch.wav");
                    AUDIO.Play();
                    DEMO= 2;                //COM にセット
                    Sleep(500);
                    SetTimer(hWnd,ID_TIMER,0,NULL);
                    return TRUE;
                case 2:     //COM のプレイ
                    ComPlay(hWnd);
                    DEMO= 1;                //挑戦者の手番
                    return TRUE;
            }
            return TRUE;
        
  7. WM_PAINT: に対戦成績と山の石を描画するコードを記述します。
        char    buf[16];
    
        case WM_PAINT:
            hdc = BeginPaint (hWnd, &ps);
            // TODO: この位置に描画用のコードを追加してください...
            //+++
            App->Disp(hdc);
            EndPaint( hWnd, &ps );
            break;
        
  8. WM_DESTROY: で Object を開放して下さい。
        case WM_DESTROY:
            AUDIO.Stop2();
            if (App) { delete(App);  App= NULL; }
            PostQuitMessage( 0 );
            break;
        
  9. コンピュータがプレイする ComPlay() 関数です。
    App->Play() でコンピュータがプレイします。
    コンピュータの手番で石が無いときは、最後の石を取らされた人間側の負けです。
        //★ Computer Play
        LRESULT ComPlay(HWND hwnd)
        {
            if (App->Stone())
            {   App->Play();
                SetTimer(hwnd,ID_TIMER,400,NULL);   //アニメ速度
                return TRUE;
            }
            //One Game End
            AUDIO.Load("Win.wav");
            AUDIO.Play();
            Sleep(2000);
            if (DEMO)           //DEMO Mode Next Game
            {   App->Win_c++;
                App->Start();
                NUM=DEMO= 1;
                SetTimer(hwnd,ID_TIMER,0,NULL);
                return TRUE;
            }
            MessageBox(hwnd,"私の勝ちです","負け", MB_OK);
            App->Win_c++;
            App->Start();
            NUM= 1;
            return  TRUE;
        }
        
  10. miyama.h で定義されている T[3] を public: に移して下さい。
    ヘッダファイルを変更したときは、コンパイルもやり直して下さい。
    コンパイルエラーは出ませんが T[3] に直接値を設定することができませんでした。
        class Miyama
        { //★非公開部分
          protected:
            //BYTE        T[3];               //三山 Table
                 :
                 :
          //★公開部分
          public:
            BYTE        T[3];               //三山 Table
        
  11. この状態でコンパイルすればパソコンとの対戦ゲームが楽しめます。

通信機能を組み込む

通信プログラムでは Message を転送したからと行って、それが処理された訳ではありません。
転送してから処理されるまでにタイムラグが生じることを考慮しなければなりません。
  1. HOST と TEBAN と DEMO の関係を説明します。
    HOST はセッションを開設したときに 1 が設定されます。
    TEBAN は通信ゲーム中は 0,1 に、対戦相手待ちは 2 になります。
    DEMO は HOST が対戦相手待ちのときにデモプレイを表示します。
    TEBAN 説明
    0 対戦相手からの Message を受け付けます
    1 石をマウスでクリックできます
    2 デモプレイ中です(DEMO で判定)
    デモプレイは HOST を立ち上げたときに実行されます。
    DEMO 説明
    0 Host と Client が対戦中です
    1 デモプレイ中で先手の手番です
    2 デモプレイ中で後手の手番です
  2. Miyama.cpp の先頭に次のコードを追加して下さい。
    HOSTNAME は私がテストした三種類のパソコンの IP アドレスで、プログラム内で直接設定するときに使います。
    一般に接続する場合の IP アドレスは DialogBox からタイプ入力するので、この記述は不要です。
        #include    "stdafx.h"
        #include    "resource.h"
        #include    <dplay8.h>
        #include    <stdio.h>
    
        #define MAX_LOADSTRING 100
    
        #include   "Miyama.h"
        #define     ID_TIMER 32767
    
        #define ERMSG(x)        MessageBox(g_hWnd,x,"Miyama",MB_OK | MB_ICONWARNING);
        #define HOSTNAME        "169.254.45.41"     //VAIO
        //#define HOSTNAME        "172.16.104.43"     //GAME XP
        //#define HOSTNAME        "172.16.103.244"    //GAME ME
        #define     TCPPORT     12345
        #define     MSG_START 0                     // ゲーム終了
        #define     MSG_PLAY  1                     // 初期設定
        #define     MSG_LOSE  2                     // あなたの負け
        #define     MSG_WIN   3                     // あなたの勝ち
        #define     MSG_SET   4                     // ゲームプレイ
        #define     WM_ADDPLAYER (WM_APP + 0)
        #define     WM_DELPLAYER (WM_APP + 1)
        #define     WM_NEW       (WM_APP + 2)
        #define     WM_CONNECT   (WM_APP + 3)
        #define     MAXPLAYERNUM 2                  // 接続可能な最大プレイヤー数(HOST含む)
        typedef struct 
        {
            WORD        msgType;        // メッセージの種類
            int         data[3];        // 三個の山の石数
            DPNHANDLE   hBufferHandle;  // メモリを解放するためのハンドル。コールバック関数が設定する
        } MYMESSAGE;
    
        // アプリケーションのGUID
        // {740AEEE2-6A2B-40bc-9DCB-B637D20ABFD9}
        static const GUID APPGUID =
        { 0x740aeee2, 0x6a2b, 0x40bc, { 0x9d, 0xcb, 0xb6, 0x37, 0xd2, 0xa, 0xbf, 0xd9 } };
    
        IDirectPlay8Peer *lpDirectPlay8Peer;
        Audio       AUDIO;
        Miyama     *App= NULL;          // Miyama Object Class
        HWND        g_hWnd;
        short       HOST= 0;            // HOST Flag
        short       TEBAN= 2;           // 0:Client  1:Host  2:待機
        short       DEMO=0;             // 0:対戦中、1:先手・2:後手
        POINT       pt;                 // CLICK の位置を記録
        LPSTR       BmpFile= { "jewel.bmp" };
        long        nVolume= 2000;      //+20db
        char        Rule[]=
        { "三個の山に石が積まれています。\n"
          "この石を私とあなたが交互に取り除きます。\n"
          "一度に一つの山からであれば、幾つでも取り除くことができます。\n"
          "最後の石を取らされた方が負けです。"
        };
    
        // Debug Message
        void    Debug(char *chr, int n)
        {   char    str[80];
            wsprintf(str,"%s [%d(%X)]\n",chr,n,n);
            OutputDebugString(str);
        }
    
        // グローバル変数:
        HINSTANCE hInst;                            // 現在のインスタンス
        TCHAR szTitle[MAX_LOADSTRING];              // タイトル バー テキスト
        TCHAR szWindowClass[MAX_LOADSTRING];        // タイトル バー テキスト
    
        // このコード モジュールに含まれる関数の前宣言:
        ATOM                MyRegisterClass( HINSTANCE hInstance );
        BOOL                InitInstance( HINSTANCE, int );
        LRESULT CALLBACK    WndProc( HWND, UINT, WPARAM, LPARAM );
        LRESULT CALLBACK    About( HWND, UINT, WPARAM, LPARAM );
        LRESULT             ComPlay(HWND hwnd);
        LRESULT CALLBACK    DialogProc( HWND, UINT, WPARAM, LPARAM );
        HRESULT WINAPI      myMessageHandler(PVOID pvUserContext, DWORD dwMessageType, PVOID pMessage);
        BOOL                InitDP8Peer();
        HRESULT             MakeNewSession();
        void                Destr();
        HRESULT             ConnectSession();
        void                SendMsg(WORD, int, int, int);
        LRESULT             ComeMsg(DPNID playerID, MYMESSAGE *pMsg);
        LRESULT             SessionClose();
        
  3. InitInstance() の追加と修正です。
    Windows のサイズをゲームサイズに合わせて設定します。
    Miyama Object を生成するときには HWND g_hWnd が必要で、通信の初期化は Miyama App を生成した後で行います。
        hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
               100,100,600,420,NULL,NULL,hInstance,NULL);
    
        if( !hWnd ) 
        {
           return FALSE;
        }
    
        g_hWnd = hWnd;
        App= new Miyama(g_hWnd,BmpFile);
        DialogBox(hInst,MAKEINTRESOURCE(IDD_DIALOG1),hWnd,(DLGPROC)DialogProc);
    
        App->down=App->Win_c=App->Lose_c= 0;
        AUDIO.Load2("037_klar.mid");
        AUDIO.Play2(200);
        Sleep(3000);
        AUDIO.Load("Start.wav");
        AUDIO.SetVol(&nVolume);
        AUDIO.Play();
        Sleep(2000);
        
  4. WndProc() の追加と修正です。
    受信 Message の処理以外の主要な処理は、ここに集約してまとめて行っています。
    HOST が起動して対戦相手を待つ間に、TIMER 割り込みを使ってデモプレイを行います。
        char    buf[64];
        switch( message ) 
        {   case WM_COMMAND:
                wmId    = LOWORD(wParam); 
                wmEvent = HIWORD(wParam); 
                // メニュー選択の解析:
                switch( wmId ) 
                {   case IDM_RESULT:    //対戦成績の表示
                        wsprintf(buf,"%d勝   %d敗",App->Win_c,App->Lose_c);
                        MessageBox(NULL,buf,"Miyama",MB_OK);
                        break;
                    case IDM_SET:       //石の再設定
                        if (HOST)
                        {   App->Start();
                            SendMsg(MSG_PLAY,App->T[0],App->T[1],App->T[2]);
                        }
                        else    SendMsg(MSG_START,0,0,0);
                        break;
                    case IDM_START:     //対戦成績のクリア
                        App->down=App->Win_c=App->Lose_c= 0;
                        break;
                    case IDM_RULE:
                        MessageBox(NULL,Rule,"Miyama",MB_OK);
                        break;
                    case IDM_ABOUT:
                        DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
                        break;
                    case IDM_EXIT:
                        DestroyWindow( hWnd );
                        break;
                    default:
                        return DefWindowProc( hWnd, message, wParam, lParam );
                    }
                break;
            case WM_LBUTTONDOWN:        //石をクリック
                if (TEBAN != 1 )    break;
                if (HOST && App->Stone()<2)
                {   TEBAN= 2;
                    if (App->Stone()==0)
                    {   AUDIO.Load("Win.wav");
                        AUDIO.Play();
                        MessageBox(hWnd,"私の勝ちです","Miyama", MB_OK);
                        App->Win_c++;
                        SendMsg(MSG_LOSE,0,0,0);
                    }
                    else
                    {   AUDIO.Load("Lose.wav");
                        AUDIO.Play();
                        MessageBox(hWnd,"あなたの勝ちです","Miyama", MB_OK);
                        App->Lose_c++;
                        SendMsg(MSG_WIN,0,0,0);
                    }
                    return TRUE;
                }
                pt.x= LOWORD(lParam);
                pt.y= HIWORD(lParam);
                if (App->ManPlay(&pt)==0)
                {   MessageBox(hWnd,"エラーです","確認", MB_OK);
                    return TRUE;
                }
                //Debug("X=",pt.x);  Debug("Y=",pt.y);
                AUDIO.Load("Latch.wav");
                AUDIO.Play();
                SendMsg(MSG_SET,App->T[0],App->T[1],App->T[2]); //通信データ転送
                TEBAN= 0;
                return TRUE;
            case WM_TIMER:
                KillTimer(g_hWnd,ID_TIMER);
                if (App->down)
                {   //アニメーション処理
                    AUDIO.Load("Poku.wav");     //石を取り除く音
                    AUDIO.Play();
                    App->CntDown();
                    SetTimer(g_hWnd,ID_TIMER,500,NULL);
                    return TRUE;
                }
                switch(DEMO)
                {   case 0:                     //Game Mode
                        return TRUE;
                    case 1:                     //挑戦者のプレイ
                        if (App->Stone()==0)
                        {   AUDIO.Load("Lose.wav");
                            AUDIO.Play();
                            Sleep(2000);
                            //App->Lose_c++;
                            App->Start();       //Next Game
                            DEMO= 1;
                            SetTimer(g_hWnd,ID_TIMER,0,NULL);
                            return TRUE;
                        }
                        Sleep(1500);
                        App->DemoPlay();
                        AUDIO.Load("Latch.wav");
                        AUDIO.Play();
                        DEMO= 2;                //COM にセット
                        Sleep(500);
                        SetTimer(g_hWnd,ID_TIMER,0,NULL);
                        return TRUE;
                    case 2:     //COM のプレイ
                        ComPlay(hWnd);
                        DEMO= 1;                //挑戦者の手番
                        return TRUE;
                }
                return TRUE;
            case WM_PAINT:
                hdc = BeginPaint (hWnd, &ps);
                // TODO: この位置に描画用のコードを追加してください...
                App->Disp(hdc);
                EndPaint( hWnd, &ps );
                break;
            case WM_NEW:        //セッションを開設してゲームを開始する
                HOST= 1;
                DEMO= 1;        //Start Demo Mode
                TEBAN= 2;       //通信ゲームは待機状態に設定 
                App->Start();
                SetTimer(g_hWnd,ID_TIMER,0,NULL);
                //MessageBox(g_hWnd,"ゲームを開始します","Miyama",MB_OK);
                break;
            case WM_CONNECT:    //セッションに接続してプレイする
                HOST= 0;
                DEMO= 0;        //Start Demo Mode
                TEBAN= 2;       //通信ゲームは待機状態に設定 
                SendMsg(MSG_START,0,0,0);
                //MessageBox(g_hWnd,"Host に接続します","Miyama",MB_OK);
                break;
            case WM_ADDPLAYER:
                //MessageBox(g_hWnd,"【1名入室確認】","Miyama",MB_OK);
                return TRUE;
            case WM_DELPLAYER:
                //MessageBox(g_hWnd,"【1名退室確認】","Miyama",MB_OK);
                DEMO= 1;        //Start Demo Mode
                TEBAN= 2;       //通信ゲームは待機状態に設定
                App->down=App->Win_c=App->Lose_c= 0;
                App->Start();
                SetTimer(g_hWnd,ID_TIMER,0,NULL);
                return TRUE;
            case WM_DESTROY:
                Destr();
                AUDIO.Stop2();
                if (App) { delete(App);  App= NULL; }
                PostQuitMessage( 0 );
                break;
            default:
                return DefWindowProc( hWnd, message, wParam, lParam );
       }
       return 0;
    }
        
  5. デモプレイで使われる ComPlay() です。
    デモプレイでは勝敗のカウントを行いません。
        //★ Computer Play
        LRESULT ComPlay(HWND hwnd)
        {
            if (App->Stone())
            {   App->Play();
                SetTimer(g_hWnd,ID_TIMER,400,NULL);   //アニメ速度
                return TRUE;
            }
            //One Game End
            AUDIO.Load("Win.wav");
            AUDIO.Play();
            Sleep(2000);
            if (DEMO)           //DEMO Mode Next Game
            {   //App->Win_c++;
                App->Start();
                DEMO= 1;
                SetTimer(g_hWnd,ID_TIMER,0,NULL);
                return TRUE;
            }
            MessageBox(hwnd,"私の勝ちです","負け", MB_OK);
            App->Win_c++;
            App->Start();
            return  TRUE;
        }
        
  6. 通信接続用の DialogBox の処理です。
        //----------------------------------------------------------------
        LRESULT CALLBACK DialogProc(HWND hDlg,UINT msg,WPARAM wp,LPARAM lp)
        {   char  buf[64];
            switch(msg)
            {   case WM_INITDIALOG:
                    SetDlgItemText(hDlg,IDC_EDIT1,"Player Name");
                    InitDP8Peer();
                    return TRUE;
                case WM_COMMAND:
                    switch(LOWORD(wp))
                    {   case IDNEW:         //New ボタンをクリック
                            if (SUCCEEDED(MakeNewSession()))
                            {   GetDlgItemText(hDlg,IDC_EDIT1,buf,sizeof(buf));
                                SetWindowText(g_hWnd,buf);
                                //MessageBox(g_hWnd,"ゲームを開始します","Miyama",MB_OK);
                                PostMessage(g_hWnd,WM_NEW,0,0);
                                EndDialog(hDlg, TRUE);
                            }
                            break;
                        case IDCONNECT:     //Connect ボタンをクリック
                            if (SUCCEEDED(ConnectSession()))
                            {   GetDlgItemText(hDlg,IDC_EDIT1,buf,sizeof(buf));
                                SetWindowText(g_hWnd,buf);
                                //MessageBox(g_hWnd,"Host に接続します","Miyama",MB_OK);
                                PostMessage(g_hWnd,WM_CONNECT,0,0);
                                EndDialog(hDlg, TRUE);
                            }
                            break;
                        case IDCLOSE:
                            EndDialog(hDlg, TRUE);
                            DestroyWindow(g_hWnd);
                            return TRUE;
                    }
                    break;
            }
            return FALSE;
        }
        
  7. Direct Play の諸関数です。
        //初期設定?
        BOOL InitDP8Peer()
        {   HRESULT hr;
            CoInitialize(NULL);
            // DirectPlay8Peerインタフェースを取得する
            lpDirectPlay8Peer = NULL;
            hr = CoCreateInstance(CLSID_DirectPlay8Peer, NULL, CLSCTX_INPROC_SERVER,
                                  IID_IDirectPlay8Peer, (void **)&lpDirectPlay8Peer);
            if (FAILED(hr))
            {   // 取得に失敗した
                ERMSG("IDirectPlay8Peerインタフェースの取得に失敗しました");
                return FALSE;
            }
            // DirectPlay8Peerインタフェースの初期化
            hr = lpDirectPlay8Peer->Initialize(g_hWnd, &myMessageHandler, 0);
            if (FAILED(hr))
            {   // 初期化に失敗した
                ERMSG("IDirectPlay8Peerインタフェースの初期化に失敗しました");
                lpDirectPlay8Peer->Release();
                return FALSE;
            }
            return TRUE;  // TRUE を返すとコントロールに設定したフォーカスは失われません。
        }
    
        //新規セッション作成?
        HRESULT MakeNewSession()
        {   // 新規にセッションを開始する
            IDirectPlay8Address *prgDeviceInfo;
            HRESULT hr;
            // DirectPlay8Addressオブジェクトの作成
            hr = CoCreateInstance(CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC_SERVER,
                                  IID_IDirectPlay8Address, (void **)&prgDeviceInfo);
            if (FAILED(hr))
            {   ERMSG("IDirectPlay8Addressインタフェースの取得に失敗");
                return E_FAIL;
            }
            // サービス・プロバイダとしてTCP/IPを選択する
            hr = prgDeviceInfo->BuildFromURLA("x-directplay:/provider=%7BEBFE7BA0-628D-11D2-AE0F-006097B01411%7D");
            if (FAILED(hr))
            {   ERMSG("サービス・プロバイダの設定に失敗");
                prgDeviceInfo->Release();
                return E_FAIL;
            }
            // ポート番号の設定
            DWORD port = TCPPORT;
            hr = prgDeviceInfo->AddComponent(L"port", (void*)&port, sizeof(DWORD), DPNA_DATATYPE_DWORD);
            if (FAILED(hr))
            {   ERMSG("ポート番号の設定に失敗");
                prgDeviceInfo->Release();
                return E_FAIL;
            }
            // プレーヤー情報を設定する
            DPN_PLAYER_INFO dpnPlayerInfo;
            dpnPlayerInfo.dwSize = sizeof(DPN_PLAYER_INFO);
            dpnPlayerInfo.dwInfoFlags = DPNINFO_NAME;
            dpnPlayerInfo.pwszName = L"dummy";
            dpnPlayerInfo.pvData = NULL;
            dpnPlayerInfo.dwDataSize = 0;
            dpnPlayerInfo.dwPlayerFlags = 0;
            hr = lpDirectPlay8Peer->SetPeerInfo(&dpnPlayerInfo, NULL, NULL, DPNSETPEERINFO_SYNC);
            if (FAILED(hr))
            {   ERMSG("プレーヤー情報の設定に失敗");
                prgDeviceInfo->Release();
                return E_FAIL;
            }
            // セッションを作成する
            DPN_APPLICATION_DESC    dpnAppDesc;
            // 構造体を0で初期化する
            memset((void*)&dpnAppDesc, 0, sizeof(DPN_APPLICATION_DESC));
            // 各メンバを設定する
            dpnAppDesc.dwSize = sizeof(DPN_APPLICATION_DESC);
            dpnAppDesc.dwFlags =  DPNSESSION_MIGRATE_HOST;
            dpnAppDesc.guidApplication = APPGUID;
            dpnAppDesc.dwMaxPlayers = MAXPLAYERNUM;
            dpnAppDesc.pwszSessionName = L"MiyamaGame";
            hr = lpDirectPlay8Peer->Host(&dpnAppDesc, &prgDeviceInfo, 1, 
                                     NULL, NULL, NULL, 
                                     DPNHOST_OKTOQUERYFORADDRESSING);
            // IDirectPlay8Addressインタフェースの解放
            prgDeviceInfo->Release();
            if (FAILED(hr))
            {   ERMSG("セッションの作成に失敗");
                return E_FAIL;
            }
            return S_OK;
        }
        
  8. DirectPlay のメッセージハンドラです。
    届いた Message の処理は ComeMsg() で行い、その他の主要な処理は PostMessage() で WndProc() に伝えます。
        //DirectPlay8Peer メッセージハンドラ
        HRESULT WINAPI myMessageHandler(PVOID pvUserContext, DWORD dwMessageType, PVOID pMessage)
        {   // メッセージハンドラ
            //  pvUsercontext = Initializeメソッドの第1引数に渡した値
            //  dwMessageType = 届いたメッセージの種類
            //  pMessage = 届いたメッセージ本体。内容はdwMessageTypeの値によって異なる
            HRESULT hr;
            switch (dwMessageType)
            {   case DPN_MSGID_CREATE_PLAYER:
                    // プレーヤーが作成された
                    PDPNMSG_CREATE_PLAYER   pDPNcreateplayer;
                    DPNID   playerID;
                    // プレーヤーIDを得る
                    pDPNcreateplayer = (PDPNMSG_CREATE_PLAYER)pMessage;
                    playerID = pDPNcreateplayer->dpnidPlayer;
                    // プレーヤーの情報を得る
                    DPN_PLAYER_INFO *pdpnPlayerInfo;
                    DWORD   cbSize;
                    // 必要なメモリのバイト数を取得
                    cbSize = 0;
                    hr = lpDirectPlay8Peer->GetPeerInfo(playerID, NULL, &cbSize, 0);
                    if (hr != DPNERR_BUFFERTOOSMALL)
                    {   // 何らかのエラーが発生
                        break;
                    }
                    // 必要なメモリを確保
                    pdpnPlayerInfo = (DPN_PLAYER_INFO*)malloc(cbSize);
                    memset(pdpnPlayerInfo, 0, cbSize);
                    pdpnPlayerInfo->dwSize = sizeof(DPN_PLAYER_INFO);
                    hr = lpDirectPlay8Peer->GetPeerInfo(playerID, pdpnPlayerInfo, &cbSize, 0);
                    if (FAILED(hr))
                    {   //何らかのエラーが発生
                        free(pdpnPlayerInfo);
                        break;
                    }
                    // プレーヤーのDPNIDとプレーヤーの名前をメッセージとして送信する
                    HGLOBAL hMem;
                    char *szName;
                    hMem = GlobalAlloc(GHND, 512);
                    szName = (char *)GlobalLock(hMem);
                    WideCharToMultiByte(CP_ACP, 0, pdpnPlayerInfo->pwszName, -1, szName, 256, NULL, NULL);
                    GlobalUnlock(hMem);
                    PostMessage(g_hWnd,WM_ADDPLAYER,(WPARAM)playerID,(LPARAM)hMem);
                    return S_OK;
                    break;
                case DPN_MSGID_DESTROY_PLAYER:
                    // プレーヤーが削除された
                    PDPNMSG_DESTROY_PLAYER  pDPNdestroyplayer;
                    pDPNdestroyplayer = (PDPNMSG_DESTROY_PLAYER)pMessage;
                    // プレーヤーのDPNIDをメッセージとして送信する
                    PostMessage(g_hWnd,WM_DELPLAYER,(WPARAM)pDPNdestroyplayer->dpnidPlayer,0);
                    return S_OK;
                    break;
                case DPN_MSGID_RECEIVE:
                    // メッセージが届いた
                    PDPNMSG_RECEIVE pDPNreceive;
                    MYMESSAGE *mymsg;
                    pDPNreceive = (PDPNMSG_RECEIVE)pMessage;
                    // 届いたメッセージを処理する
                    mymsg = (MYMESSAGE*)pDPNreceive->pReceiveData;
                    mymsg->hBufferHandle = pDPNreceive->hBufferHandle;
                    ComeMsg(pDPNreceive->dpnidSender, mymsg);
                    // DPN_SUCCCESS_PENDINGを返してメモリを解放しないようにする
                    return DPNSUCCESS_PENDING;
                    break;
                case DPN_MSGID_TERMINATE_SESSION:
                    // セッションが終了した
                    SessionClose();
                    return S_OK;
                    break;
            }
            return S_OK;
        }
        
  9. 届いたメッセージの種類を識別して処理する ComeMsg() 関数です。
    ゲームの進行は HOST 側で行います。
        LRESULT ComeMsg(DPNID playerID, MYMESSAGE *pMsg)
        {   int     id;
            switch(pMsg->msgType)
            {   case MSG_START:         // ゲームの開始
                    if (HOST)
                    {//ERMSG("1 HOST START を受信");
                        App->down= 0;
                        DEMO= 0;        // DemoMode を解除
                        TEBAN= 0;       // Client の先手
                        App->Start();
                    //ERMSG("2 HOST PLAY を送信");
                        SendMsg(MSG_PLAY,App->T[0],App->T[1],App->T[2]);
                        KillTimer(g_hWnd,ID_TIMER);
                    }
                    break;
                case MSG_PLAY:          // 初期設定
                    if (HOST==0)
                    {//ERMSG("3 CLIENT PLAY を受信");
                        App->T[0]= pMsg->data[0];
                        App->T[1]= pMsg->data[1];
                        App->T[2]= pMsg->data[2];
                        InvalidateRect(g_hWnd,NULL,TRUE);
                        UpdateWindow(g_hWnd);
                        TEBAN= 1;       // Client の先手
                    }
                    break;
                case MSG_SET:           // 相手側の手を受信
                    if (TEBAN)  break;
                    App->T[0]= pMsg->data[0];
                    App->T[1]= pMsg->data[1];
                    App->T[2]= pMsg->data[2];
                    InvalidateRect(g_hWnd,NULL,TRUE);
                    UpdateWindow(g_hWnd);
                    TEBAN= 1;           // こちら側の手番
                    break;
                case MSG_WIN:           // あなたの勝ち
                    if (HOST==0)
                    {   AUDIO.Load("Win.wav");
                        AUDIO.Play();
                        App->Win_c++;
                        id = MessageBox(g_hWnd,"あなたの勝ちです","次のゲーム", MB_OKCANCEL | MB_ICONQUESTION);
                        if (id==IDOK)   SendMsg(MSG_START,0,0,0);
                        else            PostMessage(g_hWnd,WM_CLOSE,0,0);
                    }
                    break;
                case MSG_LOSE:          // あなたの負け
                    if (HOST==0)
                    {   AUDIO.Load("Lose.wav");
                        AUDIO.Play();
                        App->Lose_c++;
                        id = MessageBox(g_hWnd,"あなたの負けです","次のゲーム", MB_OKCANCEL | MB_ICONQUESTION);
                        if (id==IDOK)   SendMsg(MSG_START,0,0,0);
                        else            PostMessage(g_hWnd,WM_CLOSE,0,0);
                    }
                    break;
            }
            // バッファを解放する
            lpDirectPlay8Peer->ReturnBuffer(pMsg->hBufferHandle, 0);
            return 0;
        }
        
  10. //+++接続先の IP アドレスを設定すると、DialogBox を表示しないで接続することができます。
        //切断?
        LRESULT SessionClose()
        {   // セッションが閉ざされた
            HRESULT hr;
            hr = lpDirectPlay8Peer->Initialize(NULL, &myMessageHandler, 0);
            if (FAILED(hr))
            {   // 初期化に失敗した
                ERMSG("IDirectPlay8Peerインタフェースの初期化に失敗しました");
                lpDirectPlay8Peer->Release();
                return FALSE;
            }
            return 0;
        }
    
        //既存のセッションにつなぎます?
        HRESULT ConnectSession() 
        {   // TODO: この位置にコントロール通知ハンドラ用のコードを追加してください
            // 既存のセッションに参加する
            IDirectPlay8Address *prgDeviceInfo; // 接続に使うサービス・プロバイダなど
            IDirectPlay8Address *prgHostAddr;   // 接続先のホスト情報
            HRESULT hr;
            // DirectPlay8Addressオブジェクトの作成
            hr = CoCreateInstance(CLSID_DirectPlay8Address, NULL, CLSCTX_INPROC_SERVER,
                                  IID_IDirectPlay8Address, (void **)&prgDeviceInfo);
            if (FAILED(hr))
            {   ERMSG("IDirectPlay8Addressインタフェースの取得に失敗");
                return E_FAIL;
            }
            // サービス・プロバイダとしてTCP/IPを選択する
            hr = prgDeviceInfo->BuildFromURLA("x-directplay:/provider=%7BEBFE7BA0-628D-11D2-AE0F-006097B01411%7D");
            if (FAILED(hr))
            {   ERMSG("サービス・プロバイダの設定に失敗");
                prgDeviceInfo->Release();
                return E_FAIL;
            }
            // 接続先の情報を設定する
            // ここではDuplicateメソッドを使って複製することでTCP/IPが選択されたIDirectPlay8Addressインタフェースを得る
            hr = prgDeviceInfo->Duplicate(&prgHostAddr);
            if (FAILED(hr))
            {   ERMSG("IDirectPlay8Addressインタフェースの複製に失敗");
                prgDeviceInfo->Release();
                return E_FAIL;
            }
        /*    //+++接続先の IP アドレスの設定
            WCHAR *host =    L"169.254.45.41";	//(13+1)*2= 28
            hr = prgHostAddr->AddComponent(L"hostname",(void*)host,28,DPNA_DATATYPE_STRING);
            if (FAILED(hr))
            {   ERMSG("IP アドレスの設定に失敗");
                prgDeviceInfo->Release();
                prgHostAddr->Release();
                return E_FAIL;
            }
        */
            // ポート番号の設定
            DWORD port = TCPPORT;
            hr = prgHostAddr->AddComponent(L"port", (void*)&port, sizeof(DWORD), DPNA_DATATYPE_DWORD);
            if (FAILED(hr))
            {   ERMSG("ポート番号の設定に失敗");
                prgDeviceInfo->Release();
                prgHostAddr->Release();
                return E_FAIL;
            }
            // プレーヤー情報を設定する
            DPN_PLAYER_INFO dpnPlayerInfo;
            dpnPlayerInfo.dwSize = sizeof(DPN_PLAYER_INFO);
            dpnPlayerInfo.dwInfoFlags = DPNINFO_NAME;
            dpnPlayerInfo.pwszName = L"dummy";
            dpnPlayerInfo.pvData = NULL;
            dpnPlayerInfo.dwDataSize = 0;
            dpnPlayerInfo.dwPlayerFlags = 0;
            hr = lpDirectPlay8Peer->SetPeerInfo(&dpnPlayerInfo, NULL, NULL, DPNSETPEERINFO_SYNC);
            if (FAILED(hr))
            {   ERMSG("プレーヤー情報の設定に失敗");
                prgDeviceInfo->Release();
                prgHostAddr->Release();
                return E_FAIL;
            }
            // セッションに接続する
            DPN_APPLICATION_DESC    dpnAppDesc;
            // 構造体を0で初期化する
            memset((void*)&dpnAppDesc, 0, sizeof(DPN_APPLICATION_DESC));
            // 各メンバを設定する
            dpnAppDesc.dwSize = sizeof(DPN_APPLICATION_DESC);
            dpnAppDesc.guidApplication = APPGUID;
            // ここでは同期的に接続することにする
            hr = lpDirectPlay8Peer->Connect(&dpnAppDesc, prgHostAddr, prgDeviceInfo, 
                          NULL, NULL, NULL, 0, NULL, NULL, NULL, 
                          DPNHOST_OKTOQUERYFORADDRESSING | DPNCONNECT_SYNC);
        //+++                  DPNCONNECT_SYNC);
            // IDirectPlay8Addressインタフェースの解放
            prgDeviceInfo->Release();
            prgHostAddr->Release();
            if (FAILED(hr))
            {   ERMSG("セッションの接続に失敗");
                return E_FAIL;
            }
            return S_OK;
        }
        
  11. Message を送信する関数です。
    Message の形式は一種類で、メッセージのタイプと三山の石の数を送信します。
        void SendMsg(WORD type, int x, int y, int z) 
        {   // TODO: この位置にコントロール通知ハンドラ用のコードを追加してください
            MYMESSAGE       msg;
            DPN_BUFFER_DESC dpnMsg;
            DPNHANDLE       aSync;
            msg.msgType = type;
            msg.data[0] = x;
            msg.data[1] = y;
            msg.data[2] = z;
            dpnMsg.dwBufferSize = sizeof(MYMESSAGE);
            dpnMsg.pBufferData = (BYTE *)&msg;
            // 発信元には送信しない
            lpDirectPlay8Peer->SendTo(DPNID_ALL_PLAYERS_GROUP, &dpnMsg, 1, 0, NULL, &aSync,
                                      DPNSEND_NOLOOPBACK | DPNSEND_GUARANTEED);
        }
        
  12. Object を開放します。
        //終了?
        void Destr()
        {   if (lpDirectPlay8Peer != NULL)
            {   lpDirectPlay8Peer->Close(0);
                lpDirectPlay8Peer->Release();
            }
            if (App) { delete(App);  App= NULL; }
        }
        

リソースとして取り込む

MIDI と WAV をリソースとして取り込みます。
リソースとして取り込むと *.exe の中に MIDI や WAV が組み込まれるのでプログラムが扱いやすくなり、 また専用のツールを使わない限り、リソース自体を取り出したり改変することを防ぐことができます。
  1. 今までの Miyama.lib は、リソースとして取り込んだ画像を扱えませんでした。
    リソースを扱えるように LoadBMP() のソースコードを修正したライブラリを使用して下さい。
    Audio は最初からリソースを扱えるようになっていました。
  2. 画像ファイル(Jewel.bmp)をリソースとして取り込みます。
    取り込んだリソースの ID を引用符で囲って("IDB_BITMAP1")下さい。
  3. Midi File を Wave File を Resource として取り込みます。
    [挿入][リソース]から[インポート]ボタンをクリックして Midi を選択します。
    リソースのタイプを聞いてくるので WAVE とタイプします。

    Midi File も Wave として Resource に取り込みます。
    続いて Wave File も同じように取り込みます。
  4. Resource として取り込んだ状態です。

  5. Midi(Wave) が定義されている Resource ファイルとヘッダーです。
        // WAVE
        //
    
        IDR_WAVE1               WAVE    DISCARDABLE     "037_klar.mid"
        IDR_WAVE2               WAVE    DISCARDABLE     "Start.wav"
        IDR_WAVE3               WAVE    DISCARDABLE     "Win.wav"
        IDR_WAVE4               WAVE    DISCARDABLE     "Lose.wav"
        IDR_WAVE5               WAVE    DISCARDABLE     "Latch.wav"
        IDR_WAVE6               WAVE    DISCARDABLE     "Poku.wav"
        
        #define IDR_WAVE1                       131
        #define IDR_WAVE2                       132
        #define IDR_WAVE3                       133
        #define IDR_WAVE4                       134
        #define IDR_WAVE5                       135
        #define IDR_WAVE6                       136
        
  6. BmpFile に関する Main.cpp の修正です。
        //LPSTR       BmpFile= { "jewel.bmp" };
    
        //App= new Miyama(g_hWnd,BmpFile);
        App= new Miyama(g_hWnd,"IDB_BITMAP1");
        
  7. MIDI, WAVE に関する Main.cpp の修正です。
    他のソースコードも同じように修正します。
        //AUDIO.Load2("037_klar.mid");
        AUDIO.Load2(g_inst,IDR_WAVE1);
    
        //AUDIO.Load("Start.wav");
        AUDIO.Load(g_inst,IDR_WAVE2);
               :
               :
        

[Previous Chapter ↑] Client 同士が対戦する「三山くずしゲーム」

前田稔(Maeda Minoru)の超初心者のプログラム入門

超初心者のプログラム入門(C言語 Windows)