#TL; DR
はじめて2Dゲームを作るにあたって、ユニティちゃん2Dの仕組みを理解することがとても参考になったので、
本記事はユニティちゃん2Dを触りつつ、改変を加えることでどういう動きになるのかを試しながら、2Dキャラの動きを理解していこう、という趣旨になります。
ユニティちゃん2Dは、みなさんご存知オープンソース系アイドル「ユニティちゃん(Unity-Chan)」の2Dゲームのサンプルプロジェクトです。
2014年6月に最新版のv1.1が公開されて以降更新はありませんが、2Dキャラの移動だけでなく、敵キャラクターの存在や破壊できるブロック、取得できるコインなど、2Dゲームの要素として基本的なものが揃っている大変いい教材だと思います。
http://unity-chan.com/download/releaseNote.php?id=UnityChan2D
#導入手順
導入手順については詳細は省きますが、簡単に以下のような流れでできます。
- ユニティちゃん2DのDLページ(上記URL)から、「
UnityChan2D.unitypackage
」をダウンロードする - Unityから空の2Dプロジェクトを開き、DLした
unitypackage
を読み込む - Titleのシーンを開き、実行して起動できれば完了
※なお、確認時は5.5.4p3を使用しています
#とりあえず動かしてみる
「左右キー」や「スペースキー」を押すことで、ユニティちゃんが左右に動いたり、ジャンプしたりします。
#どうやって動いているのか
なぜ左右キーを押すと左右に動き、スペースキーを押すとジャンプするのかを見ていこうと思います。
Inspector
のユニティちゃんを見てみると、UnityChan2DController.cs
がAddComponent
されています。
ユニティちゃんの制御はこのコントローラーによって行われているようですので、中身を見てみると、下記のような処理があります。
void Update()
{
if (m_state != State.Damaged)
{
float x = Input.GetAxis("Horizontal");
bool jump = Input.GetButtonDown("Jump");
Move(x, jump);
}
}
どうやらUpdate()
の中で常に呼ばれているこのMove()
というメソッドがユニティちゃんを動かしているような気がしますね。
では、より細かく内容を見ていきます。
なぜ横キーを押すと横に動くのか?
なぜユニティちゃんは右キーを押すと右に、左キーを押すと左に動くのでしょうか。
これは、Move()
に引数として渡されているx
によって決まっています。
float x = Input.GetAxis("Horizontal");
ここのInput.GetAxis
は、キーボードやジョイスティックの入力によって、-1から1の間で値を返すメソッドです。そして、引数のString
によって、下記のように使い分けられます。
-
Horizontal
-> 左右の入力を対象とする -
Vertical
-> 上下の入力を対象とする
要するにここは、「何も押してなければ0、左右キーが押されれば、-1〜1の間の値を返す」という処理のようです。
例えばここを以下のように書き換えます
float x = Input.GetAxis("Vertical");
これでゲームを実行すると、ユニティちゃんは左右キーを押しても反応せず、上キーを押すと右に、下キーを押すと左に動きます(Horizontal
の"右"と同様に、Vertical
の"上"が「正」の値を取るためです)
というわけで、(Horizontal
なら)x
には左右キーを押すことで-1〜1の値を取ることがわかりました。
おそらくこのx
がMove()
の中でユニティちゃんを左右に動かしているのでしょう。
なぜ徐々に加速するのか?
ユニティちゃんはキーを押すと、緩やかに加速して最高速度に達すると一定の速度になっていることがわかります。
これも、Input.GetAxis()
の仕様によるものです。
試しにDebug.Log
を差し込み、キーの「右」を押し込んでみます。
すると、x
の値は0から徐々に1になっていくことがわかります。
また、離すと、x
は徐々に0に近づいていきます。
ジョイスティックなどの場合は傾きの度合いを考慮して値を取得できるのですが、キーボードの場合はこのように、押し続けることで徐々に値が上昇するという特性があります。
その結果、ユニティちゃんは横キーを入れることで一瞬で最速点に到達になるわけではなく、徐々に速度が上がり最高速度に到達するようです。
では、実際に決定したx
がどう使われているのか、Move()
関数の中を見て行こうと思います。
void Move(float move, bool jump)
{
if (Mathf.Abs(move) > 0)
{
Quaternion rot = transform.rotation;
transform.rotation = Quaternion.Euler(rot.x, Mathf.Sign(move) == 1 ? 0 : 180, rot.z);
}
...
}
まずはじめにここの処理を見て行こうと思います。
Mathf.Abs()
は絶対値を取るので、このif
はmove
が0以外、要するに何かしらのキーが押されていたらtrue
になるようです。
Mathf.Sign()
は引数が0と正の値の場合は1、負の値の場合は-1を返します。
if
の中の処理は、自身のQuaternion
(回転情報)を取得して、y軸に関してのみ、正の値であれば0、負の値であれば180としています。
要するにここは、入力されたキーにより、ユニティちゃんの「右向き」か「左向き」かを制御している部分だということがわかりました。
次に下記の処理
void Move(float move, bool jump)
{
...
m_rigidbody2D.velocity = new Vector2(move * maxSpeed, m_rigidbody2D.velocity.y);
m_animator.SetFloat("Horizontal", move);
...
}
m_rigidbody2D
という変数のvelocity
に新しいVector2
を入れています。
まずこのm_rigidbody2D
ですが、これはクラスの最初に定義されていて、ユニティちゃん自身のGameObject
のComponent
のRigidbody2D
をさしています。
Rigidbody2D
は簡単に言うとUnity標準搭載の物理エンジンで、重力や加速度や衝突などを制御するコンポーネントです。
ユニティちゃんにはこのRigidbody2D
がAddComponent
されています。
で、問題はそのRigidbody2D.velocity
に値を代入している点です。
Rigidbody2D.velocity
は「速度」のことで、型はVector2
です。
要するにここはx軸とy軸に対しての速度情報を更新している処理になります。
y軸に対しては、自身のvelocity
のy値を入れているので、つまり「変更なし」ということになります。
一方、x軸はmove * maxSpeed
(クラスの上のほうで10f
と宣言されています)を入れています。
move
は Input.GetAxis("Vertical")
によって得られたfloat
値でしたよね。
つまり、ここは、入力された-10〜10
の値が代入されています。
横キーを押したことでユニティちゃんが横に動くのは、まさにここで速度情報であるvelocity
に値が代入されているから、というわけですね。
##なぜスペースキーを押すとジャンプするのか?
続いて、スペースキーを押すとなぜユニティちゃんがジャンプをするのかを見ていこうと思います。
まずはmove()
に入る前のUpdate()
内の処理から
bool jump = Input.GetButtonDown("Jump");
なるほど、Jump
を押したらjump
がtrue
になるんですね(?)
イメージだと、ここは「スペースキーを押すと」、jump
がtrue
になる、といった処理にしそうですが、このInput.GetButtonDown
の引数には、Jump
というstring
が渡されています。
つまり、どこかでこのJump
というのは「スペースキー」のことである、ということが設定されている、ということになります。
その場所は、下記を開くと設定されています。
Edit > Project Setting > Input
上記を開くと、Inspector
にAxes
という項目が表示され、この中に先ほどのJump
という記述があり、その中のPositive Button
というキーに対して、space
と設定されていることがわかります。
これはInput Manager
と呼ばれるもので、キーやJoystickの入力による各種設定が入っています。
ここでspace
という文言を、例えばw
としてみると、ユニティちゃんはスペースキーを押してもジャンプしなくなり、代わりにw
キーを押すことでジャンプすることがわかります。
このように、キーバインドはこのInput Manager
で管理されている、ということですね。
Positive Button
以外の各種細かな設定は別途UnityのDocumentation
のInput Manager
を見られるといいと思います。
では、実際にこのスペースキーを押すとtrue
になるjump
はmove()
内でどのように使われているのかを見ていきます。
void Move(float move, bool jump)
{
...
if (jump && m_isGround)
{
m_animator.SetTrigger("Jump");
SendMessage("Jump", SendMessageOptions.DontRequireReceiver);
m_rigidbody2D.AddForce(Vector2.up * jumpPower);
}
}
まず、if (jump && m_isGround)
ですが、この中のm_isGround
は、名前からしておそらく「地面に立っているかどうか」を判定しているbool
だと思われますが、実際にどこでどう設定されているかというと、
MonoBehaviour.FixedUpdate()
にて
void FixedUpdate()
{
Vector2 pos = transform.position;
Vector2 groundCheck = new Vector2(pos.x, pos.y - (m_centerY * transform.localScale.y));
Vector2 groundArea = new Vector2(m_boxcollier2D.size.x * 0.49f, 0.05f);
m_isGround = Physics2D.OverlapArea(groundCheck + groundArea, groundCheck - groundArea, whatIsGround);
}
といった記述があります。
Physics2D.OverlapArea
とは、第一引数Vector2
と第二引数Vector2
によって作られた四角の領域が、コライダーと衝突しているかを返します。
pos
はユニティちゃんのposition
、
groundCheck
はユニティちゃんのちょっと下(m_centerY
は1.5f
)、
つまりこのPhysics2D.OverlapArea
は、
ユニティちゃんのちょっと下にある横長い四角形というわけですね。
そして、このPhysics2D.OverlapArea
が、第三引数のwhatIsGround
に限定したレイヤーのコライダーに重なっているかどうか?が、m_isGround
というわけです。
このように、ユニティちゃんの足元にOverlapArea
を用意しておき、これが地面に接していれば、結果的にユニティちゃんが「地面に立っている」と判断しているわけですね。
さて、ようやくこれで
if (jump && m_isGround)
の中に入る条件がわかりました。
つまり、「地面に接している状態でかつジャンプがtrueになった瞬間」というわけです。
では、肝心のその中の処理ですが
m_rigidbody2D.AddForce(Vector2.up * jumpPower);
ユニティちゃんがジャンプする処理は、まさにここになります。
ここでまた新しく、Rigidbody2D.AddForce
というものが登場しました。
これはその名の通り、Rigidbody2D
に「Vector2
の力を加える」メソッドです。
Vector2.up
とは、Vector2(0f, 1f)
と同義なため、つまりこの処理は
y軸方向に、固定のjumpPower
(1000f
と定義されています)分、力を加える、という意味ですね。
これにより、ユニティちゃんは1000f
のパワーをy軸の正の方向に受けることで、上に飛び上がり、
そしてRigidbody2D
の物理演算により、落下する、といった原理になります。
##velocityとAddForce
最後に、今回ユニティちゃんが移動するにあたって左右の移動にはRigidbody2D.velocity
を、ジャンプにはRigidbody2D.AddForce
が使われていました。
ここでふと思うのが、
- 左右移動に
Rigidbody2D.AddForce
を使ってもいいのでは? - ジャンプに
Rigidbody2D.velocity
を使ってもいいのでは?
という点です。
そもそもこの2つはどういった違いがあって、どのように使い分けるべきなのか?について触れていきたいと思います。
###velocityとAddForceの違い
velocity
は速度情報のプロパティであり、AddForce
は指定のベクトルに対し力を加えるメソッドです。
つまり、そもそもvelocity
は重力、摩擦など物理演算によって算出される情報にあたります。そのため、velocity
を直接書き換えるということは、物理演算を無視して速度そのものを直書きする行為となるわけです。
AddForce
に100f
の力を加えると、物理演算が考慮されますが、velocity
に100f
を加えると、突然速度100f
になる、というわけです。
この違いは、下記のようにコードを書き換えるとわかりやすいです。
//m_rigidbody2D.AddForce(Vector2.up * jumpPower);
m_rigidbody2D.velocity = Vector2.up * jumpPower;
ユニティちゃんのジャンプで、「y軸方向に1000f
の力を与える」という既存の処理を、「y軸方向を1000f
の速度にする」という処理に書き換えてます。
この状態でユニティちゃんがジャンプをするとどうなるかというと、消えます。正しくはy軸の正の方向にとんでもない速度で吹っ飛んでいきます。重さなどが考慮されずに突然1000f
の速度になるからですね。
ちなみに、velocityに1000f
ではなく20f
くらいにすると、だいたいAddForce
に1000f
を入れるのと同じくらいになります。
さて、ではなぜユニティちゃんは横移動の際にvelocity
を使っているのか?横移動もAddForce
を使った方が良いのでは?と思うわけですが、これについては下記のような理由があるかと思われます(あくまで憶測です)
まず、試しに下記のようにコードを書き換えてみます。
//m_rigidbody2D.velocity = new Vector2(move * maxSpeed, m_rigidbody2D.velocity.y);
m_rigidbody2D.AddForce(new Vector2(move * maxSpeed, m_rigidbody2D.velocity.y));
ジャンプの時と同様、同じ値をvelocity
直書きではなく、AddForce
の引数に渡すパターンです。
これで実行して横キーを少し押しっぱなしにしてみると、ユニティちゃんは入力されたキーの方向に力が加わるので動き始めるわけですが、キーを離しても止まらず、すーーーっと滑り続けます。
この理由は、ユニティちゃんのRigidbody2D
のLinear Drag
(位置移動の減衰値)が0に設定されているためです(滑る床などで使えそうなプロパティですね)。
では試しに、このLinear Drag
を2に変更してみます。
すると、ユニティちゃんはキーを話すことで徐々に減速して止まるようになるわけですが、今度は最高速度が下がったように感じます。
それもそうで、このLinear Drag
はキーを押し続けているときももちろん有効ですので、移動中も常に減衰が入っているわけですね。
しかも、妙にキーの入力に対して加速度が遅くもっさりしています。
と、こんな感じで、横移動に関しては変に物理演算が入ることが、かえって妙なもっさり感や違和感を出してしまう場合もあるようです。
もちろん、もっとAddForce
で加える力を大きくしたり、Input.GetAxis("Horizontal");
で得た値をそのまま使わずMathf.Sign
などにかけてより急に力を加えるようにする、などのチューニングにより、自然さを出すことができるかと思いますが、それならもうvelocity
に直接速度を書き込むほうが簡単なようにも感じます。
もっと横を押したらスッと動き出して、止まるときもスッと止まって欲しい。
そんなスッキリした操作感に、velocity
を直接書き換えることで対応しているのではないかと思います。
#おわりに
というわけで、ユニティちゃん2Dを使って、2Dキャラクターの動きの基本に当たる「横移動とジャンプ」を学んで見ました。いろいろと応用ができそうなものや、まだ触ってないプロパティなども多数あったので、それをうまく使って、楽しく2Dゲームを作れればと思います。
#ライセンス
本記事に登場したキャラクター(ユニティちゃん)、ゲーム画面、ソースコード等はユニティちゃんライセンスで提供されています。