Unityゲームの作り方メモ

【Unity】2DアクションRPGの作り方

Unity

ユニティーを使用したアクションRPGの作り方のメモです。ゲーム制作の参考にしていただければ嬉しいです。トグルボタンをクリックすると記事が表示されます。小さくて見ずらい画像はクリックで拡大できます。

素材の準備

お好きなプレイヤーのキャラチップ画像とマップチップを用意します。

私はこちらの素材を使用しました。

用意した素材をUnityにインストールしておきます。

ScriptsやAnimationsなどのよく使うフォルダを作成しました。

プレイヤーを作成する

まずはプレイヤーを作成します。

createemptyを作成します。名前はPlayerにしました。

inspector画面を開いてaddcomponentからrigidbody2Dを追加しておきます。

rigidbody2DのGravityScaleを0にしておきます。

前を向いているときのアニメーションを作成します。

キャラチップの切り分け方はこちらの記事を参考にしてください。

使用するキャラチップの前向きの画像を全て選択してPlayerにドラッグ&ドロップします。

ドラッグ&ドロップするとアニメーションの保存先を指定できるので好きな場所にanimファイルを保存します。名前はfrontにしました。

Playerのinspector画面を見るとspriteRendererとAnimatorが追加されています。

spriteRendererのspriteがNoneになっているので、正面を向いている画像を設定しておきます。

同じように右、左、後のモーションを作成します。

キャラチップを複数選択してドラッグ&ドロップして、LEFT、RIGHT、BACKのアニメーションを作成します。

Unityを再生するとアニメーションが再生されます。

Animationコントローラーダブルクリックするとanimatorタブが開きます。

Animatorを開くと先ほど作成したアニメーション並んでいます。

inspector画面のspeedからモーションの速度を変更できます。

モーションが少し早かったので私は0.5に変更しました。

LEFTなどを右クリックしてSetasLayerをクリックして再生すると、上下左右のアニメーションをチェックできます。

C#スクリプトを作成します。名前はPlayerControlerにしました。

作ったスクリプトをPlayerにドラッグ&ドロップしてアタッチします。

アタッチしたらスクリプトを開きます。

スクリプトの内容
移動スピード アニメーション リジットボディを格納する変数を作成する
キー入力を受け取ってプレイヤーを移動させる

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField, Tooltip("移動速度")]
    private int moveSpeed;
    [SerializeField]
    private Animator playerAnim;

    public Rigidbody2D rb;
   

    
    void Update()
    {
        rb.velocity = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")).normalized * moveSpeed;
    }
}

保存してUnityを再生するとプレイヤーをASDWで操作できるようになります。

このままでは正面を向いたままなので移動方向によってモーションを変更するようにしていきます。

Animatorを開いてアニメーションを一度全て消去します。

Animatorのタブの中のところで右クリックをしてCreateState→fromnewBrendTreeを選択します。

作成したらパラメータータブをクリックします。

+ボタンからfloatを追加してX,Yを作成します。

brendTreeをダブルクリックしてBlendTreeのinspector画面のbrendTypeを2DSimpledirectionalに変更します。

その下のパラメータをXとYに変更します。

モーションの+ボタン会クリックしてaddmotionfieldを選択して全てのアニメーションを設定していきます

front、BACK,left,rightのアニメーションをドラッグ&ドロップして設定します

数値はこんな感じになりました。

スクリプトを開いてUpdateに移動した方向とアニメーションを連動させるコードを書き足します。


        if (rb.velocity != Vector2.zero) 
        {
            if (Input.GetAxisRaw("Horizontal") != 0)
            {
                if (Input.GetAxisRaw("Horizontal") > 0)
                {
                    playerAnim.SetFloat("X", 1f);
                    playerAnim.SetFloat("Y", 0);
                }
                else
                {
                    playerAnim.SetFloat("X", -1f);
                    playerAnim.SetFloat("Y", 0);
                }

            }
            else if (Input.GetAxisRaw("Vertical") > 0)
            {
                playerAnim.SetFloat("X", 0);
                playerAnim.SetFloat("Y", 1f);
            }
            else 
            {
                playerAnim.SetFloat("X", 0);
                playerAnim.SetFloat("Y", -1f);
            }
        
        }

これでアニメーションを連動することができました。

フィールドの作成

2DObject→tilemap→rectangularを選択してtilemapを作成します。

名前はFloorにしました。

Window→2D→tilepalletを選択するとtilepalletが開きます

ダウンロードしたアセットが追加されているのでフィールドを適当に塗っていきます。

gridを選択して右クリック2dobject→tilemap→rectangularを選択してtilemapを作成します。

名前はWallにしました。

wallが床の下に表示されないようにorderinlayerを1にしておきます。

PlayerのspriteRendererのorderinlayerを2に変更しました。

activetilemapをwallにして適当に壁を作成していきます

wallのinspector画面のaddcomponentからtilemapcoleiderを選択して追加します。

追加すると配置した壁一つ一つにコライダーがつきます。

tilemapcoriderのused bycompositeにチェックを入れます。

wallのinspector画面のaddcomponentからconpositecorider2dを選択して追加します。

追加すると一つ一つについていたコライダーが繋がります。

conpositecorider2dを追加したときに自動的にrigidbody2dも追加されます。

rigidbodyのbodytypeをkinematicに変更します。

これを追加すると先ほど作った壁に当たり判定がつきます

playerにBoxColider2dを追加します。

editcoliderでプレイヤーに合わせてコライダーのサイズを調整します。

プレイヤーについているrigidbodyのfreezerotationZにチェックを入れて回転しないようにしておきます。

これでプレイヤーが壁を通り抜けられなくなりました。

武器の作成

剣はこちらのアセットを使用して作成しました。

プレイヤーを選択してCreateemptyを作成します。名前はSwordholderにしました。

Swordholderの下にcreateemptyを作成します。名前はSwordにしました。

SwordにspriteRendererを追加します。

用意した剣の画像をドラッグ&ドロップして設定します。

シーンビューでSwordを動かして位置を調整します

Playerのlayer作成して設定します。

playerを選択してinspector画面のspriteRendererのsortinglayerをクリックしてaddsortinglayerをクリックします。

+を押してPlayerのlayerを作成します

PlayerとSwordにlayerを設定します。

Swordholderのzを変更するとplayer中心に剣が回るようになります。

Windowメニューからanimationウィンドウを開きます

Swordholderを選択してaddcomponentからAnimatorを追加します。

animetionウィンドウのCreateボタンを押してアニメーションを作成します。

Createnewclipから待機モーションを作成します。名前はIdleにしました。

作成したら赤い丸のボタンを押してSwordを選択します。

addpoptyを選択してSwordのIsactiveを選択して

アニメーションの開始と終わりのチェックを外して赤い丸を押します。

次に前攻撃モーションを作成します名前はFrontAttackにしました

赤い丸のボタンを押してSwordholderを選択します。

addpoptyを選択してSwordholderのrotationを選択します。

Zを変更するとプレイヤーを中心に剣が回転するので前方向モーションを作成します

Backattack、Leftattack、Rightattackも同じように作成します。

Animatorウィンドウを開いて

swordholderを表示してidle以外のモーションを一度消します

右クリックCreateStatefromnewbrendtreeを選択

パラメータタブの+ボタンを押して+からfloat X、Y、Trigger Attackを作成します。

anystateから→brendtree→idleになるように→を追加する

anystateの矢印を選択してcounditionsの+ボタンをクリックにしてattackにします

これで攻撃ボタンををして時にアニメーションを再生できるようになります。

settingを開いてtransitiondurationを0にしてhasexitTIMEのチェックを外します。

brendtree→idleの矢印を選択してtransitiondurationを0にします

hasexittimeにチェックを入れてexittimeを1にします。

ブレンドツリーを以下のように設定します。

PlayerControllerにコードを追加します。

  • アニメーターを入れる変数を作る。
  • キー入力に合わせてパラメーターの数値を変更する。
  • クリックで攻撃できるようにする。

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField, Tooltip("移動速度")]
    private int moveSpeed;
    [SerializeField]
    private Animator playerAnim;

    public Rigidbody2D rb;

    [SerializeField]
    private Animator attackAnim;



    void Update()
    {
        rb.velocity = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")).normalized * moveSpeed;

        if (rb.velocity != Vector2.zero) 
        {
            if (Input.GetAxisRaw("Horizontal") != 0)
            {
                if (Input.GetAxisRaw("Horizontal") > 0)
                {
                    playerAnim.SetFloat("X", 1f);
                    playerAnim.SetFloat("Y", 0);

                    attackAnim.SetFloat("X", 1f);
                    attackAnim.SetFloat("Y", 0);
                }
                else
                {
                    playerAnim.SetFloat("X", -1f);
                    playerAnim.SetFloat("Y", 0);

                    attackAnim.SetFloat("X", -1f);
                    attackAnim.SetFloat("Y", 0);
                }

            }
            else if (Input.GetAxisRaw("Vertical") > 0)
            {
                playerAnim.SetFloat("X", 0);
                playerAnim.SetFloat("Y", 1f);

                attackAnim.SetFloat("X", 0);
                attackAnim.SetFloat("Y", 1f);

            }
            else 
            {
                playerAnim.SetFloat("X", 0);
                playerAnim.SetFloat("Y", -1f);

                attackAnim.SetFloat("X", 0);
                attackAnim.SetFloat("Y", -1f);

            }
        
        }

        if (Input.GetMouseButtonDown(0)) 
        {
            attackAnim.SetTrigger("Attack");
        
        }
    }
}

新しく作った変数にSwordholderをドラッグ&ドロップして完了です。

HPバーの作成

UI→sliderを作成します。名前はHPSliderにしました。

作成したら表示位置を変更します。inspector画面の左上のマークをクリックします。

altキーを押しながら左上を選択すると画面の左上に固定できます。

canvasのUIScalemodeをScaleWithScreenSizeに変更します

サイズはX1920Y1080にしました

画面サイズが変更されたときにUIのサイズを自動調整してくれます

HPSliderのサイズを好きな大きさに調整します。

backGroundを赤にします。

fillのimageのカラーを緑にします。

FillAreaとFillのrecttranceformのサイズをを0にします。

HPSliderを選択してMaxvalueを100にします

ハンドルは使用しないのでhandleslideareaを消去します。

HPSliderのvalueを変更するとHPバーのようになります

新しくC#スクリプトを作成します。

名前はGameManagerにしました。

新しく空のオブジェクトを作成してスクリプトをアタッチしておきます。

GameManager
static化してどこからでも呼び出せるようにする
UI、PlayerControlerを格納する変数の作成
UIを更新する関数の作成

using UnityEngine;
using UnityEngine.UI;

public class GameManager : MonoBehaviour
{
    public static GameManager instance;

    [SerializeField]
    private Slider hpSlider;
    
    [SerializeField]
    private PlayerController player;
    private void Awake()
    {
        if (instance == null) 
        {
            instance = this;
        
        }
    }
   

    public void UpdateHealthUI() 
    {
        hpSlider.maxValue = player.MaxHealth;
        hpSlider.value = player.CurrentHealth;
    
    }
}
PlayerController
HP、最大HPの変数の作成
Start関数でHP=最大HPに設定する
Start関数でGameManagerのUI更新関数を呼び出す
[System.NonSerialized]
public int CurrentHealth;
    public int MaxHealth;

    private void Start()
    {
        CurrentHealth = MaxHealth;
        GameManager.instance.UpdateHealthUI();
    }

inspector画面からplayerのmaxHealthを設定します。

GameManagerにHPSliderとplayerを設定します。

プレイするとHPバーが更新されて最大になります。

カメラの設定

Cinemachineという機能を使ってカメラをプレイヤーに追従させます。

Window→packageManagerを開いてCinemachineを検索してインストールします

インストールが完了するとメニューバーのComponentにCinemachineが追加されています。

メインカメラにaddcomponentからChinemaChineBrainを追加します。

ヒエラルキーウィンドウ内を右クリックCinemachine→2DCameraを選択します。

CM vcam1というオブジェクトが作成されます。

CMvcamのinspector画面を見るとfollowという項目があるのでプレイヤーをドラッグ&ドロップして設定します。

設定するとプレイヤーをカメラが追従してくれます。

マップの端まで来るとマップの外が移ってしまうので映らないように設定します。

空のオブジェクトを作成します。名前はConfinerにしました。

Transformは初期化しておきます。

inspector画面の右上のほうにあるLayerをクリックしてaddlayerから新しくConfinerというLayerを作成して設定します。

projectSettingを開いてPhisics2DタブをクリックするとLayerのチェックボックスがあるのでConfinerの項目のチェックを全て外します。

addcomponentからPoligonColider2Dを追加します。

PoligonColider2Dは自由に形を決めることができるコライダーです。

追加したコライダーをマップの端に合わせてサイズ調整します。

CMvcamのinspector画面の下のほうにextensionsという項目があります。

addextensionからCinemachineConfinerを選択するとスクリプトが追加されます。

CinemachineConfinerのBoundingShape2Dにオブジェクトを追加する項目があるので先ほど作成したConfinerを設定します。

これでマップの端に近づいてもマップ外が見えなくなります。

敵の作成

空のオブジェクトを作成します名前はEnemyにしました。

プレイヤーのモーションと同じ手順でEnemyのモーションを作成しました。

Idle停止しているときのモーション
move動いているときのモーション

モーションを作成するとspriteRendererが追加されているので立ち絵を設定します。

Enemyにコンポーネントを付けていきます

追加するコンポーネント
BoxColider2Dサイズを調整する
rigidbody2DGravityScaleを0にする、freezerotationZにチェックを入れる

アニメーターを開いてIdlemoveに両方から矢印

パラメータータブからbool型のパラメーターを作成します。名前はmovingにしました。

Idle→を選択してtransitionDuratiorを0

counditionsをmoving、trueに設定します。

move→を選択してexittime0、transitionDuratior0

counditionsをmoving、false設定します。

このBoxColider2Dの中を敵が動き回るようになります。

空のオブジェクトを作成します。名前はAreaにしました。

BoxColider2Dを追加してサイズを調整します。

C#スクリプトを作成してEnemyにアタッチします。名前はEnemyControlerにしました。

作成する変数変数の内容Start関数で実行Update関数で実行
rbRigidbody2Dを格納する各コンポーネントの取得タイマーを減らしていく
enemyAnimAnimatorを格納するタイマーの設定移動先を決めるコードを書く
moveSpeedEnemyの移動速度エリア内しか動けないようにする
waitTimeEnemyの待ち時間
walkTimeEnemyの動く時間
waitCountorタイマー
moveCounterEnemyの動く方向
moveDirEnemyの動く方向
areaEnemyの行動範囲
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

public class EnemyControler : MonoBehaviour
{

    private Rigidbody2D rb;
    private Animator enemyAnim;

    [SerializeField]
    private float moveSpeed, waitTime, walkTime;

    private float waitCountor, moveCounter;

    private Vector2 moveDir;

    [SerializeField]
    private BoxCollider2D area;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        enemyAnim = GetComponent<Animator>();

        waitCountor = waitTime;
    }

    void Update()
    {

        if (waitCountor > 0)
        {

            waitCountor -= Time.deltaTime;
            rb.velocity = Vector2.zero;

            if (waitCountor <= 0)
            {
                moveCounter = walkTime;

                enemyAnim.SetBool("Moving", true);

                moveDir = new Vector2(Random.Range(-1f, 1f), Random.Range(-1f, 1f));
                moveDir.Normalize();

            }
        }
        else 
        {
            moveCounter -= Time.deltaTime;

            rb.velocity = moveDir* moveSpeed;

            if (moveCounter <= 0)
            {
                enemyAnim.SetBool("Moving", false);

                waitCountor = waitTime;

            }
        
        }

        //エリア内しか動けないようにする
        transform.position = new Vector3(Mathf.Clamp(transform.position.x, area.bounds.min.x + 1, area.bounds.max.x-1),
            Mathf.Clamp(transform.position.y, area.bounds.min.y + 1, area.bounds.max.y - 1),transform.position.z);

    }
}

EnemyControlerの設定はこんな感じにしました。

これで敵がエリア内を徘徊するようになりました。

範囲内に入ったらプレイヤーを追跡して離れたら止まるようにしていきます。

プレイヤーのタグを設定しておきます。

追加する変数Start関数に追加Update関数を編集
chase追いかける判定プレイヤーの位置を取得する追いかける判定によってプレイヤーを追いかけるようにする
isChaseing追いかけている判定プレイヤーが一定以上離れたら追いかけるのをやめる
cheaseSpeed追いかける速度
rangeTochaseプレイヤーに気づく範囲
targetプレイヤーの位置
waitAfterHitting攻撃する間隔
attackDamage攻撃力

  [SerializeField]
    private bool chase;

    private bool isChaseing;

    [SerializeField]
    private float cheaseSpeed, rangeTochase;

    private Transform target;

    [SerializeField]
    private float waitAfterHitting;

    [SerializeField]
    private int attackDamage;

void Start()
    {
        target = GameObject.FindGameObjectWithTag("Player").transform;
    }

 void Update()
    {
        if (!isChaseing)
        {
            if (waitCountor > 0)
            {

                waitCountor -= Time.deltaTime;
                rb.velocity = Vector2.zero;

                if (waitCountor <= 0)
                {
                    moveCounter = walkTime;

                    enemyAnim.SetBool("Moving", true);

                    moveDir = new Vector2(Random.Range(-1f, 1f), Random.Range(-1f, 1f));
                    moveDir.Normalize();

                }
            }
            else
            {
                moveCounter -= Time.deltaTime;

                rb.velocity = moveDir * moveSpeed;

                if (moveCounter <= 0)
                {
                    enemyAnim.SetBool("Moving", false);

                    waitCountor = waitTime;

                }

            }

            if (chase)
            {
                if (Vector3.Distance(transform.position, target.transform.position) < rangeTochase)
                {
                    isChaseing = true;

                }

            }

        }
        else 
        {
            if (waitCountor > 0)
            {
                waitCountor -= Time.deltaTime;
                rb.velocity = Vector2.zero;

                if (waitCountor <= 0)
                {
                    enemyAnim.SetBool("Moving", true);

                }
            }
            else 
            {
                moveDir = target.transform.position -transform.position;
                moveDir.Normalize();

                rb.velocity = moveDir * moveSpeed;
            
            }

            if (Vector3.Distance(transform.position, target.transform.position) > rangeTochase)
            {
                isChaseing = false;

                waitCountor= waitTime;

                enemyAnim.SetBool("Moving", false);

            }

        }

        

        //エリア内しか動けないようにする
        transform.position = new Vector3(Mathf.Clamp(transform.position.x, area.bounds.min.x + 1, area.bounds.max.x-1),
            Mathf.Clamp(transform.position.y, area.bounds.min.y + 1, area.bounds.max.y - 1),transform.position.z);

    }

Unityを使用したARPGの作り方のメモです。

攻撃判定

PlayerControllerを開いてコードを書いていきます。

新しく作る変数新しく作る関数アップデートに追加する
isknockingback吹き飛び判定吹き飛ばし用の関数KnockBack無敵時間かどうかの判定
knockDir吹き飛ぶ方向ダメージ用の関数DamagePlayer現在ノックバック中かの判定ノックバック中ならプレイヤーの行動を制限する
knockbackTime吹き飛び時間
knockbackForce吹き飛ぶ力
knockbackCounter吹き飛びタイマー
invincibilityTime無敵時間
invincibilityCounter無敵時間タイマー

  private bool isknockingback;
    private Vector2 knockDir;

    [SerializeField]
    private float knockbackTime, knockbackForce;
    private float knockbackCounter;

    [SerializeField]
    private float invincibilityTime;
    private float invincibilityCounter;

  void Update()
    {
 if (invincibilityCounter > 0) 
        {
            invincibilityCounter-= Time.deltaTime;
        }

        if (isknockingback) 
        {
            knockbackCounter-= Time.deltaTime;
            rb.velocity = knockDir * knockbackForce;
            if (knockbackCounter <= 0)
            {
                isknockingback = false;
            }
            else 
            {
                return;
            }

        }
   
    public void KnockBack(Vector3 position) 
    {
        knockbackCounter = knockbackTime;
        isknockingback = true;

        knockDir = transform.position - position;

        knockDir.Normalize();
    
    }

public void DamagePlayer(int damage) 
    {
        if (invincibilityCounter <=0)
        {
            CurrentHealth = Mathf.Clamp(CurrentHealth - damage, 0, MaxHealth);
            invincibilityCounter = invincibilityTime;

            if (CurrentHealth == 0)
            {
                gameObject.SetActive(false);
            }
        }

        GameManager.instance.UpdateHealthUI();
    
    }

EnemyControlerに衝突判定用コードを書き足します。

 private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Player") 
        {
            if (isChaseing)
            {
                PlayerController player = collision.gameObject.GetComponent<PlayerController>();

                player.KnockBack(transform.position);
                player.DamagePlayer(attackDamage);

                waitCountor = waitAfterHitting;

                enemyAnim.SetBool("Moving", false);
            }
        
        }
    }

エディタ上でプレイヤーを選択してknockbackTime0.2

knockbackforce30、invincibilityTimeを0.5に設定してみました。

これでプレイヤーが敵にぶつかるとノックバックしてダメージを受けるようになりました。

SwordにPoligonColider2Dを追加します。

istrrigerにチェックを入れてコライダーの大きさを剣のサイズに合わせて調整します。

Enemyのタグを作成して設定しておきます。

新しくC#スクリプトを作成します。名前はWeaponにしました。

作成したスクリプトをSwordにアタッチします。

攻撃力の変数を作るattackDamage
衝突判定用の関数を作るOnTriggerEnter2D
 using UnityEngine;

public class Weapon : MonoBehaviour
{
    [SerializeField]
    private int attackDamage;
   

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Enemy")
        {
            collision.gameObject.GetComponent<EnemyControler>().TakeDamage(attackDamage, transform.position);

        }
    }
}

作成する変数Updateに追加するコードの内容作成する関数
EnemyのHPcurrentHealthノックバック関係のコードを書くノックバック用の関数KnockBack
ノックバック中かどうかの判定isKnockingBackダメージ用の関数TakeDamage
吹き飛び時間knockBackTime
吹き飛ぶ力knockBackForce
タイマーknockBackCounter, knockWaitCounter
吹き飛ぶ方向knockDir
  [SerializeField]
    private int currentHealth;
    private bool isKnockingBack;
    [SerializeField]
    private float knockBackTime, knockBackForce;
    private float knockBackCounter, knockWaitCounter;
    private Vector2 knockDir;

 void Update()
    {
        if (isKnockingBack)
        {
            if (knockBackCounter > 0)
            {
                knockBackCounter -= Time.deltaTime;
                rb.velocity = knockDir * knockBackForce;
            }
            else 
            {
                rb.velocity = Vector2.zero;

                isKnockingBack= false;
            
            }
            return;
        }
}

 public void KnockBack(Vector3 position) 
    {
        isKnockingBack= true;
        knockBackCounter = knockBackTime;

        knockDir = transform.position-position;
        knockDir.Normalize();

        enemyAnim.SetBool("Moving", false);

    }
  public void TakeDamage(int damage, Vector3 position) 
    {
        currentHealth-=damage;

        if (currentHealth <= 0)
        {
            Destroy(gameObject);
        }
        KnockBack(position);
    
    }

inspector画面からplayerの攻撃力を設定します。

EnemyのcurrentHealth、knockBackTime、knockBackForceはこのように設定しました。

おすすめのアセット

2Dアクションゲームを作成したい方に向けて、TopDown Engineを紹介します。このエンジンは2Dおよび3Dのトップダウンゲームを効率的に構築できるアセットです。キャラクターコントローラー、インベントリーシステム、デモレベル、視覚的アセットなどが含まれており、初心者でも簡単にゲーム開発を始めることができます。例えば、「The Binding of Isaac」や古い「ゼルダ」シリーズのようなゲームを簡単に作成することができます。Unityでゲーム開発を始めたい方には最適なアセットです。