概要
この記事は、Unity初心者向けに、Flappy Bird (みたいなゲーム)を作りながら操作方法を学べるチュートリアルです。
こんなゲームを作ります。
ページ作成の都合でこの記事は3ページに分かれていて、ここは最初のページとなります。
ページのリンク
- Flappy Bird を作るチュートリアル (1/3) ←今のページ
- Flappy Bird を作るチュートリアル (2/3)
- Flappy Bird を作るチュートリアル (3/3)
やることリスト
今回のチュートリアルでは以下のことを実装していきます。
- プロジェクトの作成 ←①
- プレイヤーの作成 ←①
- ブロック(障害物)の作成
- ブロック管理の作成
- スコアの実装
- ゲームオーバーの実装
- リトライの実装
第一回(このページ)では、①の部分まで進めていきます。
プロジェクト作成と環境設定
Unity Hub を起動して、プロジェクトを新規作成します。
- テンプレートは「2D」を選択
- プロジェクト名は「TestFlappyBird」
- 作成ボタンをクリックする
※保存先は任意の場所。デスクトップなどにしておくとフォルダへのアクセスがしやすいです。
レイアウトの設定
今回の説明で使用するレイアウトは「2 by 3」とします。
Projectビューのアイコンは最小化しておくと見やすくて良いです。
素材のダウンロード
今回のゲームで使用する素材は以下の場所にアップロードしています。
http://syun777.sakura.ne.jp/tmp/Unity/FlappyBird/images.zip
■フォルダ構成
images
+-- 5box.png: 障害物
+-- nasu.png: ゲーム管理の仮画像
+-- player.png: プレイヤー
+-- xbox.png: 障害物管理の仮画像
プレイヤーの作成
まずはプレイヤーを作成します。
スプライトの設定
素材画像の player.png
を Projectビューの Assets フォルダにドラッグ&ドロップします。
すると、Assetsフォルダ内に "player" というスプライトリソースが作られます。
player
を選択して、Inspectorビューから Sprite Mode を Multiple
に変更して、 Sprite Editor
ボタンをクリックします。
この際にこのようなダイアログが表示されますが、これは「 Sprite Mode を変更したのに Apply(適用する)をしていませんがよろしいですか?」という確認メッセージです。特に問題ないので、Apply をクリックして適用します。
Sprite Editor が表示されるので、Slice
のドロップダウンをクリックして、設定はそのままで Slice
ボタンをクリックします。
Slice
ボタンを押しても見た目に変化がないので、何も変わっていないように見えますが、下にある画像をクリックすると良い感じにスライスされているのが確認できます。
Sprite Editor
を閉じると、以下のようにメッセージが表示されますが、これも「 Apply
ボタンを押していないけれど大丈夫?」という確認のメッセージなので、Apply
を押して適用します。
ちなみに Projectビューの Assets フォルダから player スプライトを選択してもスライスした情報を確認できます。
player_0
と player_1
というスプライト名でスライスされたということがわかります。
なお、Sprite Mode の Multiple
という設定は、このように1つの画像から複数のスプライトを作る、というものとなります。
プレイヤーオブジェクトを作成する
早速このプレイヤーをゲーム内に配置してみます。
player_0
を Scene ビューにドラッグ&ドロップします。
すると Hierarchy ビューに player_0
というゲームオブジェクトが作られます。この player_0
をクリックして選択し、Inspectorビューから
`Player' という名称に変更しておきます。
コンポーネント 「Rigidbody 2D」 を追加する
Player にコンポーネント「Rigidbody 2D」を追加します。「Rigidbody 2D」とは、オブジェクトの物理的な挙動(移動や落下)を制御するためのものです。これをゲームオブジェクトに登録することで、プレイヤーが操作したり、落下したりすることができます。
ちなみにゲームオブジェクトには初期状態で「Transform」というコンポーネントが登録されており、これは「座標」「回転値」「拡大縮小値」を保持しています。
画像が表示できているのは「Sprite Renderer」が登録されているためです。
試しに「Sprite Renderer」の隣にあるチェックボックスをクリックするとスプライトの描画が無効になって何も表示されなくなります。
ゲームオブジェクトとコンポーネントの関係のイメージとしては以下の通りです。
ゲームオブジェクトが複数のコンポーネント(Transform / Sprite Renderer / Rigidbody 2D / Collider 2D)を持つことになります。
ゲームオブジェクトとコンポーネントの関係を説明したところで、物理挙動の機能をもつ「Rigidbody 2D」を登録します。
Player
ゲームオブジェクトを選択した状態で、Inspectorビューから「Add Component」をクリックして、 Rigidbody 2D
を選ぶと登録できます。
登録できたら、Unityエディタの上中央にある「▶︎(再生ボタン)」をクリックして動作を確認します。
実行すると Playerオブジェクトは重力で落下していきます。その動きを確認したら、もう一度「▶︎(再生ボタン)」をクリックして、再生を止めた状態にしておきます。
(凹んだ状態を解除しておく)
スクリプトを追加して動きを作る
Playerに「スクリプト」というコンポーネントを追加して動きを作ります。
- Playerオブジェクトを選択
- Inspectorビューから「Add Component」ボタンをクリック
- scriptで検索して「New Script」を選ぶ
- スクリプト名を「Player」にして「Create And Add」をクリック
スクリプトの記述
Player
スクリプトをダブルクリックしてスクリプトを開き、以下のコードを入力します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ■プレイヤー
public class Player : MonoBehaviour {
[SerializeField]
float JUMP_VELOCITY = 1000; // ①ジャンプ力の定義
Rigidbody2D _rigidbody; // ②物理挙動コンポーネント保持用
// 開始処理
void Start() {
// ③物理挙動コンポーネントを取得
_rigidbody = GetComponent<Rigidbody2D>();
}
// 更新
void Update() {
if (Input.GetKeyDown(KeyCode.Space)) {
// ④Spaceキーを押したのでジャンプ処理
_rigidbody.velocity = Vector2.zero; // 落下速度を一度リセットする
_rigidbody.AddForce(new Vector2(0, JUMP_VELOCITY)); // 上方向に力を加える
}
}
}
記述する場所には番号をつけています。
①はジャンプ力の定義です。ひとまず力の大きさは 1000
としました。[SerializeField]
をつけることで、Unityエディタからも値の変更が可能となります。
②は物理挙動コンポーネントである Rigidbody 2D
を保持する変数の定義です。コンポーネントを取得する関数は処理が重いので、あらかじめ保持するようにしておきます。
③は Rigidbody 2D
を取得して _rigidbody
変数に格納しています。 Start()
はゲームオブジェクトの最初の Update() が呼び出される前に呼び出される関数です。
詳細は UnityのAPIドキュメントに記載されています。
https://docs.unity3d.com/jp/460/ScriptReference/MonoBehaviour.Start.html
Start()
はこのような「一番最初に一度だけ呼び出される」処理をするのに便利な関数となります。
Update()
内に記述した④でキー入力を受け取ります。 Input.GetKeyDown(KeyCode.Space)
を使うと、キーボードのSpaceキーを押した瞬間かどうかを判定できます。そして Spaceキーを押したら、_rigidbody.AddForce()
で上方向に力を加えます。力を加える前に _rigidbody.velocity
に速度0 の値を代入することで、落下速度を無視したジャンプ力を加えています。
現実世界で考えると、落下速度の力を無視した上昇はできないのですが、操作感はこの方法が良くなります。ゲームでは時々、このような「ウソ物理挙動」がゲームをより面白くすることがあります。
スクリプトが入力できたら▶︎(再生ボタン)を押して実行してみましょう。
Space
キーでプレイヤーが上昇しますが、力が大きすぎて画面外へ飛び出してしまいます。
ジャンプ力の調整
ここで先ほどスクリプトで指定した [SerializeField]
が役に立ちます。
[SerializeField]
float JUMP_VELOCITY = 1000; // ジャンプ力の定義
の部分です。
スクリプトに指定した値を直接書き換えても良いのですが、 [SerializeField]
を指定することで、Unityエディタから修正ができます。
Hierarchyビューから「Player」オブジェクトを選択して、
Inspectorビューの Player(Script) の JUMP_VELOCITY
を 400 に変更します。
そうしたら▶︎(再生ボタン)をクリックして実行してみます。
良い感じのジャンプになりました!
重力の調整
個人的に少しふんわりした挙動が気になったので、もう少し強い重力をかけるようにします。
PlayerオブジェクトのInspectorから、Rigidbody 2D
> Gravity Scale
の値を 1
から 2
に変更します。
Gravity Scale
は重力加速度の強さです。これを小さくすると重力が小さくなり、大きくするほど大きな重力がかかるようになります。
重力を2倍にすることで、鋭い落下になりました。
画面外に出ないようにする
プレイヤーを画面外に出ないようにします。方法としては、画面の端の位置を計算して、そこからはみ出ていたら押し戻す、という処理を行います。
画面の端の取得には Camera.main.ViewportToWorldPoint()
という関数を使用します。この関数で画面に表示されている位置を取得できます。この関数の座標系は以下の通りです。
左下が(0, 0)となり、右上が(1, 1)です。これを使って画面の上の部分と下の部分を取得するコードの記述例は以下の通りです。
// 画面上を取得する
float GetTop() {
// 画面の右上のワールド座標を取得する
Vector2 max = Camera.main.ViewportToWorldPoint(Vector2.one);
return max.y
}
// 画面下を取得する
float GetBottom() {
// 画面の左下のワールド座標を取得する
Vector2 min = Camera.main.ViewportToWorldPoint(Vector2.zero);
return min.y;
}
Vector2.zero
は (0, 0)を、Vector2.one
は (1, 1) を表します。
では、Player.cs
を開いて、以下のようにコードを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ■プレイヤー
public class Player : MonoBehaviour {
[SerializeField]
float JUMP_VELOCITY = 1000; // ジャンプ力の定義
Rigidbody2D _rigidbody; // 物理挙動コンポーネント保持用
// 開始処理
void Start() {
// 物理挙動コンポーネントを取得
_rigidbody = GetComponent<Rigidbody2D>();
}
// 更新
void Update() {
if (Input.GetKeyDown(KeyCode.Space)) {
// Spaceキーを押したのでジャンプ処理
_rigidbody.velocity = Vector2.zero; // 落下速度を一度リセットする
_rigidbody.AddForce(new Vector2(0, JUMP_VELOCITY)); // 上方向に力を加える
}
}
// 固定フレーム更新
private void FixedUpdate() {
// ①座標を取得
Vector3 position = transform.position;
// ②画面外に出ないようにする
float y = transform.position.y;
float vx = _rigidbody.velocity.x;
if (y > GetTop()) {
_rigidbody.velocity = Vector2.zero; // 速度を一度リセットする
position.y = GetTop(); // ③押し戻しする
}
if (y < GetBottom()) {
// ④下に落ちたらオートジャンプ
_rigidbody.velocity = Vector2.zero; // 落下速度を一度リセットする
_rigidbody.AddForce(new Vector2(0, JUMP_VELOCITY));
position.y = GetBottom(); // 押し戻しする
}
// ⑤座標を反映する
transform.position = position;
}
// 画面上を取得する
float GetTop() {
// 画面の右上のワールド座標を取得する
Vector2 max = Camera.main.ViewportToWorldPoint(Vector2.one);
return max.y;
}
// 画面下を取得する
float GetBottom() {
// 画面の左下のワールド座標を取得する
Vector2 min = Camera.main.ViewportToWorldPoint(Vector2.zero);
return min.y;
}
}
追加した部分は、FixedUpdate()
と GetTop()
/ GetBottom()
関数です。
FixedUpdate()
一定間隔で呼び出されることが保証される Update()
です。Update()
は呼び出される間隔が不定なので、物理挙動を行う場合は FixedUpdate()
内に記述すると都合が良いためとなります。
では FixedUpdate()
をみていきます。まずは①です。
// ①座標を取得
Vector3 position = transform.position;
float y = transform.position.y;
ここでは Playerオブジェクトの位置を取得するために、transform コンポーネントから Y軸の値を取得しています (transform.position.y
) 。Playerオブジェクトは上下のみの移動なので、Y軸の値だけで判定できます。
続いて②・③の部分です。
// ②画面外に出ないようにする
if (y > GetTop()) {
_rigidbody.velocity = Vector2.zero; // 速度を一度リセットする
position.y = GetTop(); // ③押し戻しする
}
画面上からはみ出してしまったら、上昇速度をリセットして(ゼロにする)、はみ出ない位置まで押し戻します。
また速度をゼロにすることで、移動によるめり込みを防ぐようにしています。
④は画面下の判定です。
if (y < GetBottom()) {
// ④下に落ちたらオートジャンプ
_rigidbody.velocity = Vector2.zero; // 落下速度を一度リセットする
_rigidbody.AddForce(new Vector2(0, JUMP_VELOCITY));
position.y = GetBottom(); // 押し戻しする
}
押し返し処理としては同じですが、自動でジャンプ処理を行うようにしました。
最後に⑤で押し返した位置を transform
に反映させます。
// ⑤座標を反映する
transform.position = position;
では実行して動きを確認してみます。
画面上からはみ出なくなり、画面下では自動でジャンプするようになりました。
ジャンプアニメーションの追加
ジャンプ上昇中はキャラクター画像を切り替える簡単なアニメーションを実装します。
Unityでアニメーションを作る場合、Animatorを使うのが一般的ですが、少し複雑なので、ここではスクリプトで制御する方法で実装してみます。
Player.cs を開いて、以下のように修正します。
// ■プレイヤー
public class Player : MonoBehaviour {
// ①スプライト番号定義
const int SPR_FALL = 0; // 落下中
const int SPR_JUMP = 1; // ジャンプ中
[SerializeField]
float JUMP_VELOCITY = 1000; // ジャンプ力の定義
public Sprite[] SPR_LIST; // ②アニメーション用スプライトの保持
Rigidbody2D _rigidbody; // 物理挙動コンポーネント保持用
// 開始処理
・
・
・
①のところで、スプライト番号に対応する画像定数 (SPR_FALLL
/ SPR_JUMP
) を定義しています。
②で スプライトの配列 SPR_LIST
を定義して、スプライト番号に対応するスプライトを格納します。
記述できたら、Unityエディタに戻って、Playerオブジェクトの Scriptコンポーネントに注目します。
JUMP_VELOCITY
の下に、SPR_LIST
という項目が増えているのが確認できます。ここの Size
を 2
に変更して、スプライト player_0
/ player_1
を割り当てます。
これにより、SPR_LIST[0]
には "player_0"、SPR_LIST[1]
に "player_1" が割り当てられたことになります。
これを Playerオブジェクトの状態によって切り替えるようにします。
Player.cs を開いて、以下のように修正します。
// ■プレイヤー
public class Player : MonoBehaviour {
// スプライト番号定義
const int SPR_FALL = 0; // 落下中
const int SPR_JUMP = 1; // ジャンプ中
[SerializeField]
float JUMP_VELOCITY = 1000; // ジャンプ力の定義
public Sprite[] SPR_LIST; // アニメーション用スプライトの保持
Rigidbody2D _rigidbody; // 物理挙動コンポーネント保持用
SpriteRenderer _renderer; // ①スプライト描画
// 開始処理
void Start() {
// 物理挙動コンポーネントを取得
_rigidbody = GetComponent<Rigidbody2D>();
// ②スプライト描画コンポーネントを取得
_renderer = GetComponent<SpriteRenderer>();
}
// 更新
void Update() {
if (Input.GetKeyDown(KeyCode.Space)) {
// Spaceキーを押したのでジャンプ処理
_rigidbody.velocity = Vector2.zero; // 落下速度を一度リセットする
_rigidbody.AddForce(new Vector2(0, JUMP_VELOCITY)); // 上方向に力を加える
}
// ③プレイヤーの状態でスプライトを切り替える
if (_rigidbody.velocity.y < 0) {
// 落下中
_renderer.sprite = SPR_LIST[SPR_FALL];
} else {
// 上昇中
_renderer.sprite = SPR_LIST[SPR_JUMP];
}
}
・
・
・
①でスプライト描画コンポーネントである SpriteRenderer
を格納する変数を定義しています。
SpriteRenderer _renderer; // ①スプライト描画
スプライト描画コンポーネントを取得するために、② (Start関数) で取得処理を行っています。
// 開始処理
void Start() {
// 物理挙動コンポーネントを取得
_rigidbody = GetComponent<Rigidbody2D>();
// ②スプライト描画コンポーネントを取得
_renderer = GetComponent<SpriteRenderer>();
}
そして③(Update関数)で落下方向によってスプライトの切り替えを行っています。下方向の移動(yがマイナス)であれば落下とし、上方向の移動であればジャンプ中、という判定としています。
// 更新
void Update() {
if (Input.GetKeyDown(KeyCode.Space)) {
// Spaceキーを押したのでジャンプ処理
_rigidbody.velocity = Vector2.zero; // 落下速度を一度リセットする
_rigidbody.AddForce(new Vector2(0, JUMP_VELOCITY)); // 上方向に力を加える
}
// ③プレイヤーの状態でスプライトを切り替える
if (_rigidbody.velocity.y < 0) {
// 落下中
_renderer.sprite = SPR_LIST[SPR_FALL];
} else {
// 上昇中
_renderer.sprite = SPR_LIST[SPR_JUMP];
}
}
では実行して、ジャンプ中と落下中でスプライトが切り替わることを確認します。
次回
次のページでは障害物となるブロック、それを生成するブロック管理を作成します。