top of page

2Dランゲーム編

Lesson1 プレイヤーを実装しよう

1-0

1-0 このパートで作るゲームについて

1-0 このパートで作るゲームについて

1

​はじめに

 この「2Dランゲーム編」では3Dアクションゲーム編では説明できなかった2Dゲームの作り方や、マウス入力の取得などに重点を置いて説明していきます。また、後からステージを追加したりできるように拡張性の高さも意識していきます。

 2Dゲームを作ると言っても基本的な操作は3Dゲームと変わりません。3Dアクションゲーム編の内容もちょくちょく出てくるので、わからない時は過去のレッスンを見返しながら進めていきましょう。

​ まずはどんなゲームかイメージできるように2Dランゲーム編のメインページからサンプルゲームを遊んでみてください。

1-1

1-1 新規プロジェクトを作る

1-1 新規プロジェクトを作る

1

プロジェクト作成

 まずは新しいプロジェクトを作り、素材をインポートして制作の準備をしましょう。

​ 流れは3Dアクションゲーム編とほぼ同じです。

​ Unity Hub内の「新しいプロジェクト」をクリックしてください。

 プロジェクト名は自分が分かりやすいものでOKです。

 今回は2Dゲームを作るので「2D」を選択してください。

2

画面サイズ変更

 プロジェクトが作成できたら、Gameを選択して画面サイズを16:10に変更しておいてください。

3

素材のインポート

 素材の配布先(https://drive.google.com/file/d/1KOhAjE9LaF2U1d1DVoeqGcc19NVFcv1g/view?usp=sharing)からダウンロードしたパッケージをプロジェクト内にドラッグ&ドロップして、​素材をインポートしてください。

​ Spriteフォルダが追加されたらOKです。

4

シーンを作成

 Scenes内に新しいシーン「Stage1」を作成して、ダブルクリックで切り替えてください。このシーン内にゲームを作っていきます。

​ 最初にあったSampleSceneは削除しても構いません。

1-2

1

プレイヤーを追加

Unity Tips!

 今回配布した素材の配布元です。

 配布用にカットした素材もたくさんありますので、ぜひダウンロードしてみてください。この作品に取り込むもよし、別のゲームを作るもよし!

 

『コーゲンシティ・オールスターズ!』ユニティちゃんピクセルアートパック for アクションゲーム Vol.2

ユニティちゃんライセンスを守りましょう)

​・superpowers-asset-packs

 今回インポートしてもらったスプライト素材は、ドット表現に適した表現になるように設定を変更しています。自分の好きな素材を使いたい場合は同じ設定にすることをオススメします。

・Filter Modeを「Bilinear」から「Point(no filter)」に変更

・Compressionを「Normal Quality」から「None」に変更

​※この設定はあくまでドット絵を使用するゲームに向けた設定です。普通の画像を使うゲームには適していないので注意してください。

Unity Tips!
1-2 プレイヤーの準備

1-2 プレイヤーの準備

 まずはプレイヤーの画像を追加しましょう。

 「Sprite」→「Unitychan」→「BasicActions」から「Unitychan_Run_1」の画像をシーン上にドラッグ&ドロップしてください。

​ 目的の画像が見つからない時は右下のスライダーを一番左へ持っていくと名前を一覧で確認できます。

2

カメラを調整

3

プレイヤーに
​重力を適用する

 配置したユニティちゃんの画像の座標を左下辺りに移動させて、少し大きくしてください。

​(画像ではPosition X=-7 Y=-4 Z=0 ScaleはXY共に4)

 また、プレイヤーであることを識別できるようにPlayerタグをつけておきます。

​ MainCameraを選択してサイズを6にしておいてください。

 ちょっとした小技を紹介!

 値を入力できる項目名にマウスカーソルを合わせて、マウスをドラッグすると値を変更することができます。​大雑把に値を調整したい時に便利です。

​ ちなみにAltキーを押しながら操作すると小さく、Shiftキーを押しながら操作すると大きく値を変更できます。

Unity Tips!

 3Dアクションゲーム編と同じようにプレイヤーに当たり判定と重力を加えましょう。

​ 

 まずはユニティちゃんが落下するようにします。​

 ユニティちゃんを選択して、Add Componentから「Rigidbody2D」を追加してください。2Dゲームでの物理演算はそれぞれコンポーネントの最後に2Dがついている方を選ぶことに注意しましょう。

 Rigidbody2Dの設定をします。

​・Linear Drag を1にします。

  これは空気抵抗の値で、大きいほど物体が

 動きにくくなっていきます。

  0のままだと後ほど坂を実装する時にうまく

 登れなくなってしまうので変更しておきましょう。

・Angular Drag を0にします。

  これは回転の空気抵抗ですが特に必要ないので

 0にしておきましょう。

・Gravity Scale を4にします。

  これは名前の通り重力の強さです。

​  スピード感が出るように強めにしています。

  ちなみにマイナスにすると上方向に重力が

 かかるようになります。

・Collision Detection をContinuousにします。

  これはオブジェクトが高速でぶつかった際に

 すり抜けないようにする設定です。

  初期値のDiscreteは処理が軽いですが、高速で

 物にぶつかるとすり抜けることがあります。

  Continuousに変更すると処理は重くなりますが

 すり抜けないようになります。

 

・Freeze Rotation のZにチェックをいれます。

​  3Dアクションゲーム編と同じで、Z軸周りに

 勝手に回転しないようになります。

​(スクリプトでの回転は可能)

4

プレイヤーに
​当たり判定を設定

 この時点でゲームを実行するとユニティちゃんが落下していくことが確認できます。

​ 次はユニティちゃんに当たり判定を追加しましょう。

​ ユニティちゃんを選択してAdd Componentから「Capsule Collider2D」を追加してください​。

 当たり判定の位置と大きさを調整します。

​ 

 Offsetは X=0 Y=0.22、SizeはX=0.2 Y=0.42 に調整してください。

 ユニティちゃんにカプセル型の当たり判定がついたらOKです。

5

地面を追加する

 このままでは落下するだけなので、仮の地面を追加しましょう。

​ ヒエラルキーから「2D Object」→「Sprites」→「Square」を選択して、シーン上に四角いスプライトを追加してください。

 追加した地面にユニティちゃんが着地できるように、位置と大きさを調整しましょう。

​ この地面は後で削除するので適当で構いません。

6

地面に
​当たり判定を追加

 3Dアクションゲーム編の時は地面に最初からBoxColliderがアタッチされていましたが、スプライトにはColliderがアタッチされていません。
 

​ Add Componentから「Box Collider2D」を選択して、地面に当たり判定を追加してください。

 ここまでできたらゲームを実行して、ユニティちゃんに重力と当たり判定が適用されていることを確認してみてください。

1-3

1-3 プレイヤーを常に右へ移動させる

1-3 プレイヤーを常に右へ移動させる

1

右へ移動する
​スクリプトを書く

 このゲームはゲームオーバーやクリアなど特殊な状況でない限り、プレイヤーは常に右へ移動し続けます。プレイヤーが右へ移動し続けるようにしましょう。

​ Scriptフォルダを作成して、その中に新しいスクリプトPlayerMoveを追加してください。

 PlayerMoveを開いて、以下のように入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 

public class PlayerMove : MonoBehaviour
{
    public float MoveSpeed = 8.0f;

 

    void Start()
    {
        
    }

 

    // Fixedなので注意!
    void FixedUpdate()
    {
        // 常に右へ移動する
        Vector3 move = Vector3.zero;
        move.x = MoveSpeed * Time.deltaTime;
        transform.Translate(move);
    }

 

    void Update()
    {
        
    }
}

2

プレイヤーへ
​アタッチ

 FixedUpdateは一定間隔で実行される関数です。また、Updateより先に実行されます。

 FixedUpdateに移動処理を書き、後の1-4でUpdateにカメラの更新処理を書くことで、処理の実行順が移動→カメラになります。

 こうすることによってカメラの位置を変更した後にプレイヤーが移動することがなくなり、画面内のプレイヤーの位置を常に同じにすることができます。

 更新処理の順番についてはある程度知っておかないと良きせぬ挙動を招く原因になりますので、よく使う処理だけでも実行順を把握しておくと良いでしょう。

【イベント関数の実行順序】

https://docs.unity3d.com/ja/2022.3/Manual/ExecutionOrder.html

Unity Tips!

 スクリプトが書けたら保存して、PlayerMoveスクリプトをユニティちゃんにアタッチするのを忘れないようにしましょう。

 アタッチできたら実行して、ユニティちゃんが右へ移動することを確認してみてください。

 例のごとくMoveSpeed変数がインスペクターに表示されているので、お好みで調整してみてください。速いほど爽快感は出ますが、その分難しくなるので注意しましょう。

1-4

1

カメラの追尾処理を実装

1-4 カメラをプレイヤーに追尾させる

1-4 カメラをプレイヤーに追尾させる

 ジャンプの実装をする前に、カメラがプレイヤーについてくるようにしましょう。

 今回カメラに実装する処理は、

・プレイヤーが左下になるように座標を調整する

・一定範囲外に移動しないようにする(ステージの外が見えないように)

 といったものになります。

​ Scriptフォルダ内にGameCameraスクリプトを作成してください。

​ GameCameraスクリプトに以下のように入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 

public class GameCamera : MonoBehaviour
{
    GameObject m_player;

 

    [SerializeField]
    Vector3 CameraAddPos = Vector2.zero;

 

    [SerializeField]
    Vector2 CameraMaxPos = Vector2.zero;
    [SerializeField]
    Vector2 CameraMinPos = Vector2.zero;

 

    void Start()
    {
        // Playerタグのついたオブジェクトをターゲットにする
        m_player = GameObject.FindGameObjectWithTag("Player");
        // 最初に座標更新しておく
        CameraUpdate();
    }

 

    void Update()
    {
        // プレイヤーがいないなら何もしない
        if (m_player == null)
        {
            return;
        }

        // 座標の更新
        CameraUpdate();
    }

 

    void CameraUpdate()
    {
        // カメラの位置を設定
        transform.position = m_player.transform.position + CameraAddPos;

 

        // 座標がオーバーしないように調整
        Vector3 cameraPos = transform.position;
        cameraPos.x = Mathf.Clamp(cameraPos.x, CameraMinPos.x, CameraMaxPos.x);
        cameraPos.y = Mathf.Clamp(cameraPos.y, CameraMinPos.y, CameraMaxPos.y);
        transform.position = cameraPos;
    }
}

・[SerializeField] というコードがありますが、これはAttribute(アトリビュート)と呼ばれるもので変数に属性を付与することができます。publicやprivate(アクセス指定子)とは異なり、アトリビュートはインスペクター上での属性を付与するものが主です。

 変数をpublicにするとインスペクター上に表示できますが、publicにするということはどこからでもアクセスできてしまう…つまり外部から簡単に変更できてしまうということになります。変数をprivateにしつつインスペクター上に値を表示したい時は、変数の前に[SerializeField] という属性を付与することで実現することができます。

 アトリビュートについてはEXでも解説しているので参考にしてみてください。

​ 「アクセスがどうとかよくわかんないよ!」という人は気にしなくてOKです。

【プログラムの解説】

2

パラメータを設定

・Mathf.Clamp関数は3Dアクションゲーム編でも何度か登場しましたが、変数を上限と下限の範囲内に収めてくれる関数です。これによってカメラが上下左右の範囲からはみ出ないようにしています。

​ ここまでコードが書けたら保存して、MainCameraにアタッチしてください。

​ インスペクター上に先ほど[SerializeField]を付与した変数が表示されています。publicにしていなくても、この属性を付与した変数はインスペクター上に表示できることを覚えておいてください。

 インスペクターからそれぞれのパラメータを調整してください。

・CameraAddPos … プレイヤーを中心としたカメラの移動量

 X=7 Y=1 Z=-10

・CameraMaxPos … カメラが移動できる最大座標

 X=500 Y=19(ステージの大きさによって自由に変えてOK)

・CameraMinPos … カメラが移動できる最小座標

​ X=0 Y=0(ステージの大きさによって自由に変えてOK

 設定できたら実行して、カメラがユニティちゃんを追尾すること、ユニティちゃんが落下してもカメラはついていかないことを確認してみてください。

 これで追尾カメラの実装は終了です​。

1-5

1-5 ジャンプの実装

1-5 ジャンプの実装

1

接地判定用

​オブジェクト追加

 ユニティちゃんがジャンプできるようにしましょう。

 基本的な処理は3Dアクションゲーム編と同じですがいくつか違う点があります。

・SPACEキーではなく、左クリックでジャンプする

・空中でも指定した回数までジャンプできるようにする

 まずは接地判定用の空オブジェクトをユニティちゃんの子オブジェクトとして追加してください。​オブジェクト名は自分が分かりやすいもので構いません。

2

当たり判定を追加

 接地判定用オブジェクトにBoxCollider2Dをアタッチして、位置やサイズを調整してください。

​ IsTriggerにチェックを入れるのも忘れないようにしましょう。

​【教材での設定】

 Offset X=0 Y=0.04

 Size X=0.1 Y=0.1

 ユニティちゃんの足元に箱型の当たり判定が追加されたらOKです。

3

接地判定の
​処理を実装

 Scriptフォルダ内にGroundCheckスクリプトを作成して、以下のように入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 

public class GroundCheck : MonoBehaviour
{
    // 接地しているかを格納する変数
    bool m_isGround;
    // 地面に触れているかを返す関数
    public bool GetIsGround()
    {
        return m_isGround;
    }

 

    // 毎フレーム最初に接地判定をリセットする
    private void FixedUpdate()
    {
        m_isGround = false;
    }

 

    // 2Dがつくので注意!
    private void OnTriggerStay2D(Collider2D collision)
    {
        // 地面のタグが付いたオブジェクトに衝突している
        if (collision.CompareTag("Ground"))
        {
            m_isGround = true;
        }
    }
}

4

地面にタグを設定

【プログラムの解説】

・3DではOnTriggerStay関数を使っていましたが、2DではOnTriggerStay2D関数を使います。間違えないように注意しましょう。関数名は違いますが使い方はほとんど同じです。

 次は接地判定をするために地面にGroundタグをつけましょう。

 Add Tagから「Ground」タグを作成して、地面にタグを設定してください。

(タグは名前が完全一致するかどうかで検索しているので打ち間違いに注意)

5

接地判定を
​使ってジャンプ

 地面にタグを設定できたら、PlayerMoveスクリプトを開いて赤い部分のコードを追加してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 

public class PlayerMove : MonoBehaviour
{
    public float MoveSpeed = 8.0f;
    public float JumpPower = 16.0f;

 

    [SerializeField] // privateでもインスペクターには表示される変数
    GroundCheck m_groundCheck;

 

    [Header("空中ジャンプできる回数")]
    public int MaxAirJump = 1;
    int m_airJumpCount = 0;

 

    // 2Dなので注意!
    Rigidbody2D m_player_rb2d;

 

    void Start()
    {
        // 自身にアタッチされているRigidBody2Dを取得する
        m_player_rb2d = GetComponent<Rigidbody2D>();
        // 空中ジャンプ回数を初期化
        m_airJumpCount = MaxAirJump;

    }

 

    // Fixedなので注意!
    void FixedUpdate()
    {
        // 常に右へ移動する
        Vector3 move = Vector3.zero;
        move.x = MoveSpeed * Time.deltaTime;
        transform.Translate(move);
    }

 

    void Update()
    {
        // 接地しているなら空中ジャンプ回数をリセット
        if (m_groundCheck.GetIsGround())
        {
            m_airJumpCount = MaxAirJump;
        }

 

        // 左クリックされた時にジャンプ
        if (Input.GetMouseButtonUp(0) && m_airJumpCount > 0)
        {
            if (m_groundCheck.GetIsGround())
            {
                Jump();
            }
            else if (m_airJumpCount > 0)
            {
                m_airJumpCount--;
                Jump();
            }
        }

    }
 

    // ジャンプ
    void Jump()
    {
        // 加わっている力を一旦リセット
        m_player_rb2d.velocity = Vector2.zero;
        // 上方向に力を加える
        m_player_rb2d.AddForce(new Vector2(0.0f, JumpPower), ForceMode2D.Impulse);
    }


}

【プログラムの解説】

・[Header("表示したい文")] は前述したアトリビュートの一種です。この文を書くことでインスペクター内に文章を表示することができます。日本語も使えます。インスペクターを見やすく整理するためにぜひ使ってみてください。

6

プレイヤーと
​接地判定を繋げる

・Input.GetMouseButtonUp(0) は「マウスが左クリックされて離された瞬間」にtrueを返します。

 他にもGetMouseButton で「押されている間」、GetMouseButtonDown で「押された瞬間」を検知できます。

 また、引数の0はマウスの左ボタンを表しており、1で右ボタン、2で中ボタンを指定できます。

 

・Rigidbodyのメンバ変数であるvelocityは、現在かかっている力を格納する変数です。空中でジャンプしようとした時、オブジェクトには落下する力(重力)がかかっていますが、それを一旦リセットすることで空中でも地上と同じようにジャンプできるようにしています。

 よくわからない人は該当するコードをコメントアウトして違いを確かめてみてください。空中でのジャンプに重力がかかって思うように飛べないと思います。

​ インスペクター内にGroundCheckを格納する変数が表示されているので、GroundCheckerオブジェクトをドラッグ&ドロップしてください。

 GroundCheckの関連付けができたらゲームを実行して、クリックでジャンプできることを確認してみてください。

 今ある地面のオブジェクトをコピーして簡単なステージを作り、空中でも1回ジャンプできることを確認してみましょう。

​ プレイヤーのMoveSpeed(移動速度)やJumpPower(ジャンプ力)、MaxAirJamp(空中でジャンプできる回数)もお好みで調整してみてください。

1-6

1-6 坂道への対応

1-6 坂道への対応

1

​坂道を作る

 今のままでもランゲームは作れますが、上り坂や下り坂も使えるようにして、よりゲームの自由度を上げてみましょう。ここでは少し複雑な話も出てきますが100%理解する必要はありません。

​ まずは適当に斜め45°の坂道を作ってください。2DゲームではZ軸周りに回転させることで、オブジェクトをそのまま回転させることができます。

 そこから横、右下へ伸ばして上り坂と下り坂を作ってみましょう。できるだけ段差ができないようにしてください(完璧じゃなくても大丈夫です)

 ここで試しにゲームを実行してみると、ユニティちゃんが坂道を登れません。坂を登ろうとしても重力によって落ちていってしまいます。とはいえ、単純に重力を消してしまうとジャンプができなくなってしまいます。

 この問題を解決するためには「前方に坂があることを検知」して「坂に触れている間は重力を無視する」システムを実装する必要があります。

 前方に坂があることを検知するためには「法線」と「レイ」を使う必要があります。

​【以下の法線の説明は少し上級者向けです】

 ポリゴンには法線ベクトルというものがあります。詳しい説明はDirectXなどでもしていると思いますが、簡単に言うと面に垂直なベクトルのことです。

(画像はWikipediaから。青い矢印が法線ベクトル)

 3Dに限らず、2Dにも法線が存在します。この画面だと平地の法線は真上に、上り坂の法線は左上に伸びています。ユニティちゃんの前方の法線が取得できれば、上り坂を登ろうとしているかどうか検知できるという訳です。

 そして前方の法線を取得する時にRay(レイ)という機能を使います。

 Rayは「光線」のような意味で、文字通り光線を発射して、衝突したオブジェクトの情報を取得することができる機能です。

​ ユニティちゃんの足元から右下へレイを発射して、衝突した場所の法線を取得します。そして取得した法線と上ベクトルの成す角度を使って坂道を登ろうとしていることを検知します。

 また、この機能を使えばユニティちゃんが壁にぶつかったかどうかも検知できます。

 壁にぶつかった時はゲームオーバーにしたいので、坂道と同時にゲームオーバー判定も行うことができます。

2_1_35

2

レイの始点を作成

 高度な話が出てきましたが理解できなくてもOKです。よくわからなかった人は「足元から光線を飛ばして坂道かどうか検知してるんだなー」程度に捉えておいてください。

​ それでは実際に画像の①のように、ユニティちゃんの足元からレイを飛ばしてみましょう。

​ ユニティちゃんの子オブジェクトに、レイの始点になる空オブジェクトを追加してください。オブジェクト名は何でもOKですが、教材では「RayPoint」にしておきます。

 RayPointの座標をユニティちゃんの右下あたりへ調整します。レイの長さとの兼ね合いで、教材と位置が違うとうまく判定できないことがあるので位置をしっかり合わせてください。

【教材での設定】

 Position X=0.1 Y=0.07

3

レイを発射する

 RayPointを始点にレイを飛ばしてみましょう。

 PlayerMoveスクリプトを開いて赤い部分のコードを追加してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 

public class PlayerMove : MonoBehaviour
{
    public float MoveSpeed = 8.0f;
    public float JumpPower = 16.0f;

 

    [SerializeField] // privateでもインスペクターには表示される変数
    GroundCheck m_groundCheck;

 

    [Header("空中ジャンプできる回数")]
    public int MaxAirJump = 1;
    int m_airJumpCount = 0;

 

    // 2Dなので注意!
    Rigidbody2D m_player_rb2d;

 

    [SerializeField, Header("レイの始点")]
    GameObject m_rayOrigin;

 

    void Start()
    {
        // 自身にアタッチされているRigidBody2Dを取得する
        m_player_rb2d = GetComponent<Rigidbody2D>();
        // 空中ジャンプ回数を初期化
        m_airJumpCount = MaxAirJump;
    }

 

    // Fixedなので注意!
    void FixedUpdate()
    {
        // 常に右へ移動する
        Vector3 move = Vector3.zero;
        move.x = MoveSpeed * Time.deltaTime;
        transform.Translate(move);
    }

 

    void Update()
    {
        // 接地しているなら空中ジャンプ回数をリセット
        if (m_groundCheck.GetIsGround())
        {
            m_airJumpCount = MaxAirJump;
        }

 

        // 左クリックされた時にジャンプ
        if (Input.GetMouseButtonUp(0) && m_airJumpCount > 0)
        {
            if (m_groundCheck.GetIsGround())
            {
                Jump();
            }
            else if (m_airJumpCount > 0)
            {
                m_airJumpCount--;
                Jump();
            }
        }

 

        // 坂道
        Slope();

    }

 

    // ジャンプ
    void Jump()
    {
        // 加わっている力を一旦リセット
        m_player_rb2d.velocity = Vector2.zero;
        // 上方向に力を加える
        m_player_rb2d.AddForce(new Vector2(0.0f, JumpPower), ForceMode2D.Impulse);
    }

 

    // 坂道判定
    void Slope()
    {
        RaycastHit2D raycastHit2D;
        Vector3 origin = m_rayOrigin.transform.position;                // レイの始点
        Vector2 direction = -transform.up;                              // 下に伸びるベクトル
        direction = Quaternion.Euler(0.0f, 0.0f, 45.0f) * direction;    // 45°回転させる
        float distance = 1.0f;                                         // レイの長さ

 

        // レイ(光線)を発射
        raycastHit2D = Physics2D.Raycast(
            origin,
            direction,
            distance
            );

 

        // デバッグ
        Debug.DrawRay(origin, direction * distance, Color.green, 0.05f, false);
        Debug.Log(raycastHit2D.normal);
    }

}

4

発射の設定をする

【プログラムの解説】

​・[SerializeField, Header("レイの始点")] のように,(カンマ)で区切ることで複数のアトリビュートを同時に使用することができます。

​・RaycastHit2Dは2Dのレイを発射して得た情報を格納するクラスです。3DではRaycastHitを使用します。

・direction変数でレイを発射する方向を決めています。

​ -transform.up で自身の上方向へ伸びるベクトルの逆、下方向へ伸びるベクトルを取得しています。

・Quaternion.Euler​関数で任意の軸周りにベクトルを回転させています。これによって右下へ伸びるベクトルを取得しています。

・Physics2D.Raycast関数で2Dのレイを発射することができます。

 第一引数にレイを発射する始点、第二引数にレイを発射する方向のベクトル、第三引数にレイの長さを指定します。

 発射したレイが当たったオブジェクトの情報が、RaycastHit2D型の戻り値として返ってきます。

​・Debug.DrawRayはデバッグ用の関数で、レイを描画することができます(実際のゲーム画面には描画されません)

 第一引数にレイの始点、第二引数にレイの方向と長さ(乗算すればOK)、第三引数にレイの色、

 第四引数はレイが描画される時間、第五引数はレイがオブジェクトに隠れた時に描画するかどうかの設定です。

​・Debug.Log(raycastHit2D.normal); でレイがヒットした場所の法線をコンソールに出力しています(normalは法線のことです)

 ここまで書けたら保存して、インスペクターにレイを発射する始点となるオブジェクトを設定しましょう。​​先ほど作成したRayPointオブジェクトをドラッグ&ドロップしてください。

 自分が発射したレイがユニティちゃん自身に当たらないように設定しておきましょう。

 ユニティちゃんを選択して、Layerを「Ignore RayCast」に変更してください。このレイヤーに設定したオブジェクトはレイが当たらなくなります。今後も坂道判定に引っかかってほしくない当たり判定にはこの設定をしていくことになります。

 ユニティちゃんの子オブジェクトのレイヤーも変更するか訊かれるので、Yesを選択してください。

 Layerが変更できたら設定は完了です。

 ここまで設定できたらゲームを実行して、実行中に一時停止ボタンを押してシーンを確認してみてください(Ctrl+Shift+Pでも可)

​ ユニティちゃんの足元から緑色のレイが発射されていることが確認できます。

5

坂道の処理をする

 これで坂道に衝突した瞬間を判定できるようになりました。

 後はこの法線の情報を使って坂道用の処理を実装しましょう。

​ PlayerMoveスクリプトに以下の赤い部分のコードを追加してください。結構長いコードになりますが頑張りましょう(難しかったらコピー&ペーストでOKです)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 

public class PlayerMove : MonoBehaviour
{
    public float MoveSpeed = 8.0f;
    public float JumpPower = 16.0f;

 

    [SerializeField] // privateでもインスペクターには表示される変数
    GroundCheck m_groundCheck;

 

    [Header("空中ジャンプできる回数")]
    public int MaxAirJump = 1;
    int m_airJumpCount = 0;

 

    // 2Dなので注意!
    Rigidbody2D m_player_rb2d;

 

    [SerializeField, Header("レイの始点")]
    GameObject m_rayOrigin;
    // 坂に触れているかどうか
    bool m_isSlope = false;
    // 坂道かつ空中にいるかどうか
    bool m_isSlopeAir = false;
    // 重力の初期値を保存
    float m_defGravity = 0.0f;

 

    // 角度変更タイマー
    float angleTimer = 0.0f;
    const float ANGLE_LIMIT = 0.1f;

 

    void Start()
    {
        // 自身にアタッチされているRigidBody2Dを取得する
        m_player_rb2d = GetComponent<Rigidbody2D>();
        // 空中ジャンプ回数を初期化
        m_airJumpCount = MaxAirJump;
        // 重力の初期値を保存
        m_defGravity = m_player_rb2d.gravityScale;

    }

    // Fixedなので注意!
    void FixedUpdate()
    {

~中略~

    // 坂道判定
    void Slope()
    {
        // タイマー減少
        if (angleTimer > 0.0f)
        {
            angleTimer -= Time.deltaTime;
        }

 

        RaycastHit2D raycastHit2D;
        Vector3 origin = m_rayOrigin.transform.position;                // レイの始点
        Vector2 direction = -transform.up;                              // 下に伸びるベクトル
        direction = Quaternion.Euler(0.0f, 0.0f, 45.0f) * direction;    // 45°回転させる
        float distance = 0.02f;                                         // レイの長さ

 

        // レイ(光線)を発射
        raycastHit2D = Physics2D.Raycast(
            origin,
            direction,
            distance
            );

 

        // デバッグ
        //Debug.DrawRay(origin, direction * distance, Color.green, 0.05f, false);
        //Debug.Log(raycastHit2D.normal);

 

        // 下り坂判定
        if (raycastHit2D.collider == null)
        {
            // 前に何もないなら後ろを判定
            direction = -transform.up;
            direction = Quaternion.Euler(0.0f, 0.0f, -45.0f) * direction;
            distance = 0.7f;    // レイは少し長め

            raycastHit2D = Physics2D.Raycast(
                origin,
                direction,
                distance
                );

 

            //Debug.DrawRay(origin, direction * distance, Color.green, 0.05f, false);
        }

 

        // なす角度を計算
        float angle = Vector3.SignedAngle(Vector3.up, raycastHit2D.normal, Vector3.forward);
        //Debug.Log(angle);

 

        // 角度に合わせてプレイヤーを回転させる
        if (Mathf.Abs(angle) < 60.0f && angleTimer <= 0.0f)
        {
            transform.rotation = Quaternion.AngleAxis(angle, new Vector3(0, 0, 1));
        }

 

        // 角度を元に判定
        if (Mathf.Abs(angle) < 60.0f && Mathf.Abs(angle) > 20.0f)
        {
            // 坂に接地中
            if (m_isSlope == false)
            {
                m_player_rb2d.velocity = Vector2.zero;
                m_isSlope = true;
                // 回転タイマー開始
                angleTimer = ANGLE_LIMIT;
            }

 

            // 坂道に接地中は重力を無視する
            if (m_groundCheck.GetIsGround())
            {
                m_player_rb2d.gravityScale = 0.0f;
                if (m_isSlopeAir)
                {
                    m_player_rb2d.velocity = Vector2.zero;
                    m_isSlopeAir = false;
                }
            }
            else
            {
                m_player_rb2d.gravityScale = m_defGravity;
                m_isSlopeAir = true;
            }
        }
        else if (angle >= 60.0f)
        {
            // 壁に衝突
            GameOver();
        }
        else
        {
            // 普通の地面
            if (m_isSlope == true)
            {
                m_player_rb2d.gravityScale = m_defGravity;
                m_isSlope = false;
                m_isSlopeAir = false;
                // 回転タイマー開始
                angleTimer = ANGLE_LIMIT;
            }
        }

 

    }
    
    // ゲームオーバー
    public void GameOver()
    {
        // とりあえず今は自身を削除するだけ
        Debug.Log("ゲームオーバー!");
        Destroy(gameObject);
    }

}

【プログラムの解説】

 コードが長いので簡単な説明にしておきます。

・ANGLE_LIMIT(角度変更の間隔)はユニティちゃんの角度が変更される間隔です。

 間隔を設けないと坂道と平地の境目でガタガタしてしまうため、制限を設けています。

​・const を付けた変数は定数として扱われ、プログラムの実行中に値を変更することができなくなります。

・レイの長さを1.0fから0.06fに短く変更しているので注意してください(ジャンプとの兼ね合いのため)

​・前にレイを飛ばしてヒットしなかった場合、後ろにレイを飛ばして下り坂の判定をしています。

 実際にどのようにレイが飛んでいるか確認したい場合は、下り坂判定内のコメントアウトしているDrawRay関数を実行してみてください。

・Vector3.SignedAngle関数によって上方向のベクトルと取得した法線をつかって成す角度を計算しています。実際にどのような戻り値が返ってきているのかはコメントアウトしている Debug.Log(angle); を使って確認してみてください。

 

Mathf.Absは絶対値を返す関数です。成す角度は上り坂は正の数、下り坂は負の数になっているので絶対値を計算することで「上り坂か下り坂かは関係なく、角度がついている時」を求めています。

​・Quaternion.AngleAxis関数で任意軸周りにオブジェクトを回転させることができます。

・教材では20°以上60°未満の傾斜を坂道として扱っています。この値はお好みで変更しても構いません。

​・レイが60°以上の傾斜に衝突した際は壁にぶつかったと判定してゲームオーバー処理を実行しています。現時点ではゲームオーバー処理は仮実装で、自身を削除してログを出力しているだけです。

 複雑な処理に見えますが、実際にやっていることは比較的シンプルなのでわかる範囲で理解してもらえればOKです。

 保存してからゲームを実行して、坂道での判定が取れていることを確認してみてください。​うまく動かない時はコードを見返してみてください。

​(上り坂でジャンプすると常に前進している関係上すぐに着地してしまいます。気になる方はJumpPowerを大きめに調整してください)

​ また、壁に衝突するとゲームオーバーになることも確認してみてください。

 冒頭でも説明しましたが全部は理解しなくてもOKです。坂道の挙動は人によって好みがあると思いますので、調整できそうな人はパラメータや処理を色々調整してみてください。

【参考程度に教材でのパラメータ設定】

・Gravity Scale=6

・MoveSpeed=6

・JumpPower=24

1-7

1-7 プレイヤーのアニメーション

1-7 プレイヤーのアニメーション

1

走る
アニメーション

 ここまでで基本の挙動は実装できたので、最後にアニメーションを実装しましょう。

 Animatonフォルダを追加して、Runのアニメーションを作成してください。

 作成できたらインスペクター内のユニティちゃんにドラッグ&ドロップしてください。自動でAnimatorControllerが作成されます。

 RunアニメーションをダブルクリックしてAnimatonウィンドウを開いてください。Animatonウィンドウは見やすい場所へ移動させて構いません。

 Animatonウィンドウが開いたら、インスペクター内のユニティちゃんをクリックしてください。

​ 「Add Property」のボタンがクリックできるようになったらOKです。

 プロジェクト内の「Sprite」→「UnityChan」→「BasicActions」を選択して、ユニティちゃんの画像をAnimaton内に配置していってください。

 教材では4フレームごとにRun1→2→3→4→5→6→1 と配置しています。

​(最後に1を置くのはRun6が4フレーム間表示されるようにするため)

 ウィンドウ配置を調整して、Runアニメーションがどのように再生されるか確認してみてください。アニメーションは自由に調整してもOKです

2

地上ジャンプの
​アニメーション

 同じように必要なアニメーションを設定していきましょう。枚数が多いため少し大変ですが、画像を参考に配置してください。

 「Create New Clip」を選択してください。

 Animatonフォルダ内に「Jump_Ground」アニメーションを追加してください。

 以降のアニメーションも4フレームごとに配置していきます。

 Jump_Groundアニメーションは、

 Jump_Landing→Jump_MidAir_1→Jump_MidAir_2→Jump_MidAir_3

 のように配置してください。

3

空中ジャンプ開始
​アニメーション

 Jump_AirStartアニメーションを追加してください。空中ジャンプに入る瞬間だけ再生するアニメーションになります。

 Jump_AirStartアニメーションは、0フレーム目と4フレーム目にJump_Landing​を配置してください。

4

空中ジャンプ
​アニメーション

 Jump_Airアニメーションを追加してください。空中ジャンプ中に再生するアニメーションになります。

 Jump_Airアニメーションは、

 Jump_Up_1→Jump_Up_2→Jump_Up_1

​ のように配置してください。

5

落下
​アニメーション

 Jump_Fallアニメーションを追加してください。落下中に再生するアニメーションになります。

 Jump_Fallアニメーションは、

 Jump_Fall_1→Jump_Fall_2→Jump_Fall_1

​ のように配置してください。

2_1_62

6

ゲームオーバー
​アニメーション

 GameOverアニメーションを追加してください。

 GameOverアニメーションは0フレーム目にDamage_4を配置するだけでOKです。

7

クリア
​アニメーション

 Clearアニメーションを追加してください。

 Clearアニメーションは、

 Positive_1→Positive_2→Positive_3→Positive_4→Positive_5→Positive_6

 のように配置してください。

​ Positiveの画像はBasicActionsではなくExtraActionsフォルダ内にあるので注意してください。

 ここまでで7種類のアニメーションが作成できていればOKです。

8

ループ設定をする

 アニメーションのループ設定をします。

 アニメーションを選択してLoopTimeのチェックを操作してください。

【チェックあり】

 GameOver、Jump_Air、Jump_Fall、Run

【チェックなし】

​ Clear、Jump_AirStart、Jump_Ground

9

Animatorの設定

 AnimatorControllerをダブルクリックしてAnimatorを開いてください。先ほど作ったアニメーションステートが並んでいると思います。

 ステートを見やすいように並べてください。

10

遷移の設定

 Transition(遷移)を画像のように設定してください。

​ 3Dアクションゲーム編に比べて複雑なので間違えないように注意しましょう。ClearとGameOverは後のLessonで設定するので、今はどこにも繋がなくてOKです。

【余計わかりにくい気がするけど設定を文章化しました】

・Runから繋ぐステート

  Jump_Ground、Jump_AirStart、Jump_Fall

​・Jump_Groundから繋ぐステート

  Run、Jump_AirStart、Jump_Fall

・Jump_AirStartから繋ぐステート

  Jump_Air

・Jump_Airから繋ぐステート

  Run、Jump_Fall

・Jump_Fallから繋ぐステート

​  Run、Jump_AirStart

11

パラメータを作成

 遷移条件を設定する前にパラメータを作成しましょう。

​ Bool型のパラメータ「JumpFlag」「AirJumpFlag」「FallFlag」を追加してください。

12

遷移条件を設定

 遷移条件を設定していきます。

 項目は多いですが、Jump_AirStart→Jump_Air以外の設定は遷移条件以外同じです。

・Has Exit Timeのチェックを外す

​・Transition Durationを0にする

​・遷移条件を設定する

【Runから繋ぐトランジション】

【Jump_Groundから繋ぐトランジション】

【Jump_AirStartから繋ぐトランジション】

​※ここだけ他と設定が違うので注意!

​ Jump_AirStartはJump_Airに入る前準備のようなステートで、アニメーションの再生が終わったら自動的にJump_Airに遷移するようになっています。

【Jump_Airから繋ぐトランジション】

【Jump_Fallから繋ぐトランジション】

13

遷移優先度を設定

 同時に複数条件を満たした場合の遷移先の優先度を設定することができます。

​ ステートのインスペクター内のTransitionsで、上に設定されているTransitionが優先されます。ドラッグ&ドロップで並べ替えてください。

【Run】

・Run -> Jump_AirStart

・Run -> Jump_Ground

​・Run -> Jump_Fall

【Jump_Ground】

・Jump_Ground -> Jump_AirStart

Jump_Ground -> Jump_Fall

​・Jump_Ground -> Run

【Jump_Air】

・Jump_Air -> Jump_Fall

​・Jump_Air -> Run

Jump_Fall

Jump_Fall -> Jump_AirStart

​・Jump_Fall -> Run

14

遷移優先度を設定

 これでAnimatorControllerの設定は完了です。

 最後にPlayerMoveスクリプト内で適切なタイミングでパラメータを切り替えましょう。

​ PlayerMoveスクリプトを開いて赤い部分のコードを追加してください。最後なのでPlayerMoveスクリプト全体を貼ります。

※ Jump関数に引数が追加されています

※ パラメータをリセットするAnimationReset関数が一番最後にあるので注意!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 

public class PlayerMove : MonoBehaviour
{
    public float MoveSpeed = 8.0f;
    public float JumpPower = 16.0f;

 

    [SerializeField] // privateでもインスペクターには表示される変数
    GroundCheck m_groundCheck;

 

    [Header("空中ジャンプできる回数")]
    public int MaxAirJump = 1;
    int m_airJumpCount = 0;

 

    // 2Dなので注意!
    Rigidbody2D m_player_rb2d;

 

    [SerializeField, Header("レイの始点")]
    GameObject m_rayOrigin;
    // 坂に触れているかどうか
    bool m_isSlope = false;
    // 坂道かつ空中にいるかどうか
    bool m_isSlopeAir = false;
    // 重力の初期値を保存
    float m_defGravity = 0.0f;

 

    // 角度変更タイマー
    float angleTimer = 0.0f;
    const float ANGLE_LIMIT = 0.1f;

 

    // アニメーションコントローラー
    Animator m_animator;
    // アニメーション管理用フラグ
    bool m_airFlag;

 

    void Start()
    {
        // 自身にアタッチされているRigidBody2Dを取得する
        m_player_rb2d = GetComponent<Rigidbody2D>();
        // 自身にアタッチされているAnimatorを取得する
        m_animator = GetComponent<Animator>();

        // 空中ジャンプ回数を初期化
        m_airJumpCount = MaxAirJump;
        // 重力の初期値を保存
        m_defGravity = m_player_rb2d.gravityScale;
    }

 

    // Fixedなので注意!
    void FixedUpdate()
    {
        // 常に右へ移動する
        Vector3 move = Vector3.zero;
        move.x = MoveSpeed * Time.deltaTime;
        transform.Translate(move);
    }

 

    void Update()
    {
        // 接地しているなら空中ジャンプ回数をリセット
        if (m_groundCheck.GetIsGround())
        {
            m_airJumpCount = MaxAirJump;
        }

 

        // 左クリックされた時にジャンプ
        if (Input.GetMouseButtonUp(0) && m_airJumpCount > 0)
        {
            if (m_groundCheck.GetIsGround())
            {
                Jump(false);
            }
            else if (m_airJumpCount > 0)
            {
                m_airJumpCount--;
                Jump(true);
            }
        }

 

        // 坂道
        Slope();

 

        // 落下アニメーション切り替え
        if (m_groundCheck.GetIsGround() == false && m_player_rb2d.velocity.y < -9.8f)
        {
            AnimationReset();
            m_animator.SetBool("FallFlag", true);
        }

 

        // ジャンプアニメーション切り替え
        if (m_groundCheck.GetIsGround() == false)
        {
            m_airFlag = true;
        }

        if (m_airFlag && m_groundCheck.GetIsGround() && m_player_rb2d.velocity.y == 0.0f)
        {
            m_airFlag = false;
            // アニメーションも戻す
            AnimationReset();
        }

    }

 

    // ジャンプ
    void Jump(bool airJump)
    {
        // 加わっている力を一旦リセット
        m_player_rb2d.velocity = Vector2.zero;
        // 上方向に力を加える
        m_player_rb2d.AddForce(new Vector2(0.0f, JumpPower), ForceMode2D.Impulse);

        // アニメーション
        m_animator.SetBool("FallFlag", false);
        if (airJump)
        {
            m_animator.SetBool("AirJumpFlag", true);
        }
        else
        {
            m_animator.SetBool("JumpFlag", true);
        }

    }

 

    // 坂道判定
    void Slope()
    {
        // タイマー減少
        if (angleTimer > 0.0f)
        {
            angleTimer -= Time.deltaTime;
        }

 

        RaycastHit2D raycastHit2D;
        Vector3 origin = m_rayOrigin.transform.position;                // レイの始点
        Vector2 direction = -transform.up;                              // 下に伸びるベクトル
        direction = Quaternion.Euler(0.0f, 0.0f, 45.0f) * direction;    // 45°回転させる
        float distance = 0.02f;                                         // レイの長さ

 

        // レイ(光線)を発射
        raycastHit2D = Physics2D.Raycast(
            origin,
            direction,
            distance
            );

 

        // デバッグ
        //Debug.DrawRay(origin, direction * distance, Color.green, 0.05f, false);
        //Debug.Log(raycastHit2D.normal);

 

        // 下り坂判定
        if (raycastHit2D.collider == null)
        {
            // 前に何もないなら後ろを判定
            direction = -transform.up;
            direction = Quaternion.Euler(0.0f, 0.0f, -45.0f) * direction;
            distance = 0.7f;    // レイは少し長め
            raycastHit2D = Physics2D.Raycast(
                origin,
                direction,
                distance
                );

            //Debug.DrawRay(origin, direction * distance, Color.green, 0.05f, false);
        }

 

        // なす角度を計算
        float angle = Vector3.SignedAngle(Vector3.up, raycastHit2D.normal, Vector3.forward);
        //Debug.Log(angle);

 

        // 角度に合わせてプレイヤーを回転させる
        if (Mathf.Abs(angle) < 60.0f && angleTimer <= 0.0f)
        {
            transform.rotation = Quaternion.AngleAxis(angle, new Vector3(0, 0, 1));
        }

 

        // 角度を元に判定
        if (Mathf.Abs(angle) < 60.0f && Mathf.Abs(angle) > 20.0f)
        {
            // 坂に接地中
            if (m_isSlope == false)
            {
                m_player_rb2d.velocity = Vector2.zero;
                m_isSlope = true;
                // アニメーションも戻す
                AnimationReset();

                // 回転タイマー開始
                angleTimer = ANGLE_LIMIT;
            }

 

            // 坂道に接地中は重力を無視する
            if (m_groundCheck.GetIsGround())
            {
                m_player_rb2d.gravityScale = 0.0f;
                if (m_isSlopeAir)
                {
                    m_player_rb2d.velocity = Vector2.zero;
                    m_isSlopeAir = false;
                }
            }
            else
            {
                m_player_rb2d.gravityScale = m_defGravity;
                m_isSlopeAir = true;
            }
        }
        else if (angle >= 60.0f)
        {
            // 壁に衝突
            GameOver();
        }
        else
        {
            // 普通の地面
            if (m_isSlope == true)
            {
                m_player_rb2d.gravityScale = m_defGravity;
                m_isSlope = false;
                m_isSlopeAir = false;
                // 回転タイマー開始
                angleTimer = ANGLE_LIMIT;
            }
        }

    }

    // ゲームオーバー
    public void GameOver()
    {
        // とりあえず今は自身を削除するだけ
        Debug.Log("ゲームオーバー!");
        Destroy(gameObject);
    }

 

    void AnimationReset()
    {
        // アニメーションのパラメータをリセットする
        m_animator.SetBool("JumpFlag", false);
        m_animator.SetBool("AirJumpFlag", false);
        m_animator.SetBool("FallFlag", false);
    }

}

 PlayerMoveスクリプトが非常に長くなってしまいましたが、基本的な処理はこれで完成です(プレイヤーのスクリプトはどうしても長くなってしまうものですが…)

 うまく動かない場合はコピー&ペーストしても構いませんが、【プログラムの解説】はしっかり読んでおいてください。

 コードが書けたら保存して、アニメーションが再生できていることを確認してみてください。

​ プレイヤーが完成したので次はステージを作っていきましょう。

まとめ

・2Dゲームで物理演算を行う際は最後に2Dとついているものを選ぶ

(Rigidbody2D、CapsuleCollider2D など)

​・privateな変数の前に [SerializeField] とつけることで、インスペクターに値を表示することができる

​・このように変数やクラスに属性をつけるものを Attribute(アトリビュート)という

​・Unityには見えない光線を発射して、衝突したオブジェクトの情報を取得できる「Ray」という機能がある

評価テスト

評価テスト

河原電子ビジネス専門学校
​ゲームクリエイター科

bottom of page