前回までの記事
この記事はUNITYの公式チュートリアル『Ruby's Adventure』を日本語で解説している記事です。
↓↓記事一覧↓↓
#5 攻撃とカメラ追従 ←いまここ
(番外編)うまく動かない場合の対処法
Rubyの攻撃を作ろう
発射物を作ろう
さて、次はRubyが歯車を投げて攻撃できるようにしていきます。
ちなみに「攻撃」といっても、#3で説明したようにこのゲーム上では「壊れたロボットを直すための歯車を投げる」というアクションです。
まずProjectウインドウ内のArt→Sprites→VFXから「CogBullet」というファイルを探し、Pixel Per Unitを300に変更してApply(適用)してから、Hierarchyウインドウにドラッグ&ドロップしてください。
次にそのオブジェクトをマップ外にでも置いておき、コンポーネントRigitbody2DとBox Collider 2Dを追加し、Rigitbody2DのGravity Scaleを0にしてください。
次に、Scriptsフォルダに「Projectile」という名前のC#スクリプトを作成してください。Projectileは直訳で「発射物」という意味で、このチュートリアルで何度も使うことになる単語なので覚えておいてください。
次に、そのスクリプトを開いて次のように書き換えてください。
Rigidbody2D rigidbody2d;
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
}
public void Launch(Vector2 direction, float force)
{
rigidbody2d.AddForce(direction * force);
}
void OnCollisionEnter2D(Collision2D other)
{
Debug.Log("Projectile Collision with " + other.gameObject);
Destroy(gameObject);
}
さて、もう説明しなくてもおおよそ解るのではないでしょうか?
「Rigidbody2D rigidbody2d;」でコンポーネントRigitbody2Dを取得するための変数を定義し、
Start()内の「rigidbody2d = GetComponent<Rigidbody2D>();」でコンポーネントRigitbody2Dを取得、
少し飛ばして「void OnCollisionEnter2D(Collision2D other)」で歯車が衝突したときのメソッドを追加し、
その内容には、まず「Debug.Log("Projectile Collision with " + other.gameObject);」で当たったオブジェクトのログを表示させてから、
「Destroy(gameObject);」で歯車を消滅させています。ここまでは経験済みですね。
さて、「public void Launch(Vector2 direction, float force)」ですが、ここで歯車を移動させるためのメソッドを作っており、「rigidbody2d.AddForce」はRigitbody2Dに力を加えるメソッドで、directionは方向、forceは加える力を定義するための変数です。
このようにメソッドの括弧()に変数を入れておくと、別の場所からこのメソッドを呼び出すときに「Launch(方向, 値)」という感じで内容を指定して呼び出すことができます。
で、そのなかの「rigidbody2d.AddForce(direction * force);」でコンポーネントRigitbody2D(rigidbody2d)のなかの(ドット.)力(Force)に方向×力(direction * force)の値を入れて(Add)います。これによって、別の場所から「Launch(方向, 値)」と呼び出せば、その方向と力で歯車を撃ち出せるわけですね。
続いて、歯車を発射するRuby側の設定を作ります。
まず、RubyControllerスクリプトに次のコードを記述してください。
public GameObject projectilePrefab;
「GameObject」は、オブジェクトに関する処理を行うためのコードです。このように変数(projecttilePrefab)をpublicで定義することにより、Inspectorウインドウにオブジェクトをアタッチできる場所が現れますので、一度UNITY画面に戻って確認してみてください。
で、↑この場所にオブジェクトCogBulletをアタッチしてください。
次に、Update()内に次のメソッドを書き加えてください。
if(Input.GetKeyDown(KeyCode.C))
{
Launch();
}
これにより、Cキーを押したときにLaunch()メソッドが処理されるようにします。
続いて、そのLaunch()メソッドを作ります。Start()やUpdate()と並列に、次のメソッドを書き加えてください。
void Launch()
{
GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity);
Projectile projectile = projectileObject.GetComponent<Projectile>();
projectile.Launch(lookDirection, 300);
animator.SetTrigger("Launch");
}
「GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity);」ですが、長いですね…。
まず「projectileObject」は、歯車を定義する変数です。
「Instantiate」は「オブジェクトを生成するコード」です。括弧()内には(生成するオブジェクト, 位置, 回転)を記述します。
で、その括弧()内の「生成するオブジェクト」の部分にある「projectilePrefab」は、先ほどアタッチしましたね。歯車オブジェクトです。
次に「位置」の部分にある「rigidbody2d.position + Vector2.up * 0.5f」は「先ほど取得したコンポーネントRigitbody2Dのなかのposition(位置)にy軸+1(Vector2.up)×0.5fを加える(+)」という意味で、つまり、「Rubyのいる位置の0.5fうえ」を表します。歯車はRubyが手で投げるモノなので、Rubyの上半身から歯車が出現する感じにするわけですね。
次に「回転」の部分にある「Quaternion.identity」は「回転していない」という命令のできるコードです。
以上により、「GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity);」は「歯車を回転させずにRubyの上半身あたりに出現させる」という意味です。
続いて「Projectile projectile = projectileObject.GetComponent<Projectile>();」ですが、先に意味を説明すると、これは先ほどアタッチした歯車オブジェクトに貼り付けられた「Projectileスクリプト」を取得するためのコードです。他のオブジェクトに貼り付けられたスクリプトを読み込むにはこういうコードになるものだと認識してください。これはもう覚えるしかないと思います…。
次の「projectile.Launch(lookDirection, 300);」は、Launch()メソッドを起動していますが…これ、メチャクチャわかりにくいのですが、ここでいうLaunch()はこのスクリプトのメソッドではなく、いま読み込んだ歯車スクリプトに書いたほうのLaunch()メソッドです。
言うまでもなく、自分でゲームを開発する際はこんな解りにくいスクリプトを書いてはいけません。Ruby側を「LaunchFromRuby()」「LaunchToCog()」みたいに分けるとか…何でもいいんですが、とにかく同じ名前のメソッドを書かないようにしましょう。何も良いことありません。
気を取り直して、Cogスクリプトのほうのメソッドでは「Launch(Vector2 direction, float force)」と書いていたので、このスクリプトの「(lookDirection, 300)」は「Launch(方向はRubyの見ている方向, 力は300)」=「Rubyの見ている方向に値300で歯車を飛ばす」という意味になります。
最後の「animator.SetTrigger("Launch");」は、歯車を投げるRubyのアニメーションを再生するためのパラメーターをAnimatorに送っています。被ダメージの時と同じですね。
さて、この状態で試動してCキーを押してみると、歯車が一瞬出現してすぐに消え、さらに次のエラーがコンソールウインドウに表示されます。
「NullReferenceException: Object reference not set to an instance of an object
Projectile.Launch (UnityEngine.Vector2 direction, System.Single force) (at Assets/Scripts/Projectile.cs:16)
RubyController.Launch () (at Assets/Scripts/RubyController.cs:101)
RubyController.Update () (at Assets/Scripts/RubyController.cs:65)」
(at Assets/Scripts/Projectile.cs:16)とあるので、このエラーはProjectileスクリプトの16行目、つまりLaunch()メソッドのなかの「rigidbody2d.AddForce(direction * force);」の行で発生したエラーです。
エラーの内容は、「参照しようとしてるオブジェクト(Rigitbody2D)にForceを入れる(Add)する場所がないよ」という意味です。しかし、Rigitbody2DにはAddForceを入れる場所があるはずです。
つまり、Rigitbody2Dを取得できていないのがエラーの原因です。
しかし、このスクリプトのStart()内でコンポーネントは取得しているはずです。↓
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
}
なぜ↑これでコンポーネントが取得できないのでしょうか?
実は、処理開始時に表示(アクティブ)されるオブジェクトの場合、Start()内の処理は1フレーム遅れて処理される(らしい)のです。
歯車オブジェクトは「Instantiate」によって複製が新たに生成されてRubyから撃ち出される関係上、最初は無かったオブジェクトが撃ち出した瞬間に表示(アクティブ)されるため、Start()内の処理が行われずエラーとなったわけですね。
こんな時はAwake()を使います。Start()とAwake()の違いはちょっと難しいのですが、Start()よりも先に処理できるメソッドがAwake()くらいの認識でいいと思います。
void Awake()
{
rigidbody2d = GetComponent<Rigidbody2D>();
}
↑こんな感じで書き換えます。
さて、この状態で試動してみると、エラーは消えたものの歯車は出てくれません。よくみると、コンソールウインドウに「Projectile Collision with Ruby (UnityEngine.GameObject)」というデバッグログが出ています。
思い出してみると、この歯車は「他の(Rigitbody2Dを持つ)コライダーと衝突したら、デバッグログ(Debug.Log)を表示させてから消える(Destroy)」仕組みになっていました。
つまり、歯車は出現した瞬間にRubyと衝突して消えているわけですね。
でも、どちらのオブジェクトにもRigitbody2Dが必要なので、今回はレイヤーを分ける方法で解決したいと思います。
オブジェクトのレイヤーはInspectorウインドウ右上に表示されおり、初期設定は「Default(レイヤー0)」となっています。
今回は新たにレイヤーを作成するので、そのドロップダウンメニューから「Add layer...」をクリックしてください。
こんなウインドウが表示されるので、Layer 8に「Character」、Layer 9に「Projectile」と入力します。
次に、UNITY画面上部のEdit→Project Settings→Physics 2Dを開き、一番下のLayer Collision Matrix(レイヤー同士の衝突判定)のチェックボックスのうち「Character」と「Projectile」がクロスしている3つのチェックを外してください。
これで完了です。
敵が歯車に当たったら無力化する処理を追加しよう
それでは、歯車が当たった時に敵(壊れたロボット)が「直る」反応を追加しましょう。
…あまりにも解りにくいので、念のためもう一度書いておきます。
このチュートリアルで作っているゲーム『Ruby's Adventure』は、
壊れたロボットに歯車を投げつけて直すことで敵を無力化していくというアクションゲームです。
そのため、スクリプト上では「broken(壊れている)」が「攻撃を受けていない」という意味で、
「fix(直す)」が「無力化」の意味です。
ほんと、初心者向けのチュートリアルなのにこんな解りにくい設定にしたUNITYの担当者は正気じゃないと思います…。
EnemyControllerスクリプトの上のほうに並んでいる定義に次の定義を追加してください。
bool broken = true;
これは、敵ロボットが壊れている(=攻撃を受けていない)ことを判定するフラグで、初期値にtrueを入れています。
次に、Update()のなかの最初にこれを追加して、
if (!broken)
{
return;
}
FixedUpdate()のなかの最初にこれを追加してください。
if (!broken)
{
return;
}
この二つを追加することによって、「!broken」=攻撃を受けた(brokenに!なので否定の意味)場合にはこのメソッド以降の処理を行わないようにしています。つまり、攻撃を受けたら何もしなくなるということです。
次に、Start()などと並べて次のメソッドを追加してください。
public void Fix()
{
broken = false;
rigidbody2D.simulated = false;
animator.SetTrigger("Fixed");
}
これは「Fix(直す)」つまり「敵を無力化したとき」のメソッドです。何度も言いますが、このゲームの敵は壊れたロボットなので「歯車をぶつけて直す=無力化する」なんです。
その内容は、まず「broken = false;」でbroken(壊れてる)フラグをfalseにしています。これによって前述の「if(!broken)」が発動するようになります。
次に「rigidbody2D.simulated = false;」ですが、なにやらRigitbody2Dのなかの「simulated」をOFFにしていますね。
「simulated」は↓この部分です。初期設定でONになっています。
「simulated」をOFFにすると、物理演算機能が停止し、コライダーの判定を行わなくなります。これで無力化はOKですね。
次に「animator.SetTrigger("Fixed")」ですが、これは後ほど設定する「無力化された敵のアニメーション」を再生するためのトリガーです。
以上で敵のスクリプトは完了です。
歯車側の処理を追加しよう
次に、歯車のProjectileスクリプトのOnCollisionEnter2D()内を次のように書き換えてください。
void OnCollisionEnter2D(Collision2D other)
{
EnemyController e = other.collider.GetComponent<EnemyController>(); // ←ここから
if (e != null)
{
e.Fix();
} // ←ここまで
Destroy(gameObject);
}
「Debug.Log("Projectile Collision with " + other.gameObject);」はもう不要なので削除しました。
まず「EnemyController e = other.collider.GetComponent<EnemyController>();」で、other(メソッドの括弧()内で取得したもの=衝突したオブジェクト)のコンポーネントから敵のスクリプトを取得しています。
「EnemyController e = other.collider.GetComponent<EnemyController>();」の「collider」ですが、これは変数ではありません(定義してないし)。このあたりは調べてもよくわからなかったです。
恐らく「UNITYで衝突したオブジェクトの中身を取得するときは .collider. というコードを使うもの」ということなんだと思います。たぶん…。
で、その入れ物として「e」という(適当すぎる名前の)変数を定義しています。
次の「if (e != null)」は、EnemyControllerスクリプト(e)がカラッポ(null)ではない(!=)ときに処理するif文ですね。
その内容に「e.Fix();」と書かれていますが、ここでEnemyControllerスクリプトのFix()メソッド(敵が無力化される処理)を発動しています。
つまり、この歯車はオブジェクトに当たると、「そのオブジェクトに貼り付けられたEnemyControllerスクリプトのFix()メソッドを発動」します。敵に当たらなければそもそもEnemyControllerスクリプトが無いので、歯車自体が消えるだけです。
さて、これで「歯車がオブジェクトに衝突したときの処理」は完了ですが、このままでは「どのオブジェクトにも衝突しなかった時」、歯車が永久にマップ外を飛び続けてしまいます。
そのため、スクリプトに次の記述を追加しましょう。
void Update()
{
if(transform.position.magnitude > 1000.0f)
{
Destroy(gameObject);
}
}
さて、「magnitude」は長さでしたね。
つまり、「transform.position.magnitude」は、「歯車のtransformのpositionの長さ(magnitude)」です。これが1000.0fを超えた(>)ときに消滅(Destroy)するif文ですね。
ちなみに他の方法としては、「歯車出現と同時に動き出すタイマー」を新たに定義して、その秒数を消滅のトリガーにすることもできそうですね。
以上で歯車側の処理は完了です!
敵に無力化時のアニメーションを追加しよう
続いて、敵が無力化したときのアニメーションを追加しましょう。
アニメーション作成の概要は#4で説明済みですが、再度順を追って説明します。
まず敵オブジェクトを選択した状態でAnimationウインドウの左上からNew Clipで新規アニメーションクリップを作成、名前を「RobotFixed」とでも名付けて、Sumple Rateを4に変更し、次にProjectウインドウ内のCharactersフォルダを開き、敵の画像の▶ボタンを押して展開、最初から4番目までの4つの画像をShiftキーを押しながら複数選択して、Animationウインドウに貼り付けます。↓
次にAnimatorウインドウにタブを切り替え、左側の上のほうにある+ボタン→Trigger型でパラメーターを作成して「Fixed」と名付け、Blend Treeを右クリック→「Make Transition」を押して、生成されている新たなノード「RobotFix」に矢印を繋げます。で、その矢印を選択した状態でInspectorウインドウの「Has Exit Time」をOFFにして、Conditionsから「Fixed」のTriggerを選べば準備完了です。↓
これで、先ほどEnemyControllerスクリプトに追加した「animator.SetTrigger("Fixed");」というコードによってアニメーションが再生されるようになりました。
先ほど、敵は「broken」フラグがONになると全てのメソッドが「returen」によって処理されないようになっているので、無力化されるとその場に留まってRobotFixアニメーションを再生し続けます。
これで完了です!
カメラを追従させよう
Cinemachineをインポートしよう
さて、見た目の骨格はだいたい良くなってきたので、Rubyにカメラを追従させたいと思います。
UNITY画面上部のWindow→Package Managerを開き、Cinemachineをインポートしてください。
次に、Hierarchyウインドウで右クリック→Cinemachine→Virtual Cameraをクリックします。これでCM vcam(シネマシーン バーチャルカメラ)オブジェクトが生成され、シネマシーンが有効化されました。
さて、この時点でUNITYのGameタブからゲーム画面を確認していただき、
↑こんな風に表示されている場合は、MainCameraオブジェクトのInspectorウインドウからProjection(投影法)をOrthographic(正投影図)に変更し、さきほど生成されたCM vcamオブジェクトを選択した状態でInspectorウインドウを確認し、Orthographic Size(画面の縦半分に何Unit入るか)を「5」にしてください。
Perspective(遠近法)が消失点のある3D的表現、Orthographic(正投影法)が消失点のない2D的表現です。
次に、オブジェクトCM vcamを選択してInspectorウインドウ内の「Follow(追従するもの)」にオブジェクトRubyをドラッグ&ドロップしてください。
はい、これで追従完了です。簡単ですね。
カメラの移動範囲を設定しよう
このままではRubyがマップ外に出てしまうので、侵入不可能な池タイルで外縁を埋めましょう。
これでとりあえずRubyがマップ外に出る事はなくなりましたが、マップの外縁付近まで歩くとマップ外のタイルを塗っていない場所が見えてしまいます。↓
カメラの視界が届く範囲を全て池タイルで埋め尽くすという方法もあるのですが、今回はカメラの移動範囲を制限したいと思います。
CM vcamのInspectorウインドウの下部のAdd Extension→CinemachineConfinerを選択します。
次に、Hierarchyウインドウで右クリック→Create Emptyでカラのオブジェクトを作成し、「CameraConfiner」と名付け、そのオブジェクトにコンポーネントPolygon Collider2Dを追加します。すると、ゲーム画面上に5角形の当たり判定が出現します。
追加したPolygon Collider 2D内のPoints→Paths→Element 0の値を「4」に変更して枠の頂点を4個にして、Edit Colliderの横にあるボタンをクリックすると枠の頂点をゲーム画面上で動かせるようになるので、頂点をマップの外縁あたりに移動させ、終わったら先ほどのボタンをもう一度クリックして編集を終了します。
あと、必須作業ではありませんが、↑頂点をだいたいの位置に置いたら、Inspectorウインドウできれいな数字に整えましょう。
次に、CM vcamに戻りCinemachineConfinerのBounding Shape 2Dに先ほど作ったオブジェクトをアタッチしましょう。
↑こんな感じ。
よし!これで完了だ!
…と思って試動してみると、Rubyが画面外に押し出されてしまいます。
それもそのはず、Polygon Collider2Dは当たり判定ですから、同じく当たり判定を持つRubyは押し出されてしまいます(敵も)。
なので、歯車のレイヤーを分けたときと同じ事をします。Inspectorウインドウ右上のLayer→Add layerから「Confiner」という新たなレイヤーを作成し、オブジェクトCameraConfinerをそのレイヤーに割り当てて、Edit→Project Settings→Physics2Dから、Confinerと交差する全てのチェックを外しましょう。
以上で完了です!お疲れ様でした!
次の記事はこちらです
ここまでのスクリプト
こんな感じです。