ユニティーを使用したアクションRPGの作り方のメモです。ゲーム制作の参考にしていただければ嬉しいです。トグルボタンをクリックすると記事が表示されます。小さくて見ずらい画像はクリックで拡大できます。
素材の準備
お好きなプレイヤーのキャラチップ画像とマップチップを用意します。
私はこちらの素材を使用しました。
用意した素材をUnityにインストールしておきます。
ScriptsやAnimationsなどのよく使うフォルダを作成しました。

プレイヤーを作成する
まずはプレイヤーを作成します。
createemptyを作成します。名前はPlayerにしました。
inspector画面を開いてaddcomponentからrigidbody2Dを追加しておきます。
rigidbody2DのGravityScaleを0にしておきます。

ドラッグ&ドロップするとアニメーションの保存先を指定できるので好きな場所に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);
}
}
これでアニメーションを連動することができました。
フィールドの作成
名前は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を動かして位置を調整します

Playerのlayer作成して設定します。
playerを選択してinspector画面のspriteRendererのsortinglayerをクリックしてaddsortinglayerをクリックします。

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

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

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

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を追加します。

CMvcamのinspector画面の下のほうにextensionsという項目があります。
addextensionからCinemachineConfinerを選択するとスクリプトが追加されます。

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

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

敵の作成
空のオブジェクトを作成します名前はEnemyにしました。
プレイヤーのモーションと同じ手順でEnemyのモーションを作成しました。
Idle | 停止しているときのモーション |
move | 動いているときのモーション |
モーションを作成するとspriteRendererが追加されているので立ち絵を設定します。
Enemyにコンポーネントを付けていきます
追加するコンポーネント | |
BoxColider2D | サイズを調整する |
rigidbody2D | GravityScaleを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関数で実行 |
rb | Rigidbody2Dを格納する | 各コンポーネントの取得 | タイマーを減らしていく |
enemyAnim | Animatorを格納する | タイマーの設定 | 移動先を決めるコードを書く |
moveSpeed | Enemyの移動速度 | エリア内しか動けないようにする | |
waitTime | Enemyの待ち時間 | ||
walkTime | Enemyの動く時間 | ||
waitCountor | タイマー | ||
moveCounter | Enemyの動く方向 | ||
moveDir | Enemyの動く方向 | ||
area | Enemyの行動範囲 |
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に設定してみました。

これでプレイヤーが敵にぶつかるとノックバックしてダメージを受けるようになりました。
新しく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のHP | currentHealth | ノックバック関係のコードを書く | ノックバック用の関数 | 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でゲーム開発を始めたい方には最適なアセットです。