C#
あくまでこの記事では、Unityをベースとした前提で話す。 命名規則
※Unityで使うC#の命名や、オブジェクトの命名など。
C#
Classファイル名
アッパーで書くが、_を挟むこともある。
自分の学習している教材では少なくとも混在している。
Player.cs, Player_IdleState.cs
メソッド名
PascalCaseで書く。
code:c#
public virtual void Enter()
{
...
enter()とはしない。
フィールドとプロパティ
フィールド(メンバ変数)
camelCaseで書く。
player, stateMachine, isFinished
プロパティ
PascalCaseで書く。
IsFinished, Speed, Health
? フィールドとプロパティの違いは?
code:c#
// フィールド
private bool isFinished; // 内部的に使うものは、フィールド
// プロパティ
public bool IsFinished;// 外部に出す想定のものがプロパティ
protected bool IsFinished; // protectedもプロパティ扱い
? 定数は?
定数の役割は、どちらかというとプロパティに近い。なので、PascalCaseで書く。
code:c#
private const int FirstComboIndex = 1;
public const int FirstComboIndex = 1;
⭐FIRST_COMBO_INDEXとは書かない (C++やPHPではこう書くが)
Unity
フォルダの名称
大文字で書けばよい。
Animation, InputSystem
オブジェクト
大文字で書けばよい。
Player, Animator, AttackPoint
アニメーター関連
Controller
Playerと大文字でよい
Clip(.anim)
ローワーキャメル。playerIdle, playerMove
パラメータなど
ローワーキャメル。idle, isGrounded, yVelocity, xVelocity
ドキュメンテーションコメント
code:c#
/// <summary>
/// ゲームオーバー画面からステージを再開します。
/// </summary>
/// <remarks>
/// 初期化処理後、現在のステージ番号を基にシーンをロードします。
/// </remarks>
public void Retry() { ... }
丁寧に書くならこんな感じだが、実務ではあまり書かないっぽい。
1行で収められるなら1行でまとめてしまうのがよい。
C#
変数の定義
code:c#
using UnityEngine;
public class Player : MonoBehaviour
{
public string playerName = "Bob the hero"; // This box (variable) called "playerName" holds the text "Bob".
public int age = 25; // This box (variable) called "age" holds the number 25.
// floatを定義するときは、2.5fのように、fをつける
public float moveSpeed = 2.5f; // This box (variable) called "moveSpeed" holds the decimal number 2.5.
public bool gameOver = true; // This box (variable) called "gameOver" holds the value true.
public Rigidbody2D rb;
private void Start()
{
}
}
クラスなど
code:c#
using UnityEngine;
public class Example : MonoBehaviour
{
private void Awake()
{
Debug.Log("Awake called");
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
Debug.Log("Start called");
}
// Update is called once per frame
// ex: Skill cooldowns, checking for input, etc.
void Update()
{
Debug.Log("Update called");
}
// FixedUpdate is called on a fixed timer, independent of frame rate
// Defalt is 0.02 seconds (50 times per second)
// It can be changed in Edit > Project Settings > Time > Fixed Timestep
private void FixedUpdate()
{
Debug.Log("FixedUpdate called");
}
}
}
Start, UpdateなどはMonoBehaviorに用意されたメソッド
MonoBehaviorは、ゲームの基礎機能が用意されたクラス
実際にこの部分をコメントアウトすると、Hierarchy > Inspectorのコンポーネントとして追加することができない
なので、Unityの要素として紐づける場合は基本的に必要となる
ライフサイクル
Update(), FixedUpdate()の違い
フレーム単位で呼ぶか、時間単位で呼ぶか。
例えば全体の挙動をフレームで設定すると、240fpsの環境と60fpsの環境で違いが出る。
そのため、そのあたりを設定できるゲームなら秒単位、つまりFixedUpdate()で定義するべき。
コンテキストメニュー
code:c#
private void Flip()
{
transform.Rotate(0, 180, 0);
facingRight = !facingRight; // 呼ばれるたび反転させる
}
https://scrapbox.io/files/68f194ea2acd03b5bbcea8d1.png
こんな感じで、関数を呼べるようになる
SerializeField
エディタ上で値の設定などができるようにする
デバッグ時につけて、問題なければ外すこともある
Header
インスペクタでグループに見出しをつけるための装飾。
https://scrapbox.io/files/68f1a77b0c514bd82693f7cf.png
こういうこと
ほかにも[]の付属するものはあるが、インスペクタ上で便利にするための設定であることが多い。
衝突判定
code:c#
private void OnDrawGizmos()
{
Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, -groundCheckDistance));
}
Gizmos
デバッグ用の描画ツール。これで地面への距離などを測る。ゲームには影響しない。
DrawLine
transform.position
キャラの現在位置(transformのインスペクタの値)
new Vector3(0, -groundCheckDistance)
下方向に、groundCheckDistance分だけ進んだベクトル
に対して、直線を書くという意味になる
code:c#
private void HandleCollision()
{
isGrounded = Physics2D.Raycast(transform.position, Vector2.down, groundCheckDistance, whatIsGround);
}
これも同じで、GizmosのようなRaycastを指定の方向に放ち、特定のレイヤーに当たったら感知させるということ
キャラの現在位置から
下向きに
groundCheckDistanceの距離だけ。
whatIsGroundで指定したレイヤーに当たったら。
今回の場合、Unity側で
PlayerのWhat is GroundにGroundのレイヤーを割り当てる(ないので作った)
https://scrapbox.io/files/68f1acf8902b3e254cdca89b.png
GroundというレイヤーをPlatformに割り当てて、何がGroundなのかをはっきりさせる
(Platformに指定のRaycastが当たると、isGroundedがtrueになるようになった
https://scrapbox.io/files/68f1acba8585d06e31834fc0.png
という処理が実現できている。
敵へのダメージ判定
code:player.cs
public void DamageEnemies()
{
Collider2D[] enemyColliders;
enemyColliders = Physics2D.OverlapCircleAll(attackPoint.position, attackRadius, whatIsEnemy);
foreach (Collider2D enemy in enemyColliders)
{
enemy.GetComponent<Enemy>().TakeDamage();
}
}
ダメージを与える関数
これは、Animationに紐づけたスクリプトのPlayerAnimationEvents.csから呼ぶ。
中心座標から, Radius(直径)に、whatIsEnemy(レイヤー)を対象に触れたものを配列に入れている
enemy.GetComponent<Enemy>().TakeDamage();
触れたColliderコンポーネントの、Enemyスクリプトを取得し、TakeDamage()を呼んでいる
code:PlayerAnimationEvents.cs
public class PlayerAnimationEvents : MonoBehaviour
{
private Player player;
private void Awake()
{
player = GetComponentInParent<Player>();
}
public void DamageEnemies() => player.DamageEnemies();
public void DamageEnemies() => player.DamageEnemies();という形で呼び出す
このメソッドを、攻撃アニメーションの攻撃判定を出したいタイミングでイベントとして呼べばよい
https://scrapbox.io/files/68f21b362dea63d02a7c4157.png
という感じ
クールダウンタイムの考え方
1つめ
code:c#
private void Update()
{
timer -= Time.deltaTime;
if(timer < 0 && sr.color != Color.white)
sr.color = Color.white;
}
private void UpdateTimer() => timer = redColorDuration;
public void TakeDamage()
{
Debug.Log(gameObject.name + " took some damage!");
sr.color = Color.red;
timer = redColorDuration;
}
Update()
Time.deltaTime
直前Fと現在のFで経過した時間を返す
なので、フレームが経過するほどtimer変数に時間が蓄積されていくことになる動きになる
今回、timerが0より下で、対象オブジェクトが白でなかったら白くするという処理
UpdateTimer()
コンテクストメニュー用
(Unityの画面でリセットする用のメソッドということ)
呼ばれたとき、timerの値をその変数の値に変える
ようするに、timerがリセットされて、またTime.deltaTimeが減算されていく
TakeDamage()
呼ばれたとき、色を赤に変える
そして、timerの値をその変数の値に変える
つまり
redColorDurationが5だったとき、敵に当たったら5秒間赤色になる
減算されて0秒を切ると白色になるということ
2つめ
code:c#
private void Update()
{
currentTimeInGame = Time.time; // アプリケーションを起動してからの現在のフレームを秒単位で出す
if (currentTimeInGame > lastTimeWasDamaged + redColorDuration)
{
if (sr.color != Color.white)
sr.color = Color.white;
}
}
public void TakeDamage()
{
sr.color = Color.red;
lastTimeWasDamaged = Time.time;
}
同じ考え方
クールダウン3秒のスキルがあったとしたら、
現在は8秒 > 6秒時点でダメージ + クールダウン3秒
この場合は9秒時点までクールダウンが必要なので、まだ動作させないというif文になる
コルーチン(Coroutine)
処理を途中で一時停止敷いて、時間をおいて続きから再開できる。
ゲーム動作を止めずに重い処理をすることもできる(1万回のループを少し区切って、ちょっとずつ処理していくなど)。
code:c#
using System.Collections;
using UnityEngine;
public class Test : MonoBehaviour
{
private Coroutine sayMessageCo;
private IEnumerator SayMessageCo()
{
Debug.Log("Hello world.");
yield return new WaitForSeconds(2);
Debug.Log("Hey. Are you still there?");
yield return new WaitForSeconds(3);
Debug.Log("HEY I M TALIKNG TO YOU");
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.F))
{
if (sayMessageCo != null)
StopCoroutine(sayMessageCo);
sayMessageCo = StartCoroutine(SayMessageCo());
}
}
}
この処理の場合、
Fを押すと、コルーチン関数sayMessageCoが始まる
すでに押されていたら、ストップしたのち実行
そうでなければ、ただ実行(StartCoroutine
特徴
変数に格納できる
private Coroutine sayMessageCo
定義するとき
private IEnumerator SayMessageCo()
呼び出すとき
通常のメソッドのようには呼べない。
StartCoroutine(SayMessageCo())というように呼び出す。
⭐MonoBehaviorから継承する必要があるので、それ以外のクラスからは呼べないので注意。
OOPの話
オブジェクト指向Object Oriented Programming
PHPなどにも通ずる話なので、気になったとこだけ書く。
継承Inheritanceの話
Entityという基底クラスを作り、そこにFlip()やhealthなどの要素を持たせる
そこからPlayer, Enemyなどに小分けしていく作り方がベースになる
code:c#
// 親
private float moveSpeed;
// 継承先
public class Player : Entity
{
private void Update()
{
moveSpeed = 10; // 子でも書き換えようと思ったら、privateでなくprotectedで定義
// メソッドなども同様。
}
多様性 Polymorphism
親クラスのメソッドを子クラスで子クラスなりの振る舞いにさせるということ
C#のoverrideはこんな感じで書ける
code:c#
protected virtual void Attack()
{
Debug.Log(enemyName + " attacks!");
}
----------
protected override void Attack()
{
base.Attack();
StealMoney();
}
カプセル化 Encapsulation
例えば、Player.cs側のデバッグログなどでEnemyのprotected string enemyNameを参照したいとき
code:c#
enemy.GetComponent<Enemy>().enemyName
としても、protectedなので、保護レベルの問題上参照できない(PlayerはEnemyを継承しているわけではないので)
ここで、getter / setterを用意してやるという話になる。
code:c#
public string GetEnemyName()
{
return enemyName;
}
-----------
Enemy enemyScript = enemy.GetComponent<Enemy>();
enemyScript.TakeDamage();
string enemyName = enemyScript.GetEnemyName();
Debug.Log("I damaged enemy : " + enemyName);
これは、もっと明快に書ける
code:Enemy.cs
public string enemyName { get; private set; }
とすると
ほかのスクリプトからもenemyNameは呼び出せる(getできる)
ただし、 enemy.GetComponent<Enemy>.enemyName = "aaa"というようにsetはできない
OOPの各構成要素について
何が便利なのか、どこで使うのかを覚えるとよい。
インターフェース
例: IDamageSource
なにがダメージを与えられる存在なのかを表す
シューティングだと、PlayerBullet, EnemyBullet, SpikeFloorに付与した。
Damageを提供できるという契約(Contract)性を持つ。
利点
持たせることで、実装されていなければコンパイル時にエラーで気づく。
「ダメージを与えられるもの」という性質を明確にできる。
扱う側で、「ダメージを与えられるもの」が何かを特定しなくてもよくなる。
例えばHealthクラスがあったとき、何かとぶつかったときにIDamageSourceを持っているかを見るだけでよい。持っていればそこからDamageを取る。
code:c#
var source = collision.GetComponent<IDamageSource>();
if (source == null) return;
TakeDamage(source.Damage);
Healthは「誰」かは知らず、IDamageSourceがあるという事実だけに依存する。これによりクラス間の依存が減り、責務が明確となる(疎結合)
単体テストが容易になる。TestDamageというクラスを作ったとして、IDamageSourceを付与してHealthにぶつければHealthの検証ができる。
無いと、どういった実装になる?
Healthクラスが何かとぶつかったとき、
以下の工程が必要
ぶつかったものの特定
ぶつかったクラスからdamageを取得
それを使って呼び出し、となる
→ぶつかるものが増えたら、Health側のコードに「新しくぶつかるものの条件」を追加する必要が出てくる
code:c#
if (collision.GetComponent<PlayerBullet>()) { ... }
else if (collision.GetComponent<EnemyBullet>()) { ... }
else if (collision.GetComponent<Trap>()) { ... }
⭐以下の書き方はどうか?
code:C#
# collisionが、damageを持っている前提で書くケース
var damage = collision.damage;
TakeDamage(damage);
PHPっぽい、悪い慣習の書き方(だから時折理解に迷うのかも)
この場合、インターフェース無しでぶつかったものを特定せずに書く挙動。collisionはdamageを持っているだろうという隠れた前提をベースに書いているが、本当にdamageを持っているかわからない(※動的型付けなので、C#ではそもそも使えない)。なので、まず型を特定しなければならないが、そのためにif - elseでチェックをするはめになる
ぶつかりそうなもの全てにDamageという変数をつけることになる。これでは、どれがダメージを与えるものかわからず、意味のないプロパティが多発する。Bullet, SpikeFloorなどが同じ意味のDamageを持つことは偶然。
※インターフェースは「Damageを提供できる」という契約を共通化するもののため、偶然生まれた同名のプロパティをまとめるものではない
インターフェースは「共通する性質」に依存させることで、コードを拡張しやすく、安全に、読みやすく保つための仕組み。
? どこで使う?
「扱い方が同じであればよいが、実装自体はバラバラなもの」をひとまとめにしたい時。
「ダメージを与えられる能力がある」という性質だけを使いたいとき、Bullet, Spike, Trapなど、共通の親を作れない種類でも同様に扱えるようになる。
Action / Eventのまえに: delegate, Action, eventの関係
delegate
関数を変数のように扱うための型。
下記は、int型を受け取り、voidを返すメソッドを格納できる型という書き方
code:c#
// delegate型の宣言(int を受け取って void を返す)
public delegate void MyDelegate(int x);
public static void PrintNumber(int y) {
Console.WriteLine("delegate: " + y);
}
public static void Main()
{
// ラムダ式で書くケース
MyDelegate d1 = (int x) => Console.WriteLine("delegate: " + x);
d1(11);
// 通常で呼ぶケース (※d3はショート構文)
MyDelegate d2 = new MyDelegate(PrintNumber);
MyDelegate d3 = PrintNumber;
d2(22);
d3(33);
}
ただし、これは現在ではほぼ書かれない(delegateを定義するのが面倒)。
Action
そのため、生まれたのがActionという書き方。型宣言が必要なく、コンパクトに書ける。
code:c#
public class Program
{
public static void PrintNumber(int x) {
Console.WriteLine("Action: " + x);
}
public static void Main()
{
Action<int> Print = PrintNumber;
Print(10);
}
}
注意点として、このままの書き方だと外部から書き換えることができてしまう
code:c#
// 定義元のclass
public static Action<int> OnDamage;
// このアクションを呼び出す、外部コード
OnDamage(999);
OnDamage = null;
OnDamage = DoXXX;
event
そのために用意されたのがeventという修飾子。
記述することで自由に代入されることや、勝手な呼び出しを防いでくれる。
eventをつけると、
外部からInvokeできない
外部から代入できない
外部は、+= と -= (購読 / 解除)のみが可能となる。
⭐static
イベントをクラスに属させるための修飾子。
無い場合
例えばEnemyインスタンスごとに、別々のイベントを持つことになる
有る場合
指定したイベントが、全的共通のイベントになる
どの敵が倒れても通知されるというロジックを作りやすくなる
staticで「enemyが被弾したとき、斬撃音を鳴らす」というイベントを紐づけた場合、画面にいる敵全員から音が鳴るということになる。注意!(やらかした)
? どこで使う?
複数システムに同時通知が必要なとき
Action / Event
⭐️26.2.14
通知側(Publisher)と、購読側(Subscriber)に分かれる。
通知側は、出来事が起きたことだけを発信する。
購読側は、その出来事に対して必要な処理を登録する。
利点
複数システムの同時購読が可能となる。
例: EnemyHealthで敵がDieしたとき
経験値、スコア、ドロップ、サウンド、ログ....など、いくらでも追加できる
Publisher(EnemyHealth)は何も知らないまま、Die()が起こったときの挙動を追加できる。
code:EnemyHealth.cs
public static event Action<EnemyHealth> OnAnyEnemyDied;
private void Die()
{
OnAnyEnemyDied?.Invoke(this);
Destroy(gameObject);
}
code:(ex)GameManager.cs
private void OnEnable()
{
// 購読
EnemyHealth.OnAnyEnemyDied += OnEnemyDied;
}
private void OnDisable()
{
// 解除
EnemyHealth.OnAnyEnemyDied -= OnEnemyDied;
}
// EnemyHealth.Die()が走ったとき、同時にしたいこと
private void OnEnemyDied(EnemyHealth enemy)
{
enemyCount++;
AddExp(enemy);
AddScore(enemy);
}
誰が、誰を呼ぶのかを0にできる(疎結合)
EnemyHealthは誰が反応するのかを知らない。つまり、Manager側などだけで自由に振る舞いを追加することができる。
既存コードの非改変で機能追加ができる
新しく実績システムの追加が必要になった場合、EnemyHealthを一切変更せずに拡張できる。
code:AchievementManager.cs
private OnEnable() {
EnemyHealth.OnAnyEnemyDied += UnlockAchievement;
}
依存方向が正しくなる(DIP)
Action / eventを利用すると、上位システムは下位のイベントにだけ依存する形となる。nemyHealthは具体的なManagerを一切知らない。
無いとどうなる?
EnemyHealthが複数システムに一方向に依存してしまう
code:EnemyHealth.cs
private void Die()
{
GameManager.Instance.addEnemyCount();
ExpManager.Instance.addExp(gameObject);
ScoreManager.Instance.addScore(gameObject);
}
この例だと、下位層(敵HP)のクラスが、上位システム(ゲーム全体を管理するスクリプト)を呼んでいるため、よくない書き方である
新しい処理を追加するたびに、EnemyHealthを編集する必要がある
アクションイベントは、複数システムに同時通知しつつも、Publisher側の依存を0にできる。
? どこで使う?
Publisher側のイベントと併せて、同時に行いたい上位オブジェクトの処理があるとき。
「出来事そのもの」と「それに対するアクション」を分離するとき
複数の関心事(スコア、演出、ログ)が1つの出来事にぶら下がるとき。
⭐イベント引数<>について
通知するときに、追加情報を一緒に渡すための型。購読側でラムダを使えば必要な引数が多くても受けることができるので定義があいまいになりがち。極論あいまいでも動くが、購読側が苦労する。
ラムダでの購読
code:c#
# イベント定義, 購読側の順
## 引数が0個
public event Action OnDied;
Player.Health.OnDied += () => TriggerGameOver(GameOverCause.PlayerDied);
## 引数が1個
public event Action<ObjectiveHealth> OnDestroyed;
Objective.Health.OnDestroyed += _ => TriggerGameOver(GameOverCause.ObjectiveDestroyed);
_は、慣習的な使わない引数の名前。捨て引数。
捨て引数
code:c#
# ObjectiveHealth 通知側
// ゲームオーバー通知とかに使う
public event Action<ObjectiveHealth> OnDestroyed;
# GameManagerの例 購読
objectiveHealth.OnDestroyed += HandleObjectiveDestroyed;
private void HandleObjectiveDestroyed(ObjectiveHealth _)
{
TriggerGameOver(GameOverCause.ObjectiveDestroyed);
}
# CameraManagerの例 購読
objectiveHealth.OnDestroyed += _ => HandleDeathShake();
private void HandleDeathShake()
{
DeathShake();
}
ようするにイベントでやりたいことがあるが、渡した_は使わなくてもよい処理の場合。
こういうケースを慣習的に、_で命名する。
イベント側が引数を渡してくるので、_で受け取っている。
NGパターン
1. 購読方法
code:c#
# NG
Player.Health.OnDied += TriggerGameOver(GameOverCause.PlayerDied);
# OK
## 冗長な書き方の例
private void OnPlayerDied()
{
TriggerGameOver(GameOverCause.PlayerDied);
}
Player.Health.OnDied += OnPlayerDied; // これならOK
## ラムダの例。冗長さを取り除くと、こう書ける
Player.Health.OnDied += () => TriggerGameOver(GameOverCause.PlayerDied);
これは、メソッドの結果を購読させようとしているのと同じ。なので、関数そのものを渡さないといけない。
2. 購読の解除
code:c#
# NG
OnDied += () => TriggerGameOver(GameOverCause.PlayerDied);
OnDied -= () => TriggerGameOver(GameOverCause.PlayerDied);
# OK
## ---------------- 変数の例 ----------------
private Action _onPlayerDiedHandler;
_onPlayerDiedHandler = () => TriggerGameOver(GameOverCause.PlayerDied);
Player.Health.OnDied += _onPlayerDiedHandler;
Player.Health.OnDied -= _onPlayerDiedHandler;
## ---------------- メソッドで括るケースの例 ----------------
Player.Health.OnDied -= HandlePlayerDied;
Player.Health.OnDied += HandlePlayerDied;
private void HandlePlayerDied()
{
TriggerGameOver(GameOverCause.PlayerDied);
}
NGのほうは、購読解除時、購読していたインスタンスが何かを特定できない。
変数に入れておくと、購読していたインスタンスを参照し、解除ができるようになる。
もしくは、使いたいメソッドをHandle等のメソッドで括り直す。
⭐依存性について
EnemyHealthとGameManagerで考える
イベントを使わない場合、GameManager側のメソッド名を変えると、EnemyHealth側のメソッド名の修正も必要になる。(例えば敵がDieしたとき、GameManager.ScoreUp()を走らせるという例の場合、ScoreUpをリネームすると敵側の修正も必要になる。)つまりこれは、「ゲームのルールや上位仕様が、下層レイヤーのオブジェクト(敵の体力)にまで波及してしまっている」という状態
イベントを使う場合、EnemyHealth側は上位モジュールを知らない(誰がこれを使うかは知らない)。上位が、下位のイベントを使うという挙動にできる。これで依存性の逆転(DIP)を実現できる。EnemyHealthは「自身が死んだことの通知」という契約だけを提供して、ゲームのルール変更などはすべて上位側で差し替えることができるようになる。
仮にEnemyHealth側のイベントを消したとき、Subscriber側で当然エラーになるが、それは下位クラスが上位クラスに依存したことで発生する、正しい壊れ方と言える。
Enum
? どこで使う?
状態や種類を、マジックナンバーでない意味のある名称を付与して表現するときに使う。
code:ItemType.cs
public enum ItemType
{
Damage,
FireRate,
LifeTime
}
code:ItemBase.cs
public abstract class ItemBase : MonoBehaviour
{
public ItemType Type => itemType;
...
protected virtual void Apply(PlayerStatus status)
{
GameManager.Instance.StatusUIHolder.UpdateAll(status, Type, ItemColor);
}
code:StatusUIHolder.cs
public void UpdateAll(PlayerStatus status, ItemType type, Color itemColor)
{
switch (type)
{
case ItemType.Damage:
damageRow.SetValue(status.ShotDamage, itemColor);
break;
case ItemType.FireRate:
fireRateRow.SetValue(status.FireRateLevel, itemColor);
break;
case ItemType.LifeTime:
distanceRow.SetValue(status.ShotLifeTimeLevel, itemColor);
break;
}
}
無いとどうなるか
たとえば、ItemBase側で int型にて Damage, FireRate, LifeTimeを管理したとき、case 0:のようにマジックナンバーが発生する。また、StatusUIHolder側で改めて定義したとしても、番号がズレたら、コンパイルでは気づけないバグの原因となりうる。新しい効果のアイテムを増やす場合もマジックナンバーが増えていってしまう。
Enumを使うことで、それぞれの数値を意味付きで扱えるようになる。
ItemType に新しい値を追加すると、switch文で以下の点で対応しやすい。
ItemType.と記入すると補完が効くので気づきやすい
リファクタリングしやすい(リネームなどでもIDEが一括で直してくれる)
⭐以下のように記述しておくと、Enumを追加してswitchを忘れた場合、気づきやすくなる
code:c#
case ItemType.LifeTime:
....
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
ItemBaseとStatusUIHolderなど、共通のItemTypeを共有しているということが分かり、型が保証される。
public classの使いどころ
GameManager.csで、PlayerRunDataというクラスを定義している。
このクラスは以下の理由から、GameManager.csに書いて問題ない。
DTO(Data Transfer Object)用途、データを運ぶための入れものとして使っている。
Update()などの挙動は不要であり、MonoBehaviourにする必要がない。
GameManager 内部でだけこのデータを使っており、他クラスから参照される前提がない。
小さなデータを扱いたい場合、同ファイルに定義するのは一般的なデザインパターンだから。
どんな時に別ファイルにすべきか?
データの規模が増えて、構造が複雑になってきたとき
GameManager以外のクラスでも共通利用し始めたとき
将来的に再利用される前提になったとき
⭐再利用とは
データが GameManager以外の文脈で使われ始めた瞬間
データの “意味” が GameManager のスコープを越え始めた瞬間
成長値や、進行度など
データを外部保存・UI・他システムが触り始めた瞬間
セーブ機能(jsonとして保存)など
テスト、分析、調整などで Data と Manager を分けたくなった瞬間
単体テストをしたいとき、GameManagerも呼ばないと検証できない
? いつ同一ファイルに書いてもいいと判断するか?
ファイルに書かれたクラス専用の内部データに留まる場合。
データを管理する入れものとして小規模であり、そのクラス以外が直接参照される設計になっていない場合。
つまり以下の場合に限り、同一ファイルにデータクラスを置くのは正しい判断と言える。
用途が局所的である
責務が小さい
再利用されない
readonly
オブジェクトプールで用いた書き方。
コンストラクタか、初期化時にしか代入できないことを表す。中身の変更はOKだが、別のインスタンスを代入することはできない。オブジェクトプールは、プール自体を変えられると困るが、プールの内容を出し入れして使いたいので、readonlyが適している。
code:c#
private readonly Queue<GameObject> pool = new Queue<GameObject>();
pool = new Queue<int>(); // NG
var obj = pool.Dequeue(); // OK
pool.Enqueue(obj); // OK
⭐constとの違い
値そのものを変えることができない。
int, float, string等の値型に使える(readonlyは、Class, List, Queueなどの参照型に使う)。
? どこで使う?
参照先は固定したいが、中身の操作は許可したいとき。
変数の公開の使い分け
code:c#
public int MaxLevel;
public int MaxLevel => maxLevel;
public int ShotDamage { get; private set; }
単純公開はDTO用途以外には基本NG。Inspectorで書き換えが出来たり、外部クラスからstatus.MaxLevel = 999;と変更されてしまう。
読み取り専用プロパティ(public int MaxLevel => maxLevel;)は、外から読むだけを許したいとき。外部から最大レベルを参照したかったり、ステータス上限を参照したかったり。
{ get; private set; }は、内部で書き変わる可能性があるが、外部では書き換えられては困る値に割り振る。
Dictionary
code:c#
# こうしておくと
private readonly Dictionary<SkillSlot, SkillId> equipped = new();
# スロットZに登録した、スキルのIDを取得できる
equipped.TryGetValue(SkillSlot.Z, out var id) ? id : SkillId.None;
IEnumerable
code:c#
foreach (var cell in EnumerateCells(c0, c1))
{
// データとして登録
grid.RegisterBlockedCell(cell);
lastRoadCells.Add(cell);
// ...
private IEnumerable<Vector2Int> EnumerateCells(Vector2Int a, Vector2Int b)
{
// ...
while (true)
{
yield return new Vector2Int(x0, y0);
// ☆...
}
yield return
指定の値を返すが、foreachなどで次の要素が要求されたときに、続きから再開して処理を始める仕組み。
code:実行イメージ.cs
* foreach (var cell in EnumerateCells(c0, c1) が呼ばれる
* yield return new Vector2Int(x0, y0); で返される
* foreachの2巡目になる
* 呼ばれる前に、// ☆... の部分の処理
* それが終わったら、またyield return で値が返る
=====================
エラー1
Library\PackageCache\com.unity.visualscripting@6279e2b7c485\Runtime\VisualScripting.Flow\Unit.cs(30,29): error CS0246: The type or namespace name 'ConnectionCollection<,,>' could not be found (are you missing a using directive or an assembly reference?)
Unityを無理やり落とした(PCがとまった)のが原因っぽい。パッケージの破損
Package Managerを開く
Window → Package Management → Package Manager
今回、VisualScriptingなのでそれをdelete
Unity再起動
必要なら、safe modeで起動
Package Managerを開く
同じようにVisualScriptingをインストール
動作確認で治った(と思う)
エラー2
NullReferenceException: Object reference not set to an instance of an object
Player.DamageEnemies () (at Assets/Player.cs:60)
PlayerAnimationEvents.DamageEnemies () (at Assets/PlayerAnimationEvents.cs:12)
enemy.GetComponent<Cooldown_Example>().TakeDamage();の処理
Enemyのオブジェクトに、Cooldown_Exampleのコンポーネントが設定されていないというエラー