The Ultimate Guide to Creating an RPG Game in Unity 6
まずは
Git管理しよう
やったこと
Unity Hubでプロジェクトを作る
Sourcetreeを開発端末に入れる
GitHubの鍵を持ってきて設定
GitHubでリポジトリを作る
.gitignoreはUnityという用意されたプロファイルがあるのでそれにする
作ってローカルに持ってくる
その空っぽのファイルに、Unity Hubで作ったプロジェクトの中のファイルを入れる
Assets Packagesなどいろいろ
push
helloworldのコードをpushしてみたけど、まあ多分行けてそう
Section2
ツール画面
Hierarchy
ゲームのオブジェクトのことを指す
Scene
画面の開発者モード的なもの
Game
実際に画面として映るもの
Inspector
オブジェクトをクローズアップして、その設定をする項目
Project
これまでに追加したすべてのリソースが表示される
Sceneのホットキー
Horizontalの場合、上からq,w,e,r,t,yで選択していくことができる
ハンド q
画面全体を動かして確認する機能。マウスホイールでもできる
移動 w
オブジェクトの移動ができる
回転 e
オブジェクトの回転。
スケール r
オブジェクトのサイズ変更だが、Inspectorで調整したほうが確実ではある。
レクト t
オブジェクトを引き延ばす。
UIを作ったりステージの土台を作ったりできる。
トランスフォーム y
ツールのいろいろな機能を詰め合わせたツール。w,e,r,tで調整するのが基本。
コンポーネント
オブジェクトの振る舞いを設定するための要素。
可視化とか、色を変えたりとか。
Rigidbody
物理的な動きをさせるようにする。ctrl + P でプレイモードを実行して体験できる
Constraints
物理的な動きを固定できる
例えばステージ(落ちてはいけない)なら、x,yを固定するようにしてやると空中に留まる。
Angular Damping
傾斜減衰。回転させる場合、その抵抗力。
Linear Damping
空気抵抗。オブジェクトの動きの制御。
Mass
質量。重ければ重いなりの動きになる。回りにくかったりとか。
Collider
いわゆる、当たり判定の定義
C#の話
クラス、ライフサイクルなど
Section3
複雑な設計に耐えられるような土台にする
Finite State Machine(FSM)
オブジェクトの動作を様々な状態に分割し、状態から状態へ移動する方法を定義することで、オブジェクトの動きを制御することができる。
テレビを例にしてみる
state: 電源OFF
入力を待つ
エネルギー消費を抑える
state: 電源ON
チャンネルの切り替えを待つ
テレビ番組を見ることができる
キャラクターを例にする
idle
入力を待つ
jump
ジャンプボタンが押されたとき
地面のオブジェクトに触れるとidleに戻る
attack
アタックボタンが押されたとき
アタックモーションが終わるとidleに戻る
実際に用意した構造を見てみる
EntityState.cs
今後用意する各stateの親クラスとして使う。すべての状態クラスの土台。
statemachineとして使うので、MonoBehaviorの継承を外す
Enter()
状態に入った時に呼んでもらう。
Update()
毎フレーム呼んでもらう。
MonoBehaviorを継承していないので、これが直接毎フレーム呼ばれるわけではない
↓なので、MonoBehaviorを継承しているPlayer側のUpdate()で対応している
code:Player.cs
private void Update()
{
stateMachine.currentState.Update();
}
Exit()
状態を変更するときに呼んでもらう。
StateMachine.cs
EntityStateを変数として持つ。(現状、currentStateという名前で)
Initialize (EntityState startState)
currentStateを、引数で渡したEntityStateに更新する
EntityState.Enter()を実行
ChangeState(EntityState newState)
EntityState.Exit()を実行し、変更前の準備をする
currentStateを、引数で渡したEntityStateに更新
EntityState.Enter()を実行
Player.cs
StateMachineと、それぞれの状態であるEntityState系統のものをクラス変数として持つ
今回はとりあえずということで、private EntityState idleStateという形で所持
Awake()
stateMachine = new StateMachine();で新しいインスタンスを作る
StateMachineのメソッドを呼べるようになる
idleState = new EntityState(stateMachine, "Idle State");で新しいインスタンスを作る
EntityStateのコンストラクタが走り、それぞれ引数の値が格納される
code:EntityState.cs
public class EntityState
{
protected StateMachine stateMachine;
protected string stateName;
public EntityState(StateMachine stateMachine, string stateName)
{
this.stateMachine = stateMachine;
this.stateName = stateName;
}
Start()
stateMachine.Initialize(idleState);
ゲーム開始時、idleStateという状態で初期化する
StateMachine.currentStateはidleState状態になる
そのまま、EntityState.Enter()も実行される。
Update()
stateMachine.currentState.Update();
EntityStateクラスのUpdate()が毎フレーム実行されていく。
Entity Stateの役割
各状態に必要な要素を親クラスとして持たせる。
たとえばRigidbodyやAnimatorやタイマーなど。
EntityStateクラスを直接使うことはないので、abstractの定義をする
このクラスは設計図に過ぎず、直接使うことはないということ
それぞれStateを定義したい場合は、新しく暮らすファイルを用意していく
Input Actionsを使った設定
Assetsに、Input Actionsを選択して作る
アクションマップ
Playerデフォルトの動きか、それとも専用のコンフィグを作るのかなどを定義できる
アクション
移動したりするならここで定義する。
キーバインドを割り当てられる
スクリプトの作成
このアクションに沿った、csスクリプトをUnityが生成してくれる
呼び出しなど
code:Player.cs
private PlayerInputSet input;
public Vector2 moveInput { get; private set; }
private void Awake()
{
input = new PlayerInputSet();
// ...
}
/// <summary>
/// スクリプトのライフサイクルの1つ。Awakeの後に実行される
/// </summary>
private void OnEnable()
{
input.Enable();
//input.Player.Movement.started - input just begins (おしはじめ)
//input.Player.Movement.performed - input is performed (ホールド) 移動など。
//input.Player.Movement.canceled - input stops (キーを離す)
input.Player.Movement.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
input.Player.Movement.canceled += ctx => moveInput = Vector2.zero;
}
code:Player_IdleState.cs
public override void Update()
{
base.Update();
if (player.moveInput.x != 0)
stateMachine.ChangeState(player.moveState);
}
PlayerInputSet
入力アクションのまとめ。Unityで定義したものがクラスとして格納されている
started, performed, canceledなどを呼べばその処理をこなしてくれる
input.Player.Movement.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
Movementという入力がperformされたとき、ctxからVector2の入力値を読み取り、moveInputに代入している
ctx.ReadValue<Vector2>()
WASDを押した数字に対するx, yの値を返している
moveInputはVector型なので、(0, 1) とか、(-1, 0)とかそういった座標の値を格納できる
input.Player.Movement.canceled += ctx => moveInput = Vector2.zero;
同じ。入力がおわったら、(0, 0)の座標に戻すというだけ。
補足: ラムダ式
code:c#
// こちらと同じ処理をしている
input.Player.Movement.performed += OnMovementPerformed;
private void OnMovementPerformed(InputAction.CallbackContext ctx)
{
moveInput = ctx.ReadValue<Vector2>();
}
アニメーションをつける
Animatorというゲームオブジェクトを用意
Sprite Renderer
オブジェクトにビジュアルを与えるためのコンポーネント
Playerにつける必要があるが、Player自体には付与せず、子要素としてAnimatorというゲームオブジェクトを今回用意したのでそちらにつける
親ではロジック、子ではビジュアルと2つの異なるゲームオブジェクトを持てる
ピボットポイントを調整し、反転したときにうまい具合になるようにする
Animator
それぞれ、Controllerが必要
Controllerを作り、アニメーションウィンドウへ
リジットボディ
Body Type
Dynamic or Kinematic or Static. 使いたい物理演算によって使い分ける
Material
摩擦や空気抵抗を考慮できるようになる
ジャンプ処理
state系クラスに、ジャンプボタンが押されたらという処理を追加する
EntityStateに入れるのではなく、idle, moveなど特定の子クラスにしか必要がない
では、今後もそのようなものが増えてきたら?
スキル, 攻撃, etc...
そのあたりも管理できるように、1段階上のスーパーステートのクラスを用意する
idle, moveを包括するようなクラス。
EntityState
- IdleState
- MoveState
EntityState
GroundedState ←新規用意
IdleState
MoveState
コンボ処理
1 > 2 > 3とコンボができる。ただし、2 が終わった後に別の行動を行い、その後攻撃しても3となる
なので、その間に一定の秒数が経過した場合、インデックスをリセットさせてやればよい
code:example.cs
private void Update()
{
inGameTime = Time.time;
if (inGameTime > lastTimeAttacked + comboResetTime)
Debug.Log("I can reset counter");
else
Debug.Log("I cannnot reset counter.");
}
inGameTimeに、常にゲーム中の時間を記録させる
攻撃したとき、lastTimeAttackedにTime.timeを記録
その時間とコンボリセット時間を定義し、それが過ぎたらインデックスをリセットしたりすればよい
なので
BasicAttackState.Exit()にlastTimeAttackedを仕込む
BasicAttackState.Update()で判定処理を仕込んで、インデックスをリセットさせる
この判定処理はリセットインデックスメソッドの部分に追加で仕込める。
アタックキュー
コンボ途中にidlestateが挟まり、間が開く
basicAttackStateのUpdate()中でキューを作ってやればよい
コルーチンを使う
Section5
タイルセット
ドット関連のイラストを使うときは、圧縮の削除などを忘れないこと。
タイルのコライダーはmergeして扱う
cinemachine
指定したオブジェクトを追従するカメラ
Main Cameraのオブジェクトの内容をオーバーライドして動く仕組みなので、すべてcinemachineで調整することになる
カメラの移動範囲はコライダーで調整できる。調整するときに見直してみよう
パララックスparallax
視差のこと。背景を異なる速度で動かして遠近感を出す
背景を繰り返しにする
背景の画像の幅などはスクリプトから取得ができる
カメラの特定の幅などにいきついたとき、画像自体を移動させてしまえばよいという考え方
カメラの右端と左端を計算する
デモレベルの作成
うまく背景外部分を隠す
https://scrapbox.io/files/68fc3a31ef8add5f5aa3af1c.png
空白が目立つので、工夫して見せないようにすることが大事
とげを設置して即死させるようにしたり、地下室のタイルを張り付けたりetc.
今回、とげをいれた
とげをタイルマップに入れると、全体のピクセルがずれてしまった
これはとげタイルが縦長だったから。正方形のタイルで固定したいので、とげ自体をsprite editorで正方形になるように調整する
他にも見えない壁を置いたり。
地下室にすれば、黒いタイルを敷き詰めても違和感がなくなるわけだ
発想が必要
Section5
Enemyの実装
同じようにStateMachineなどを実装していく
現状は、Player.cs
component, velocity, collision detection, input, flipなどの機能を持っている
新しく、Enemy.csという要素で敵を管理する
これはPlayer側と同じようなものを持つケースがあるだろう(Flip()など)
なので、このクラスの親Entity.csを作っ手管理する
EntityStateについて
これも同じように、この要素を親とし、その後PlayerStateとEnemyStateというクラスに分ける
Skeletonの実装
おさらい
Objectをつくる
スクリプトを割り当てる
Create Objectで、Animator用のオブジェクトをつくる
アニメーターコントローラの作成
ファイル作成側で、Animation -> Animator Controller
ドラッグして割り当てる
add componentsからスプライトレンダラーの割り当て
割り当てたいグラフィックすべてを選択し、調整
compression, filter mode, Pixel per unit, Sprite Mode(singleにする)などなど
スプライトエディターで調整する
調整したスプライトを、作ったAnimatorオブジェクト, sprite rendererに割り当てる
反転しても問題ないように、キャラクタの中心を調整する
親のEnemy_Skeletonが真ん中になり
https://scrapbox.io/files/68fdd901fe8b6c9a10fb0f24.png
子のAnimatorが少しずれるような形になればよい
https://scrapbox.io/files/68fdd8fa3249bca380555958.png
RigidBodyを与える
横に倒れないように、Z軸を固定する
コライダーの設定(Capsule Collider)
レイヤー設定
こんな感じの順番
https://scrapbox.io/files/68fdda8404882cceb1ed7a17.png
攻撃アニメーションにつけるメソッドの役割のおさらい
アニメーショントリガーを呼び、triggerCalledをtrueとする
AttackState中にそのtriggerCalledがtrueになったら(特定のタイミングとなったら)
stateをidleStateに変える。
要するに、1度しか攻撃アクションは呼ばれないようにしている
triggerCalledは、EntityStateがEnterされた時点でfalseに戻るので、また次呼ばれるときはfalseとなっている
デバッグ
code:EnemyState.cs
public class EnemyState : EntityState
{
protected Enemy enemy;
public EnemyState(Enemy enemy, StateMachine stateMachine, string animBoolName) : base(stateMachine, animBoolName)
{
this.enemy = enemy;
rb = enemy.rb;
anim = enemy.anim;
}
public override void Update()
{
base.Update();
if (Input.GetKeyDown(KeyCode.F))
stateMachine.ChangeState(enemy.attackState);
anim.SetFloat("moveAnimSpeedMultiplier", enemy.moveAnimSpeedMultiplier);
}
}
if (Input.GetKeyDown(KeyCode.F))というような表記で指定のstateに変えるようにすれば、気軽に動作チェックできる
playerを攻撃する処理
イメージ
戦闘にかかる時間が終わったら、idleに戻す
敵の攻撃範囲にプレイヤーがいたら、sttackStateにする
いなければ、歩行させる
感知判定を作る(detected)
Gizmoの関数をprivateからprotectedとして
enemyでオーバーライドできるようにして
色を変えたり、感知判定を作ったりして描画は完成
Unity側でPlayerCheckに、スケルトン自体のobjectを割り当てる
今まで
壁や地面判定はboolを返して判定していたが、プレイヤー判定は、プレイヤーとの距離などを獲得したい
なので、Raycastの属性で返そう
enemyとplayerがぶつかる
https://scrapbox.io/files/6904779adac264841807d9a4.png
Unity自体のEdit > Project Setting > Physics 2Dから調整ができる
enemyを感知させ、しばらくしたら追跡を止める
Enemy.csにTime.timeを持たせ、Update()で更新し続ける
敵に感知したとき、別の変数に同じくTime.timeを持たせる
これで感知が外れた時、Time.timeの更新が止まるため、そのタイマーと比較させればよい
Section6
敵への攻撃実装の流れ
攻撃判定を出したいアニメーションで、イベントトリガーを設置する
検知が必要
レイヤー
distance
チェック用オブジェクトのtransformを持たせる
https://scrapbox.io/files/6905723a8845d3ebe1f4496d.png
そもそも、Transformとは
GameObjectが持つ、ワールド空間上の位置を指す
https://scrapbox.io/files/69057cb01637aaef6cbe2156.png
今回の場合、targetCheckという変数に、Hierarchy上につくったTargetCheckオブジェクトのTransformを持たせている
これで、C#側でtargetCheck.positionとかを叩くと空間情報を参照できるようになる。
ダメージシステム
Healthというスクリプトを作る
そこでダメージを計算する処理を書く
用意していたアニメーションのスクリプト部分で、その処理を呼んでやる
code:c#
public void PerformAttack()
{
foreach (var target in GetDetectedColliders())
{
Entity_Health targetHealth = target.GetComponent<Entity_Health>(); // script取得
targetHealth?.TakeDamage(damage); // nullとならず取得できていたら実行
}
Player, Skeletonなどにそのスクリプトを追加する(Add Component)という形になる
敵に攻撃したとき、BattleStateとする
後ろから攻撃し放題なので、調整する
ダメージ効果
VFX : Visual Effect。視覚効果。
ダメージを受けた時にオブジェクトを白くして、攻撃を受けた感を出す
アクションイベント
code:c#
public static event Action OnPlayerDeath;
Action
引数なし、戻り値なしの関数を入れられる型
event Action
外部から関数を登録・解除ができるようになる
OnPlayerDeathの中に、登録した関数が複数入っている
OnPlayerDeath?.Invoke()
登録されている関数があれば、全てまとめて実行させるという意味合いとなる
関数の登録
Player.OnPlayerDeath += HandlePlayerDeath;
Player.OnPlayerDeath -= HandlePlayerDeath;
こんな感じで登録・解除ができる
今回、OnEnable()に関数登録, OnDisable()に関数解除を行っている
要するに敵が起動したら、呼び出す関数を登録
敵が倒されたら、関数の登録を解除としているわけだ
? 現状まだdisable()は起動しない. dead時にSetActive(false)などの処理を書いていないので。
インターフェースを使った実装
code:IDamagable.cs
using UnityEngine;
public interface IDamagable
{
public void TakeDamage(float damage, Transform damageDealer);
}
基本的にIをつける
つかう
code:Chest.cs
public class Chest : MonoBehaviour, IDamagable
{
}
このままだとエラーになる
インターフェースをアタッチした場合、そのインターフェースの機能を実装しなければならないという制約がつく
どう良くなるのか?
この場合、「ダメージを受けられる」という共通の約束を使えるようになる
もう少し詳細に
ダメージを受けられるものがあったとする
Player, Enemy, Chest
public void PerformAttack()で、攻撃を与える処理を書くとする
対象がPlayerなら, PlayerのTakeDamageを
対象がEnemyなら, EnemyのTakeDamageを
対象がChestなら, ChestのTakeDamageを...となる
インターフェースを使うと
code:c#
foreach (var target in GetDetectedColliders())
{
IDamagable damagable = target.GetComponent<IDamagable>();
damagable?.TakeDamage(damage, transform);
}
こんな感じで、ParformAttack()側で処理を書かず、target側の関数に分岐後の処理をまとめられる
code:txt
従来の if 文構造:
PerformAttack()
if で分岐
├─ Player.TakeDamage()
├─ Enemy.TakeDamage()
└─ Chest.TakeDamage()
インターフェース構造:
PerformAttack()
└─ IDamagable.TakeDamage() ← 共通の入り口だけ渡す
├─ Player.TakeDamage()
├─ Enemy.TakeDamage()
└─ Chest.TakeDamage()
->TakeDamage()の中身は、Player, Enemy, Chest側で決められるということ
カウンター攻撃の実装
これもインターフェースで実装
カウンター攻撃で対抗できる要素に、インターフェースとしてくっつける