いままで作ったプログラムは、ShowMessage位の手続きしか使わないファミコンとかのゲームとは程遠いものでした。そろそろみんなゲームプログラミングしたいと思っている頃だと思うので、ゲームプログラミングを始めましょう。
とりあえず最初はキャラを敵から逃がすだけの「避けゲー」をつくります。新しいコンポーネントとかが次々と出てくるので注意してください。
ゲームを作るためにはキャラを表示させる必要があります。キャラが表示されなかったら風のリグレット(←暇があったらぐぐってみよう)になってしまいます。
で、キャラを表示させる一番簡単な方法がTImageを使うことです。TImageを使うと画像を読み込むだけでキャラの表示ができます。(これ、実は他言語だと可也難しい作業なんですよね…)
TImageはコンポーネントパレットの「Additional」の左から六番目にあるです。これを今までと同じように貼り付ければ、四角形が表示されるかと思います。
そして貼り付けた「Image1」のPictureプロパティのところをダブルクリックしてください。
が表示されるので、「読み込み」を押して適当な画像を読み込んでからOKボタンを押せば画像が表示されます。
画像を読み込んだ後は、Image1のAutosizeプロパティをTrueにすればWidthが画像の横幅に、Heightが画像の縦幅に設定されます。
これからこのキャラは「自機」として話をすすめるので、わかりやすくするためにnameプロパティを「ziki」にしておいてください。
あなたは何故アニメにおいてキャラが動いて見えるのか知っていますか?あれは、少しずつキャラを微妙に動かした静止画を連続して見せているからキャラが動いて見えるのです。
ゲームにおいてキャラが動いて見えるのも同じです。毎ループ毎にキャラの位置を少しずつ動かしているのです。
さて、ちゃんとアニメーションするためには「このImageをこれだけ動かす」という命令を一秒間に何回も実行する必要があるわけですが、今まで「何もしなくても常に命令の実行を繰り返す」というイベントはありませんでした(OnClickイベントはクリックした時にしか実行されなかったし、OnCreateイベントは起動時にしか実行されませんでしたね)。
ここでTTimerの出番となります。TTimerのOnTimerイベントを利用すればキャラに動きをつけることができるのです。
コンポーネントパレットの「System」からを貼り付けてください。位置はどうでもいいです。
次に、貼り付けたTimer1のIntervalプロパティを50あたりにしてください。このIntervalプロパティは「1秒を1000とするとどれくらいの間隔で実行するか」というものです。50だと0.05秒に一回実行されます。ちなみに、子の作業を忘れるとキャラの動きが異常に遅くなります。
いじったら、OnTimerイベントのところをダブルクリックして
procedure TForm1.Timer1Timer(Sender: TObject);
begin
ziki.top:=ziki.top + 5;
end;
こう書いて実行してください。絵はどのように動いたでしょうか。動かない人は多分Timer1のIntervalプロパティをいじるのを忘れています。
この「Top」というプロパティは、zikiの上端のy座標を表しています。ちなみに左端を表すプロパティは「Left」です。
ここで、ある程度数学の知識を持った人ならば、少し疑問に思うと思います。なぜなら数学における座標はy座標が増えるほど上に行くからです。
これは、プログラミングの際使われる座標の種類が数学のとは違う「スクリーン座標系」であるからです。コンピュータは描画を画面の上端から行うため、y座標の向きが逆になるのです。
さて、下に動かしたところで今度は右に動かしてみましょう。
procedure TForm1.Timer1Timer(Sender: TObject);
begin
ziki.left:=ziki.left + 5;
end;
タイトルのまんまです。構想を練ってみると
if <上キーが押されている> then ziki.Top:=ziki.Top - 5;
if <下キーが押されている> then ziki.Top:=ziki.Top + 5;
if <左キーが押されている> then ziki.left:=ziki.left - 5;
if <右キーが押されている> then ziki.left:=ziki.left + 5;
こうなります。これを実装するためには、各キーが押されているかどうか調べる必要があるのですが、どうすればいいのでしょうか。
キーの状態を調べるにはGetKeyState関数を使います。引数として仮想キーコードというのを渡すと、キーの状態を調べてくれます。キーが押されていれば戻り値は0以下になります。GetKeyState関数を使って構想を実現してみると
procedure TForm1.Timer1Timer(Sender: TObject);
begin
if GetKeyState(VK_Up) < 0 then ziki.Top :=ziki.Top - 5;
if GetKeyState(VK_Down) < 0 then ziki.Top :=ziki.Top + 5;
if GetKeyState(VK_Left) < 0 then ziki.left:=ziki.left - 5;
if GetKeyState(VK_Right) < 0 then ziki.left:=ziki.left + 5;
end;
VK_Downとか何?ってひとはヘルプで「仮想キーコード」と調べればなんとなくわかると思います。
なんかGetKeyStateとか長くて覚えられないよ!って人は使う時この文を写せばOKでしょう。
実行すれば、絵が動きます…うまく行けば。
今までのプログラムを実行すると気付くでしょうが、移動するたびに画像がぶれてしまいます。なぜ画像がブレてしまうのでしょうか? 理由として
というのがあります。とりあえず、この二つの問題を解決しましょう。
実は、画像の座標を直接いじるのをこれからやる方法で修正するといろいろ便利になります。
…とまあ、100利あって1害なしって感じなのです。どのようにするのかというと
なんか新しい関数が結構出てきて良くわからないと思いますが、実際書いてみると「あー…そういうことか」ってな感じになるのでまずは写して実行してみてください。
{$R *.dfm}
var
Dx,Dy:Extended;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
//移動量を予測する。
if GetKeyState(VK_Up) < 0 then Dy:=Dy - 5;
if GetKeyState(VK_Down) < 0 then Dy:=Dy + 5;
if GetKeyState(VK_Left) < 0 then Dx:=Dx - 5;
if GetKeyState(VK_Right) < 0 then Dx:=Dx + 5;
//移動予測量を切り捨てて代入(Ziki.TopとかはInteger型のため)
Ziki.Top :=Ziki.Top + Trunc(Dy);
Ziki.Left:=Ziki.Left+ Trunc(Dx);
//Dx,Dyを小数部分のみにする。これを忘れると速度がありえないくらい上昇しちゃいます
Dx:=Frac(Dx);
Dy:=Frac(Dy);
end;
ちなみに、運動予測量の代入とかの部分は最後に書いたほうがいいです。OnTimerの中間らへんとかに書くとあまりよくないです。
これでちょっと画像のブレが緩和されます…が、まだ画像のブレがあると思います。完全に画像のブレを無くすため、最後の「ダブルバッファリングをしていない」という問題を解決しましょう。
メモリ上に表示されない「裏」の画面のようなものを作ってその上にいろいろ描画を行い、その「裏」の画面を最終的に「表」の画面にコピーする画像のブレを無くすための技法です。
実は画像の表示はとても手間のかかる作業で、OnTimer部で画像を移動させる速度なんかよりずっと遅いのです。そのため、まだ書き途中でも表示されてしまう時がでてしまい、画像のブレが生じるのです。
なんだかんだいって、プレイヤーに完全に描画しきった画面だけ表示するようにすればブレがなくなるわけですよね?
ダブルバッファリングで表示されない画面をつくり、その上にまず完全に描画しきります。その完成した「裏」の画面を「表」の画面にコピーすれば、プレイヤーは完成した画面だけを見ることができるのです。
ここまでいろいろと難しい御託を並べてきましたが、実はたったForm1のOnCreateイベントに一行追加するだけでできちゃいます。
procedure TForm1.FormCreate(Sender: TObject);
begin
DoubleBuffered:=True;
end;
これだけで終わりなんです。実装する事自体はめちゃくちゃ簡単なんですね。ちなみにDoubleBufferedはBoolean型の変数で、Trueは「真」Falseは「偽」を表します。
ちなみに、ダブルバッファリングを行うと「裏」の画面をつくるために使用メモリ領域がめちゃくちゃ上がることにも注意する必要があります。
いままで「ziki.top」「ziki.left」などと書き続けてきましたが、これちょっとメンドクサイと思いませんか?
というか「Form1.Top」とかはいじる事がないわけで、「此処から此処までの『Top』『Left』は『Ziki』のものだよ」ということができればかなり楽になるわけです。
この役割をするのがwith文です。
with <親となるもの> do
begin
end;
こうすれば、beginとend;の間のものはある程度省略できるようになるのです。
今までのプログラムを修正してみましょう。
procedure TForm1.Timer1Timer(Sender: TObject);
begin
with ziki do
begin
//beginの後は2文字分インデントしたほうが見易い。
//移動量を予測する。
if GetKeyState(VK_Up) < 0 then Dy:=Dy - 5;
if GetKeyState(VK_Down) < 0 then Dy:=Dy + 5;
if GetKeyState(VK_Left) < 0 then Dx:=Dx - 5;
if GetKeyState(VK_Right) < 0 then Dx:=Dx + 5;
//移動予測量を切り捨てて代入(Ziki.TopとかはInteger型のため)
Top :=Top + Trunc(Dy);
Left:=Left+ Trunc(Dx);
//Dx,Dyを小数部分のみにする。これを忘れると速度がありえないくらい上昇しちゃいます
Dx:=Frac(Dx);
Dy:=Frac(Dy);
end;
end;
「あんま短くなってなくね?てかこれやった方がめんどくさくね?」と思うかもしれませんが、絶対やったほうがいいです。大規模なプログラムだと労力が全然違うし、小規模なものでも「Ziki」を「Mario」などと名称変更する必要あった場合修正が楽ですので。
というかこれから先変えさせるんですけどね…
今までのプログラムを実行すればわかると思いますが、キャラが画面外に飛び出してもそのまま移動し続けてしまう為、ちょっと修正する必要があります。
行き先の座標が画面の範囲外にあったら、動く分を無効にしてやればいいのです。この時、画面の右端に当たった時は必ずキャラが右に動いているということに注意してください。こういう「いまどっちむきに動いてるか」ってのが知れるからDx,Dyを使うと便利なんですよね…
procedure TForm1.Timer1Timer(Sender: TObject);
begin
with ziki do
begin
//移動量を予測する。
if GetKeyState(VK_Up) < 0 then Dy:=Dy - 5;
if GetKeyState(VK_Down) < 0 then Dy:=Dy + 5;
if GetKeyState(VK_Left) < 0 then Dx:=Dx - 5;
if GetKeyState(VK_Right) < 0 then Dx:=Dx + 5;
(******************こっから******************)
if (top + Dy < 0) and (Dy < 0) then Dy:=0;
if (top + Dy > form1.height - height) and (Dy > 0) then Dy:=0;
if (left + Dx < 0) and (Dx < 0) then Dx:=0;
if (left + Dx > form1.Width - Width) and (Dx > 0) then Dx:=0;
(***************ここまでが追加分*************)
//移動予測量を切り捨てて代入(Ziki.TopとかはInteger型のため)
Top :=Top + Trunc(Dy);
Left:=Left+ Trunc(Dx);
//Dx,Dyを小数部分のみにする。これを忘れると速度がありえないくらい上昇しちゃいます
Dx:=Frac(Dx);
Dy:=Frac(Dy);
end;
end;
こうすればキャラが画面の外に出るなんてことはなくなります。追加する位置に注意してください(移動量予測と代入との間に書いてください。)
GetKeyState関数はWin32APIの一つです。
ソフトウェアを開発する際に使用できる先人が作られた命令や関数の集合のことです。
Windows 95/98/Me/NT/2000/XPにてマイクロソフトが提供しているAPIです。これを利用すれば、キーが押されているか押されてないか判定できたり、CDトレイを開閉したり、マウスの位置を取得したりといろいろできます。
ためしにForm1のOnCreateにSwapMouseButton(True);
と書いて実行してみると、Win32APIの便利さが体感できるかも?
困ったら再起動してね