独習 Unity アプリ開発

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

Unity で8番出口風ゲームを作る!(はじめに)

Unityでゲームを作る

Unity で8番出口風ゲームを作る!

 Unity Version 2022.3.16

 

Unity を使ったゲーム開発の流れを理解するために、最近はやり(もう終わったかもですが)の8番出口風ゲームを作ってみます。8番出口ってなに?という方は、ドズルさんの動画を参照ください。

 

目次


 

 

プロト


WebGL版のプロト(PCブラウザのみ、スマフォはNG)

from20150817.github.io

 

 


目次に戻る

 

【Unity 備忘録】非アクティブなGameObject の検索

目次に戻る


 

GameObject の検索


ゲーム開始後に動的に生成されるオブジェクトなど、事前に登録できないGameObject をゲーム動作時に検索したいケースがある。

 

  • GameObject.Find
    • 検索対象のGameObject を名前で検索可能
    • 全てのアクティブなGameObject が検索範囲
    • 同一名のGameObject が複数ある場合は、最初に見つかったものを返す

 

  • GameObject.FindWithTag
    • 検索対象のGameObject をTag で検索可能
    • 全てのアクティブなGameObject が検索範囲
    • 同一名のTagを持つGameObject が複数ある場合は、最初に見つかったものを返す

 

 

  • Transform.Find
    • 検索対象のTransform を名前で検索可能(GameObject の名前)
    • 自身 の子Transform が検索範囲
    • 非アクティブなGameObject も検索可能
    • 階層を指定することで検索範囲を指定することが可能
      • 例) "Body/LeftArm/Hand" 

 

ということで、調べた感じでは、非アクティブなGameObject も検索可能なのはTransform.Find のみ。

 

 


目次に戻る

 

【Unity 備忘録】UI Layout の調整方法

目次に戻る


 

UI Layout 調整


スマフォ向けのゲームを作る上で必須の異なる画面サイズでのUI Layout 調整方法について調べる。

 

スマフォの画サイズ


まずは、最近のスマフォの画サイズにどんなものがあるのかを把握するところから始める。ちょうどいいサイトがあったのでそこから代表的なものを抜粋すると下表のようになっている。ひと昔前は、FHD(1080 x 1920)が主流だったが、最近はより縦長なアスペクトが主流になっているみたい。

 

機種

高さ

アスペクト比

iPhone8

1080

1920

9:16

iPhone15 Pro Max

1290

2796

9:19

iPad 9th

1620

2160

3:4

Pixel7

1080

2400

9:20

Galaxy S22 Ultra

1440

3088

9:19

 

UI Layout の調整を考える上で重要なのは、画素数ではなくてアスペクト比の方なので、上表からおおよそ3:4 ~ 9:20 くらいまでの間のアスペクト比で破綻なくUI が表示できるように調整できればよいことがわかる。(縦レイアウトのみをサポートする場合)

 

Canvas Scalerによるレイアウト調整


Unity では、異なる画サイズの画面にレイアウトが自動調整できるような、いろいろな仕組み(コンポーネント)が存在する。まずは、その中でも基本となりそうなCanvas Scaler について調べる。Canvas Scaler は、Canvas オブジェクトにデフォルトでアタッチされているスケーリングコンポーネントである。全てのUI オブジェクトは、Canvas オブジェクトの子要素として構成されるので、親であるCanvas オブジェクトのスケーリングができれば、子要素もそれに従って自動で調整されることになる。

 

Canvas Scaler コンポーネント

  • UI Scale Mode 

Canvas Scaler には、どのような基準でスケーリングを行うかを決める"UI Scale Mode" というパラメータがあり、スケーリング方法には以下の3つが存在する。

 

1. Constant Pixcel Size モード

Constant Pixel Size モード

"Scale Factor" で設定した倍率で、レイアウト時のアスペクト比は保ったまま、スケーリングを行って表示するモード。画面サイズによらず、一定のスケーリングが適用される。例として512x512px のUI Image オブジェクトを縦6個、横3個に並べて、ゲームビューの画サイズを変更して表示させた場合の結果を下図に示す。

Constant Scale


Constant Pixel Size モードは単純にどの画面サイズでも同率のスケーリングが適用されるので、1290 x 2796 画面サイズでは、縦に約5.5個、横に約2.5個が表示さるが、それを1080 x 1920 画面サイズのゲームビューで表示すると、縦に約3.8個、横に約2.1個が表示される。単純ではあるが、このスケールモードでは基準とした画サイズより小さな画面サイズで表示した場合は、UIオブジェクトが表示しきれないことがあるため、これだけでは使い物にならない。スクリプト上から画サイズを取得して、それによって"Scale Factor"を動的に変更するということで調整できなくもないが、それではCanvas Scaler を使う意味があまりない。

 

2. Scale With Screen Size モード

Scale with Screen Size モード

"Reference Resolution" で設定された画サイズを基準にスケーリングを行うモード。

 

  • Screen Match Mode : Match Width Or Height

"Screen Match Mode" が "Match Width Or Height" の場合、"Reference Resolution" の幅か高さか(又はその比率か)のどれかを基準にしてスケーリングすることができる。どれを基準にするかは、"Match" バーで設定する。

"Match" バーを0(一番左)位置に設定すると "Reference Resolution" の"幅を基準"にスケーリングが行われる。"幅が基準"とは、"Reference Resolution" の画面サイズで、表示できているUI オブジェクトの"水平方向"が、どの画面サイズに変更して表示されることを保証してくれるようにスケーリングされることを意味している。

Scale With Screen, 幅基準でスケール

Constant Pixel Size モードの時と同様に1290 x 2796 画面サイズに、512x512px のUI Image オブジェクトを縦6個、横3個に並べ、Scale with Screen Size モードでスケールさせたときの結果を上図に示した。 "Reference Resolution" は 1290 x 2796 で、"Match"バーを0(幅を基準)に設定している。

上図のように、ゲームビューの画面サイズを1080 x 1920 にしようと、1620 x 2160 にしようと、水平方向には、すべて約2.5個分のUI Imageが表示されていることがわかる。

一方、垂直方向も水平方向と同じ倍率でスケーリングされるため、UI Image オブジェクトのアスペクト比が崩れることはない(円の画像がつぶれるようなことはない)。その代わり、垂直方向は切れることになる。

例では、幅を基準にしたが、"Match"バーを1に設定すれば、"高さを基準"にすることができる。この場合は垂直方向は、"Reference Resolution" と同じ個数のUI Image オブジェクトが表示されることが保証され、その代わり水平方向の表示個数は変わる。

 

対象としているスマフォの画サイズが、9:19 (1290 x 2796)、9:16 (1080 x 1920)、3:4 (=9:12) (1620 x 2160) の場合、みな縦長のアスペクト比である。9:19 (1290 x 2796) が一番縦長なので、これを"Reference Resolution" にしてレイアウトを決定し、"Match" を 1(高さを基準)にすれば、下図のように9:16 画面でも 3:4 画面でもUI Imageが切れることなく表示できるようになることがわかる。(その代わりに水平方向には余白ができるが、これはAnchorの設定で調整するようにする)。

Scale With Screen, 高さ基準でスケール
  • Screen Match Mode : Expand

"Expand"モードでは、"Reference Resolution" が縦長画サイズの場合は、高さを基準にスケーリングし、"Reference Resolution" が横長サイズの場合は、幅を基準にスケーリングする。("Match Width Or Height" でも同様のことが設定で可能なため、このモードを使う理由はあまりなし?と思われる)

 

  • Screen Match Mode : Shrink

"Shrink"モードは、Expand と逆で"Reference Resolution" が縦長画サイズの場合は、幅を基準にスケーリングし、"Reference Resolution" が横長サイズの場合は、高さを基準にスケーリングするモードである。("Match Width Or Height" でも同様のことが設定で可能なため、使う理由はあまりなし?と思われる)

 

3. Constant Physical Size Mode

(よくわからい、、、)

 

以上から、縦長画面前提で、UI Layout を考える場合は、最も縦長のアスペクト比の画面でレイアウト設計を行い、Canvas Scaler コンポーネントによるスケーリングは、Scale With Screen Size で、Match Width Or Height を"高さ基準"で適用すればよいことがわかった。

 

Anchorによるレイアウト


Canvas Scaler によるレイアウト調整では、基準レイアウト画サイズとは異なる画サイズで表示したときの表示切れ問題は解決できるが、水平方向の位置調整の問題が残る。下図のように基準の画サイズとは異なるアスペクト比の画面サイズで表示すると、不自然な余白が残ってしまう。

余白

そこで重要になるのがAnchor という機能である。Anchor の詳細は、マニュアルに詳しく記載されているので、基本的な部分はそちらを参照。

まずは、UI Image オブジェクトを4つ画面中央に整列させたレイアウトを準備して、それぞれAnchor の設定を変えて、画サイズ(アスペクト比)を変更した結果が下記。

Canvas Scaler は、Scale With Screen Size で、Match Width Or Height を"高さ基準"にしている。

Anchor によるレイアウト調整の比較

 

  • 固定Anchor

固定Anchor

固定Anchor は、親オブジェクトの特定の一点をAnchor(基準)に指定してレイアウト位置を決定する方法である。UI オブジェクトの"Rect Transform" の"Anchors" 設定にある"Min" と "Max" が同一の値の場合、Anchor は特定の一点を示すようになる。上図の例では、親オブジェクトの左上頂点の一点を基準にしている。Anchor 位置からの距離でレイアウトされるため、画サイズ(アスペクト比)が変わったとしても、変更前と変わらない位置にレイアウトされる。アスペクト比がより横長になった場合には、上図のように右側に変な余白の偏りができてしまうことになる。

 

  • Custom Anchor(+ Preserve Aspect

カスタムAnchor

カスタムAnchor の表示

カスタムAnchor は、Anchor の"Min" と"Max" に"0", "1" 以外の値を設定する方法である。特に"Min" と"Max" にそれぞれ異なる値を設定すると、親オブジェクトの特定の二点をAnchor(基準)に指定してレイアウト位置とサイズを決定することができる。固定Anchor との違いはAnchor が2点になるため、位置だけでなくサイズにも影響を与えることができる点である。上図の例では、Anchor の"Min" と"Max" のY座標は同一だが、X座標が異なっている。この場合、例えば画サイズが変更されてオリジナルよりもアスペクトが横長になった場合は、"Min" と"Max" のX座標で指定した比率に従ってUI オブジェクトの位置だけでなく、水平方向の長さも引き伸ばされることになる。

これでアスペクト比が変更になった場合にも、余白の偏りをなくすことができるが、今度はUI オブジェクト自体のアスペクト比も変更されてしまう状態となる。もし、UI オブジェクトのアスペクト比がデザイン上それほど重要でない場合はそれでもよいが、9パッチ以外の特定のアスペクト比でデザインされた画像を使う場合は困る。そこで利用できるのが"Preserve Aspect" というオプション機能である。

 

"Preserve Aspect" 設定は、UI Image コンポーネントにある設定で、"Source Image" に設定されているスプライト(テクスチャ)のオリジナルのアスペクト比を保って拡縮するオプション設定である。

Preserve Aspect 設定

 

"Preserve Aspect" をONにしている場合、たとえUI オブジェクト自体のアスペクト比が変わったとしても、下図のように"Source Image" のアスペクトを保って中心位置に表示することができる。

Preserve Aspect 設定時のImage

Anchor で指定した開始位置と実際のImage の表示位置は多少ズレることになるが、これで余白の偏りをなくし、かつ、アスペクトを綺麗に保って表示することができる。

 

  • Stretch Anchor(+ Horizotal Layout Group)

Stretch Anchor は、Anchor の"Min" と"Max" のX 座標、又はY 座標、又は両方が、"Min" は"0" に"Max" は "1" に設定されているAnchor のことである。

Stretch Anchor

考え方は、Custom Anchor と変わりはないが、Horizontal Layout Group コンポーネントと合わせて使用することで、Custom Anchor の説明で示したやり方とは別の方法でLayout の調整を行うことができる。

 

Horizontal Layout Group コンポーネント

例として、Horizontal Layout Group コンポーネントをアタッチしたPanel UI オブジェクトと、その子オブジェクトとしてImage UI オブジェクトを配置した場合で説明する。

 

Horizontal Layout Group を使ったLayout

Horizontal Layout Group コンポーネントは、子オブジェクトを水平方向に均等に整列させるLayout コンポーネントである。"Padding" を設定することで、上下左右に余白を作ることができる。

上図の例では、Horizontal Layout Group コンポーネントがアタッチされている親オブジェクト(Panel UI)のAnchor を水平方向のStretch Anchor に設定し、画サイズ一杯に広がるようにしている。"Padding" は左右に93 pixel 取り、その中に子オブジェクトを整列するように配置した。

 

Horizontal Layout Group を使ったLayout (アスペクト変更時)

画サイズのアスペクトがより横長になった場合は、自動的に子オブジェクトが均等になるように再配置されることがわかる。

 

 


目次に戻る

 

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

Unityでゲームを作る

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

 Unity Version 2022.3.16

 

前回は、InputSystem を使ったマウスポジションの検出方法とPolygonCollider を使ったField オブジェクトの実現方法について確認した。今回は、スイカ風ゲームの全体のクラス構造について整理する。

 

クラスの構成としては、プレーヤー、フルーツ、フィールド、Next、得点、UI画面を管理するクラスとそれぞれに指示を出すゲームコントロールクラスで構成する。ゲームコントロールクラスから各クラスの依存は許すが、それ以外の各クラス間、各クラスからのゲームコントロールクラスへの逆方向への依存は"なるべく"しないように設計するというルールで進める。

 

UIコントロール


UI の表示/非表示の制御方法は、テトリス風ゲームの時と同様にUI Panel GameObject のSetActive を使う。UIControl クラスが各画面のインスタンスを保持しており、SwitchUI メソッドを経由して画面の制御を行う構造とする。

 

UI コントールクラス

 

 

フルーツの生成とFieldへの落下


フルーツオブジェクトの生成は、Next クラスがFruitGenerator クラスを使って生成する。生成したFruitBase クラスのインスタンスをGameControl クラスが受け取ってPlayer クラスに渡す。Player クラスからフルーツ投下イベント(OnPlayerEvent)を受信すると、今度はそのFruitBase クラスのインスタンスをField クラスに渡すという流れ。

フルーツの生成とフィールドへの落下

 

 

フルーツの結合と得点の更新


フルーツの結合は、FruitBase クラスの衝突検知から開始される。同一種のフルーツ衝突を検知した場合は、OnFruitEvent をGameControl に通知する。GameControl クラスは、衝突した2 つのFruitBase クラスのインスタンスを削除して、新しいFruitBase クラスのインスタンスを生成する。それをField クラスに渡すのと同時にLevelScore クラスの得点の加算のメソッド(AddScore)を呼び出す。LevelScore クラスは、得点加算時にScoreText オブジェクトのテキストを更新する。

フルーツの結合

 

次の記事


(準備中)

 

 

 


目次に戻る

 

 

 

 

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

Unityでゲームを作る

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

 Unity Version 2022.3.16

 

前回は、フルーツオブジェクトの物理演算について確認した。今回はInputSystem を使ったマウスポジションの検出方法を整理する。

 

InputSystem でマウスポジションを検知する


InputSystem については、テトリス風ゲーム作成時に以下でざっと説明している。

from20150817.hatenablog.com

 

テトリス風ゲームの時との違いは、キーボードやゲームパッドのボタンが"押された"というON/OFFのイベントではなく、マウスの座標(X, Y)を取得する必要がある点である。マウス座標を取得するには、Input Action の"Action Properties" で"Action Type" に"Value" を選択し、"Control Type" に"Vector 2" を選択する。こうすることで、マウス座標がVector2 型のパラメータで取得できるようになる。

InputSystem Actionの設定

次にPlayer オブジェクト(Prefab)に"Player Input" コンポーネントをアタッチして、Input Action のコールバックを取得できるように設定する。

Player オブジェクト(Prefab)のPlayerInput 設定

Input Action に設定したマウスの"Move" Action は、Player クラスのOnMoveEvent メソッドで受け取るように設定する。

 

Player.cs の抜粋

マウス座標の情報は、コールバックに設定したメソッドの引数InputAction.CallbackContext から取得できる。ただし、取得できる座標情報は、ディスプレイ上のScreen 座標であるため、利用する場合はWorld 座標に変換する必要がある。World 座標への変換は、Camera クラスのScreenToWorldPoint メソッドを利用する。プレーヤーの移動は、物理演算に従う必要はないので、World 座標に変換した座標をTransform.localPosition に直接設定してオブジェクトを移動させる。

InputAction クラスを利用するには、UnityEngine.InputSystem をUsing 宣言しておく必要があるので注意。

  1.   public void OnMoveEvent (InputAction.CallbackContext context)
  2.   {
  3.     if (_pause) return;
  4.  
  5.     var screenPos = context.ReadValue<Vector2>();
  6.     var worldPos = Camera.main.ScreenToWorldPoint(screenPos);
  7.     Move(worldPos.x);
  8.   }
  9.  
  10.   private void Move (float x)
  11.   {
  12.     var min = _leftWallX + _fruitSize.x * this.transform.localScale.x * 0.5f;
  13.     var max = _rightWallX - _fruitSize.x * this.transform.localScale.x * 0.5f;
  14.  
  15.     if (x <= min)
  16.     {
  17.       x = min +0.01f;
  18.     }
  19.     else if (max <= x)
  20.     {
  21.       x = max -0.01f;
  22.     }
  23.  
  24.     this.transform.localPosition =
  25.       new Vector3(x, this.transform.localPosition.y, 0);
  26.     
  27.     DrawLine();
  28.   }

 

Player 移動

 

Polygon Collider で箱オブジェクト


フルーツが入る箱オブジェクト(以下Field オブジェクト)には、PolygonCollider2D をアタッチして、フルーツとの衝突判定を実現する。Polygon Collider は、四角や丸と言った単純な図形ではなく、任意の数の頂点を自由に定義してその頂点をつなぎ合わせた複雑な図形にCollider の境界線を設定することができる。この特性を活かしてField のテクスチャに合わせた形にCollider 境界線を作ることにする。

箱のPolygonCollider

PolygonCollider で使うCollider の境界線(頂点)情報は、Sprite Editor 使って視覚的に編集することができる。まずはFiled オブジェクトに使うスプライトのInspector から、"Sprite Editor" ボタンを押してEditor を起動する。

SpriteEditor 起動

 

Sprite Editor が起動したら、左上のメニューボタンから"Custom Physics Shape" を選択し、Collider 編集モードに切り替える。その後、画面上部にある"Generate" ボタンを押して、一度Colliderの頂点を自動生成させる。

Collider Editor

自動生成で大まかな頂点が描けたら、後は自分で頂点(白いポイント)を選択して位置を修正したり、頂点を追加(境界線上をクリック)したり、頂点を削除(頂点にフォーカスして"Delete"キー押し)したりして、テクスチャに合うようにCollider を編集する。

※頂点編集機能は、Unity Version 2022.3.4 あたりでバグが発生し、一時期使えない状態となっていた。この機能を使う場合は、最新のバージョンを取得して試すか、Unity のリリース情報を確認するとよい。

Collider 頂点編集

 

編集し終わったCollider は、PolygonCollider コンポーネントのInspector から頂点の座標情報を確認することができる。今回のFiled テクスチャは, 1024 x 1024 px の画像であり 128 px を 1Unit と設定しているため、一辺は8 Unit の長さとなっている。

編集したCollider 頂点がうまく反映されていない場合は、一度PolygonCollider オブジェクトを削除して、再度アタッチしなおすと反映される。

 

Collider 頂点情報

実際にField オブジェクトのCollider を設定し終わった結果がこちら。

Field と フルーツの衝突判定

 

Player の動作範囲の特定


Player が動ける範囲は、Field の箱型のテクスチャの内側の範囲である。この位置を決めるために、Collider の頂点情報を利用する。PolygonCollider コンポーネントの頂点情報をInspector で確認し、箱型のテクスチャの内側のX 座標の位置をFIELD_LEFT_WALL_X_RATIO とFIELD_RIGHT_WALL_X_RATIO の定数として定義する。Field オブジェクトは、画面サイズに合わせて拡大させるため、最終的には拡大率とオフセット位置とを合わせてLeftWallX、RightWallX のプロパティに保存しておくようにする。Player クラスからこのプロパティを使って、左右の移動可能範囲を割り出している。

  1.   public float LeftWallX {get; private set;}
  2.   public float RightWallX {get; private set;}
  3.   public float LimitY {get; private set;}
  4.   public Vector2 Size {get; private set;}
  5.   
  6.   public const float FIELD_PER_CHERRY = 14.0f; // 14 cherry = 1 field width
  7.   private const float FIELD_UNIT = 1024f / ScreenConfig.PX_PER_UNIT; // Field Texture = 1024 x 1024 px
  8.   private const float FIELD_LEFT_WALL_X_RATIO = 0.430f;
  9.   private const float FIELD_RIGHT_WALL_X_RATIO = 7.57f;
  10.   private const float FIELD_BOTTOM_Y_RATIO = 0.156f;
  11.   private const float LIMIT_Y_VS_FIELD_WIDTH_RATIO = 1.1f;
  12.   private Dictionary<int, FruitBase> _fruits;
  13.  
  14.   public void Init()
  15.   {
  16.     // Field size is calculated from screen size and margin
  17.     var fieldHightUnit =
  18.       ScreenConfig.Instance.OrthographicSize*2f
  19.       - ScreenConfig.TOP_MARGIN - ScreenConfig.BOTTOM_MARGIN;
  20.     
  21.     // scale to calculate field size
  22.     var scale = fieldHightUnit / FIELD_UNIT;
  23.     this.transform.localScale = new Vector3(scale, scale, 1f);
  24.     
  25.     // Actual size of field
  26.     Size = new Vector2(FIELD_UNIT * scale, FIELD_UNIT * scale);
  27.     
  28.     // field texture pivot is bottom-left
  29.     var offset_x = ScreenConfig.Instance.CameraCenter.x - FIELD_UNIT * scale * 0.5f;
  30.     this.transform.localPosition =
  31.       new Vector3(offset_x, ScreenConfig.BOTTOM_MARGIN, 0f);
  32.  
  33.     // define field wall inside and limit line
  34.     LeftWallX = FIELD_LEFT_WALL_X_RATIO * scale + offset_x;
  35.     RightWallX = FIELD_RIGHT_WALL_X_RATIO * scale + offset_x;
  36.     var height = (RightWallX - LeftWallX) * LIMIT_Y_VS_FIELD_WIDTH_RATIO;
  37.     var offsetY = this.transform.position.y + this.transform.localScale.y * FIELD_BOTTOM_Y_RATIO;
  38.     LimitY = height + offsetY; // Gameover line
  39.     DrawLimitLine();
  40.  
  41.     _fruits = new Dictionary<int, FruitBase>();
  42.   }

 

次の記事


from20150817.hatenablog.com

 

 


目次に戻る

 

 

 

 

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

 

目次に戻る

 

 

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

Unityでゲームを作る

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

 Unity Version 2022.3.16

 

ScriptableObject を使ってフルーツデータの管理


前回の仕様確認の結果から、フルーツの種類が全部で11種類と意外と多いことが分かった。各フルーツに必要なパラメータを考えてると、テトリスとは異なり物理演算が必要になることから、RigidBody 系のパラメータも必要になると思われる。

 

フルーツのパラメータ

  • サイズ(Transform.localScale)
  • 色(SpriteRenderer.color)
  • 重さ(RigidBody2d.mass)
  • 空気との回転摩擦(RigidBody2d.angularDrag)
  • 空気との移動摩擦(RigidBody2d.drag)
  • 摩擦係数(PhysicsMaterial2D.friction)
  • 反発係数(PhysicsMaterial2D.bounciness)
  • 落下時に加える力(Addforce で与える力を計算するため)

 

ざっと考えるだけでもこれくらいのパラメータ数が存在する。これらのデータを管理するためにデータクラスを作成して、スクリプト上にリテラルで直接値を定義をしてしまってもよいが、後々調整しながらゲームバランスを整えることを考えると、手軽に変更できる手段で実装しておきたい。

 

そこで、今回はUnity のScriptableObject という仕組みを使ってデータ管理を行うように実装する。ScriptableObject の詳細はUnity のマニュアルや解説サイトを参照いただくとして、ここでざっくり説明しておくと、ScriptableObject とはUnity でゲームに使用する各種の定数データ(動的に書き換えることもできるが)をアセット上に保存し、それをスクリプトから参照できるようにする仕組みである。アセットなので、定義したデータをInspector から直接編集して変更することが可能なため、コンパイルせずにパラメータ調整を行うことが可能となる。使用方法は、ScriptableObject から派生したクラスを作成し、そのクラス内に各種データ用のメンバを定義する。Edior から定義したScriptableObject アセットファイルを作成して、Inspector から値を設定する。スクリプトから利用する際はPrefab の時と同様にResources.Load メソッドなど読み込むと通常のクラスと同様にスクリプト上でアクセスすることができる。

 

文章で説明するだけだとよくわからいなので、実際にスイカ風ゲームで適用した方法を記載する。

FruitDataSet.cs の抜粋

※ScriptableObject を定義するファイル名は、ScriptableObject のクラス名と同一にする

FuitDataSet がScriptableObject から派生したクラス。クラス定義の前にある属性[CreateAssetMenu] は、Unity Editor のAssets/Create メニューからこのアセットを作成できるようにするために必要な指定。FruitDataSet クラスにてList で保持されるFruitData クラスもInspector 上から値を設定できるようにするために[Serializable] 属性の定義を付ける必要がある。

  1. [Serializable]
  2. public class FruitData
  3. {
  4.     public string Name;
  5.     public float Size;
  6.     public float Weight;
  7.     public float AngularDrag;
  8.     public float LinearDrag;
  9.     public float Friction;
  10.     public Color FColor;
  11. }
  12.  
  13. [CreateAssetMenu(menuName = "ScriptableObject/FruitDataSet", fileName = "FruitDataSet")]
  14. public class FruitDataSet : ScriptableObject
  15. {
  16.   public float MassBase;
  17.   public float DropForce;
  18.   public List<FruitData> Data;
  19. }

 

次に上記で作成したScriptableObject をアセットとして生成する。今回は"Resources" フォルダ配下に"Data" フォルダを作成し、そのフォルダの中で右クリックのメニューから"Create"→"ScriptableObject"→"FruitDataSet" を選択する。すると"FruitDataSet" というアセットファイルが作成される。

ScriptableObjectの生成

"FruitDataSet" アセットが作成できたら、具体的な設定値を設定していく。"FruitDataSet" アセットファイルを選択すると、Inspector 上にFruitDataSet クラスで定義したパラメーターが見えるので、それぞれの値を設定していく。これでアセットファイルとして各フルーツのデータが保存できたことになる。

FruitDataSetアセットの設定

 

作成した"FruitDataSet" アセットは、スクリプト上からResources.Load メソッドでロードして利用することが可能である。

 

スクリプト上でScriptableObject アセットをロードする例)

  • _fruitDataSet = Resources.Load<FruitDataSet>("Data/FruitDataSet");

 

ロードさえしてしまえば、後は通常のクラスのメンバにアクセスするのと同様に各パラメータを利用することできる。

これで、後から各パラメータを微調整する際には、Inspector からいつでも変更できてコンパイル不要で動作確認を行うことができるようになった。

 

次の記事


from20150817.hatenablog.com

 

目次に戻る