ひつじを動かそう その2 のバックアップ(No.12)


はじめに

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

 

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

 

本格的にひつじを動かそう

 

現段階ではひつじはA,Dキーの入力で左右に動かせるだけです。
しかしこれでは、ゲームとして不完全です。
あと必要そうな機能は、

1.重力の力を受ける
2.キー入力に合わせてひつじの向きを変える
3.キー入力でジャンプする

  などがあります。
では、1つずつ実装していきましょう。

 

ひつじに重力を与える

 

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

 

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

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

   というものです。

 

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

 

①の問題は、坂に当たった時の物理演算をz軸に関しても行っているせいで坂とひつじの間の摩擦力によって回転してしまうのです。
そのため、z軸に関する物理演算を止めてあげる、つまり、ひつじをz軸回転しないように固定してあげる必要があります。
固定する方法は簡単で、ひつじのインスペクターから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のインスペクターからも見ることができない(もちろん編集できない)変数として定義することができます。
今回定義する変数は、ひつじの物理演算を司るコンポーネントを格納するということで、基本的に中身が変更しない変数になります。
このような変更のない変数、或いは変更されては困るような変数を定義するときに使います。
(以前使った「public」もアクセス修飾子の一つで、一番権限レベルの易しいものになります。ほかのプログラムやインスペクターからも操作できるため、中身を変更することがある場合は「public」をつけましょう。)

 

次は、Start関数の中の"rigidBody = this.GetComponent<Rigidbody2D>();"です。
これは、ひつじについている「Rigidbody2D」のコンポーネントを取得して、先ほど定義してあげた「rigidBody」に入れる、という操作になります。これで、「rigidBody」という、変数を通じてひつじについた「Rigidbody2D」設定をゲームの最中でも変更できるようになりました。

 

最後に、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」を変更することによって変えられます。
「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 ひつじに上方向の加速度を一回与える

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

 

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

キー入力の受け取りを実装します。
ひつじを動かしてみる その2の時と同じようにしてスペースキーの入力も検知できます。
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.     }
 

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

はねる という動きを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軸方向への力として使います。
もちろんインスペクターからの編集もできるようにアクセス修飾子として「public」を指定しておいてください。
これで、スペースキーが押されたときに上方向に飛び上がるようになります。

 

接地判定を取る

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

 

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

 

#画像

 

ひつじの子オブジェクトにすることがポイントです。
Unityでは、子オブジェクトは親オブジェクトを基準に座標が計算されます。
つまり、特にスクリプトを書かずとも、子オブジェクトは親オブジェクトとの位置関係をキープし続ける、つまり勝手についてきてくれる、というわけです。

 

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

 

#画像

 

そのままではひつじと同じ大きさの当たり判定になってしまい、足元だけの判定にならないので、
インスペクターの「BoxCollider」コンポーネントから「EditCollider」を押し
シーンビューでコライダーの大きさを調整します。
(横幅はひつじの半分程度、縦幅は小さく、位置はひつじの足元になるように)

 

#画像

 

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

 

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

 

#画像

 

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

 

次に設置判定用のスクリプトを作っていきます。
「GroundCheck」を使って「MoveSheep」内で設置判定を受け取るには、
「GroundCheck」のなかに「public」でフラグを作り、設置していればフラグをTrueにする。
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」のインスペクターから「Tag」>「AddTag」を選択、「Tags」の「+」を押して、
「NewTagName」に「ground」と入力し「Save」します。
もう一度「Tilemap」のインスペクターから「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. }

今回新たに「OnTriggerEnter2D」と「OnTriggerExit2D」という2つの関数が出てきました。
これはそれぞれ当たり判定が「重なり始めたとき」と「重なりがなくなったとき」に呼び出させる関数になります。
これ以外にも「OnTtriggerStay2D」という当たり判定が「重なっているとき」に呼び出される関数などがあります。
他にもいくつかありますがここでは省きます。ぜひ自分で調べてみてください。
さて、今回はどちらも引数を指定しています。
「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」メゾットは、引数に子要素の名前を文字列として指定してあげる(「'」または「"」記号で囲う)と、
その子要素を取得してくれるというものです。
今回の例でいうと、「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」のジャンプ条件に入れることができ、
無事ジャンプが完成しました。
実際に動かしてみましょう。
ひつじの華麗な放物線ジャンプは見られましたか。
(インスペクターからjump変数を変えて、ジャンプ力を調整しよう)