TL; DR
はじめて2Dゲームを作るにあたって、ユニティちゃん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ゲームを作れればと思います。
ライセンス
本記事に登場したキャラクター(ユニティちゃん)、ゲーム画面、ソースコード等はユニティちゃんライセンスで提供されています。
