独習 Unity アプリ開発

独習でスマフォ向けアプリ開発を勉強中

Unity で スイカ風ゲームを作ってみる#3

Unityでゲームを作る

Unity でスイカ風ゲームを作る!

 Unity Version 2022.3.16

 

前回は、ScriptableObject を使ってフルーツの各種データを管理する方法を確認した。今回は、物理演算を行うための準備、Rigidbody2D とCollider について整理する。

 

フルーツオブジェクトの構成


まずはフルーツオブジェクトを作成から。Hierarchy から "2D Object" → "Sprites" → "Circle" で、SpriteRenderer コンポーネントがアタッチされているGameObject を作る。そこにRigidbody2D コンポーネントとCircleCollider2D コンポーネントを追加する。

 

フルーツオブジェクト(Prefab)のコンポーネント構成

 

SpriteRenderer コンポーネントには、下図のテクスチャを指定する。フルーツには11種類が存在するが、今回は下図のテクスチャの色を変更すること同一のテクスチャを使いまわす。真ん中に線が入っているのは、後々回転状況を確認しやすくするため。

 

フルーツテクスチャ

 

フルーツスプライトアセットで、"Pixels Per Unit" を512 に設定する。テクスチャ画像のサイズは512 x 512 px で準備したので、(スケールを変更しない場合は)Unity スクリーン上に1 Unit の大きさで表示されるようになる。フルーツの大きさは種類によってことなるが、フルーツオブジェクトのTransform.localScale を使ってフルーツごとに大きさを変更することで対応する。"Pivot" も "Center" から"Bottom Left" に変更しておく。 これは好みの問題なので"Center" のままでも問題ない(もちろん位置計算方法もそれに合わせて変更する必要があるが)

フルーツスプライトの設定

 

CircleCollider2D の設定では、Offset を X = 0.5, Y = 0.5 に変更する。これはスプライトの"Pivot" を"Buttom Left" に変更したため、オフセットを付けないとコライダーの位置とテクスチャの位置がづれてしまう。PhysicsMaterial には何も指定していないが、Rigidbody2D 側のPhysicsMaterial に設定するのでこれでよい。Unity のドキュメントによるとPhysicsMaterial は、Collider → Rigidbody → global → デフォルト値の順で指定されているもが検索されて適用されるらしいので、Collider か Rigidbody のどちらかに設定しておけばよい。

Circle Collider 2D の設定



Rigidbody2D の設定では、"Material"(PhysicsMatrial のこと)に別途作成しておいた"FruitPhysic" アセットを設定し、"Simulated" のチェックを外しておく。その他の物理系パラメータは、スクリプト上からフルーツごとに設定するのでこの時点は適当な値のままでよい。

Rigidbody2D の設定

 

ここまで設定ができたら、フルーツオブジェクトをPrefab化しておく。

 

Rigidbody2D を使った落下処理


フルーツは雲のようなプレーヤーキャラクターから投下されて箱の中に落下する。フルーツオブジェクトにはRigidbody2D コンポーネントをアタッチしているので、画面上に配置するだけで自由落下を開始する。だが自由落下ではスイカゲームのプレイ動画と比較すると速度が足りていない。そこでRigidbody2D.Addforce メソッドを使って落下を加速させることにする。

テトリス風ゲームでは、Transform.localPosition に直接位置情報を書き込んで、GameObject を移動させていた。これはテトリミノの挙動が物理法則とは無関係で、落下速度や左右移動速度などを自分で決めて動かす必要があったためだ。

一方スイカ風ゲームでは、フルーツの挙動は物理法則に則って動かす必要があるため、Rigidbody2D コンポーネントをアタッチし、Unity Engine の物理演算の対象に入れるようにしている。Rigidbody2D を使って物理演算の対象に入れた場合は、"基本的に" Transform のパラメータは自分で勝手に書き換えることはできない(できるけど、その場合は物理法則外の力が加わることになるので、物理演算の結果は期待値どおりにならなくなる)

そこで登場するのがRigidbody2D.Addforce メソッドである。Addforce メソッドは、Rigidbody2D がアタッチされているオブジェクトに物理法則に則った力を加えて、物体を加速させることができる。

 

自由落下(左)とAddforce の適用(右)

左側が何の力も加えずに重力のみで自由落下させたオブジェクトで、右側がAddforce を使って下向きに力を加え加速させたオブジェクトである。Adforce を使えば、落下速度を上げることができることはわかるが、問題はどれくらいの力を加えればよいのかをどう計算するかだ。

 

上図の例では、ボールの落下位置から地面までの距離が8[m]、ボールの質量は1[Kg]としている。自由落下の場合に地面に着地するまでの時間は、等加速度直線運動方程式から、

  • 位置 = 1/2 x 重力加速度 x 時間の2乗

である。この式を変形して時間を求めるように変えると

  • 時間 = ルート(2 x 位置 / 重力加速度)

となる。よって今回のケースでは着地までにかかる時間は、

  • ルート(2 x 8 / 9.81)= 1.277 (秒)

となる。上図のGif だとわかりずらいが、おおよそ計算どおりになっていそうなことがわかる。

次に、これをAddforce を使うことで、0.5秒程度で地面に着地することを考える。

Addforce メソッドは、2つのパラメータを設定することができる。

public void AddForce (Vector2 forceForceMode2D mode= ForceMode2D.Force);

パラメータ force は、加える力をベクトルで表現したもので、mode は、力の加え方についての指定方法である。ForceMode2D は、

ForceMode2D.Force

ForceMode2D.Impulse

の2種類が存在する。両者の違いについては、こちらのサイトが詳しい。ざっくり言うと、ForceMode2D.Force はForce(力)をFixedUpdate 毎に連続して加えるようなケースに使用し、ForceMode2D.Impulse はForce(力)を単位時間(1秒)分の量で一瞬に加えるようなケースで使用する。つまり初速を与えるようなケースで使用できる。

今回のスイカ風ゲームでは、ForceMode2D.Impulse を使って、フルーツ落下開始時に初速を与え、落下時間を短くする方法で考える。

 

運動法定式から

  • 速度 = 初速 + 加速度 x 時間

であるので、初速を0、時間を単位時間(1秒)と考えると、この式は 速度 = 加速度 となる。また、

  • 力 = 質量 x 加速度

の関係式と合わせると

  • 速度 = 力 / 質量

となる。落下距離 8[m]、質量 1[Kg] で 着地まで0.5 [s] にしたい場合、Addforce で加える力(= 初速を決める力)は等加速度直線運動方程式から、

  • 落下距離 = 初速 x 時間 + 1/2 x 重力加速度 x 時間の2乗
  •      = 力 / 質量 x 時間 + 1/2 x 重力加速度 x 時間の2乗 
  • 8 = 力 / 1  x 0.5 + 1/2 x 9.81 x 0.25
  • 力 = 13.55

となる。スイカ風ゲームでは、プレイ動画からフルーツごとに質量が異なると思われるため、質量が1 [Kg] 以外のことも想定し、

  • 力 = 13.55 x 質量

という計算式を用いることにする。落下時間を変更したい場合は再度上記の式から計算しなおす必要がある。

 

FruitBase.cs の抜粋

FruitBase.Drop メソッドは、フルーツを落下させるときに呼び出されるメソッドである。Rigidbody2D.simulated を true にして物理演算処理を開始させ、Addforce を使って落下速度を加速させている。_dropForce には、上記で計算した値が入ることになるが、これはFruitDataSet アセットから取得するようにしている。

  1.   public void Drop()
  2.   {
  3.     _IsDrop = true;
  4.  
  5.     var rigidbody2D = GetComponent<Rigidbody2D>();
  6.     rigidbody2D.simulated = true;
  7.  
  8.     var force = _dropForce * rigidbody2D.mass;
  9.     rigidbody2D.AddForce(Vector2.down * force, ForceMode2D.Impulse);
  10.   }

 

 

Collider を使った衝突検出とフルーツの結合


フルーツオブジェクトには、Rigidbody2D に加えCircleCollider2D をアタッチしているのでOnCollisionEnter2D コールバックを通して衝突を検知することができる。Rigidbody2Dを付けずに、Collider コンポーネントだけをアタッチしている場合は、Collision 検出はできないので注意が必要。詳細は、こちらのサイトが詳しい。

 

FruitBase.cs の抜粋

OnCollisionEnter2D のパラメータ Collision2D には、衝突した相手のGameObject や、衝突した位置などの情報が設定されている。フルーツの結合時は、衝突位置を起点として1段階大きなフルーツを生成させる必要があるため、GetContact でContactPoint2D を取得後にpoint メンバから取得しておく。_fruitEvent はUnityEvent を使ったオブザーバーパターンの通知で、衝突情報をメッセージに詰めて、オブザーバーに通知(Invoke)している。

  1.   void OnCollisionEnter2D(Collision2D col)
  2.   {
  3.     var count = col.contactCount;
  4.     if (count > 0)
  5.     {
  6.       _IsDrop = false;
  7.       var contact = col.GetContact(0);
  8.       var other = col.gameObject.GetComponent<FruitBase>();
  9.       if (other != null && other.Type == Type)
  10.       {
  11.         FruitEventParam param =
  12.           new FruitEventParam(
  13.             FruitEventParam.Type.Combine, this, other, contact.point);
  14.         _fruitEvent.Invoke(param);
  15.       }
  16.     }
  17.   }

 

GameControl.cs の抜粋

FruitBase クラスの衝突イベントは、最終的にGameControl クラスのOnFruitCombineEvent メソッドに通知される。注意点として、この衝突イベントは、衝突した2 つのフルーツオブジェクトからそれぞれ発行されるため、合計2 回呼び出されることである。そのため、2 回目の呼び出し時点では既に該当のフルーツオブジェクトが削除されている可能性があるため、そのチェックを入れている。また、フルーツがスイカだった場合も、結合する必要がないのでイベントを無視するように事前チェックを入れた。

結合処理自体は簡単で、衝突した2 つのフルーツオブジェクトを削除してから、新しいフルーツオブジェクトを衝突位置に生成するだけである。

  1.   private void OnFruitCombineEvent (FruitBase src, FruitBase dst, Vector2 pos)
  2.   {
  3.     if (src.Type == FruitBase.FruitType.Watermelon)
  4.     {
  5.       return;
  6.     }
  7.     
  8.     var nextType = (FruitBase.FruitType)( (int)src.Type + 1);
  9.     if (!_field.ContainFruit(src.Id))
  10.     {
  11.       return; // Second message for the same collision event.
  12.     }
  13.  
  14.     _field.RemoveFruit(src.Id);
  15.     _field.RemoveFruit(dst.Id);
  16.     FruitGenerator.Instance.DestroyFruit(src);
  17.     FruitGenerator.Instance.DestroyFruit(dst);
  18.     var fruit = FruitGenerator.Instance.CreateFruit(nextType);
  19.     fruit.gameObject.transform.localPosition =
  20.       new Vector3(// pivot is bottom-left
  21.         pos.x - fruit.Size.x * 0.5f,
  22.         pos.y - fruit.Size.y * 0.5f,
  23.         0);
  24.     fruit.AddListener(OnFruitEvent);
  25.     _field.AddFruit(fruit);
  26.     _levelScore.AddScore(nextType);
  27.   }

 

 

次の記事


from20150817.hatenablog.com

 

目次に戻る