前回のUnityでブロック崩しをつくってみるで簡単なブロック崩しをつくってみましたが、立体のブロック崩しもあっていいのではないかと思いつくってみました。ボールも3次元空間を移動するタイプのものもつくってみましたが、うまくいきませんでした。ゲーム自体はできたのですが、実際にボールを跳ね返すのが難しすぎます。そこでボールは平面を移動して、立体構造になったブロックのかたまりが回転するタイプのものをつくってみました。
Contents
フィールドに表示するオブジェクトをつくる
まず周囲の枠をCubeからつくります。横幅は前回の半分にしました。またブロックが回転するため床はつくりません。回転は4つとも(0, 0, 0)です。
WallTop
位置 0,0,15.8
拡大率 16,1,0.5
WallBotom
位置 0,0,-15.8
拡大率 16,1,0.5
WallLeft
位置 -8,0,0
拡大率 0.5, 1, 32
WallRight
位置 8,0,0
拡大率 0.5, 1, 32
ボールはSphereから生成します。位置は移動するので適当、拡大率は(1, 1, 1)、回転は(0, 0, 0)です。マテリアルは適当な色を指定します。コンポーネントの追加から 物理 ⇒ リジットボディーを追加します。そして位置を固定のチェックボックスのY座標部分、回転を固定のチェックボックスのすべてにチェックをいれます。これをわすれるとプレイ中にボールが枠の外へ飛んでいってしまい、ゲーム続行不可になってしまいます。
それからAssetsのなかに物理マテリアルを追加し、これをBallにドラッグアンドドロップします。追加した物理マテリアルの項目のうち、DynamicFrictionとStaticFrictionを0に、Bouncinessを1に、BounceCombineをMaximにFrictionCombineをMinimumに設定します。
パドル(プレイヤー)は位置は(0, 0, -14)、回転は(0, 0, 0)、拡大率は(3, 1, 0.5)です。これもマテリアルは適当に設定します。
次にGameManagerオブジェクトを生成して、C#スクリプトでもGameManager.csを生成して両者を紐付けます。C#スクリプトが必要なのはBallとPlayerです。Ball.csとPlayer.csを生成して、それぞれのオブジェクトに紐付けます。
Playerを移動させるスクリプト
最初にPlayer.csを示します。方向キーが押されたらその方向に移動しますが、枠を突き破って移動しないように制限をかけています。
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { public float speed = 4.0f; void Update() { if (Input.GetKey(KeyCode.LeftArrow)) { if (this.transform.position.x > -6) this.transform.position += Vector3.left * speed * Time.deltaTime; } if (Input.GetKey(KeyCode.RightArrow)) { if (this.transform.position.x < 6) this.transform.position += Vector3.right * speed * Time.deltaTime; } if (Input.GetKey(KeyCode.UpArrow)) { if (this.transform.position.z < -11) this.transform.position += new Vector3(0,0,1) * speed * Time.deltaTime; } if (Input.GetKey(KeyCode.DownArrow)) { if (this.transform.position.z > -14) this.transform.position += new Vector3(0, 0, -1) * speed * Time.deltaTime; } } } |
Ballを移動させるスクリプト
次にBall.csですが、インスペクターのタグにMiss、Block、Ballを追加しておいてください。そして一番手前の枠(WallBotom)のTagにMiss、パドルにPlayer、ボールにBallを設定しておいてください。
またBallのインスペクターのBallスクリプトのGameManagerの部分にGameManagerオブジェクトをドラッグアンドドロップしておきましょう。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Ball : MonoBehaviour { public float Speed = 1.0f; public GameManager gameManager; float initVelocity = 0; void Start() { BallStart(); } public void BallStart() { gameObject.transform.position = new Vector3(2, 0, -4); Rigidbody rigidbody = gameObject.GetComponent<Rigidbody>(); rigidbody.velocity = new Vector3(1, 0, 1) * Speed; if (initVelocity == 0) initVelocity = Mathf.Sqrt(Speed * Speed * 2); } private void OnCollisionEnter(Collision collision) { string collisionTag = collision.gameObject.tag; if (collisionTag == "Miss") { gameManager.Miss(); // 前回作成したMiss()、Hit()、BlockBreak()と大差ない } if (collisionTag == "Player") { gameManager.Hit(); } if (collisionTag == "Block") { collision.gameObject.SetActive(false); gameManager.BlockBreak(); } } private void Update() { CorrectionBall(); } void CorrectionBall() { // 現在のボールの速度を取得 Rigidbody rigidbody = gameObject.GetComponent<Rigidbody>(); float x = rigidbody.velocity.x; float z = rigidbody.velocity.z; float nowVelocity = Mathf.Sqrt(x * x + z * z); if (nowVelocity < initVelocity * 0.8f || IsUnbalanceRatioVelocityXZ(x, z)) { float newX = x > 0 ? 1 : -1; float newZ = z > 0 ? 1 : -1; rigidbody.velocity = new Vector3(newX, 0, newZ) * Speed; } } bool IsUnbalanceRatioVelocityXZ(float vx, float vz) { float x = Mathf.Abs(vx); float z = Mathf.Abs(vz); if (z / x < 0.6f || x / z < 0.1f) return true; else return false; } } |
ボールの速度に補正をいれる
ブロックが回転していることもあり、ボールの当たり方によっては変な跳ね方をしたり、速度が落ちてしまうことがあります。実際にやってみるとボールが最上段で止まってしまうことがあります。これではゲームにならないのでボールの速度に補正をいれます。それが自作メソッドのCorrectionBallです。
CorrectionBallメソッドではボールの速度を調べて遅くなりすぎている場合に速度調整をいれます。最初にフィールド変数 initVelocityを0にしておき、ボールに初速を与えるBallStart()メソッド内で速度を調べ、initVelocity == 0のときにこれを代入しておけば最初の速度を保存することができます。
またボールが跳ねたときにまっすぐ上に上がった場合、その先にブロックがないとボールは上下に往復するだけで、これまたゲーム続行不可になります。そこで速度のX成分とZ成分の割合を調べて、自作メソッド IsUnbalanceRatioVelocityXZメソッドであまりにもおかしな割合(これをアンバランスという言葉で表現していいのか?)のときは強制的に絶対値が1:1になるように設定しなおします。
ボールの進行方向によって適切に再設定しないといけないのですが、ここはそのときのX成分とZ成分を調べて三項演算子でリセットすべき速度に設定します。冒頭の動画ではこれをやらずになんでも rigidbody.velocity = new Vector3(1, 0, 1) * Speed;にしていました。そのためそのままブロックのなかにボールがめり込みやすくなり、右側のブロックのほうが壊れやすくなっています。
OnCollisionEnterメソッドではボールが何に当たったかで処理をわけています。GameManagerクラスの自作メソッドであるMiss、Hit、BlockBreakを呼び出していますが、これは前回作成したブロック崩しとたいした違いはありません。
GameManagerのスクリプト
次にGameManager.csをどうするかですが、そのまえに・・・
ブロックを生成する
まず回転するブロックをつくるのですが、これの元になるブロックをCubeでつくります。大きさは1でいいのですが、隙間なくならべてしまうとブロック同士の境界線がみえないので、拡大率は(0.95, 0.95, 0.95)と1よりも少し小さな値にしています。回転は(0, 0, 0)、位置は後で変更するので適当に設定して問題ありません。マテリアルは適当に設定しておきましょう(動画では赤にしています)。
まず最初におこなわれる処理の部分 Startメソッドとこれに関連する部分(メソッドとフィールド変数)をしめします。フィールド変数でBallとoriginalBlockをつかっているのでインスペクターの該当する部分に該当するオブジェクトをドラッグアンドドロップしておきましょう。originalBlockは上のパラグラフで作成したオブジェクトです。
Startメソッドのなかで自作メソッドであるBlocksInitを呼び出して、ブロックを生成しています。ゲームでは5× 5のブロックが16列回転します。そこで 25× 16のブロックをoriginalBlockをもとに生成します。生成後は必要ないのでSetActive(false)として表示されないようにします。
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using UnityEngine.UI; public class GameManager : MonoBehaviour { public Ball ball; int RestMax = 2; public GameObject originalBlock; List<GameObject> blocks = new List<GameObject>(); int columCount = 16; void Start() { BlocksInit(); // IsGameOverプロパティ Restプロパティ Scoreプロパティは後述 IsGameOver = false; Rest = RestMax; Score = 0; } // originalBlockを元に ブロックを5×5× columCount個つくる void BlocksInit() { for (int i = 0; i < 25 * columCount; i++) blocks.Add(Instantiate(originalBlock, new Vector3(0, 0, 0), Quaternion.identity)); // もう使わないのでSetActive(false)に originalBlock.SetActive(false); } } |
ブロックを回転させる処理
次にUpdateメソッド内で、上記の方法で生成したブロックを回転させる処理をおこないます。
自作メソッド RotateBlocksのなかでフィールド変数 angleを1増加させ、各ブロックの中心位置を計算します。
まずX座標を計算します。ブロックのインデックスからブロックが何列目になるかがわかります。真ん中の列がフィールドの中央に表示されるように左側に7.5平行移動させた位置がブロックのX座標です。25で割った商の整数部分が列であり、その剰余を5で割った商と剰余からY座標とZ座標も計算できます。これでX軸を中心にあつまったブロック群の初期の位置がわかります。
次にX軸を中心とした回転処理を考えます。ここは回転行列を使います。初期の座標と回転行列の積から回転後のブロックの中心座標がわかります。あとはこれを向こう側に平行移動(Z方向に8移動)させます。これでフィールド上で回転しているブロックの座標がわかります。あとは移動させるだけです。ブロック自身も回転しているのでその処理も忘れずに・・・。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
public class GameManager : MonoBehaviour { // ブロックがどれだけ回転するか float angle = 0; void Update() { RotateBlocks(); } void RotateBlocks() { angle += 1f; int blockCount = blocks.Count; for (int i = 0; i < blockCount; i++) { if (!blocks[i].activeSelf) continue; // 1列のブロックの数は25 // -7.5f平行移動させて真ん中の列のブロックが中心にくるようにする float x = i / 25 - 7.5f; // 1列のブロックは5×5 int joyo = i % 25; float y = joyo % 5 - 2; float z = joyo / 5 - 2; // これでX軸を中心に回転する前のブロックの座標は取得できた Vector3 blockCenter = new Vector3(x, y, z); // 回転行列を生成 var quaternion = Quaternion.Euler(new Vector3(angle, 0, 0)); var matrix = Matrix4x4.Rotate(quaternion); blockCenter = matrix.MultiplyPoint(blockCenter); // Z方向に8平行移動させる行列を生成 var matrix2 = Matrix4x4.Translate(new Vector3(0, 0, 10)); blockCenter = matrix2.MultiplyPoint(blockCenter); // 行列をつかってブロックを移動させるとともにブロック自身も回転させる blocks[i].transform.position = blockCenter; blocks[i].transform.rotation = Quaternion.AngleAxis(angle, new Vector3(1, 0, 0)); } } public void Hit() { // ボールをパドルで跳ね返したらスコアを追加 Score += 5; // 少しだけボールを速くする Rigidbody rigidbody = Ball.GetComponent<Rigidbody>(); rigidbody.AddForce(new Vector3(0, 0, 1), ForceMode.VelocityChange); // ブロックがすべて壊れていたらすべて再生させる if (blocks.Count(x => x.activeSelf) == 0) { foreach (var box in blocks) box.SetActive(true); } } // ブロックを壊したらスコア追加 public void BlockBreak() { Score += 10; } } |
点数表示 ミス・ゲームオーバー判定
次に点数表示とミス時の処理、ゲームオーバー判定をおこないます。
ゲームオーバーの表示と点数の表示ですが、ヒエラルキーで右クリック、UI ⇒ テキストを選択します。するとCanvasというオブジェクトが生成され、そのなかにTextというオブジェクトが表示されるので、名称をCanvasをGameUIに、TextをGameOvetTextに変更します。さらにヒエラルキーでGameUIを右クリックしてUI ⇒ テキストを選択してもうひとつTextオブジェクトを追加して、名前をScoreTextに変更します。
さらにGameUIを右クリックしてUI ⇒ ボタンを生成して、この名前をRetryButtonに変更します。GameManagerクラスにgameOverText、retryButton、scoreTextというフィールド変数を生成して、GameManagerのインスペクターのGameManagerスクリプトの下に表示されているGameOverText、RetryButton、ScoreTextの部分にヒエラルキーで作成したそれぞれのオブジェクトをドラッグアンドドロップします。
GameOverText、RetryButton、ScoreTextの設定ですが、
GameOvetText
位置 (0,40, 0)
幅・高さ (160, 30)
文字は Game Over
フォントサイズは48
中央寄せ
水平・垂直オーバーフロー Overflow
ScoreText
位置 (0,200, 0)
幅・高さ (160, 30)
文字は あとで変更するので「New Text」のままでOK
フォントサイズは24
中央寄せ
水平・垂直オーバーフロー Overflow
RetryButton
位置 (0,-20, 0)
幅・高さ (160, 30)
ボタンの色は適当でかまいませんが、クリック時の処理はGameManager.ReStartを指定してください。
RetryButtonの子オブジェクトにTextがありますが、そのテキストにボタンに表示させたい文字と文字色を設定しておいてください。
ScoreプロパティとRestプロパティで値が変更されたら表示も同時に変更されるようにしています。IsGameOverプロパティではtrueになったらGame Overの文字列とリトライのボタンをボタンを表示して、ゲームが開始されたらこれらを非表示にさせています。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
public class GameManager : MonoBehaviour { public GameObject gameOverText; public GameObject retryButton; public GameObject scoreText; public void Miss() { // ミスをしたら残機を減らす rest--; // 残機0ならゲームオーバー処理 // そうでないならボールを初期位置にもどしてゲーム再開 if (rest == 0) { Destroy(ball.gameObject); IsGameOver = true; } else { Ball.BallStart(); } } // スコアに関するプロパティ int Score { get { return _score; } set { _score = value; ShowScoreText(); } } int _score = 0; // 残機に関するプロパティ int Rest { get { return _rest; } set { _rest = value; ShowScoreText(); } } int _rest = 0; void ShowScoreText() { Text score_text = scoreText.GetComponent<Text>(); if (score_text != null) { string str = string.Format("Score {0} 残 {1}", _score, _rest); score_text.text = str; } } // ゲームオーバーになったらゲームオーバー表示とリトライのボタンを表示する bool IsGameOver { set { gameOverText.SetActive(value); retryButton.SetActive(value); } } // ゲームオーバー後に再挑戦するときの処理 public void ReStart() { UnityEngine.SceneManagement.SceneManager.LoadScene("game"); } } |