ひつじを動かそう その2

Last-modified: Tue, 02 May 2023 20:38:14 JST (360d)
Top > ひつじを動かそう その2

はじめに

ステージを作ろうの続きです。

 

これまでで羊をキー入力で操作できるようにして、ステージまで制作しました。
しかし今のままでは、ひつじの動きとステージがあっていませんね。
そのでこのページでは、ゲームに合わせたひつじの動きの完成版を作りましょう。

 

今回は

ひつじに重力を与える
ひつじをキー入力に合わせて左右反転させる
ひつじをジャンプさせる

という機能を実装していきます。

 

ひつじに重力を与える

 

これは一度プロジェクトを作成しようでやりましたね。
ひつじのオブジェクトに Inspector から Rigidbody2D のコンポーネントを追加します。
追加の方法は覚えているでしょうか。
Inspector から AddComponrnt > Rigidbody2D をクリックすればいいんでしたね。
これで重力が追加されたはずです。
実際動かしてみましょう。
ちゃんと、ステージの上に立って、キー入力で左右に動くようになりましたか?

 

しかし今の段階では、いくつか問題があります。
それは、

1. 坂を上り下りしようとするとひつじが回転してしまう
2. 壁に突っ込むとめり込んでしまう
3. パソコンの処理能力差でひつじのスピードが変わってしまう

というものです。

 

問題① 坂を上り下りしようとするとひつじが回転してしまう

 

①の問題は、坂に当たった時の物理演算をz軸に関しても行っているために、坂とひつじの間の摩擦力によって回転してしまうせいで起こります。
そのため、z軸に関する物理演算を止めてあげる、つまり、ひつじをz軸回転しないように固定してあげる必要があります。
固定する方法は簡単で、ひつじの Inspector から Rigidbody2D > Constraints > FreezeRotation > Z にチェックを入れてあげます。
これだけです。

 

問題② 壁に突っ込むとめり込んでしまう

 

②の問題は、「Rigidbody2D」の物理演算の結果が、ひつじを左右に動かそうとするときに再計算されて衝突計算が無効になってしまうせいです。
詳しく説明すると、「Rigidbody2D」の物理演算は「MoveSeap.cs」の中にあるUpdate関数よりも先に計算されるようになっており、
Update関数の中にある「Transform.position」による移動で衝突計算を再計算するようになっているんです。
この問題を解決するには、Update関数内で移動の計算を行わないようにしなければなりません。
ということで、以前作成したひつじを動かすプログラムの「MoveSeap.cs」を直していきます。

 
  1. // 省略
  2.     private Rigidbody2D rigidbody = null; // 変数の定義
  3.  
  4.     public int speed = 0.01f;
  5.  
  6.     // Start is called before the first frame update
  7.     void Start()
  8.     {
  9.         rigidbody = this.GetComponent<Rigidbody2D>(); // 「Rigidbody2D」コンポーネントの取得
  10.     }
  11.  
  12.     // Update is called once per frame
  13.     void Update()
  14.     { 
  15.         Vector2 pos = rigidbody.position; // 「This.transform」から「rigidbody」に
  16.  
  17.         if (Input.GetKey(KeyCode.D)) 
  18.         {
  19.             pos.x += speed;
  20.         }
  21.  
  22.         if (Input.GetKey(KeyCode.A))
  23.         {
  24.             pos.x -= speed;
  25.         }
  26.  
  27.         rigidbody.position = pos; // 「This.transform」から「rigidbody」に
  28.     }
  29. }
 

このように書き換えてください。
Start関数の上にかいた 「private Rigidbody2D rigidbody = null;」 というのは、前にも一度やった変数の定義となります。
今回は、 Rigidbody2D という型(コンポーネントやクラスも型に含みます)に「rigidbody」という名前を付けて定義しています。

 

最後にある null というのは、「無効な値、何もない、ゼロ」という、無効な参照を表す特別な値です。
詳しくはこの後にやりますが、この変数にはゲームオブジェクトのコンポーネントを格納するために使います。
ですが、ここではまだそのコンポーネントを取得していないので、一旦 null という値を置きます。(特に書かなくもデフォルトで null が入ります。)

 

最初の private というのは、アクセス修飾子の一種です。
アクセス修飾子というのは、簡単に言えばその変数を利用できる権限レベルを指定するためのものです。今回付けた private は最も厳しい制限で、このクラスの中でしか使えないようになります。
これがつくと、Unity の Inspector からも見ることができない(もちろん編集できない)変数として定義することができます。
今回定義する変数は、ひつじの物理演算を司るコンポーネントを格納するということで、基本的に中身が変更しない変数になります。
このような変更のない変数、或いは変更されては困るような変数を定義するときに使います。
(以前使った public もアクセス修飾子の一つで、一番権限レベルの易しいものになります。ほかのプログラムや Inspector からも操作できるため、中身を変更することがある場合はpublicをつけましょう。)

 

次は、Start関数の中の「rigidbody = this.GetComponent<Rigidbody2D>();」です。
これは、ひつじについている Rigidbody2D のコンポーネントを取得して、先ほど定義した「rigidbody」に入れる、という操作になります。
これで、「rigidbody」という変数をスクリプト内で操作することで、ひつじについた Rigidbody2D の設定をゲームの最中でも変更できるようになりました。
基本的に、ゲームオブジェクトについたコンポーネントをスクリプトから操作する際は、Start関数の中で取得してあげます。(もちろん使う度に取得しても構いませんが...)

 

最後に、Update関数の中に2か所あった This.trsnform を rigidbody に置き換えてあげます。
こうすることで、これまでのゲームオブジェクトの位置を直接変える操作から、 Rigidbody2D の物理エンジンによる物理的な移動運動としての操作に切り替えることができます。
これで、キー入力での左右の移動を、これまで先に計算されていた Rigidbody2D での物理演算と一緒に演算できるようになりました。
つまり、衝突演算等が再計算されることはなくなったわけです。
このように、ゲームオブジェクトに物理的な操作(重力、衝突など)をする際は、できるだけ transform による操作よりも Rigidbody2D による操作をするようにしましょう。

 

問題③ パソコンの処理能力差でひつじのスピードが変わってしまう

 

最後に③の問題を解決しましょう。
Unityでは、処理の負荷が軽くかったり、重かったりするとフレームの長さが変わってしまいます。
更にゲームをプレイするデバイスの性能差によっても、処理能力の差でフレームの長さにばらつきが出てきます。
そこで、フレームの長さに合わせて進むスピードを調整できるように「MoveSeap.cs」を書き換えていきましょう。

 
  1. // 省略
  2.     //Update関数の中
  3.  
  4.         if (Input.GetKey(KeyCode.D)) 
  5.         {
  6.             pos.x += speed * Time.deltaTime; //「* Time.deltaTime」を追加
  7.         }
  8.  
  9.         if (Input.GetKey(KeyCode.A))
  10.         {
  11.             pos.x -= speed * Time.deltaTime; //「* Time.deltaTime」を追加
  12.         }
  13. // 省略
 

このように、「position」の加減の際に「Time.deltaTime」をかけてあげます。
少し説明すると、「Time.deltaTime」とは、前回のフレームから現在までの時間を表します

加減の際に「speed」に「Time.deltaTime」をかけてあげることで、
フレーム間隔が短いときはその分遅く、長いときは早くしてあげることができ、
フレームの長さに関係なく一定の速さで動くように見せることげできます。

 

これで、①~③の問題もすべてクリアでき、キー入力による操作は完成になります。

 

キー入力で左右で反転させる

 

次は、ひつじの画像を進む方向が正面になるようにキー入力に合わせて反転させる機能を追加します。
ゲームオブジェクトの向きはデフォルトでついている SpriteRenderer というコンポーネントの FlipX を変更することによって変えられます。
(x軸方向での反転の時は FlipX 、y軸方向での時は FlipY )
FlipX の初期状態はチェックマークの入った所謂 true で、元の画像と同じ向きです。
ここで、チェックを外した false の状態にしてあげると、ゲームオブジェクトは左右で反転します。
これを「MoveSeap.cs」からキーの入力で判定をとって true と false を切り替えることで、今回の機能を実装していきます。

 
  1. // 省略
  2.     private Rigidbody2D rigidbody = null;
  3.     private SpriteRenderer spriteRenderer = null; // 変数の定義
  4.  
  5.     public float speed = 0.01f;
  6.  
  7.     // Start is called before the first frame update
  8.     void Start()
  9.     {
  10.         rigidbody = this.GetComponent<Rigidbody2D>();
  11.         spriteRenderer = this.GetComponent<SpriteRenderer>(); //  SpriteRenderer コンポーネントの取得
  12.     }
  13.  
  14.     // Update is called once per frame
  15.     void Update()
  16.     { 
  17.         Vector2 pos = rigidbody.position; 
  18.  
  19.         if (Input.GetKey(KeyCode.D)) 
  20.         {
  21.             pos.x += speed * Time.deltaTime; 
  22.             spriteRenderer.flipX = true; // Dキーを押しているとき FlipX は true
  23.         }
  24.  
  25.         if (Input.GetKey(KeyCode.A))
  26.         {
  27.             pos.x -= speed * Time.deltaTime; 
  28.             spriteRenderer.flipX = false; // Aキーを押しているとき FlipX は false
  29.         }
  30. // 省略
 

四か所書き加えました。
一番上が、前のステップでもやった取得するコンポーネントの変数の定義です。
今回は SpriteRenderer コンポーネントを「spriteRenderer」の名前で定義します。もちろん初期値は null です。
次は Start() 関数の中で、「spriteRenderer」にこのオブジェクトの SpriteRenderer コンポーネントを取得してあげます。
最後に、Update関数の中の左右それぞれのキー入力判定の場所で、 FlipX を true または false に変更する文を足してあげれば完成となります。

 

実際に動かしてみると、キー入力に合わせてひつじが左右を向くようになったのではないでしょうか。

 

ジャンプをする

これで、ひつじを Rigidbody2D コンポーネントを用いて左右に移動できるようになりました。
次は縦方向の移動手段「ジャンプ」を作ります。
ジャンプ機能はこれまでの操作と比べて少し難しいので、
ジャンプ機能を作るにあたり、まずはジャンプの仕様を考えます。

仕様1 スペースキーが押されたらジャンプをする
仕様2 空中ではジャンプできない

次に、この仕様を実現するために必要な処理を考えます。

処理1 スペースキーが入力されたかを調べる
処理2 ひつじが地面に触れているかを調べる
処理3 ひつじに上方向の加速度を一回与える

これらの処理を一つずつ実装し、ひつじのジャンプを作っていきます。

 

キー入力を一度だけ受け取る

まずはキー入力の受け取りを実装します。
ひつじを動かそうの時と同じようにしてスペースキーの入力も検知できます。
「MoveSheep.cs」の Update() 関数の中に if 文でスペースキー入力の検知を追加しましょう。

  1. void Update()
  2.     {
  3. // 省略
  4.         // スペースキーの入力チェック
  5.         if (Input.GetKey(KeyCode.Space)) 
  6.         {
  7.             // ここにジャンプの処理を書く
  8.         }
  9.     }

これでよさそうな気もしますが、このままでは、「キーが押されている間、毎フレームジャンプの処理が行われる」ことになります。
30fpsとすれば0.1秒押し続けているだけで3回もジャンプが行われるわけです。
このままではひつじが空のかなたに消えてしまいそうなので、「キーが押されたときに一回だけジャンプの処理を行う」ように改良します。
今回は、Input.GetKey() 関数の代わりに Input.GetKeyDown() 関数を用いることでこれを解決します。
「Input.GetKey()」 が、「キーが押されているとき」true になるのに対し、
「GetKeyDown() 」は「キーが押されたときに一回だけ」true になります。

  1. void Update()
  2.     {
  3. // 省略
  4.         if (Input.GetKeyDown(KeyCode.Space)) 
  5.         {
  6.             // ここにジャンプの処理を書く
  7.         }
  8.     }
 

これで、スペースキーを押して一度だけ呼び出される条件文ができました。

 

ひつじに上方向の力を与える

次は、if文の中、命令文を書いていきます。
はねる という動きをUnityで再現したい場合、Rigidbody2D の AddForce() 関数の Impulse モードが便利です。
「AddForce」は文字通り、オブジェクトに「力を加える」関数です。
「AddForce」には

・継続的に力を加え続ける(加速させる)Force モード
・瞬間的に力を加える Impulse モード

などのモードがあり、力の加え方を決めることができます。
ここでは Impulse モードで上方向に一瞬だけ力を加えることで上に飛ぶ動作を再現します。

  1. // 省略
  2.     public float speed;
  3.     public float jump; // ジャンプする大きさ用の変数を定義
  4. // 省略
  5.     void Update()
  6.     {
  7. // 省略
  8.         if (Input.GetKeyDown(KeyCode.Space)) 
  9.         {
  10.             // モードは ForceMode2D.Impulse, ForceMode2D.Forceのように指定
  11.             rigidbody.AddForce(new Vector2(0, jump), ForceMode2D.Impulse);
  12.         }
  13.     }

飛ぶ強さは調整が必要になってくるため「jump」という名前であらかじめ定義しておいて、 AddForce() 関数のy軸方向への力として使います。
もちろん Inspector からの編集もできるようにアクセス修飾子として public を指定しておいてください。
これで、スペースキーが押されたときに上方向に飛び上がるようになります。

 

接地判定を取る

地面に触れているか、はタイルマップにひつじの下面が触れているか、と考えて作ります。
通常の当たり判定では、どの方向がタイルマップに当たっているかを調べることができません。
そこで、新しくひつじの足元にオブジェクトを作り、そのオブジェクトがタイルマップに触れているかを調べることでこの問題を解決します。

 

まずは、設置判定用のオブジェクトを作ります。
Hierarchy からひつじを右クリックし、メニューから create empty を選択して
空のひつじの子オブジェクトを作ります。
名前は「GroundCheck」にしておきます。

 

#画像

 

ひつじの子オブジェクトにすることがポイントです。
Unityでは 、ゲームオブジェクトを階層化して配置することができます
この時の下層に当たるゲームオブジェクトを子オブジェクトと呼びます。
(上層のゲームオブジェクトは親オブジェクトと呼び、階層の深さに合わせて孫オブジェクトなどもあります)
子オブジェクトは親オブジェクトを基準に座標が計算されます。
つまり、特にスクリプトを書かずとも、子オブジェクトは親オブジェクトとの位置関係をキープし続ける、つまり勝手についてきてくれるというわけです。

 

オブジェクトを作ったら、
「GroundCheck」に AddComponent > BoxCollider を追加します。

 

#画像

 

そのままではひつじと同じ大きさの当たり判定になってしまい、足元だけの判定にならないので、
当たり判定の大きさ、位置を編集していきます。
方法は、Inspector の BoxCollider コンポーネントから EditCollider を押し
Scene ビューから GUI 的に調整できます。
(横幅はひつじの半分程度、縦幅は小さく、位置はひつじの足元になるように)

 

#画像

 

横幅を小さくしておかないと、横の壁も地面と判定されてしまうので注意です。

 

調節できたら、 Inspector から BoxCollider の isTrigger にチェックを入れます。

 

#画像

 

Collider コンポーネントの isTrigger オプションは、他の Collider と反発するかしないかを決めるオプションです。
チェックを入れると反発しなくなる、つまり貫通して重なるようになります。
今回は、設置判定用のオブジェクトが地面と反発すると親子の位置関係を保とうとしてひつじのほうが浮いてしまうので isTrigger で反発を無効化しておきます。

 

これで、接地用の当たり判定が出来ましたが、
次に設置判定用のスクリプトを作っていきます。
「GroundCheck」を使って「MoveSheep」内で設置判定を受け取るには、

「GroundCheck」のスクリプトを作って、その中に接地していれば true になる変数(フラグ)を public で作る
「MoveSheep」から「GroundCheck」のフラグの値を読み取る

という方法が考えられます。

 

まずはGroundCheckのスクリプトを作ります。

 

「GroundCheck」に AddComponent > NewScript で「GroundCheck」というスクリプトを追加します。
追加したらスクリプトを開き、Start関数やUpdate関数は消して、代わりにアクセス修飾子public をつけて設置判定の結果を入れるフラグを作ります。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4.  
  5. public class GroundCheck : MonoBehaviour
  6. {
  7.     //地面と当たっているかのフラグ
  8.     public bool isGround = false;
  9. }

初期値には false を入れておきます。

 

次に、肝心の接地判定を作ります。
まず、重なっているのが地面なのかどうか識別するためにタグというものを地面(「Tilemap」)につけます。
タグとは、複数(一つでも可)に割り当てることのできる識別用のラベルのことです。
タグはスクリプトを行うときにゲームオブジェクトを識別するのに役立ちます。
「Tilemap」の Inspector から Tag > AddTag を選択、 Tags の + を押して、

NewTagName に「ground」と入力し Save します。

もう一度「Tilemap」の Inspector から Tag を押し、次は先ほど作った「ground」を選択します。
これで「Tilemap」に「ground」というタグをつけることができました。

 

次に Collider の当たり判定をスクリプトから受け取り、設置判定を取ります。

  1. // 省略
  2. public class GroundCheck : MonoBehaviour
  3. {
  4.     public bool isGround = false; // フラグ変数の定義
  5.     private string groundTag = "ground"; // タグ名の変数の定義 初期値は「ground」
  6.  
  7.     // 地面と触れたときにフラグをtrueに
  8.     void OnTriggerStay2D(Collider2D col)
  9.     {
  10.         if (col.tag == groundTag) 
  11.         {
  12.             isGround = true;
  13.         }
  14.     }
  15.  
  16.     // 地面から離れたときにフラグをfalseに
  17.     void OnTriggerExit2D(Collider2D col) 
  18.     {
  19.         if (col.tag == ground) 
  20.         {
  21.             isGround = false;
  22.         } 
  23.     }
  24. }

今回新たに OnTriggerStay2DOnTriggerExit2D という2つの関数が出てきました。
これはそれぞれ当たり判定が「重なっている間ずっと」「重なりがなくなった瞬間に一度」呼び出される関数になります。
これ以外にも OnTriggerEnter2D という当たり判定が「重なった瞬間に一度」呼び出される関数などがあります。
(他にもいくつかありますがここでは省きます。ぜひ自分で調べてみてください)
さて、今回はどちらも引数を指定しています。
Collider2D というのは、重なった(いる、いた)オブジェクトの情報を持った型です。
それを「col」という名前で利用していきます。
ここでは、それぞれ対象のオブジェクトが「ground」のタグを持っていたら、
設置判定用のフラグ「isGraund」の true 、 false が切り替わるようにしています。
これで、「Tilemap」に設置しているときは「isGraund」は true 、いないときは false になるようにできました。

 

次に、「MoveSeap.cs」でこの設置判定のフラグを受け取って、フラグの立っているときのみジャンプできるように編集します。

 

「MoveSeap.cs」を編集していきます。

  1. // 省略
  2.     private Rigidbody2D rigidbody = null;
  3.     private SpriteRenderer spriteRenderer = null;
  4.     private GroundCheck groundCheck = null; // 「GroundCheck」取得用の変数を定義
  5. // 省略
  6.     void Start()
  7.     {
  8.         rigidbody = this.GetComponent<Rigidbody2D>();
  9.         spriteRenderer = this.GetComponent<SpriteRenderer>();
  10.         groundCheck = this.transform.Find("GroundCheck").GetComponent<GroundCheck>(); //「GroundCheck」オブジェクトの「GroundCheck」コンポーネントを取得する。
  11.     }
  12.  
  13.     void Update()
  14.     {
  15. // 省略
  16.         if (Input.GetKeyDown(KeyCode.Space) && groundCheck.isGround) //「スペースキーが押された時」かつ「接地状態時」で true
  17.         {
  18.             rigidbody.AddForce(new Vector2(0, jump), ForceMode2D.Impulse);
  19.         }
  20.     }
 

まず、これまで通りコンポーネントを取得するための変数定義と、コンポーネントの取得を行います。
ただし、今回取得する「groundCheck」コンポーネントは、ひつじの子要素が持つコンポーネントです。
そのため、これまでのように「This.GetComponent<型名>()」の形では取得できません。
そこで、一旦子要素のオブジェクトをスクリプトで取得してから GetComponent() 関数を利用します。
子要素の取得には標準である Transform コンポーネントにある Find() 関数を使います。
Find() 関数は、引数に子要素の名前を文字列として指定してあげる(「'」または「"」記号で囲う)と、
その子要素を( Transform 型で)取得してくれるというものです。
今回の例でいうと、「this.transfrom.Find("GroundCheck")」で、子要素である「GroundCheck」オブジェクトを取得し、
次ぐ「.GetComponent<GroundCheck>()」で、「GroundCheck」オブジェクトのもつ「GroundCheck」コンポーネントを取得する、
という流れになります。

 

次に、ジャンプできる条件を変更します。
ジャンプが可能なのは「スペースキーが押された時」かつ「接地状態の時」です。
「接地状態の時」というのは「GroundCheck」の「isGround」がTrueの時でしたね。
「かつ」というのは && 演算子を用いて、

  1. if (条件文A && 条件文B)
  2. {
  3.     命令文
  4. }

と、いう風に書きます。(「または」という条件を表すときは || 演算子で表します。)
というわけで、「スペースキーが押された時」かつ「接地状態の時」というのは、
「Input.GetKeyDown(KeyCode.Space) && groundCheck.isGround」という風に表すことができるわけです。

 

これで「GroundCheck」の設置判定フラグを「MoveSeap」のジャンプ条件に入れることができ、
無事ジャンプが完成しました。
実際に動かしてみましょう。
ひつじの華麗な放物線ジャンプは見られましたか。
( Inspector から jump 変数を変えて、ジャンプ力を調整しよう)