Unity
Unity6.2で環境を作って、HelloWorldした
https://scrapbox.io/files/68ef3e49b0403ad88845fcfd.png
動きそう
学習用のメモ
2D
3D
Unityの各用語
UpdateとTime.deltaTime
Time.deltaTime
直前フレームと今のフレーム間で経過した秒数を返す。下記コードのように移動をスムーズに表現するために使うが、実際は何をしているのか?
code:c#
private Vector3 _velocity = new Vector3(1, 0, 0);
void Update()
transform.position = transform.position + (_velocity * Time.deltaTime);
}
今回はUpdate()を例とした。Update()は1フレームごとに呼ばれる(30fpsなら30回)。距離 = 速度 * 時間。上の式は、キャラクターの1フレームで進む距離はどの程度になるかということで、現在位置 + 速度(_velocity) * 時間(Time.deltaTime)という形で求めている。
deltaTimeを使わずUpdate()で移動処理を実現したとする。速度が5としてUpdate()は1フレームごとに呼ばれるので、
60fps: 1秒で60回呼ばれ、5 * 60 = 300進む
30fps: 1秒で30回呼ばれ、5 * 30 = 150進む
と、描画フレームの違いで処理にも違いが出てくる。
deltaTimeは、フレーム間の時間を数として返してくれるため齟齬を防止できる。進んだ距離を、ベースで設定した速度と時間を掛け合わせて、求めるために使っているだけ。
code:txt
// positionm = positionm + (velocitym/s * times) // m = m + m ←両辺で単位が同じになった FixedUpdate
固定の時間ステップで実行する。物理演算に使う。下記のコードがあったとして、
code:c#
rb.AddForce(Vector3.forward * 10f); // ForceMode.Force(既定)
これはざっくり、10fの力を加えるという意味のコード。
Update()を用いた場合、フレームごとに10fの力を加えることになる。30fps環境だと30回、120fps環境だと120回。FixedUpdate()を使うと固定のステップで実行されるので、環境に依存しない結果が出る。
? Time.deltaTimeを活用すれば、Update()でもある程度安定した結果が得られるのではないか?
Update()は刻み幅が環境依存するので、フレーム落ちやスパイクが発生した場合、deltaTimeが大きく変わることがある。そこで壁との衝突処理などがあった場合すり抜けるケースが発生する。FixedUpdateは内部で値を持っているので、そういったことを防げる。なので可能な限り物理演算にはFixedUpdateを用いよう。
座標
ワールド座標
transform.positionが返す、ゲーム内の絶対的な位置を表す座標
スクリーン座標
画面のピクセル座標。左下が(0, 0), 右上が画面最大の座標。
UIの設定などでよく使う。
ビューポート座標
画面を0から1の範囲で正規化した座標。
要するに左下が(0, 0), 中央が(0.5, 0.5), 右上が(1, 1)である
つまり、以下のように判定することができる
左に飛び出した: x < 0
右に飛び出した: x > 1
下に飛び出した: y < 0
上に飛び出した: y > 1
座標変換のメソッド
WorldToViewportPointなど、ワールド座標をビューポート座標に変換できる
他の変換メソッドも当然ある。ViewportToWorldPoint, WorldToScreenPoint...
Objectの現在のポジションをビューポートとして、画面外に出たらゲームオーバーにしたい処理を書く場合はこんな感じで書ける。
code:c#
Vector3 v = Camera.WorldToViewportPoint(transform.position);
if (v.y < 0f)
hasGameOvered = true;
RectTransformとTransform
UIはRectTransformを持ち、ObjectはTransformを持つ。
Transform
position(ワールド座標), rotation, scaleなど、空間位置を粟原素。
RectTransform
Transformを継承したものだが、UI専用の情報が付与されている。
sizeDelta
UIの幅と高さ。文字だけなら、最初は文字サイズのAuto Sizeに任せても良い。
pivot
中心位置
...
UIの位置をtransform.positionで動かすとCanvas上の位置関係が崩れるので非推奨。基本的には、RectTransform.ancoredPositionやRectTransfrom用のライブラリで動かす。
UIの基準点について
アンカー + anchoredPositionを元に座標が決定する
アンカーがどの位置を基準とするか決定するののが、十字の照準みたいなマーク
こうなら↓、xは0, yはマイナスに下がっている(アンカーが原点。それより下なので)
https://scrapbox.io/files/69284c1c409ac14e55c3bb4f.png
こうなら↓、x軸を+, y軸がマイナスの当たりで設定されていると読める
https://scrapbox.io/files/69284c5a495a3a9d833e05b0.png
Anchor Min / Max
UIの基準となる点と、範囲。親の中央が原点
たとえばmin(0,0), max(1,1)とすると、勝手にストレッチして全画面に被せるようなエフェクトにできる
アンカーの種類
単点アンカー(UIを固定位置に置きたいとき)
ストレッチアンカー(親サイズが変化しても伸びてくれるので、いろんなプラットフォームで出すときに使う)
フルストレッチ
Animator
基本用語
Clip
指定のオブジェクトを時間によってどう変化させるかのデータ(.anim)
Controller
複数のClipを、どのタイミングで切り替えるかのStateMachine
コンポーネント
GameObjectに付与させる、Controllerを再生させるためのパーツ
カメラ(2D)
2DのカメラはZ座標が-10でデフォルトで設定されていることが多い。
code:txt
カメラ(Z=-10)
|
| 距離: 10
↓
プレイヤー(Z=0)
このような形だが、2DのカメラはOrthograpihc設定になっているので、平面的に切り取られている
ViewportToWorldPointで定義するとき
code:c#
float zDistance = transform.position.z - cam.transform.position.z;
Vector3 leftBottom = cam.ViewportToWorldPoint(new Vector3(0f, 0f, zDistance));
Vector3 rightTop = cam.ViewportToWorldPoint(new Vector3(1f, 1f, zDistance));
例えばこれは、画面の左下と右上の座標を取っている
float zDistance = transform.position.z - cam.transform.position.z;
transformをPlayer z=0, カメラ z = -10とする。
cam.ViewportToWorldPointの第三引数には、カメラから見てz方向にいくつ離れているのかを指定する必要がある。
今回の場合、 0 - (-10)で、10離れているということを渡している
⭐つまりPlayerをz=5の場所に置きたいのであれば、この計算式を流用してはいけない。ちゃんと15という値を静的クラスなどにまとめて、渡してやる。
Sorting Layerの注意点
Sorting LayerはSprite Renderer用のソート設定のため、UIなどの制御には使えない。
具体的には、Screen, Overlay, Space, Canvas配下のUIには影響しない。
(Canvasの場合)UIは、基本的に子階層が上のものほど奥に、下のものほど手前に表示される。
なのでシンプルに優先度順に並べてしまえば解決する。
UIが増えてきたら、キャンバスを分けて、Sort Orderという別値を調整する。
code:イメージ
UIRoot
├─ Canvas_BaseHUD (Sort Order: 0)
├─ Canvas_StageCall (Sort Order: 1)
└─ Canvas_Pause (Sort Order: 2) ← 一番上
キー入力を取る
NewInputActionの場合、割り当てたキーバインドを取ることができる。
そちらを使うことで、操作説明画面などでハードコーディングしたテキストを柔軟に変えられる
code:c#
// イメージ
...
string binding = attackAction.action.GetBindingDisplayString();
⭐複数の割り当てがある場合
https://scrapbox.io/files/692aabd6d8c7c91771988437.png
たとえばAttackなら、attackAction.action.GetBindingDisplayString()で取得できる。ただ、Movementは1つの設定にWASD, 上下左右キーが割り当てられている(複合入力: コンポジットComposite)。
code:c#
var bindings = moveAction.bindings;
中身は、こんな感じ
code:txt
0 Movement:2DVector(mode=1) 6 Movement:2DVector(mode=1) なので、以下のチェックが必要
まずコンポジットが始まったかどうかをチェック
indexを確認して、適宜処理
ちなみにindex=[1]の入力が欲しいなら、こんな感じでindexを指定することで取れる。
code:c#
Debug.Log(
moveAction.GetBindingDisplayString(1) // W
);
StateMachine
プレイヤーや敵は、状況によって異なる挙動を取る。
if分岐だけでは膨大な分岐となり、コードが読めなくなってしまうため、それを防ぐ役割を持つ。
状況ごとに責務を分け、1つの状態で今どう動くべきかだけを担当させる。
基本的な決め事
一度にアクティブにできる状態は1つだけ
各状態には独特の振る舞いがあり、状態を出た後度の状態に映るかのルールを正しく決める
例えばAttackStateは、敵を検知してダメージを与える。アニメが終わればIdleに戻る。
手順
⭐EntityStateの設定
まずEntityState.csの用意。MonoBehaviourは不要。
このクラスは、状態全ての親クラスとなる。
protected StateMachine stateMachineとして、子クラスから遷移できるようにする。
定義した変数に、作成時引数で渡したstateMachineを代入させるようにする(コンストラクタを使う)。
code:c#
public EntityState(StateMachine stateMachine, string stateName)
{
this.stateMachine = stateMachine;
this.statename = statename;
}
状態の入口処理Enter()と出口処理Exit()を定義。ExitはState終了時と、新しいStateを呼ぶ際の処理を書く。
次に、状態中にすることを書くUpdate()を定義。
EntityStateはMonoBehaviourではないのでこれは定時実行されない。なので、Player側などで定時実行する。
code:Player.cs
// イメージ
private void Update() {
stateMachine.currentState.Update(); // 現在のstateのUpdate()処理をここで回す
⭐StateMachineの設定
StateMachine.csの用意。MonoBehaviourは不要。
このクラスの役割は、現在のアクティブな状態への参照を持つこと。しかし勝手に書き換えられては困るので、public EntityState currentState { get; private set; }として所持すればよい。
currentStateにはゲーム開始時のstate設定メソッドと、状態変更時のメソッドを持たせる。
その時に先ほど用意した、入口処理と出口処理を実行しておく。
code:StateMachine.cs
// ゲーム開始時など、初期stateを割り当てるためのメソッド
public void Initialize(EntityState startState)
{
currentState = startState;
currentState.Enter(); // 入口処理
}
// 現状態を変更するためのメソッド
public void ChangeState(EntityState newState)
{
currentState.Exit(); // 出口処理
currentState = newState;
currentState.Enter(); // 新しいStateの入口処理
}
⭐Playerなどから使う
自身でStateMachineを持つ。他のコードでもplayer.stateMachine.currentStateを参照することで、プレイヤーが現在何の情報かを確認することができる。ただし、むやみに変更されても困るので、同じようにpublic StateMachine stateMachine { get; private set; }としておく。
PlayerなどはMonoBehaviourを所持しているので、
Awake()でnew StateMachine()とし、初期Stateを持たせる。
Start()でstateMachine.Initialize(idleState)などとし、初期設定 + 入口処理
Update()でstateMachine.currentState.Update()という流れになる。
code:Player.cs
private void Awake()
{
stateMachine = new StateMachine();
idleState = new PlayerIdleState(stateMachine, "Idle");
}
private void Start()
{
stateMachine.Initialize(idleState); // 初期状態の設定 + 入口処理
}
private void Update()
{
stateMachine.currentState.Update(); // 状態中の処理
}
⭐PlayerIdleStateなど、子要素を作る
EntityStateは単体では使わない、抽象クラスで基本定義する。
そちらを親として子クラスでStateを定義。コンストラクタの対応を行っていく。
Enter()等の入口処理を調整したい場合は都度overrideしていけばよい。
新しい状態を作るときの流れ(たとえば、IdleにMoveを追加する)
Stateのスクリプトを用意。コンストラクタ等設定。
Player側で、新しくそのスクリプトを持てるようにする。
code:Player.cs
private PlayerMoveState moveState;
// Awake
moveState = new PlayerMoveState(stateMachine, "move");
Moveはプレイヤーの入力を受け付け、移動する。つまり状態中の処理Update()で書くようにする。
プレイヤーの入力はPlayerが持っているが、現状まだ情報を所持していない。
EntityStateに取得処理を書くと、別Object(たとえばEnemy)にもEntityStateを割り当てたとき、Playerの情報が不要になる。なので、PlayerStateという要素をさらに作ってしまうのがよい。
code:txt
EntityState
- PlayerState
- PlayerIdleState
- PlayerMoveState
まず、EntityStateに敵味方共通の変数を定義する。rb, co, animなど。
次に、PlayerStateにはプレイヤーを操作するためのplayerを持たせるようにする。そのとき、rb, co, animをplayerが所持していたものをコンストラクタで割り当てる。今後EnemyStateを作るならenemyを持たせて、rb,co,animにenemyが所持しているものを割り当てればよい。
PlayerMoveStateなどでは、Update()中にrbを操作することで移動を実現させる。
NewInputSystemと、EventSystemへの紐づけ
Input Actionsは、キーやマウスの入力を意味のある操作に変換するための定義ファイル。
Action Mapを分けることで、UIを開いている途中はこの操作、通常プレイ中はこの操作、と分割できる。
InputSystemUIInputModuleは、EventSystemにくっついているcomponent。「決定」や「キャンセル」等の処理を、どのInput Setの割当で実行するのかを決める。
UI/NavigateというAction Mapを、EventSystemでmoveとして割り当てる
EventSystemは、今Selectedされている要素が何かという情報を持っている
https://scrapbox.io/files/6942753c6e9685e2d6561b1e.png
画像のケースなら、QuitというButtonのNavigation(Explicit)を見る。
Downに割り当てた項目に、Current Selectionを移す。
この辺をスクリプトで監視しておけば、コード上でやりたいことが実現できる
code:UITitleMenu.cs
private void Update()
{
var es = EventSystem.current;
if (es == null)
return;
// 例えば、この辺りでオブジェクトが変わったかを検知させる
var current = es.currentSelectedGameObject;
if (current == null || current == lastSelectedObject)
return;
// オブジェクトが変化し、ここまで来た場合は特定条件で音を鳴らすとか。
if (!suppressNextSelectSfx)
AudioManager.Instance?.PlayUI(selectClip);
...
ボタンが押されたとか、セレクト状態が解除されたとかも、インターフェースで管理できる。public void OnSelect(BaseEventData eventData) とか。
===============
基礎回り
基本概念
GameObjectとComponent
Objectが入れものとして存在し、Componentを付与して機能拡張を進める。RendererやColliderなどで振る舞いを構成していく。
Transform
位置、回転、拡縮を持つ。
親子関係があるが、子の変換は親の変換を合成した結果にもなる。
インスペクタの値はローカルの値を示す。ワールド座標は親を含めた最終結果となる。
Prefab
同じ構成を複数生成したいときに使う。
共通のPrefabをもとに、差分だけを派生として管理したいときにPrefab Variantを使うと良い。
共通修正が親のPrefabを修正するだけで済む。
SerializeField
privateのままInspector上に表示できて書き換えられる。未設定や参照切れ、依存関係が見えにくくなるのでコメントなどで補完する。
MonoBehavior
LateUpdate
Update後に呼ばれる。カメラ追従などの処理を触るときによく使う。
イベント購読と解除
OnEnableとOnDisableで管理するのが基本。
Coroutine
一定時間待つ、段階的に進めるなどの時間制御に便利。
Updateでも毎フレーム自前で状態管理することができるが、基本こちらで管理したほうがよい。
ただし、停止時要件や破棄されたときにOnDisableなどで止めるように意識しないと不具合に繋がる。
スクリプト間の初期化依存を減らすためには
Awakeで参照を確定させる
OnEnableで購読を済ませる
Startで初期状態を反映するという流れが好ましい。
2D: 物理 /当たり判定
RBのBodyType
Dynamic: 物理で動く対象に使う。Kinematicはスクリプト手動で動かしたい対象(移動床など)に適する。
Staticは動かない地形や壁に。一番処理としては軽い。
Collider2DとisTrigger, OnCollisionとOnTrigger
IsTriggerがfalseの場合、OnCollision系統が呼ばれる。
壁や床
IsTriggerがTrueの場合、物理反発が起きない。また、OnTrigger系が呼ばれる。
アイテム取得とかダメージ判定はこっち。
LayerとSorting Layer
Layerは物理、Raycastなどの判定に使う。
Sorting LayerはSprite描画順の制御で、判定とは関係ない。
Continuousについて
弾丸など高速で移動する物体が、コライダーを突き抜ける不具合を補うときに使う。
この挙動はトンネリングと呼ぶ
めりこみ、すり抜け対策
Continuousを使う
Raycastで対策。
移動処理に物理を使い、接地判定と速度補正をかけるのもよい。
移動系
MovePosition
衝突を考慮しつつ位置を移せる。Kinematicなど移動床はこれを使うことが多い。
AddForce
力で動かす、慣性や加速を重視するときに使う
velocity
速度を直接指定する。
プレイヤー移動とか操作感を反映しやすいが、物理感はない。
入力
旧Input
Input.GetKeyなど
入力デバイスの追加やリバインドが難しい。
New Input Systemはこの辺りを抽象化で着て、デバイス差やUI連携が設計しやすい。
Input Actionの構成
Action: ジャンプや攻撃など、意図。
Binding: キーやボタンの割当。
Control Scheme: どのデバイスセットか(マウス, gamepad, キーボードなど)
キーコンフィグ
対話的にBindingを差し替えて、結果をjsonde保存して復元する形になる。作らないと詳細に理解できないかも。
UI時とプレイ時の操作切り替え
Action Mapを分けておけば、メニューを表示するときに片方をDisable, Enableとすれば入力競合を防げる。
UpdateとFixedUpdate
入力をUpdateで取得し、RBを伴う物理反映はFixedUpdateでやるのが安全。
UI周り
CanvasのScreen Space
Overlay: 画面最前面に直接描画される。
Camera: UIを特定のカメラで描画する。
World: 看板のUIなど、ゲーム世界のUIはこれを使おう。
EventSystemの役割
選択中のUI要素の管理や入力のルーティング、イベント発火を担当する。
ちなみにNew Input Systemは、UI Input ModleがActionを受け取り、ES経由でボタンなどにイベントを流す。
ボタンナビゲーションの管理
ボタンごとに、どの方向キーでどこに移動するかを決める。Explicitとして、初期選択等を設定していこう。
解像度対応で意識すること
低解像度でつけるとUIが大きくなったりする。Scale With Screen Size設定などで防止できるので、こまめにチェック。
TextMeshPro
文字の見た目やアウトラインが安定している。描画品質も高い。
フォントを差し替えた時文字がかけたり、WebGLでは重かったりするので覚えておこう。
Animation周り
Animation Controllerについて
State: 再生するアニメのこと
Transition: 状態遷移の条件。
Parameter: 遷移判断に使う入力。
スクリプト側はParameterだけを操作するようにすると、上手く責務が分離できる。
BlendTreeの使いどころ
速度に応じてIdle, Walk, Runを使い分けるなど、連続した値でモーションを切り替えるとき。
Animation Eventトリガーについて
特定のフレームでSEを出したり攻撃判定を出せる。
イベント名の文字列に依存したり、参照がスクリプトから見えづらい。変更時に検知しづらい。
Sprite Rendererのレイヤー設計
Sorting Layerで大枠の描画グループを分けて、Order in Layerで、グループ内の前後関係を決めよう。
パララックス背景とかなら、BackGroundの前、中、後をきめたり。
データ設計
ScriptableObject
データ定義をアセットとして分離する
skill構造とか敵情報、ドロップテーブルを管理するのが一般的。
セーブロード
jsonで管理するのが一般的。作るとわかるかも。
スクリプトとかでも一時データは持たせられるが大変。
パフォーマンス
Instantiate / Destroy
コストが高いので、ガベージコレクションやフレーム落ちの原因に繋がる。ObjectPoolを使おう。
GC Allocを抑えるためには?
Updateで走らせる不要な割り当てを避ける。
文字列の連結とか、そういうのは必要なときにフラグで動かす。
Unity Profiler
どの関数が重いのか、どこで割り当てが出ているかを調査できるパフォーマンス計測ツール。
アセット周り
Addressables
アセットの管理や配信を効率化するシステム。企業ならDLC配信とかに使う。
ロード画面について
ロード開始時に入力を止めて、プログレスバーを出す。完了後に遷移。
Coroutineやasyncを使って完了を待ち、フェード演出などで遷移時のちらつきを回避すると綺麗に仕上がる。
ビルドについて
Development Build
デバッグしやすいビルド。ログなどが見れるが、リリースするものと挙動差が出るケースがある。
Deep Profilingは詳細な計測ができるがかなり重い。特定箇所について調査するために使うといい。
ビルドタイプで起こりがちな内容
性能差はもちろん、入力など。WebGLは結構制約が大きかったり、メモリ不足が懸念になることが多い。
バグ再現が難しいときの切り分け方
再現条件を出来るだけ固定して、ログを出す。
フレームレート制限や負荷をかけて再現率を上げる。
入力や乱数のシードを固定するなど、少しずつ進めていこう。
3D周り
Quaternion
回転を安定して表現できる。Slerpによる補間処理もかなり自然に動くので、回転表現は基本的にこれ。
RaycastとCamera
画面上でクリックした箇所を取りたいとする。その位置はスクリーン座標なのでCamera.ScreenPointToRayなどでワールドへのRayに変換することで、マウスの位置へのオブジェクトを取ったりできる。
Transform.forward
そのObjectの前方向ベクトル。移動や射撃は方向ベクトルで扱おう。
その他
UnityWebRequest
URLにつないで、API通信などができる。ランキングとかもWebで出せちゃうかも。
できるっぽい。Unityで投げてLaravelで受け取ってDBに保管するイメージ。
todo
よく使う設計を自分なりにまとめる
クールタイム
攻撃被弾時の光る処理とか
dotween
===============
遭遇エラーのまとめ
アニメーションのループが終わらない
アニメーションにイベントを付与し忘れていないか
https://scrapbox.io/files/68f83f8b420e13ebe0651c66.png
ループタイムのフラグは適切だろうか
https://scrapbox.io/files/68f83fd1d228159e98bb0354.png
タイルマップを敷いた時だけ挙動がおかしい
コライダーのマージが悪さをしている可能性がある。
Section5のClean up & Projectを参考にしてみる
Sprite EditorのSliceが押せず、ドット絵の調整ができない
https://scrapbox.io/files/68fdd7f9f7836a8122f50946.png
Sprite Mode がMultipleでないと押せない。
Skeletonに背中からダメージを与えても振り向かない
振り向き処理のスクリプトを割り当てていなかった
Entity_Healthを、Enemy_Healthに置き換えたらいけた
エディタ系のエラー
code:txt
NullReferenceException: Object reference not set to an instance of an object
UnityEditor.Graphs.Edge.WakeUp
単純に、Unity自体を再起動すると治ることが多い
NullReferenceExceptionのエラー
code:error
NullReferenceException: Object reference not set to an instance of an object
UI_SkillToopTip.ShowToolTip (System.Boolean show, UnityEngine.RectTransform targetRect, UI_TreeNode node) (at Assets/Scripts/UI/UI_SkillToopTip.cs:53)
UI_TreeNode.OnPointerEnter (UnityEngine.EventSystems.PointerEventData eventData) (at Assets/Scripts/UI/UI_TreeNode.cs:101)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerEnterHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at ./Library/PackageCache/com.unity.ugui@27299721f2fd/Runtime/UGUI/EventSystem/ExecuteEvents.cs:29)
UnityEngine.EventSystems.ExecuteEvents.ExecuteT (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1T1 functor) (at ./Library/PackageCache/com.unity.ugui@27299721f2fd/Runtime/UGUI/EventSystem/ExecuteEvents.cs:272) UnityEngine.EventSystems.EventSystem:Update() (at ./Library/PackageCache/com.unity.ugui@27299721f2fd/Runtime/UGUI/EventSystem/EventSystem.cs:530)
変数名を変えた後に出るケースがほとんど
今回のケースだと、
code:c#
SerializeField private TextMeshProUGUI skillRequirements; // <- typoがあったので直した というケースで、修正後コンパイルすると、以下のように割り当てが消えている
なので、自分で紐づけるデータを再度設定しなければならないので注意。
https://scrapbox.io/files/69105482231676dfe0e9cfc9.png
配置したボタンの反応がない
SortingLayerが背景などより低い可能性が高い。