スーパードンキーコング・シリーズと言えば、レア社と任天堂による、SFCの傑作2Dアクションゲーム三部作です。今回はその"バナナ"や☆の獲得後の動きを再現してみました。
本シリーズのバナナは言わば、マリオにおけるコインのような存在で、100本集めると一機増えるというコレクトアイテムになっています。今回再現するのは、プレイヤーがバナナを回収した後に起きる動きです。
プレイヤーがバナナに触ると、バナナが画面の左上、つまり獲得数が表示されている部分に向かって飛んでいき、表示と重なると数が更新されるというものです。
- プレイヤーがバナナに触れる。
- バナナが左上に飛んでいく。
- アイコン表示部と重なると、消える。
- スコアが更新される。
マリオなどでは、コインが獲得後は消えてしまうことを考えると(獲得アニメはありますが)、特徴的な演出であると言えます。とても面白みのある動きです。
再現に向けて
まずは見た目から、です。画像を用意します。しかし、バナナはめんどくさいので星にすることにしました。本作中のミニゲームでは星を集めるので、特に問題はないはずです。右の画像の通り。
コーディングで押さえるべきところ
この動きは気の利いたものですが、やろうとしていること自体は単純です。プレイヤーが触れたら、目標となる地点へ星を動かせばいいだけです。
上記のリストから、各段階のコードでの処理をまとめると・・・
- トリガーでプレイヤーを検知する。
- 検知後、フラグをオンにする。
- 画面上の左上部に移動させる続ける。
- アイコンとの距離を測る。
- 一定の距離になったら星を消す。
- 獲得数に基づき、スコア表示を更新する。
注意点としては、スクリーン上の位置とゲーム中、つまりシーン中(ゲームワールド内)の位置は異なるということです。プレイヤーが移動し続けるのであれば、画面上の位置とゲーム内の座標はズレ続けます。つまり、GUIのアイコンはスクリーン上に、星はゲーム内のアイテムとして、違う次元の存在なのでズレを考慮しないとダメだということです。プログラミング的には、星の移動中はアイコンの位置を更新し続けないとダメだということになります。
今回、対象となるスクリプトは星自体にアタッチする"CollectableStar"とゲーム全体を管理する"GameController"の二つになります。前者は当然、星の動きを管理するもので、後者は星の獲得スコアを管理します。
星の動き
まずは画像をインポートします。Spriteフォルダで"Import New Asset"を実行し、星の画像を選択し取り込みます。
シーン内に引っ張って、コライダーをアタッチします。Physics 2D >> Circle Colliderを選択し、大きさ(半径)を適当に調整します。シビアすぎてもダメなので、大らかな大きさに設定してます。
で、スクリプトである"CollectableStar"をアタッチします。ドンキーにおける星は、その場で回転しているので、それも再現します。
transform.Rotate()
これを使うと、ゲームオブジェクトを回転させることが出来ます。ベクターの各軸、X, Y, Z軸それぞれ回転数をfloatで指定して、Update関数内で使用します。見たまま時計回りにするので、回転させるのはZ軸になります。Rotateは実は負担が大きい、とのことで、それなりに対策した方が良いらしいですが、ここでは省略します。
スコアの管理
本筋に移る前に、スコア関連について。星獲得スコアは、単純に数を数えるだけなのでintタイプの変数"score"になります。星側のスクリプトから直接、そのフィールド(クラス内に直接宣言された変数)を操作するのは良くないことなので、このスコアを管理する"GameController"内部にスコアを加算し、表示を更新するUpdateScoreなる関数を作り、星側のスクリプトが外から、その関数を呼び出すという形にしました。内容はシンプルで、スコアを加算してTextの中身をそれに合わせて更新する、という処理をするだけです。
テキストについて
表示については、Textを使います。using UnityEngine.Textを書く必要があります。星の画像を表示するので、それはimageです。どちらもCanvas上に表示させます。
星を動かそう!
さて、ではいよいよ本編です。星のコライダーはトリガー・モードで使用します。つまり、プレイヤーが触れたかどうかが分かればいいだけですので。
プレイヤーの検知はおなじみ、OnTriggerEnter2Dを使います。2Dの時は、あらゆるものの後ろに2Dが付くことに注意です。"Tag"タグを使って判定するので、この場合Unity上でプレイヤーキャラに"Player"タグを設定しておく必要があります。
触ったかどうかのフラグは"IsCollected"のbool値で管理します。集めたか?ということですね。OnTriggerEnter2Dでプレイヤーが触れたら判定し、これをtrueにします。trueになった時点から、移動のための処理が始まります。(Update()内部)
スターにアタッチするスクリプト。
Update内に移動のメソッドを書いておき、フラグが立ったらこれが実行されるようにします。つまりif文です。IsCollectedがtrueになったら、移動メソッドが実行されるようになり、星が動き始めるという算段です。
問題は前述の通り、星がどこに向けて移動していくかということです。
画面上の左上部分になりますが、そもそも我々が見ているものはスクリーンであり、スクリーンはゲーム内世界の一部を映し出したものに過ぎません。つまり、星やバナナからしてみれば、画面上の位置は分からないわけで、画面上の位置をシーン内の座標に変換しないとダメです。
スクリーンに映し出されているものは、カメラを通したものなのです。CameraクラスにおけるViewportToWorldを使い、変換します。
スクリーン上の座標は0.0fから1.0fまでのfloatのx, y座標として表されます。つまり、今回の"左上"はVector(0.3f, 0.95f)です。今回のようなゲームは常にプレイヤーが動き続けるので、こうした座標の変換、計算を常に行う必要があります。IsCollectedがTrueになった瞬間からアイコンに届くまでの間、Updateメソッド内で移動の処理が毎フレームされ続けます。
そして、指定の位置に近づいたらスコア加算のメソッドを実行します。厳密に言えば、GameControllerのUpdateScoreメソッドを呼び出し、自分のゲームオブジェクトを非アクティブにすることで表示を消す、という処理です。
gameObject.SetActive(false);
Destroyでもいいかと思いますが、Destroyは負担も大きいし、星自体の数も多いので、SetActiveを使うことにしました。画面から消える、という効果は同じなので。
ちなみに星の点滅というか、色の変化の処理も書いてますが、それについては今回の本筋とは無関係なので省略します。
注意!
今回のコードですと、画面の大きさの変化に対応できません。アイコンの座標を定数で表しているからです。なので、解像度が変わればズレてしまいます。なので、本当は画面の大きさから計算して、相対的なアイコンの位置を計算して割り出すという処理が必要なのですが、今回は省略しています。
まとめ!
ということで、自分も大好きなゲームの動きを一部だけではあっても、再現することは楽しいなと思いました。こういうちょっとした演出が、ゲームに生命観を与えるのだなーと、また1つ勉強になりましたね。今後も名作ゲームの再現に挑戦していきたいと思います。あとSwitchでトロピカルフリーズやりたいなぁ。