前回までは、フィールドの設定は衝突判定のみだったので、それ以外の機能を作成していきます。
前回の記事↓
【事前準備】フィールドの設定
Field.csを開いてください。
フィールド配列の定義
まずは、フィールド上に配置されたブロックを管理しやすくするため、10×20の要素数の配列を用意します。下のコードを記述してください。
1 2 |
// ブロックが配置されていればtrue、そうでなければfalse bool[,] blocks = new bool[10,20]; |
blocksは10×20の要素を持つ二次元配列として定義しています。パズルゲームでは、二次元配列を使ってブロックの管理をすることが多いので、Unityでパズルゲームを開発する際には覚えておくと良いです。
関数の説明
そして、blocksを初期化する関数も下のコードのように作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 |
//blocksを初期化 void InitBlocks() { for(int i=0;i<blocks.GetLength(0); i++) { for(int j=0;j<blocks.GetLength(1); j++) { blocks[i,j] = false; } } } |
この関数をStart関数内に定義すれば、初期化は完了です。
1 2 3 4 |
void Start() { InitBlocks(); } |
ブロックをきれいにフィールドにはめる処理
では本題に入ります。
衝突したブロックの位置の取得
まずは、フィールドに衝突したブロックの位置を確認できるようにしましょう。まずは、スコアを表示させる3D Textを作成しましょう。Fieldに3Dオブジェクト→3D Textで作成します。名前は「ScoreView」にしてください。
設定は以下のようにしてください。(Textは「Sample」でなくても問題ありません。)
このゲームオブジェクトに新しいスクリプトScoreView.csを作成してください。
コードは以下のように書いてください。60フレームだけTextを表示できるゲームオブジェクトを生成することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ScoreView : MonoBehaviour { [SerializeField] public TextMesh textMesh; [SerializeField] int cnt = 0; const int MAXCNT = 60; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { cnt++; cnt%=60; if(cnt==0) { Destroy(this.gameObject); } transform.position += Vector3.up*0.05f; } } |
そして、ScoreViewにScoreView.csをドラッグ&ドロップしてください。ScoreViewのtextMeshに自身のScoreViewをドラッグ&ドロップしてください。
ここまで作成したら、ScoreViewをプレハブに登録してください。これで文字を表示させるゲームオブジェクトの準備ができました。
さらに、ScoreViewを参照できるように、Field.csに以下を定義してください。
1 2 |
[SerializeField] GameObject scoreViewObj; |
そして、以下のコードを書いてください。
1 2 3 4 5 6 7 |
void OnCollisionEnter(Collision other) { var p = other.transform.position; GameObject sv = Instantiate(scoreViewObj); sv.transform.position = other.gameObject.transform.position; sv.GetComponent<ScoreView>().textMesh.text = $"({p.x},{p.y},{p.z})"; } |
コードを書き終えたらFieldのscoreViewObjにプレハブのScoreViewをドラッグ&ドロップしてください。
この状態で、実行してみましょう。すると、以下のようにぶつかった位置を確認することができるようになります。
綺麗にはめる処理を考えてみる
では、ブロックをきれいに並べるためにはどうすればいいか考えてみましょう。
まず、この処理を図に表すとこんな感じです。
今回は0.1×0.1のマスのグリッド上にブロックがきれいにはまるようにしたいので、ずれた分を自動的に直す処理を実装すればいいことが分かります。
もう少し拡大して、どのようにずれた分を直していくか考えてみましょう。
① 例えば、ブロックが(0.07, 0.06)の座標にあったとき、ブロックを(0.0, 0.0)と(0.1,0)の間のグリッドの位置に配置したいと考えます。この時、ブロックにはめるべき座標は(0.05,0.05)です。
② まず、基準となるグリッドに合わせることを考えます。この実装では、最も近い左下の座標を基準の座標と考えましょう。この例では、(0.07, 0.06)に最も近い左下の座標は(0, 0)なので、ブロックの座標を(0, 0)に設定していきます。
③ あとは、基準の座標から(0.05, 0.05)だけ座標をずらせばグリッド上にはめられます。
しかし、この手順の②にあたる左下の座標の取得方法をどのように実装するのでしょうか?
解決策として、プログラムのキャストをうまく利用して②を実装していくことを考えます。
まず、x,y座標を同時に考えると混乱するため、x座標のみグリッド上に合わせる計算方法を考えます。少し複雑なので、上の図を見ながら読んでください。
まず、bは現在のブロックのx座標(1.09)、gは基準の座標(1.0)、uはグリッドの長さ(0.1)とします。この時、まず、b/uにより、uの大きさ分だけbを大きくします。今回の例では、「1.09 / 0.1 = 10.9」となります。
ここに(int)(b/u)と書いてint型でキャストを行います。すると「(int)(10.9) = 10」となり、整数のみが取り出せるようになります。
次に、その全体を(float){(int)(b/u)}によりfloat型にキャストします。これは、次の計算の際に小数の値にするためです。つまり、「(float)(10) = 10.0」みたいな感じになります。
最後に、キャストした値をuで割ります。結果は「10.0 × 0.1 = 1.0」となります。この数値はgと等しくなっていることが分かります。
綺麗にはめる処理の実装
では、実際にこの計算を追加していきましょう。
以下のコードを記述してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void OnCollisionEnter(Collision other) { var b = other.gameObject.transform.position; var u = 0.1f; var g = new Vector3( (float)((int)(b.x/u))*u, //x座標 (float)((int)(b.y/u))*u, //y座標 transform.position.z //z座標 ); //座標を反映 other.gameObject.transform.position = g; //向きを一定にする other.gameObject.transform.rotation = Quaternion.Euler(0,0,0); var rb = other.gameObject.GetComponent<Rigidbody>(); Destroy(rb); } |
先ほどの計算を行い、その結果のgを衝突したブロックの座標に反映させています。
また、衝突する前にブロックが回転してしまうため、向きもリセットを行っています。
この状態で実行してみましょう。きれいにブロックがはまる様子が確認できたでしょうか?
配置されたブロックを把握する処理
では最後に、配置されたブロックがどの場所のブロックかを検知する方法について説明していきます。まず、今回のフィールドの位置とサイズを確認しておきましょう。Fieldを以下のように設定してください。
すると、4端の位置は以下のようになります。
今回は、左下の(-0.4,0.1)にブロックがあるときblock[0,0]をtrueにすることを考えます。つまり、(-0.3,0.1)にブロックがあるときはblock[1,0]がtrueになります。これを実装していきましょう。
座標値の求め方について説明しておきます。Fieldのx座標のScaleは0.1であるため、スケールをかける必要があります。
[FIeldのx座標] + ([Fieldのxローカルスケール] × [Leftのローカルx座標]) = Leftのワールドx座標で計算できます。つまり、
0.05 + (0.1 × -5.5) = 0.05 – 0.55 = -0.45
となります。Rightなど他の位置に関しても同様の計算で求めることができます。
ここまでの内容を踏まえて、出来上がったのが以下のコードです(score表示の座標変数もpからbに変更しています)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
//座標の位置を変換する関数(座標を基に配列の番号を返す関数) int GetPosition(float value, float distance, int minRange, int maxRange) { for(int i = minRange; i<maxRange; i++) { if(Mathf.Abs(value-(float)(i)) < distance) { return i; } } return minRange-1; } void OnCollisionEnter(Collision other) { var b = other.gameObject.transform.position; var u = 0.1f; var g = new Vector3( ((int)(b.x/u))*u, //x座標 ((int)(b.y/u))*u, //y座標 transform.position.z //z座標 ); //向きを一定にする other.gameObject.transform.rotation = Quaternion.Euler(0,0,0); //左下座標を定義 float px = -0.4f; float py = 0.1f; //blocksの要素番号を取得 int bx = GetPosition((g.x-px)/u, 0.01f, 0, 10); int by = GetPosition((g.y-py)/u, 0.01f, 0, 20); //配置された位置の配列をtrueにする blocks[bx,by] = true; //参照できるように名前を設定 other.gameObject.name = $"name:{bx},{by}"; //座標を反映 other.gameObject.transform.position = new Vector3(bx*u + px, by*u + py, transform.position.z); var rb = other.gameObject.GetComponent<Rigidbody>(); Destroy(rb); // 位置を表示 GameObject sv = Instantiate(scoreViewObj); sv.transform.position = other.gameObject.transform.position; sv.GetComponent<ScoreView>().textMesh.text = $"({bx},{by})"; } |
まず、pxとpyに左下の座標を設定します。そして、bx,byにはどの位置のブロックなのかを計算した結果を格納しています。最後に、blocks[bx,by]をtrueにすることでブロックを配置した個所をtrueにしています。
この状態で実行して確認してみましょう。以下のようにブロックがきれいにはまるようになったでしょうか?
次回予告
次回は、テトリスのゲームのルールの肝であるテトリスブロックの射出処理を作成していきます。
次回の記事↓
コメント