ソフトウェア編
その4 自動再生アプリケーションの根幹を作成
前章で、1つのボタンを自動的に再生する仕組みが整いました。あとはそれらを複数持ち合わせたコントローラクラスを作成するだけです。ボタンの情報を8bitの数値に変換するCTecnicContクラスの派生クラスでボタンオペレータを8つ持たせる形になりそうです。この派生クラスCPS2Controlerクラスが、自動再生アプリケーションの根幹になります。
プレステコントローラがすべき仕様を次のようにまとめます。
プレステコントローラ仕様 ・ 各ボタンにイベント情報を登録
・ 8つのボタンを同時に再生してパラレルポートに8bit情報を発信
・ 終了(停止)
・ 再スタート
この仕様を満たすToDoリストは次の通り。
ToDoリスト ・ ×ボタンに1000ミリ秒後Pushのイベントを登録 ・ スタートしてから○、△、×、□を1000ミリ秒後にすべて押す ・ スタートしてから上と左を1000ミリ秒後にすべて押す ・ スタートしてから下と右を1000ミリ秒後にすべて押す ・ スタートしてから上下左右を1000ミリ秒後にすべて押すが、判定は上と左のみ(右下は無視) ・ スタートして停止する ・ スタートして停止後、もう一度最初からスタート
5段目がちょっとポイントです。プレステに限らず、方向キーは通常上下、もしくは左右の同時押しが出来ません。よって、この状況が生じない作りにする必要があります。
@ ×ボタンに1000ミリ秒後Pushのイベントを登録
CPS2ControlerクラスのToDoリスト1段目は、×ボタンを1000ミリ秒後に押すためのイベント情報を登録します。登録自体はCBtnOperatorクラスがその機能を持っていますから、CPS2Controlerクラスは「どのボタンに登録するか」という関数を提供するだけでよいと思われます。テストコードです。
PS2ControlerTest.h // ×ボタンに1000ミリ秒後Pushのイベントを登録
void testRegist1000msecPushToCrossBtn()
{
CPS2Controler PS2C(0x378); // パラレルポートのアドレスを登録
PS2C.AddList( BTN_CROSS, BTNTIMING(1000, BTN_PUSH) );
// チェック
assertEquals("testCBTRPush", 1000, PS2C.GetRegistData(BTN_CROSS, 0).m_uiTime);
assertEquals("testCBTRPush", BTN_PUSH, PS2C.GetRegistData(BTN_CROSS, 0).m_dwEvent);
}
CPS2Controlerオブジェクトはパラレルポートを叩きますので、そのアドレスをコンストラクタで登録します。次にAddList関数で×ボタンを1000ミリ秒後にPushする情報を登録し、GetRegistData関数でその情報を確認します。
AddList関数から行きましょう。指定のボタンに情報を書き込みます。
CPS2Controler.cpp void CPS2Controler::AddList(BYTE btncode, BTNTIMING btntiming)
{
m_spBtnOpeAry[ BtnElemHash( btncode) ]->AddList(btntiming);
}
m_spBtnOpeAryはボタンオペレータCBtnOperatorの配列です。BtnElemHash関数を通せば、引数のボタンに対応するオペレータにアクセスできます。
GetRegistData関数でも同様の処理を行います。
CPS2Controler.cpp BTNTIMING CPS2Controler::GetRegistData(BYTE btncode, unsigend int elem)
{
m_spBtnOpeAry[ BtnElemHash( btncode) ]->GetRegistData(elem);
}
問題は無いですね。m_spBtnOpeAry配列はvectorで確保しても良いですし、ボタンの数がBTN_NUMとして登録されていますから、静的配列として確保しても良いでしょう。テストはこれでグリーンシグナルとなります。
A スタートしてから○、△、×、□を1000ミリ秒後にすべて押す
ボタンを押してパラレルポートに出力する、この道のりは非常に長いです。この節、緻密な気合が必要です。まず、ToDoリスト2段目をテストします。
PS2ControlerTest.h // スタートしてから○、△、×、□を1000ミリ秒後にすべて押す
void test_C_B_T_R_Push()
{
// スタートしてから○、△、×、□を1000ミリ秒後に押す
CPS2Controler PS2C(0x378);
// ボタン情報登録
PS2C.AddList( BTN_CIRCLE, BTNTIMING(1000, BTN_PUSH) );
PS2C.AddList( BTN_TRIANGLE, BTNTIMING(1000, BTN_PUSH) );
PS2C.AddList( BTN_CROSS, BTNTIMING(1000, BTN_PUSH) );
PS2C.AddList( BTN_RECT, BTNTIMING(1000, BTN_PUSH) );
// 再生
PS2C.Start();
Sleep(1500);
// 終了
PS2C.Stop();
// チェック
assertEquals("test_C_B_T_R_Push", 1000, PS2C.Trace(BTN_CIRCLE, 0).m_uiTime);
assertEquals("test_C_B_T_R_Push", 1000, PS2C.Trace(BTN_CROSS, 0).m_uiTime);
assertEquals("test_C_B_T_R_Push", 1000, PS2C.Trace(BTN_TRIANGLE, 0).m_uiTime);
assertEquals("test_C_B_T_R_Push", 1000, PS2C.Trace(BTN_RECT, 0).m_uiTime);
}
情報登録後、自動再生を実行します。1.5秒ほど待って、再生を終了。Trace関数でスタートから実行したイベントの足跡を取得します。再生中はパラレルポートに信号が出力されます。
まずはCPS2Controler::Start関数から。パラレルポートを初期化して、保持しているボタンオペレータすべてにスレッドの準備をさせて、準備終了後直ちに同時スタートさせます。こういう感じになるでしょうか。
PS2Controler.cpp void CPS2Controler::Start()
{
// パラレルポートを初期化
InitPort();
// すべてのスレッドを準備
for(int i=0; i<BTN_NUM; i++)
m_spBtnOpe[i]->InitReady();
// 同時スタート
for(i=0; i<BTN_NUM; i++)
m_spBtnOpe[i]->Start();
}
InitPort関数ではパラレルポートを出力モードにし、0で初期化します。
PS2Controler.cpp void CPS2Controler::InitPort()
{
_outp( m_dwPortAddress + 2, 0); // コントロールレジスタを0で初期化
_outp( m_dwPortAddress, 0); // データレジスタを初期化
}
m_dwPortAddressにはパラレルポートのアドレスが与えられます。これはコンストラクタで与えるべきでしょう。コンストラクタはとりあえずこうなります。
PS2Controler.cpp CPS2Controler::CPS2Controler(DWORD para_port_address)
{
m_dwPortAddress = para_port_address;
}
続いてStop関数です。
PS2Controler.cpp void CPS2Controler::Start()
{
// すべてのスレッドを準備
for(int i=0; i<BTN_NUM; i++)
m_spBtnOpe[i]->Stop();
// パラレルポートを再初期化
InitPort();
}
ここまではまだ簡単。ちょっと面倒なのはTrace関数です。これは、それまでのイベントを保持しておいて、正しいタイミングでボタンが押されたかをチェックするための関数です。これを実現するには、CBtnOperatorクラスに経過イベント保持機能を追加する必要があります。
経過イベントを追加で保存していくCBtnOperator::WriteHistory関数を追加します。
BtnOperator.cpp void CBtnOperator::WriteHistory(unsigned int time, DWORD event)
{
m_BtnHistAry.push_back( BTNTIMING( time, event ) );
}
m_BtnHistAryという配列に経過イベントを追加していくことにしました。この関数をCBtnOperator::EventProc関数内で呼んであげます。
BtnOperator.cpp void CBtnOperator::EventProc(unsigned int time)
{
// 実行待ちのイベントを取得
BTNTIMING BtnTim = GetNextEvent();
if(time >= BtnTim.m_uiTime)
{
// イベント実行
switch(BtnTim.m_dwEvent)
{
case BTN_PUSH: // ボタンを押す
Push(time);
break;
case BTN_RELEASE: // ボタンを離す
Release(time);
break;
default:
break;
}
// 終了したボタンイベントを保持
WriteHistory( time, BtnTim.dwEvent);
// 現在のイベント状態を更新
m_dwCurEvent = BtnTim.m_dwEvent;
// イベントが実行されたら次のイベントへ
UpdateNextEvent();
}
}
そして、保持したイベントを取り出すCBtnOperator::Trace関数を定義します。
BtnOperator.cpp BTNTIMING CBtnOperator::Trace(unsigned int elem)
{
if(elem >= m_BtnHistAry.size())
return BTNTIMING ( -1, -1 );
return m_BtnHistAry[elem];
}
最後に複数のボタンに対応したCPS2Controler::Trace関数を実装します。
PS2Controler.cpp BTNTIMING CPS2Controler::Trace(BYTE btncode, unsigned int elem)
{
return m_spBtnOpeAry[ BtnElemHash( btncode ) ]->Trace(elem);
}
以上でコンパイル、実行テストともグリーンシグナルです。とりあえず、ひと段落・・・ふぅ〜。
はい、続きます。
CPS2Controlerクラスはパラレルポートを叩く必要があります。しかし、ハードウェアへの精密なテストはソフトウェアからは出来ません。そこで、厳密に正しいタイミングになっているかはわかりませんが、発光ダイオードをパラレルポートに繋いで目で見るテスト環境を整えました。ちなみに、こんな感じです。
物凄く適当に作りました。右から1bit目〜4bit目に対応してます。
以下のプログラムは発光ダイオードの様子を見ながら進めていきます。
ボタンを実際に押すのはCBtnOperatorです。CBtnOperatorは自分が押すボタンを知っている必要があるでしょう。そのために、CBtnOperatorクラスにボタン情報を登録するSetBtnCode関数を追加します。
BtnOperator.cpp void BtnOperator::SetBtnCode(BYTE btncode)
{
m_bBtnCode = btncode;
}
オペレータに対してボタンコードを与えるのはCPS2Controlerクラスです。これはコンストラクタでするのが妥当でしょうね。
PS2Controler.cpp CPS2Controler::CPS2Controler(DWORD para_port_address)
{
// ボタンオブジェクトを生成
for(int i=0; i<BTN_NUM; i++)
{
BYTE code = 0x1 << i;
m_spBtnOpe[BtnElemHash(code)].SetPtr( new CBtnOperator );
m_spBtnOpe[BtnElemHash(code)]->SetBtnCode( code );
}
}
少し技巧的です。codeには左シフトで作成したボタンのコードを格納します。それをハッシュコードで要素番号に直し、その配列へCBtnOperatorへのポインタを格納します。これで、各ボタンにBTN_CIRCLEなどの名前が付いたことになります。
作業はまだ続きます。各ボタンには勝手にポートを叩いてもらいたいのですが、Push関数とRelease関数にポートを直に書くのはちょっと気が引けます。そこでCBtnOperatorクラスを派生させてCParaBtnOperatorクラスを新設します。また、このクラスにボタン情報管理者であるCTecnicContクラスへのポインタを持たせて、叩くビット位置を委託します。大分にごちゃごちゃしてきました(^-^;
オーバーライドする関数は2つ、CParaBtnOperator::Push関数とRelease関数です。
BtnOperator.cpp void CParaBtnOperator::Push(unsigned int time)
{
m_pBtnManager->Push( m_bBtnCode );
UpdatePort(); // ポートを更新
}
void CParaBtnOperator::Release(unsigned int time)
{
m_pBtnManager->Release( m_bBtnCode );
UpdatePort(); // ポートを更新
}
UpdatePort関数内でポートを叩きます。
BtnOperator.cpp void CParaBtnOperator::UpdatePort()
{
_outp( m_PortAddress, m_pBtnManager->GetCurState() );
}
後はCPS2Contorlerクラスのコンストラクタで、CBtnOperatorオブジェクトを生成する代わりにCParaBtnOperatorオブジェクトを生成する変更、このオブジェクトにパラレルポートのアドレスを渡す変更を行えば、パラレルポートをようやく叩くことが出来るようになります。
PS2Controler.cpp CPS2Controler::CPS2Controler(DWORD para_port_address)
{
m_dwPortAddress = address;
// ボタンオブジェクトを生成
for(int i=0; i<BTN_NUM; i++)
{
BYTE code = 0x1 << i;
m_spBtnOpe[BtnElemHash(code)].SetPtr( new CParaBtnOperator(para_port_address) );
m_spBtnOpe[BtnElemHash(code)]->SetBtnCode( code );
}
}
ふ〜〜〜〜〜〜。ごちゃごちゃですねぇ・・・(^-^;;。しかし、ご覧ください、感動の瞬間です!
テストコードを実行すると・・・
ビガっと同時に!
点きました〜〜〜〜!!
B 点灯バグ発生・・・
かなりしっかり点灯したので、ちょっと感動しました。しかし、何10回もテストを繰り返すと、こういう状況が極たま〜に発生します。
他の発光ダイオードが点かない事もあります。これは明らかにバグです。トレースして調べていくと、CPS2Controlerオブジェクトが保持するビット情報がこのまんま、つまり(00000111)2=8となっていました。本来は(00001111)2=16になって欲しいんです。
このテストでは○△□×ボタンすべてを1000ミリ秒後にPushさせています。多分、ビット情報の同時書き込み時に衝突が起こったために、正しく反映されなかったものと想像します。これを解決する方法は限られていて、一般的には「スレッドの排他処理」を行います。つまり、誰かが書き込んでいる最中である場合は、他の人の書き込みを待ってもらう処置です。
今回はスレッドの排他処理で比較的簡単なミューテックスを用いることにします。ミューテックスはある人が書き込み権限を持っている間、ほかの人はその権限が自分に回ってくるまで待たされます。ミューテックスハンドルを取得するにはCreateMutex関数を用います。
HANDLE mutex = CreateMutex( NULL, FALSE, NULL); |
こうすると、誰も権限を持っていない状態でミューテックスハンドルが生成されます。権限を持つには、WaitForSingleObject関数を用います。
WaitForSingleObject(mutex, INFINITE); // 誰かの書き込みが終了するまで待たされる // 排他的書き込み ... // 書き込み終了 ReleaseMutex(mutex); // 所有権を開放 |
今回の場合、直接書き込んでいる箇所はCTecnicContクラスのPush関数及びRelease関数です。よって、ここにミューテックスを設定します。
TecnicCont.cpp BYTE CTecnicCont::Push(BYTE btncode)
{
// ボタンのフラグを立てる
// 0番が使われていないことに注意
unsigned int bit = m_BtnBitAry[ BtnElemHash(btncode) ];
if(bit){
// 書き込み権限を取得
WaitForSingleObject(m_hMutex, INFINITE);
m_BtnFlags |= (0x1 << (bit-1) );
ReleaseMutex(m_hMutex);
}
return GetCurState();
}
これだけなので、楽なもんです。一応テストもボタンフラグ情報をチェックするようにします。
PS2ControlerTest.h // スタートしてから○、△、×、□を1000ミリ秒後にすべて押す
void test_C_B_T_R_Push()
{
// スタートしてから○、△、×、□を1000ミリ秒後に押す
CPS2Controler PS2C(0x378);
//....
assertEquals("test_C_B_T_R_Push", 1000, PS2C.Trace(BTN_RECT, 0).m_uiTime);
// ボタンフラグをチェック
// デフォルトでは(00001111)2=15が正解
assertEquals("test_C_B_T_R_Push", 15, PS2C.GetCurState());
}
これでテストを繰り返したところ、まったくエラーは起こらなくなりました。待つといっても極めて短い時間ですから、これまでのテストにもまったく影響は与えませんでした。すばらしいかな、ミューテックス。
B スタートしてから上下左右を1000ミリ秒後にすべて押すが、判定は上と左のみ(右下は無視)
これはゲームコントローラ特有の問題です。上と下は同時に押せません。押した時の行動は不定です。そういう情報をパラレルポートに出力するべきではないでしょう。そのために、上と下が同時に押されていたら上を優先して下を無効にするよう機能を追加します。
まずはテストコードです。
PS2ControlerTest.h // UP,DOWN,LEFT,RIGHT同時押しテスト
void testUDLRPush()
{
// UP,DOWN,LEFT,RIGHTが同時に押されている場合に
// UPとLEFTを優先するテスト
CPS2Controler PS2C(0x378);
PS2C.AddList(BTN_RIGHT, BTNTIMING(500, BTN_PUSH));
PS2C.AddList(BTN_DOWN, BTNTIMING(500, BTN_PUSH));
PS2C.AddList(BTN_LEFT, BTNTIMING(1000, BTN_PUSH));
PS2C.AddList(BTN_UP, BTNTIMING(1000, BTN_PUSH));
// 再生
PS2C.Start();
Sleep(750);
// データ取得
// この段階でRIGHTとDOWNがONなので
// GetBit関数の戻り値から状態を計算
int State = (0x1 << PS2C.GetBit(BTN_RIGHT))
+ (0x1 << PS2C.GetBit(BTN_DOWN));
assertEquals("testUDLRPush", State, PS2C.GetCurState());
Sleep(750);
// データ取得
// この段階でUPとLEFTが有効なので
// 状態を計算
State = (0x1 << PS2C.GetBit(BTN_UP))
+ (0x1 << PS2C.GetBit(BTN_LEFT));
assertEquals("testUDLRPush", State, PS2C.GetCurState());
// 終了
PS2C.Stop();
}
ちょっと長いテストコードで申し訳ありません。ポイントは太文字部分です。最初の500ミリ秒で右下が押されます。そして1秒後にさらに左上が押されます。1.5秒後にボタンの情報を取ると、仕様より上(UP)と左(LEFT)だけが有効になりますから、Stateにはそのビットが立っている値(正解)が取得されているはずです。
この段階でコンパイルすると、当然レッドシグナルが出ます。
デバッグ内容 ■■ testUDLRPush: 期待値は 80 でしたが、引数は 240 となり異なっています。
これは上下左右がすべて有効になっているためです。パラレルポートに書き込むデータを管理しているのは、CPS2Controlerクラス内のCTecnicContオブジェクトです。CTecnicCont::GetCurState関数を次のように変更します。
CTecnicCont.cpp // 現在のボタン情報を取得
BYTE CTecnicCont::GetCurState()
{
// 上と下、左右の同時押しは許可しないようにする。
// 押される場合は上と左を優先
BYTE UVAL = (0x1 << (GetBit(BTN_UP)-1));
BYTE DVAL = (0x1 << (GetBit(BTN_DOWN)-1));
BYTE LVAL = (0x1 << (GetBit(BTN_LEFT)-1));
BYTE RVAL = (0x1 << (GetBit(BTN_RIGHT)-1));
BYTE UD = (m_BtnFlags & (UVAL+DVAL));
BYTE LR = (m_BtnFlags & (LVAL+RVAL));
BYTE tmp = m_BtnFlags;
if(UD == UVAL+DVAL)
tmp &= 255-DVAL;
if(LR == LVAL+RVAL)
tmp &= 255-RVAL;
return tmp;
}
ごちゃごちゃとやってますが、上と下のフラグ、及び右と左のフラグが同時に立っているかを判定して、立っている場合は片方を降ろす処理をしているだけです。この変更後、テストはしっかりとグリーンになります。これで安全にパラレルポートから情報を送ることが出来そうです。
非常に長い道のりでしたが、一応これで自動操作ソフトウェアの根幹が出来ました。パラレルポートもバンバン叩けております。今後はCPS2Controlerクラスを中心に、ソフトウェアを磨く作業に移ることになります。最もよく考えなくてはいけないのは、キャラクタを動かすためにどのようなデータを必要とするかです。次の章では、キャラクタを動かす方法論について検討します。