はじめに
5年前に「C++Builder XE2+FireMonkeyで昔のラケットゲームを再構築してみる」という記事を執筆し、古いラケットゲームのソースを引っ張り出してきて、WindowsとMacの双方に対応したC++Builderの機能を使って、マルチOS対応のゲームアプリケーションを作成する手順を紹介しました。あれから5年が経過し、C++Builderも満20歳を迎え、Windows、Macだけでなく、iOS、Androidにも対応したマルチデバイス開発ツールへと成長し、モバイル開発もC++でできるようになっています。
ということで、再びこのラケットゲームのソースを取り出し、今度はiOS向けに再構築してみることにします。せっかくのモバイルアプリですから、サウンドやタップ操作などにも対応し、イケてる感じのレトロゲームに仕上げたいと思います。
ゲームの概要
ゲームは、左右に動くラケットで壁に跳ね返るボールを打つ簡単なものです。左右の方向ボタンとタップ操作でラケットを動かし、ボールを打ち返します。ボールを1回打つと得点が加算され、打ち損なうとラケットは没収です。ラケットは全部で5つ用意することにします。ちょっと懐かしいですね。
ボールは、打ち返すたびに加速するようにしましょう。速いボールを打てばそれだけ高得点が得られるようにします。最初のソースは、アスキー出版局で出版された『Borland C++Builder入門』(著・藤井等、監修・ボーランド)に掲載されています。こちらはWindowsのみですが、今回は、マルチデバイスアプリケーションの新規プロジェクトから再構築します。
ソースコード、関連情報は?
今回作成したアプリのソースコードは、GitHubで入手できます。
今回のターゲットプラットフォームはiOSなので、無料で入手できるC++Builder Starterではビルドできません。無料版で試してみたい方は、C++Builderトライアル版をダウンロードしてください。
ちなみにC++Builderを購入する場合には、SEshopで実施しているキャンペーンを確認されることをお勧めします。
マルチデバイスアプリケーションの作成
マルチデバイスアプリケーション構築に対応したコンポーネント
マルチデバイスアプリケーションの開発には、C++Builderに搭載されたFireMonkeyと呼ばれるコンポーネントライブラリを使います。
FireMonkeyは、Delphi/C++Builder XE2から搭載されたコンポーネントフレームワークで、Windows、macOS、iOS、Androidの4つのプラットフォーム向けのネイティブアプリケーションの構築に対応しています。従来のコンポーネントフレームワークはVCLでしたが、こちらもWindows 10に対応するなど進化を続けています。FireMonkeyは、マルチプラットフォームのサポートに加えて、HD/3Dの高品質なベクターグラフィックを簡単に利用できるので、今回のようなエンタテイメント系アプリでも凝った作りができたりします。
それでは開発を始めましょう。UIの設計は、以前作成したWindows、macOSのマルチデバイスアプリケーションの作成と全く変わらないことに驚かれるかもしれません。
マルチデバイスアプリケーション | 新規プロジェクトを作成
C++Builderで、マルチデバイスアプリケーションを作成するには、[ファイル|新規作成|マルチデバイスアプリケーション - C++Builder]を選択します。表示されたダイアログで「空のアプリケーション」を選択すると以下のように、マルチデバイスアプリケーションプロジェクトが作成されます。
表示されたフォームのプロパティを次のように設定します。
プロパティ | 値 |
---|---|
Name | GameForm |
Caption | Retrospective Racket Game |
このフォーム(ユニット)は、GameUnitという名前で保存します。プロジェクト名は、RacketGameで保存します。
図3を参考にPanelを配置します。上部のPanelは、レイアウト用のためデフォルトの名前で十分ですが、下部のPanelはコーディングでも使用するので、わかりやすい名前をつけておきましょう。
プロパティ | 値 |
---|---|
Name | Panel1 |
Align | Top |
プロパティ | 値 |
---|---|
Name | Court |
Align | Client |
アプリの外観を決める「スタイル」
カスタムスタイルの定義
コートとなるパネルの色を、黒にします。これにはスタイルを使います。一度定義したスタイルは、他のコンポーネントにも割り当てられます。スタイルを切り替えることで、作成したアプリケーションのルックアンドフィールを簡単に変更することができます。
では、スタイルを作成してみましょう。設計画面上でCourtパネルをマウスでクリックし、右ボタンを押して、表示されたポップアップメニューから[カスタムスタイルの編集]を選択します。
すると、スタイルデザイナが表示されます。スタイルデザイナを使えば、FireMonkeyスタイルの作成や編集、テストを簡単に実行できます。
それではCourt(TPanel)のスタイル編集開始です。スタイルデザイナ上では、図のようにCourtが選択されています。右側ツールパレットからTRectangleを中央のパネル上にドラッグ&ドロップします。
配置したTRectangleのプロパティ設定を、以下のように設定します。
プロパティ | 値 |
---|---|
Fill.Color | Black |
すると、図のように黒色のパネルが確認できます。
作成した黒い色のスタイルを他でも使用できるように、StyleNameプロパティを分かりやすい名前に変更しておきます。
プロパティ | 値 |
---|---|
Name | BlackPanel |
スタイルエディタの[×]ボタンを押し、設計画面に戻ります。
作成したスタイルをCourtパネルに適用するには、Courtパネルを選択し、オブジェクトインスペクタのStyleLookupプロパティを変更します。
プロパティ | 値 |
---|---|
StyleLookup | CourtStyle1 |
パネルを使ったレイアウト
パネルの上にパネルを置く
次に、Panel1の上にスコアや予備のラケットを表示するボックスを配置します。これらのボックスもPanelを用いて作成しますが、効率よく配置するために、レイアウト用のPanelを用いてその上に配置します。ツールパレットからPanelをドラッグし、Panel1の上でドロップします。このとき、構造ペインで、Panel2がPanel1の子項目となっていることを確認してください。
もし、Panel1の下にない場合は、構造ペイン上で、Panel2をドラッグしてPanel1上にドロップします。
正しい親子関係になっていることを確認したら、Panel2のプロパティを設定します。
プロパティ | 値 |
---|---|
Align | Right |
Panel2コンポーネントは右寄せになりますから、左のボーダーをマウスでドラッグして幅を調整してください。
Panel2の上にTPanelを2つ配置します。構造ペイン上で、Panel2の子項目に配置されていることを確認してください。
配置した2つのパネルに対して、以下のようにプロパティを設定します。
プロパティ | 値 |
---|---|
Name | ScreenPanel |
Align | Top |
StyleLookup | CourtStyle1 |
プロパティ | 値 |
---|---|
Name | RacketPanel |
Align | Client |
StyleLookup | CourtStyle1 |
StyleLookupに設定しているのは、先ほど作成したスタイルです。これで、色が黒に変わります。
START/STOPボタンを配置する
スタートボタンとストップボタンを作成するために使用するコンポーネントは、TSpeedButtonです。今回作成するiOSアプリにはあまり影響はありませんが、このコンポーネントはTButtonと違いフォーカスを得ることがありません。
SpeedButtonは、Panel1の上に配置します。図を参考にレイアウトを調整してください
プロパティ | 値 |
---|---|
Name | StartButton |
Align | Left |
StyleLookup | playtoolbutton |
プロパティ | 値 |
---|---|
Name | StopButton |
Align | Left |
StyleLookup | pausetoolbutton |
Enabled | False |
ゲームで使うアイテムを用意する
ラケットを配置する
ラケットを配置します。ラケットは、Labelコンポーネントです。ゲームに使用するラケットは、Courtパネルの上に、予備のラケットは、RacketPanel上に配置します。
Labelコンポーネントのデフォルト文字は黒色(背景は透過)なので、黒のCourtパネルの上に置くと、どこに配置したか分かりにくくなっています。
配置したLabelを選択した状態で、マウスの右ボタンをクリックし、「カスタムスタイルの編集」を選択して、スタイルエディタに切り替えます。
画面中央部のデザイン部にTRectangleを配置し、プロパティを設定します。
プロパティ | 値 |
---|---|
Aligen | Client |
Fill.Color | #FFFFC107 |
Text |
このLabelスタイルの設定を先ほど配置した5つのTLabelに設定していきます。
ついでにLabel1から5までのプロパティ設定も行います。
プロパティ | 値 |
---|---|
Name | Racket |
Height | 8 |
Width | 120 |
Anchors | [akLeft,akBottom] |
Position.Y | 400 |
StyleLookup | Label1Style1 |
Text |
メイン操作するラケットです。
プロパティ | 値 |
---|---|
Name | Racket1 |
Height | 8 |
Width | 50 |
Position | (10,16) |
StyleLookup | Label1Style1 |
Text |
プロパティ | 値 |
---|---|
Name | Racket2 |
Height | 8 |
Width | 50 |
Position | (10,72) |
StyleLookup | Label1Style1 |
Text |
プロパティ | 値 |
---|---|
Name | Racket3 |
Height | 8 |
Width | 50 |
Position | (10,128) |
StyleLookup | Label1Style1 |
Text |
プロパティ | 値 |
---|---|
Name | Racket4 |
Height | 8 |
Width | 50 |
Position | (10,184) |
StyleLookup | Label1Style1 |
Text |
すべてのラベルテキストは空にしておきます。以上でラケットの配置は終了です。
ボールを配置
ボールもLabelを用いて表現します。Courtパネル上にLabelを1つ配置します。ボールの色はラケットとは別の色にしたいので、別のスタイルを定義します。作成方法は先ほどと同様です。今度は、スタイル名をBallLabelとしたスタイルを作成し、Fill.ColorをRedに指定します。
プロパティ | 値 |
---|---|
Name | Ball |
Height | 8 |
Width | 8 |
StyleLookup | BallStyle1 |
Text |
ボールのラベルテキストも同じく空にしてください。
スコア表示
スコアの表示部分にもLabelを使います。スコアは、ScorePanel上に表示します。2つのLabelをScorePanel上に配置します(このLabelコンポーネントも、黒のScorePanelパネルの上に置くと、どこに配置したか分かりにくくなりますので注意してください)。
プロパティ | 値 |
---|---|
Name | ScoreTitle |
Aligen | Left |
Margins.Left | 10 |
Margins.Right | 10 |
TextSettings.FontColor | White |
TextSettings.Font.Size | 16 |
Width | 95 |
プロパティ | 値 |
---|---|
Name | ScoreLabel |
Aligen | Client |
Margins.Left | 10 |
Margins.Right | 10 |
TextSettings.FontColor | White |
TextSettings.Font.Size | 16 |
Text | 0 |
HorzAlign | Trailing |
このScoreLabel(TLabel)にスコア点が表示されますので、初期の文字は0が入っています。
これでデザインは完了です。ただ、スタイルについては少し注意が必要です。FireMonkeyのスタイルは、各OSごとに定義することができます。通常、各OSでデフォルトのスタイルが用意されており、WindowsならWindowsの、iOSならiOSの外観になるようにスタイルが切り替わります。先ほどから作成していたカスタムスタイルは、実はWindowsプラットフォーム向けに行っていたので、iOSやAndroidで表示しようとすると、それらのデフォルトスタイルが適用され、作成したStyleBook1のスタイルは適用されません。
StyleBook1を詳しく見てみると、DefaultとWindows 10 Desktopの2つが存在します。先ほどまで作成していたカスタムスタイルは、Windows 10 Desktopに対して行っていたので、ここでは、「0 - Default」を削除しWindows 10 Desktopがデフォルトになるようにします。
アプリケーションによっては、OS標準の外観にしたい場合と、アプリ固有の外観にしたい場合(特にゲームなど)があるでしょう。FireMonkeyの場合、デフォルトスタイルをどのように定義するかで、外観を自由にコントロールできるので便利です。
画面中央上にある「スタイル」ドロップダウンリストで、スタイルをiOSに切り替えてみてください。同じスタイルが適用されていることが確認できます。
ゲームのコードを記述するための準備
ラケットを操作するためのボタン
スマートフォンアプリ版では、ラケットを操作するためのボタンを画面下の左右に配置し、画面をタップしてラケットを動かせるようにします。Court上にTLayoutを配置しTSpeedButtonを2つ右側と左側に配置します。
さらに、Racket(TLabel)をなめらかに動かすためにRacket上にTFloatAnimationを配置します。
プロパティ | 値 |
---|---|
Name | FloatAnimation1 |
Duration | 0.1 |
PropertyName | Position.X |
StartValue | 0 |
StopValue | 0 |
Parent | Racket |
左右に配置したTSpeedButtonの名前をLeftButton、RightButtonに変更し、イベントハンドラからRacketを左右に動かすためのコードを記述します。
void __fastcall TGameForm::LeftButtonClick(TObject *Sender) { //ラケットを左に移動 if (Racket->Position->X > 0) { FloatAnimation1->StartValue = Racket->Position->X; FloatAnimation1->StopValue = Racket->Position->X -60; FloatAnimation1->Start(); } } void __fastcall TGameForm::RightButtonClick(TObject *Sender) { //ラケットを右に移動 if (Racket->Position->X <= (Court->Width - Racket->Width)) { FloatAnimation1->StartValue = Racket->Position->X; FloatAnimation1->StopValue = Racket->Position->X +60; FloatAnimation1->Start(); } }
変数の用意
ボールの角度を表すFAng、移動速度を表すFSpeed、そして一回の移動量を表すFDeltaX、FDeltaYです。これらの変数は、TGameForm のメンバ変数として、Unit1.hのTGameFormクラス宣言に追加します。また、FDeltaX、FDeltaYを再計算するCalcNewDelta()とボールを移動させるためのMoveBall()関数の宣言も追加します。
private: // ユーザー宣言 int FAng, FDeltaX, FDeltaY; double FSpeed{15}; void __fastcall MoveBall(); //ボールを動かす関数定義 void __fastcall CalcNewDelta(); //FDeltaX、FDeltaYを再計算する関数定義
プロパティの用意
スコアとラケット数管理はTGameForm上にプロパティを用意します。FScoreはスコア点数、FRacketはラケット数管理です。これもint型で用意します。
private: int FScore, FRacket;//スコア、ラケット残数 void __fastcall SetScore(const int NewScore); void __fastcall SetRacketCount(const int NewRacket); public: //スコアプロパティ __property int Score = { read = FScore, write = SetScore }; //ラケット残数プロパティ __property int RacketCount = { read = FRacket, write = SetRacketCount };
それぞれの関数の実態も記述します。
void __fastcall TGameForm::SetScore(const int NewScore) { if ((FScore = NewScore) < 0) FScore = 0; ScoreLabel->Text = Format(L"%0.9d", ARRAYOFCONST((FScore)) ); } void __fastcall TGameForm::SetRacketCount(const int NewRacket) { if (FRacket != NewRacket) { (NewRacket > 5)?FRacket = 5:FRacket = NewRacket; if (FRacket < 0) FRacket = 0; for (int i = 0; i < 4; i++) { dynamic_cast(FindComponent("Racket" + IntToStr(i+1)))->Visible = (i < FRacket-1); } } }
ゲームのコードを記述する
ラケットとボールのアタリ判定
ラケットにボールが当たったかの判定は、ボールの現在位置と次の位置を結ぶ線と、ラケットが交差するかどうかで判別します。Y座標がラケットの位置のときのボールのX座標を求めるには、FDeltaX、FDeltaYを利用します。
CheckHit関数は、ラケットでボールをヒットしたかどうかの判別を行う関数です。引数のox、oyはボールの移動前の座標です。nx、nyは異動後の座標で、ラケットに当たったときには、ラケット上の座標に変更されます。Position型のr変数は、ラケットの座標を表します。この関数は、ラケットに当たったときはtrueを返し、当たっていないときはfalseを返します。
//CheckHitラケットの当たり判定定義 bool __fastcall CheckHit(const int ox, const int oy, int &nx, int &ny, const TPosition *r); bool __fastcall TGameForm::CheckHit(const int ox, const int oy, int &nx, int &ny, const TPosition *r) { int ncx; if (oy < ny) { // ボールが下に向かって動いているとき // ボールがラケットの Y座標を通過したか? if (oy < r->Y && ny >= r->Y) { // ラケットとボールの奇跡の交点のX座標を求める ncx = ox + (int)((double)FDeltaX * (double)(oy - r->Y) / (double)(oy - ny)); // 交点のX座標がラケットの矩形内にあるか? if (ncx > r->X - Ball->Width && ncx < r->X + Racket->Width) { // 交点の座標を移動後のボール座標にセット nx = ncx; ny = oy + (int)((double)FDeltaY * (double)(oy - r->Y) / (double)(oy - ny)); return true; } } } else { // ボールが上に向かって動いているとき // ボールがラケットの Y座標を通過したか? if (oy < r->Y + Racket->Height && ny >= r->Y + Racket->Height) { // ラケットとボールの奇跡の交点のX座標を求める ncx = ox + (int)((double)FDeltaX * (double)(oy - (r->Y + Racket->Height)) / (double)(oy - ny)); // 交点のX座標がラケットの矩形内にあるか? if (ncx > r->X - Ball->Width && ncx < r->X + Racket->Width) { // 交点の座標を移動後のボール座標にセット nx = ncx; ny = oy + (int)((double)FDeltaY * (double)(oy - (r->Y + Racket->Height)) / (double)(oy - ny)); return true; } } } return false; }
壁音とラケット音を作る
スマートフォンアプリならではの拡張として、今回は、壁とラケットにヒットした時に音が鳴るようにします。
//MP3サウンド再生関数定義 TMediaPlayer* __fastcall beep_start(const String& fname); static constexpr const wchar_t* racket_mp3 {L"racket.mp3"}; static constexpr const wchar_t* wall_mp3 {L"wall.mp3"}; TMediaPlayer* __fastcall TGameForm::beep_start(const String& fname) { #if defined(_PLAT_IOS) || defined(_PLAT_ANDROID) auto mp = new TMediaPlayer(this); mp->FileName = System::Ioutils::TPath::GetDocumentsPath() + System::Ioutils::TPath::DirectorySeparatorChar + fname; return mp; #else return nullptr; #endif }
TMediaPlayerを使ってbeep_start()関数をコールすると、パラメータ文字列ファイル名のmp3音が再生されます。
ラケットロストとゲームオーバー関数の実装
ボールがラケットからロストした場合のLostRacket()関数とゲームオーバー時のGameOver()関数のコードを記述します。
//ラケットロストとゲームオーバー関数定義 void __fastcall LostRacket(std::functionfunc1); void __fastcall GameOver(); void __fastcall TGameForm::LostRacket(std::function func1) { // ラケットの大きさを保管 const int orgWidth = Racket->Width; const int orgHeight = Racket->Height; TThread::CreateAnonymousThread([this, func1, orgWidth, orgHeight](){ // ラケットを少しずつ消す for (int i = 0, w = orgWidth/2; i < w; i++) { TThread::Synchronize(TThread::CurrentThread, [this, i, w, orgWidth, orgHeight](){ Racket->Width = Racket->Width - 2; Racket->Height = (orgHeight * (w - i))/ w; Racket->Position->X = Racket->Position->X + 1; }); Sleep(20); // 20msec待つ } Sleep(100); // 100msec待つ // ラケットを元の大きさに戻す TThread::Synchronize(TThread::CurrentThread,[this, orgWidth, orgHeight, func1](){ Racket->Width = orgWidth; Racket->Height = orgHeight; // ラケットを中央に配置 Racket->Position->X = (Court->Width - orgWidth) / 2; func1(); }); })->Start(); } void __fastcall TGameForm::GameOver() { // タイマーの停止 Timer1->Enabled = false; // ボールを非表示にする Ball->Visible = false; // ボタンの状態変更 StartButton->Enabled = true; StopButton->Enabled = false; // ゲームオーバーのメッセージ ShowMessage("Game Over"); }
今回は実装しませんでしたが、ゲームオーバーでも同様に音楽が鳴るようにしてもいいですね。
ボールを動かすコード
タイマーを使いボールを動かす
ボールは、Timerコンポーネントを用いて一定間隔で移動させます。ツールパレットの「System」カテゴリから、TTimerコンポーネントをフォーム上に配置します。
プロパティ | 値 |
---|---|
Enabled | False |
Interval | 10 |
TTimerは、Intervalプロパティで指定した間隔(ミリ秒)で、OnTimerイベントを呼び出します。ここにボールを移動させるコードを記述します。
//Timer1のイベントハンドラ void __fastcall TGameForm::Timer1Timer(TObject *Sender) { MoveBall(); } //ボールを移動する関数コール void __fastcall TGameForm::MoveBall() { // 移動後のボールの位置を計算します int x = Ball->Position->X + FDeltaX; int y = Ball->Position->Y + FDeltaY; TPosition *pos = Racket->Position; // ラケットの座標 auto f1 = [this](const int inp, const int in_ang, String mp3name)->int { auto beep = beep_start(mp3name); FAng = in_ang - FAng; // 跳ね返った角度を計算 CalcNewDelta(); // 新しい移動量を計算 if (beep != nullptr) { beep->Play(); } if (inp < 0){ return 0; }; return inp; }; // ラケットにヒットしたかどうかを判定 if (CheckHit(Ball->Position->X, Ball->Position->Y, x, y, pos)) { if (x < pos->X + 2 || x + Ball->Width > pos->X + Ball->Width - 2) { f1(0, 360, racket_mp3); FAng += (Random(60) - 30); // 角で引っかけた } else f1(0, 360, racket_mp3); ++Score; Ball->Position->X = x; Ball->Position->Y = y; return; } //壁に当たったかの判定 if (y + Ball->Height >= Court->Height) { // 下の壁 Timer1->Enabled = false; LostRacket( // ラケットを消す [this]() { RacketCount--; // = RacketCount - 1; if (RacketCount == 0) { GameOver(); // ゲームオーバー } else { // 次のラケットを使ったゲームの準備 FSpeed += 0.4; Ball->Position->X = Court->Width / 2; Ball->Position->Y = Court->Height / 3; FAng = Random(45) + 90; CalcNewDelta(); Timer1->Enabled = true; } }); return; } else if (y < 0) { // 上の壁 FSpeed += 0.1; // 上の壁に当たると少し早くなります y = f1(y, 360, wall_mp3); } if (x + Ball->Width >= Court->Width) { // 右の壁 x = f1(Court->Width - Ball->Width, 182, wall_mp3); } else if (x < 0) { // 左の壁 x = f1(x, 182, wall_mp3); } // ボールの位置を変更 Ball->Position->X = x; Ball->Position->Y = y; }
スタートボタンとストップボタンイベント
デザイン画面からStartButtonとStopButtonのOnClickイベントハンドラを作成し、コードを記述します。ここには、スコアやラケット数の初期化をするためのコードを追加します。
//スタートボタンイベントハンドラ void __fastcall TGameForm::StartButtonClick(TObject *Sender) { // 乱数の初期化 Randomize(); // ボールの最初の角度を設定 FAng = Random(45) + 90; // スコアとラケット数の初期化 Score = 0; RacketCount = 5; // ボールの初速 FSpeed = 15.0; // ボールの最初の位置 Ball->Position->X = Court->Width / 2; Ball->Position->Y = Court->Height / 3; // 移動量を計算 CalcNewDelta(); // ボールを表示 Ball->Visible = true; // タイマーを動作させる Timer1->Enabled = true; // ボタンの状態変更 StartButton->Enabled = false; StopButton->Enabled = true; } //ストップボタンイベントハンドラ void __fastcall TGameForm::StopButtonClick(TObject *Sender) { // タイマーの停止 Timer1->Enabled = false; // ボタンの状態変更 StartButton->Enabled = true; StopButton->Enabled = false; }
以上でコーディングは終了です。
配置と実行
MP3サウンドファイルの配置
さて、アプリをビルドする前に、iOSにMP3サウンドファイルを配置するように設定しなければなりません。サウンドファイルは壁音用「wall.mp3」とラケット音用「racket.mp3」の2つです。
メニューから[プロジェクト|配置]を選択すると図のように配置ファイルリストが表示されます。ツールバーの[ファイルの追加]アイコンをクリックすると、ファイル選択ダイアログが表示されるので、「wall.mp3」と「racket.mp3」を順番に選択して追加します。
その後、リストに加えられた新しい2つのサウンドファイルの「リモートパス」を指定します。指定する名称は「SetUp\Documents\」です。
アプリを実行する
作成したアプリをiPhoneで実行するには、PAServerという機能を使います。iOSアプリの場合、ビルドに必ずMacマシンが必要です。MacマシンにPAServerをインストールしておくことで、C++BuilderのIDEから、Macにアプリを転送し、このMacに接続されたiPhoneへとアプリを配置、起動します。
つまり、IDEからは、実行ボタンをクリックするだけで、ビルド、転送、配置、実行(必要に応じてデバッグも)できるのです。次のように、iPhone上でアプリが実行されます。
以上でiOS向けのラケットゲームは完成です。C++Builder + FireMonkeyなら、同じアプリを容易にAndroid向けにもビルドできます。