独習 Unity アプリ開発

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

Unity で テトリス風ゲームを作ってみる(実装編)#5

 Unity Version 2022.3.4

 

前回の続き


前回までテトリミノの操作について実装を説明してきたが、今回はフィールドの生成とライン削除、ライン落下について説明する。

 

フィールドの作成


フィールドの基本情報(高さ、幅、枠の厚み)は、定数として事前に定義しておく。

ScreenConfig.cs の抜粋

  1.   public const int FIELD_WIDTH = 10;
  2.   public const int FIELD_HEIGHT = 20;
  3.   public const int FIELD_BOTTOM_WIDTH = 1;
  4.   public const int FIELD_SIDE_WIDTH = 1;

 

Fieldのオフセット位置(フィールドの原点位置)とOrhographicSize は、以前検討したとおり下記の方法で算出する。

OrthographicSize/Field Offsetの計算

オフセットをScreen サイズ(Pixel)からUnit 単位に変換する必要があるので注意が必要。

Field Offset H = Screen 高さ(Pixel)の5%はPixel 単位なので、Unit に換算するには128 で割る。

OrthographicSize = Screen 高さ(Pixel)の半分をUnit 数で表したものなので、枠の高さ21ブロックの半分である10.5 ブロック(1 ブロック=1 Unit)+ Field Offset H で求められる。

Field Offset W = Screen のアスペクト比(Screen W/ Screen H)x OrthographicSize(Unit)から6 ブロック分(フィールド+枠の長さの半分)を引いたもので求められる。

以上の計算をコードに直したのものが下記。

 

ScreenConfig.cs の抜粋

  1.   private void Init() {
  2.     ScreenAspect = Screen.safeArea.width / Screen.safeArea.height;
  3.  
  4.     var offsetHight = Screen.safeArea.height * SCREEN_OFFSET_RATIO; // pixel
  5.  
  6.     OrthographicSize = (float)(FIELD_HEIGHT+FIELD_BOTTOM_WIDTH) * 0.5f
  7.                          + offsetHight / PIXEL_PER_UNIT; // Unit
  8.  
  9.     FieldOffset = new Vector2(
  10.       OrthographicSize * ScreenAspect - (float)(FIELD_WIDTH+FIELD_SIDE_WIDTH*2f) * 0.5f,
  11.       offsetHight / PIXEL_PER_UNIT
  12.     );
  13.   }

 

Field は、ブロックを生成して上記で計算した枠の位置に配置すればOK。

Field GameObject を親オブジェクトにして、枠用ブロックを子オブジェクトとすることでオフセットを一括で管理できる(親のField GameObject の位置をオフセット位置に移動すれば、子ブロックはオフセットを気にする必要がなくなる)。

まずは、Field GameObject のlocalPosition を上記で計算したOffset の位置に移動させ、あとはFor ループで、左右と底のブロックを1 列作ればよい。

(_fieldEventListenr については別途説明予定)

Field.cs の抜粋(Field生成)

  1.   private List<GameObject> _fieldBlocks = new List<GameObject>();
  2.   
  3.   public void Init(IFieldEventListener listener) {
  4.     this.transform.localPosition = new Vector3(
  5.       ScreenConfig.Instance.FieldOffset.x,
  6.       ScreenConfig.Instance.FieldOffset.y,
  7.       0); // Move to Field Offset position
  8.  
  9.     // Create Field Area
  10.     // Bottom Wall
  11.     for (int x = 0; x < (ScreenConfig.FIELD_WIDTH+ScreenConfig.FIELD_SIDE_WIDTH*2); x++) {
  12.       CreateFieldBlock(new Vector2Int(x, 0));
  13.     }
  14.  
  15.     // Left and Right Wall
  16.     for (int y = 0; y < (ScreenConfig.FIELD_HEIGHT+ScreenConfig.FIELD_BOTTOM_WIDTH); y++) {
  17.       CreateFieldBlock(new Vector2Int(0, y));
  18.       CreateFieldBlock(new Vector2Int(ScreenConfig.FIELD_WIDTH+ScreenConfig.FIELD_SIDE_WIDTH, y));
  19.     }
  20.  
  21.     _fieldEventListener = listener;
  22.   }
  23.  
  24.   // Create Field Block Object from Prefab.
  25.   private void CreateFieldBlock (Vector2Int pos) {
  26.     var prefab = Resources.Load("Prefabs/Block") as GameObject;
  27.     var copyObject = Instantiate(prefab, this.transform);
  28.     copyObject.transform.localPosition = new Vector3(pos.x, pos.y, 0);
  29.     copyObject.layer = ScreenConfig.FIELD_LAYER;
  30.     _fieldBlocks.Add(copyObject);
  31.   }

 

ライン削除処理


ライン削除は、以前検討したとおり着地したテトリミノの各ブロックの高さに横一杯の長さでRaycast を打って、ブロックが敷き詰められているかどうかを確認する方式で行う。

 

まず着地したテトリミノの各ブロックの現在位置を配列で取得する(Tetrimino.GetBlocksPosition)。そのY座標の最低と最高位置の間がRaycast を打つ高さになる。

Raycast の長さはField の幅固定でよい。ただし、フィールド枠のブロックを検知対象に含めたくないので、Field の幅から1 個分短い距離にし、半ブロック分だけ内側にスタート位置をずらしている。

Raycast で検出したブロック数がField の幅の数と同数の場合は、ライン削除の条件が満たされたものと判断し、検出したブロックのGameObject を削除している(DestoryBlocks)。

削除したライン数はカウントしておき、あとでスコア計算に使うようにする。

Field.cs の抜粋(ライン削除)

  1.   // Check Line whether it is filled or not at Tetrimino level.
  2.   private int CheckAndDeleteLines () {
  3.     var pos = _tetrimino.GetBlocksPosition();
  4.     var max = pos.Select(p => p.y).Max();
  5.     var min = pos.Select(p => p.y).Min();
  6.  
  7.     var deletedLineCount = 0;
  8.     for (float y = min; y <= max; y++) {
  9.       var deleteBlocks = CheckLine(y);
  10.       if (deleteBlocks != null) {
  11.         DestroyBlocks(deleteBlocks);
  12.         deletedLineCount++;
  13.         DropLines(y);
  14.       }
  15.     }
  16.  
  17.     return deletedLineCount;
  18.   }
  19.  
  20.   private List<GameObject> CheckLine (float y) {
  21.     var detectedBlocks = DetectBlocks(y);
  22.     if (detectedBlocks != null && detectedBlocks.Count() >= ScreenConfig.FIELD_WIDTH) {
  23.       return detectedBlocks;
  24.     }
  25.     return null;
  26.   }
  27.  
  28.   private List<GameObject> DetectBlocks (float y) {
  29.     ContactFilter2D contactFilter = new ContactFilter2D();
  30.     var start = new Vector2(
  31.       ScreenConfig.FIELD_SIDE_WIDTH + ScreenConfig.Instance.FieldOffset.x + 0.5f,
  32.       y + 0.5f
  33.     );
  34.  
  35.     var length = ScreenConfig.FIELD_WIDTH - 1f;
  36.     var hitResult = new List<RaycastHit2D>();
  37.     var hitCount = Physics2D.Raycast(
  38.       start, // レイの原点
  39.       Vector2.right, // レイの方向
  40.       contactFilter.NoFilter(), // チェックする対象を設定するフィルター
  41.       hitResult, // ヒットしたオブジェクトの情報
  42.       length // レイの長さ
  43.     );
  44.  
  45.     //Debug.DrawRay(start, Vector2.right * length, Color.red, 2f); // Debug用
  46.     
  47.     if (hitCount > 0) {
  48.       var detectedBlocks = new List<GameObject>();
  49.       foreach (var hit in hitResult) {
  50.         detectedBlocks.Add(hit.collider.gameObject);
  51.       }
  52.       return detectedBlocks;
  53.     }
  54.  
  55.     return null;
  56.   }
  57.  
  58.   // Destroy Block Objects on Field.
  59.   public void DestroyBlocks (List<GameObject> blocks) {
  60.     foreach (var block in blocks) {
  61.       _blocks.Remove(block);
  62.       Destroy(block);
  63.     }
  64.   }

 

ラインの落下もRaycast を使って実現する。

ライン削除処理にて削除したブロックの高さがわかるので、その高さより1 ブロック上から最上段までの間を1 段ずつRaycast を打って落下すべきブロックを検出する。あとは、検出したブロックを1 つずつ下に移動させればOK(tranform.position のy 座標を-1 する)

Field.cs の抜粋(ライン落下)

  1.   private void DropLines (float deletedLineYPos) {
  2.     for (float y = deletedLineYPos+1; y < ScreenConfig.FIELD_HEIGHT; y++) {
  3.       var detectedBlocks = DetectBlocks(y);
  4.       if (detectedBlocks != null) {
  5.         DropLine(detectedBlocks);
  6.       }
  7.     }
  8.   }
  9.  
  10.   private void DropLine (List<GameObject> blocks) {
  11.     foreach (var block in blocks) {
  12.       block.transform.position += Vector3.down; // down is -1
  13.     }
  14.   }

 

次の記事


from20150817.hatenablog.com

 

 


目次に戻る