RealBasic University

このコラムはStone Table Softwareのオーナーであり、またREALbasic Developerの編集者でもあるMarc Zeedar氏により書かれたものを、著者の許可を得て翻訳したものです。この翻訳はHREM Researchにより提供されています。この日本語版へのご意見はRBU-Jまでご連絡下さい。

URL: http://www.applelinks.com/rbu/019/
INDEXに戻る

REALbasicアニメーション:スプライトIII

今週、私たちはスプライトアニメーションを使用する単純なゲームShooting Gallery を終了するでしょう。先週のレッスンでは、スプライトサーフェイスコントロールおよび少数のメニューとカスタムしたクラスを加えたプロジェクトを作成しました。今日は、私たちは少数のメソッドを書き実際のアニメーションを制御するコードに入力していくつもりです。

最初に、私たちは初期化のメソッド、initSprites、から始めましょう。このメソッドは単純ですが不可欠です:それは砲台スプライト、鴨のスプライトを作ります、そしてプレーヤーの砲台の初期の位置を設定します。

これがコードです。これを新しいinitSpritesメソッドの中に書きます(ここでメソッドを作るには、Window1のコードエディタを開くために、Option-Tabを押しながらWindow1を選んで、Editメニューから"New Method"を選びます)。

   dim i, x, y as integer 
//
// ここでは10羽の鴨を生成します。これは起動後とに一回だけ
// 呼ばれます。
//

numberOfDucks = 10
redim duckSprite(numberOfDucks)
for i = 1 to numberOfDucks
// 新しい鴨のスプライトの生成
duckSprite(i) = new duckClass
next // i

// 戦艦のスプライトを生成し、底から50のピクセルで水平方向の中央
// に設定します
shipSprite = SpriteSurface1.NewSprite(shooter, spriteSurface1.width \ 2, spriteSurface1.height - 50)

// 戦艦のグループはゼロです(それはどれとも衝突しません)
shipSprite.group = 0

このルーチンは何をしているのでしょうか?我々は最初にnumberOfDucks変数を10に設定しました。鴨の数を変数で設定することで、一箇所の変更でプログラム全体の設定を変更することができます。この変数を2や50や100に変更してプログラムを走らせ、どのようになるか実験してみてください。

それぞれの鴨に関する情報は配列duckSpriteに格納されます。配列の各要素は、実際のスプライトに加えて、各鴨がどの方向へ動いているか、鴨のスピードなどを記憶しているプロパティを含んでいるduckClassオブジェクトのインスタンスを保持します。duckClassはオブジェクトですので、それを使用する前に、我々はnewコマンドで新しいものを作成しなければなりません。我々はアプリケーションの開始時にそれを一度だけ実行するようにしたいので、このメソッドの中でそれを行い、ゲームの変数(それらは各ゲームでリセットされる)を初期化するためには別のinitGameメソッドを使います。我々はここで実際に鴨のスプライトを作っているのではなく、後で実際のスプライトを格納する鴨のコンテナだけを作っているということ覚えておいて下さい。

追記

振り返ってみると、duckSpriteの各要素は実際にはスプライトではないので、duckSpriteという名前は良い名前ではありません。それはそのなかにスプライトを含む我々のカスタムクラスであるduckClassのインスタンスです。duckClassArrayがより正確だったでしょう.

さらに、我々のshipSprite変数がスプライトであることに気づくかもしれません。しかし、それは実際に船ではありません:gunSpriteがよりよい名前になっていたでしょう。それは即座にコード化する場合、よく起こることです。このプログラムについては、それはたいしたものではないかもしれませんが、より複雑なプログラムの場合、それは混乱を招きます。 私は、あなたが変数およびメソッドの名前を付ける際に良く注意して名付けること、また後々になって一つが不正確な名前であることを見つけた場合、プログラムの全体にわたる変数名を変更するためにREALbasicの検索・置換機能を使用することを強く勧めます。

次に、我々はプレーヤーの砲台に相当するスプライトであるshipSpriteを作成します。newSpriteメソッドはスプライトサーフェイスオブジェクトの一部です:それは、特定のスプライトサーフェイスにスプライトを関連させます(スプライトサーフェイスはスプライトが動き回るところと考えて下さい)。newSpriteコマンドの一部として、我々は、スプライト(この場合は砲台)に使用する画像((この場合には砲台)をその初期位置の座標(この場合、spriteSurface1の底から50のピクセルで水平方向の中央)を指定しなければなりません。

我々の最終ステップはshipSpriteグループを割り当てることです。グループはスプライトのユニークな特徴です:それによりどのスプライトがお互いに衝突可能かが設定できます。shipSpriteをグループ0(ゼロ)にセットすれば、それはどれとも衝突しません(他のスプライトがそれに接触しても、それは単に横切るか、それの陰に隠れ、衝突イベントは生じないでしょう)。鴨を取り扱うときに、さらにグループについて説明します。

我々はinitSpritesを終わりましたので、新しいメソッドinitGameを作りましょう。このルーチンはユーザがFileメニューから"NewGame"を選んだ時に必ず呼ばれます。それはゲームの全ての変数をリセットします。例えば、鴨たちはランダムに配置され、可視状態に設定され、スコアはゼロにセットされます。

 
   dim i, x, y as integer 

//
// このルーチンは全てのスプライトをその初期設定(位置、速度、
// 方向など)に初期化します
//


// 開いているスプライトを閉じます
for i = 1 to numberOfDucks
if duckSprite(i).theSprite <> nil then
duckSprite(i).theSprite.close
end if
next // i

for i = 1 to numberOfDucks
// 鴨の水平および垂直位置の設定
x = rnd * spriteSurface1.width
y = ((rnd * 5) + 1) * 40

duckSprite(i).theSprite = SpriteSurface1.NewSprite(duck, x, y)

// 速度を乱数で1 から 6 (ゼロから 5 プラス1)に設定
duckSprite(i).speed = round(rnd * 5) + 1

// その他の初期値の設定
duckSprite(i).visible = true
duckSprite(i).exploding = 0

// それぞれの鴨に固有のグループ番号を鴨lのスプライト配列の添字
// に対応するように設定する
duckSprite(i).theSprite.group = i

// 鴨の方向(右または左)を乱数で設定
x = round(rnd)
if x = 0 then
duckSprite(i).direction = true
else
duckSprite(i).direction = false
end if
next // i

// 得点およびその他の設定の初期化
gameOver = 0
score = 0
speed = 1
direction = 1
buildup = 1
drawScore

我々が最初に行うことは既存の鴨スプライトを閉じることです。スプライトを閉じるということは基本的にそれを消滅させる(それをnilにセットする)ことです。duckSprite要素と実際の鴨スプライト(duckSprite(i).theSprite)の間の重大な差に注意してください:鴨のスプライトを消滅させることはduckSpriteオブジェクトを消滅させません。これがは変数の貧弱な命名が事態を混乱させるというもう1つのいい例です。

なぜ我々は鴨スプライトを消滅させるのでしょうか。そうですね、我々は、プレーヤーが新しいゲームを始める前にすべての鴨を撃ち落としたことを保証することができません:このループにより、すべての鴨を本当に消滅させ、新しいゲームのための新しい鴨を生成することができるようになります。我々がこれをしない場合には、新しいゲームがスタートした時、そこにはまだスクリーン上に古い鴨が残ってしまっているでしょう! 我々は新しい鴨に関連したスプライトを動かしますので、古い鴨は移動しくなるでしょう:古いスプライトはまだそこにあるでしょうが、.theSpriteプロパティは今新しいスプライトを指し示していますので、我々は古い鴨をコントロールする手段がありません。

(あなたがこれを確かめたい場合には、行の先頭にアポストロフィ ' を置き.closeを注釈行にして、プログラムを実行してみましょう。)

これは次のようにも考えられます:.theSpritenewSpriteに割り当てることは既存のスプライトを置換えることではありません。それは、.theSpriteが古いものではなく新しいスプライトを参照することを単に意味します。古いスプライトは、それが閉じられていない場合には、まだ存在しているのです。

このための我々の手続きは単純です:我々は、for-nextループで全ての鴨を巡回して、各鴨のスプライトを調べ、それが存在するかどうか(nilであるかどうか)確かめます。もし、スプライトが存在すれば、我々はそれを閉じます

ユーザがはじめて"NewGame"を選んだ時には全てのスプライトはnilですので、この部分はその状況では何もしないということに注意してください。

スプライトがみな死んでしまえば、我々は新しいものを作成することができます。それは単純です:numberOfDucksだけ別のループをまわし、各鴨に任意の位置および鴨の画像を割り当てます。さらに、我々は各鴨に任意の方向(左か右)、任意の速度(1〜6:鴨がアニメーションの各フレームで移動するピクセルの数)を割り当てます。また、我々は、鴨を可視状態で、爆発していないようにします(.explodingプロパティを0に設定します)。

今、ここに、問題があります。2つのスプライトが衝突する場合、REALbasicは関与する2つのスプライトを教えてくれますが、どのduckClassオブジェクトが撃たれたかということを伝えてくれません。しかし、duckClassオブジェクトは我々が鴨に関する情報をすべて保持している場所です:ですから、我々は、実際に打たれたスプライトからduckClassオブジェクトを得る方法が必要です。これはどのようにすれば良いでしょうか。

答えは少し巧妙ですが単純です。我々は、duckSprite配列の添字番号をしめす特別のプロパティを備えたプライトクラスを作成することも可能ですが、私はその代わりにスプライトの持っているグループ属性を使用しました。私たちは各鴨にユニークなグループを割り当てます。グループ番号は鴨のduckSprite配列の添字番号と一致させます:そうすれば、我々は常に鴨のグループ番号を見つけることができますので、それにより我々はどの鴨が撃たれたかを容易に知ることことが可能になるでしょう。例えば、もし7番の鴨が撃たれれば、そのグループ番号は7となり、私はduckSprite(7)が撃たれた鴨であることが判ります。

便利でしょう? 我々はグループ属性を2重の目的で使用しています!

しかしながら、これは小さな問題を引き起こします:異なる正の数を持っているグループはすべて相互に衝突しますので、我々は衝突ルーチン内で鴨の衝突(鴨同士の墜落)を無視するためのチェックが必要となるでしょう。それは十分に単純です--ただ、忘れずにそれをしなければなりません。

鴨を初期化した後、我々はゲームおよびプレーヤーの「船」(砲台)に対するいくつかの初期値をセットします。それだけです!

我々が加える最後のメソッドはdrawScoreメソッドです。これは非常に単純です:Window1graphicsポートに描画するだけです。先ず、既存のスコアを削除するために最初にclearRectを使用します。次に、フォント、色およびテキスト・サイズをセットします。その後、我々はスコアを描きます。

 
   // 得点の表示
window1.graphics.clearRect(0, 0, 200, 48)
window1.graphics.foreColor = rgb(0, 0, 255) // blue
window1.graphics.textFont = "Geneva"
window1.graphics.bold = true
window1.graphics.textSize = 18
window1.graphics.drawString("Score: " + str(score), 20, 48, 200)

アニメーションのコーディング

Shooting Galleryの最も重要な部分はSpriteSurface1の中で起こることです。一旦、我々がspriteSurface1を開始(.runメソッドで)したならば、それは止められる(ユーザがマウスをクリックするか、ゲームが終了する)まで走り続けます。すべてのアニメーションおよびゲームの判断はspriteSurface1の内部で起ります。

最初に、背景の画像を初期化しましょう。以下のコードをspriteSurface1.Openイベントに入力しください:

   // REALbasicはIDE(属性ウィンドウ)で設定した背景を時々忘れてしまう
// ことがあるので、ここでプログラム的に設定する
me.backdrop = islandsunset

これは、単にspriteSurface1の背景にislandsunsetをセットします。コメントの中で説明しているように、私が画像をプロパティウィンドウで指定したときに、理由は不明ですが、REALbasicは背景を設定しなかったので、私はコードでその設定を行いました。私がプログラムを実行したら、それなしでは鴨は暗い背景上で飛びました。

spriteSurface1の最も重要な部分の1つは2つのスプライトが衝突する場合に起こることです。Collisionイベントに行って、次のコードを入力してください:

 
   //  
// ここに来た場合は、2つのスプライトが衝突したとき。
// どのスプライトかを判断して、適切な処理をおこなう
//

// ミサイルが発射されていて、衝突したスプライトの
// 一方がミサイルの場合
if missileLaunched and s1.group = -1 then
// 2つめのスプライトを爆発のグラフィックに設定し、
// それから鴨のスプライト(死んだので)を隠して、
// 爆発のカウントを始める(これで、爆発を0.5秒間
// スクリーンに表示することができる)
s2.image = explosion
duckSprite(s2.group).visible = false
duckSprite(s2.group).exploding = 1

// 得点を加算する
score = score + 100
if soundOn then
boom2.play
end if

// ミサイルのスプライトを削除する
shot.close
missileLaunched = false
end if // missileLaunched

spriteSurface1.Collisionイベントが2つのスプライトを渡していることに気づいたでしょう:s1s2です。それらは衝突した2つのスプライトです。そこで、我々は、何が起きたのかを知るためにそれらを検査する必要があります。

まず最初に、我々はシステム全体で使用される倫理変数missleLaunchedをチェックします。我々は、ミサイルを発射する場合に、それをtrueに設定し、ミサイルが爆発するか、スクリーンから出て行く場合に、falseに設定します。ですから、それがfalse(trueでない)の場合には、我々はミサイルが空中にないことが判ります。ミサイルが飛んでいない場合には、我々は衝突について忘れることができます。起りえる別の唯一の衝突は互いに衝突する2羽の鴨によるものですが、もしそれが起こっても無視します。

我々がチェックしたい主要な衝突は鴨とミサイルの衝突です。したがって、我々は、最初のスプライトがミサイルかどうかもチェックします。我々はそれをそのグループを調べることにより行います:それが負値の場合(-1)には、それはミサイルであることが判ります。そうでない場合には、報告された衝突は2羽の鴨で、ミサイルは空中にあることが判り、我々はその衝突を無視することができます。

もし、s1.groupが-1で、ミサイルが発射されている場合、プレーヤーが鴨を撃ったことが判ります! ミサイルが鴨に当たった場合には、何が起こるでしょうか。そうですね、いくつかのことがあります。最初に、我々は2番目のスプライト(鴨)のグラフィックを爆発するものに変更します。我々はまた鴨の可視(visible)属性をfalseにセットします。(このようにしてもスプライトが不可視にならないことに注目しましょう:可視属性はduckClassの一部である我々の特別な属性です、それで、何も起こりません。我々は撃たれた鴨とそうでない鴨を見分けるために後でそれを使います。)

次に、我々はスプライトの爆発(exploding)属性を1にセットします。なぜ我々はそうするのでしょうか。

追記

さて、私が最初にShooting Galleryを書いた時、私のduckClass爆発属性を持っていませんでした。鴨が撃たれた時、私は単にイメージを爆裂に変更し、次にスプライトを削除していました。しかし、問題がそこで起こりました:スプライトサーフェイスのframeSpeedの設定に依存して、あなたのアニメーションは恐らく毎秒30フレームか、あるいはそれ以上で描かれています。それはnextFrameが、毎秒30回実行されていることを意味しています!

その速度で、爆発のグラフィックはスプライトが削除される前にほんの瞬間表示されるだけでした。目でそれを見るには文字通りに速すぎました。プレーヤーを考える限り、射撃が目標に当たるということは両方のスプライトがただ消えることを意味しました。これではよくありません。

私の解決方法は各鴨に整数変数 -- 爆発属性 -- を加えることでした。それにより打たれてからどれくらいの時間が経ったかを記録できるようになりました。最初の一撃で、爆発が1にセットされます。その後のすべてのフレームにおいて、爆発が1だけ増加されます。私は30フレームが適当だと決定しました:30フレームで爆裂は消えます。

ゲームをやりやすくより楽しくするのはこのようなこまかな調整です。それはプログラマとしてのあなたにとって余分な仕事ですが、そのように考えてはいけません:プレーヤーの観点から物事を見なければいけません。単に目標が消えてしまうのは数フレームにわたって爆発するのに較べて面白くありません。

実際、爆発をより巧みにすることによりShooting Galleryをさらによくすることができるでしょう。例えば、少し異なる位置に炎のある別のグラフィックを爆発に追加することで、炎を点滅しているように見せることができるでしょう。あなたがさらにやりたければ、一連のグラフィックスを作って、爆発が小さく始まって、大きくなり、ゆっくりと消えていくようにすることも可能でしょう。あるいは、あなたがバイオレンスがお好みならば、鴨が爆発して羽根が飛び散るのを見たいことでしょう!

どの場合にも、あなたは爆発属性を爆発アニメーションのどの段階かを知るためにカウンターとして使用することができるでしょう。

スプライトの新しい特性をセットしたならば、プレーヤーのスコアを増加させて、もし適切であれば、爆発音を鳴らします。

我々の最終ステップは、ミサイルのスプライトを閉じて取り除き、missileLaunched変数をfalseに戻します。

いいですか、これで今週のやることの全てです。必要なすべての画像およびサウンドを含む完成したプロジェクト・ファイルがダウンロード可能です。しかし、来週、残りの少しのコードについて詳しく説明します。

次週

ShootingGalleryの最も複雑な部分(nextFrameイベント)のコードを完成し、プロジェクトを終了します。

Letters

ライアンと私は、Shooting Galleryに関して2つの手紙を交換しました。彼はMac OS Xでプログラムを実行しようとしましたが、それは動作しませんでした。私はOS 9ではうまく働き、それは恐らく、Mac OS Xでのスプライトのバグによるものだろうと指摘しました(私は、少数のバグがあると聞いています)。彼は次のような返事をくれました:

Marcさん、

あなたは正しいに違いありません。あのクラッシュに関係してはRBまたはOSのXにバグがあるに違いありません。それは9.1ではうまく動作しました(RB 3.2.1のカーボン・アプリケーションは何らかの理由で"Open in Classic"チェックボックスがありませんので、Classicでテストすることはできませんでした)。恐らく、スプライトサーフェイスが再描画されるときに、スプライトイメージを置換える何かなのでしょうか? それは多分修正されるでしょう;RBのスプライトアニメーションにとって良い兆候ではありません。

1つの提案:RB 3のドキュメント(言語レファレンス)では、単にスプライトサーフェイスへコントロールを渡し、キーが押されるのを監視する代わりに、繰り返し更新(Update)を呼ぶという新しく改善されたアニメーションのメソッドに言及しています。これはより正確なフロー制御を可能になるように思えますが、それはまた、さらにこの問題を解決するのではないでしょうか? 恐らく、自動的に更新される場合と更新が呼ばれる場合のスプライトの更新は異なってくるでしょう。恐らく、RBは実行を軽視しており、コントロールを放棄する必要がない場合にさえ、あなたに空のタイマーを使用してほしいと考えているのではないでしょうか。単なる思いつきですが。

アニメーションをコントロールするためにタイマーを使用するというREAL Software社の提案は明らかにスプライト・アニメーションの新しい方向です。古いやり方ではバグが多くMac OS Xではより信頼性がないのではないかと私は思っています。恐らく更新updateを使用することであなたの問題は解決されるでしょう。

アニメーションのフレームイベントの間には他のことが起こりえますので、タイマーは確かにプログラマにより多くのコントロールおよび柔軟性を与えます(現在のところ、あなたが実行Runコマンドでスプライトサーフェイスにコントロールを引き渡すと、それは完全に制御が引き渡されてしまいます)。

私はアニメーションの更新の手法を用いたデモを考えましたが、読者の練習問題としてそれを残そうと決断しました。一つの理由はspriteSurface1.updateコマンドにタイマーを付け加えて、明示的にspriteSurface1.runを呼ばないようにすることは困難ではないだろうということです。また、他の理由は、Shooting Galleryは私が古い方法を使用して既に完成したプロジェクトであるということです。

みなさんが本当に興味を持っている場合は、私に連絡してください。そうすれば、私は新しい更新手法を使用したShooting Galleryのバージョンを公表することも考えます。


RBU-Jの通知サービス!コラムが発表されるたびに日本語版REALbasic Universityのお知らせの emailがあなたに届きます。登録・削除は ここ から。

INDEXに戻る