Unity初心者が作るべきゲーム第1位(私調べ)に輝くブロック崩しを作っていきます。
今回はゲーム画面上に各種オブジェクトを配置して動かせるようにするところまでをやります。初めての物理エンジンですよ。わくわく。
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
プロジェクトを作る
いつものように Unity Hub で新しいプロジェクトを作成します。テンプレートは「Universal 2D」にしてみました。
2D (Built-In Render Pipeline) | Unityに従来からあるレンダリングパイプラインを採用。 軽量で動作が軽い。 学習コストが低い。 |
---|---|
Universal 2D | Unityの新しい高性能レンダリングパイプラインであるUniversal Render Pipeline (URP)を2Dに特化させたもの。 高度なグラフィックス表現が可能。 学習コストが少し高い。 |
壁を作る
ゲーム画面の上下左右に壁を作ります。
オブジェクトの作成
ヒエラルキーで「2D Object > Sprites > Square」を選択します。白い四角オブジェクトができるので細長い棒状に形を変えてゲーム画面の端に配置します。同じようにして4つの辺に壁を配置します。オブジェクトの名前はそれぞれ WallTop, WallBottom, WallLeft, WallRight としました。
Sceneタブの白い枠がゲーム画面として表示される領域です。この枠の外側に壁を置きました。これで、壁は見えないけれどゲーム画面の端でボールが跳ね返るようになりますね。
当たり判定をつける
壁にボールが当たったときに跳ね返るようにします。
初期状態だと、ボールは壁を貫通します。オブジェクト同士が衝突したときにうまいこと処理してくれるのがコライダーです。コライダーコンポーネントを壁オブジェクトに追加します。
インスペクターの「Add Component」から「Box Collider 2D」を見つけて追加します。Boxというのはコライダーの形のことで、他に Circle や Capsule などがあります。また、2Dゲームなので名前に 2D とついているものを選びます。
すべての壁にコライダーを付けます。
これで壁にボールが当たったときに跳ね返るようになりました。まだボールを作ってないから試せないけどね。
プレイヤーを作る
プレイヤー(パドル)を作ります。左右に動かしてボールを跳ね返すアレですね。
オブジェクトの作成
ヒエラルキーで「2D Object > Sprites > Square」を選択します。壁と同じですね。適当な大きさにして、画面の下のほうに配置します。名前を Player にしました。
当たり判定をつける
壁と同じく、プレイヤーにも当たり判定をつけます。インスペクターの「Add Component」から「Box Collider 2D」を追加します。
物理特性をつける
プレイヤーに物理特性を与えます。つまりオブジェクトが重力や空気抵抗の影響を受けて動作するようにします。
インスペクターの「Add Component」から「Rigidbody 2D」を追加します。そして、Body Typeを「Kinematic」に変更します。
各 Body Type の違いは次の通り。
Dynamic | 物理法則に従って自然に動き、他のオブジェクトとの衝突の影響を受ける。 |
---|---|
Kinematic | 物理法則には影響されず、スクリプトによって動きを制御する。 |
Static | 固定されたオブジェクトで、物理エンジンによる影響を受けない。 |
プレイヤーはスクリプトで動かすので Kinematic ですね。この後に作るボールは Dynamic になりそうです。壁は Static ですが、Static ならば Rigidbody をつけなくても同じっぽいです。なので、つけてません。
スクリプトを書く
プレイヤーを制御するためのスクリプトを書きます。
Assetsフォルダの下にScriptsフォルダを作り、その下にC# Scriptを作成します。名前は「PlayerController」にしました。このスクリプトをPlayerオブジェクトにアタッチします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float speed = 10.0f; // プレイヤーの移動速度
public float minX, maxX; // プレイヤーの移動範囲(X軸)
private bool canMove = false; // プレイヤーが移動可能かどうかのフラグ
void Update()
{
// 左クリックされたら移動可能にする
if (Input.GetMouseButtonDown(0))
{
canMove = true;
}
// 移動可能であれば、プレイヤーをマウスの位置に追従させる
if (canMove)
{
// マウスの位置を取得し、ワールド座標に変換
Vector3 mousePosition = Input.mousePosition;
mousePosition = Camera.main.ScreenToWorldPoint(mousePosition);
// プレイヤーの目標位置を設定
Vector3 targetPosition = new Vector3(mousePosition.x, transform.position.y, transform.position.z);
// プレイヤーを目標位置へ移動
transform.position = Vector3.Lerp(transform.position, targetPosition, speed * Time.deltaTime);
// プレイヤーの位置を制限
Vector3 newPosition = transform.position;
newPosition.x = Mathf.Clamp(newPosition.x, minX, maxX);
transform.position = newPosition;
}
}
}
今回、キーボードを使わずに、マウスカーソルの移動に追随してプレイヤーが動くようにしました。
プレイヤーは常に動かすので、Update()
の中に処理を書きます。
変数 speed, minX, maxX, canMove
speed
はプレイヤーの移動速度です。値が大きいとキビキビと動き、小さいとマウスカーソルに少し遅れてついていく感じになります。
minX
とmaxX
はプレーヤーの移動範囲です。異動範囲を指定しないと、プレイヤーがゲーム画面の外に出てもずっとマウスに追随してしまいます。値は環境によって違うと思いますが、私は-2.3
と2.3
でした。
canMove
はプレイヤーが移動可能かどうかのフラグです。ゲーム開始直後はプレイヤーを移動できないようにしておきます。
左クリックされたら移動可能にする
Input.GetMouseButtonDown(0)
でマウスの左クリックを検出します。マウスが左クリックされたらプレイヤーを動かせるようにします。
マウスの位置を取得し、ワールド座標に変換
Input.mousePosition
プロパティはマウスカーソルの座標を取得します。
ScreenToWorldPoint()
でスクリーン座標をワールド座標に変換します。
プレイヤーの目標位置を設定
Vector3()
は3Dのベクトル(位置や方向)をあらわすための構造体です。2DゲームなんだからVector2()
を使えばいいんじゃないかと思いますが、Input.mousePosition
プロパティがVector3型の値を返すのでそれに合わせています。
mousePosition.x
はマウスポインタのX座標、transform.position.y
, transform.position.z
はオブジェクトの元々のY座標とZ座標です。Y, Z座標は動かさずにX座標だけをマウスポインタに追随させようとしています。
プレイヤーを目標位置へ移動
Vector3.Lerp()
は2つのベクトル間を線形補間する関数です。わからん。簡単にいうと、ある点から別の点へオブジェクトを直線で移動させるときに、アニメーションのように滑らかに移動させてくれるそうです。
第1引数が始点、第2引数が終点、第3引数が補間率(0.0〜1.0)です。第3引数でspeed
にTime.deltaTime
を掛けるのはフレームレートの変動に対処するためだそうです。まだよく理解できてないけど、Unityでよく使われるテクニックらしいので覚えておきたい。
プレイヤーの位置を制限
Mathf.Clamp()
は数値を指定された範囲内に収める関数です。この関数を使ってnewPosition.x
(プレイヤーのX座標)がminX
とmaxX
を超えないようにしています。
これで、プレイヤーがマウスに追随して動くようになりました。
ボールを作る
オブジェクトの作成
ヒエラルキーで「2D Object > Sprites > Circle」を選択します。ボールは丸い(あたり前のことを言う人)。大きさを調整してプレイヤーの上に置きました。名前を Ball にしました。
当たり判定をつける
インスペクターの「Add Component」から「Circle Collider 2D」を追加します。Circle です。ボールは丸い。
物理特性をつける
インスペクターの「Add Component」から「Rigidbody 2D」を追加します。
そして、Body Typeを「Dynamic」に、Linear Drag, Angular Drag, Gravity Scale を「0」に変更します。
Linear Drag (線形抗力) | 物体が直線運動するときの抵抗の強さ 値が大きいほど物体の速度が減衰しやすくなる |
---|---|
Angular Drag (角抗力) | 物体が回転運動するときの抵抗の強さ 値が大きいほど物体の回転速度が減衰しやすくなる |
Gravity Scale (重力スケール) | 物体に作用する重力の強さを調整する係数 値が大きいほど物体が早く落下する |
ブロック崩しでは空気抵抗や重力の影響を受けずにボールを動かしたいのでこれらの値を 0 にします。
この時点では、まだボールは壁などに当たっても跳ね返りません。ボールを跳ね返らせるために物理マテリアルを作成します。Assetsフォルダ下にMaterialsフォルダを作成し、その下で右クリック「Create > 2D > Physics Material 2D」を選択します。名前をBallに変更。Ballを選択して、インスペクターでFrictionを「0」にBouncinessを「1」に変更します。
Friction (摩擦係数) | 衝突時の摩擦力を設定 値が大きいほど滑りにくくなる |
---|---|
Bounciness (反発係数) | 衝突時の反発力を設定 値が大きいほど弾力性が高くなる |
この Ball マテリアルを Rigidbody 2D の Material にドラッグして紐付けます。
スクリプトを書く
ボールを制御するためのスクリプトを書きます。
Assets/Scriptsフォルダの下にC#スクリプトを作成して、名前を「BallController」にしました。このスクリプトをBallオブジェクトにアタッチします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BallController : MonoBehaviour
{
public float initialSpeed = 5f; // 初期速度
private bool isLaunched = false; // ボールが発射されたかどうかのフラグ
private Rigidbody2D rb;
void Start()
{
rb = GetComponent<Rigidbody2D>(); // Rigidbody 2D コンポーネントを取得
}
void Update()
{
if (!isLaunched && Input.GetMouseButtonDown(0)) // 左クリックがされ、かつボールがまだ発射されていない場合
{
Vector2 direction = Camera.main.ScreenToWorldPoint(Input.mousePosition) - transform.position; // マウスのクリック位置に応じてボールの発射方向を計算
direction.Normalize(); // directionベクトルの大きさを1に正規化
rb.velocity = direction * initialSpeed; // ボールを発射
isLaunched = true; // ボールが発射されたことを記録
}
}
}
マウスが左クリックされたときにクリック位値に応じてボールを発射するスクリプトです。
変数 initialSpeed, isLaunched
initialSpeed
はボールの速度です。大きくするとボールが速くなります。
isLaunched
はボールが発射されたかどうかのフラグです。まだボールが発射されていない場合のみボールを発射する処理をするために必要です。
Rigidbody 2D コンポーネントを取得
GetComponent<Rigidbody2D>()
で、Rigidbody 2D コンポーネントを取得します。
マウスのクリック位置に応じてボールの発射方向を計算
マウスクリックの座標からからボールの座標を引いて発射方向を計算します。
ボールを発射
ボールに力を加えて発射します。
ボールが発射されたことを記録
isLaunched
をtrue
にして、以降ボールを発射しないようにします。
実行してみると、ボールが壁やプレイヤーに当たると跳ね返っているのがわかります。
ここで問題発生。何かのはずみでボールが壁に90度の角度で当たった場合、永遠に行ったり来たりしてしまいます。そこで BallController.csに下記のメソッドを追加しました。
void OnCollisionEnter2D(Collision2D collision)
{
Vector2 vec = rb.velocity; // 現在のボールのベクトル
vec.Normalize(); // ベクトルを正規化
if (0.25f > Mathf.Abs(vec.y) || 0.25f > Mathf.Abs(vec.x)) // ボールがほぼ水平または垂直に動いている場合
{
if (0.25f > Mathf.Abs(vec.x)) // 垂直方向
{
if (0.0f <= vec.y) // ほぼ垂直に動いている場合
vec = Quaternion.Euler(0.0f, 0.0f, 15.0f) * vec; // 時計回りに15度回転させる
else
vec = Quaternion.Euler(0.0f, 0.0f, -15.0f) * vec; // 反時計回りに15度回転させる
}
else // 水平方向
{
if (0.0f <= (vec.x * vec.y)) // ほぼ水平に動いている場合
vec = Quaternion.Euler(0.0f, 0.0f, 15.0f) * vec; // 時計回りに15度回転させる
else
vec = Quaternion.Euler(0.0f, 0.0f, -15.0f) * vec; // 反時計回りに15度回転させる
}
}
vec *= initialSpeed; // vecの大きさを初期速度に設定
rb.velocity = vec; // ボールの速度を計算した新しい速度vecに設定
}
なんか長いなぁと思ったのでChatGPT氏に最適化をお願いしたところ、次のようになりました。
void OnCollisionEnter2D(Collision2D collision)
{
Vector2 vec = rb.velocity.normalized; // 現在のボールのベクトルを正規化
// ボールがほぼ水平または垂直に動いている場合
if (Mathf.Abs(vec.y) < 0.25f || Mathf.Abs(vec.x) < 0.25f)
{
float angle = (vec.x * vec.y >= 0) ? 15.0f : -15.0f;
vec = Quaternion.Euler(0.0f, 0.0f, angle) * vec;
}
vec *= initialSpeed; // vecの大きさを初期速度に設定
rb.velocity = vec; // ボールの速度を計算した新しい速度vecに設定
}
しゅごい。こんなに短く書けるのね。
ブロックを作る
オブジェクトの作成
ヒエラルキーで「2D Object > Sprites > Square」を選択します。大きさを調整してゲーム画面の左上に置きました。名前を Block にしました。
当たり判定をつける
インスペクターの「Add Component」から「Box Collider 2D」を追加します。
壁と同じく、ブロックには Rigidbody はつけません。
Ballオブジェクトにタグをつける
ボールが当たったらブロックが消えるというスクリプトを書くのですが、その前にBallオブジェクトにタグをつけておきます。
ヒエラルキーでBallオブジェクトを選択し、インスペクターの上部にある「Tag」のドロップダウンリストで「Add Tag…」を選択します。
インスペクターが「Tags & Layers」という画面に変わったら Tags の「+」をクリックしてタグの名前を「Ball」にして「Save」をクリックします。これでタグリストにBallが追加されます。
ヒエラルキーのBallオブジェクトを選択して、インスペークターの表示を元に戻します。「Tag」のドロップダウンリストで「Ball」を選択します。
これでBallオブジェクトにタグがつきました。
スクリプトを書く
ボールが当たったらブロックが消えるというスクリプトを書きます。
Assets/Scriptsフォルダの下にC#スクリプトを作成して、名前を「BlockController」にしました。このスクリプトをBlockオブジェクトにアタッチします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BlockController : MonoBehaviour
{
void Start()
{
}
void Update()
{
}
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ball") // 衝突したオブジェクトのタグが Ball なら
{
Destroy(gameObject); // 自身を削除
}
}
}
OnCollisionEnter2D()
は他のオブジェクトと衝突したときに実行されるメソッドです。衝突相手のタグがBallだったらDestroy()
します。
プレハブを作る
今のところブロックは1つしか作っていませんが、たくさん作って並べたいですよね。そこでプレハブを使います。プレハブはオブジェクトを再利用可能なテンプレートとして保存したものです。
まず、Assetsフォルダの下にPrefabsフォルダを作ります。そして、ヒエラルキータブのBlockオブジェクトをAssets/Prefabsフォルダにドラッグ&ドロップします。
すると、Assets/Prefabsフォルダに Block が作成されます。これがプレハブです。ヒエラルキータブの Block は水色になりました。これはプレハブから作られたオブジェクトであることをあらわします。
プレハブからオブジェクトを生成するには、Assets/Prefabsフォルダの Block をヒエラルキータブにドラッグ&ドロップします。
ゲームを実行してみました。すべてがうまくいっています。
さいごに
物理エンジン楽しい!
今回はブロック崩しなので自然法則に沿わない設定になったけど、重力とか空気抵抗とか摩擦とかを生かしたゲームをいつか作ってみたいと思いました。
でわでわ
コメント