はじめに
Unityを使ったゲーム開発において、UniRxを使用して弾を一定間隔で発射するBulletShooterクラスを作成しました。
この時に
- _spawnerDisposableはどこでDispose()すれば良いのか?
- BulletShooterクラスはIDisposableを実装した方がよいのか?
- Disposeパターンというものを聞いたことがあるが、そのパターンを適用した方がよいのか?
という疑問が浮かんだので、自分なりの答えを記しておきます。
// (解説用にシンプルにしています)
public class BulletShooter // MonoBehaviourでないことに注意!
{
readonly float _fireInterval = 2.0f; // 2秒に一回発射する
IDisposable? _spawnerDisposable;
public void StartShooting(Vector3 worldPos)
{
var bulletSpawner = new BulletSpawner();
_spawnerDisposable = Observable.Interval(System.TimeSpan.FromSeconds(_fireInterval))
.Subscribe(_ =>
{
var spawnPos = new Vector3(worldPos.x, worldPos.y + 0.5f, worldPos.z);
bulletSpawner.Spawn(spawnPos);
}
);
}
public void StopShooting()
{
// [疑問] StopShooting()のような適当なメソッドでDisposeしていいのか?
_spawnerDisposable?.Dispose();
}
}
public class BulletSpawner
{
public void Spawn(Vector3 spawnPos)
{
// AddressableやResourcesからプレハブを読み込んで生成する
}
}
結論
- このクラス内でDispose()するべき
- IDisposableを実装するべき
- 今回の場合は一般的なDisposeパターンを適用させる必要はない
今回作ったBulletShooterクラスは継承されることを想定していないので、sealedクラスとするべきです。
そうすると次に説明する「継承されないsealedクラスの場合」のようにすれば良いので、Disposeパターンを適用させる必要はないです。
「IDisposableはいつどのように実装すればよいのか?」の答え
いつ
IDisposableをクラスが所有するとき。
カスケード破棄呼び出し
どのように
・継承されないsealedクラスの場合
public sealed class BulletShooter : IDisposable
{
readonly float _fireInterval = 2.0f; // 2秒に一回発射する
IDisposable? _spawnerDisposable;
public void Dispose()
{
_spawnerDisposable?.Dispose();
}
public void StartShooting(Vector3 worldPos)
{
var bulletSpawner = new BulletSpawner();
_spawnerDisposable = Observable.Interval(System.TimeSpan.FromSeconds(_fireInterval))
.Subscribe(_ =>
{
var spawnPos = new Vector3(worldPos.x, worldPos.y + 0.5f, worldPos.z);
bulletSpawner.Spawn(spawnPos);
}
);
}
}
・継承されるかもしれない通常のクラスの場合
public class BulletShooter : IDisposable
{
readonly float _fireInterval = 2.0f; // 2秒に一回発射する
IDisposable? _spawnerDisposable;
bool _disposedValue;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
// この位置でマネージリソースを開放する
_spawnerDisposable?.Dispose();
}
// この位置でアンマネージリソースを開放する
_disposedValue = true;
}
}
public void StartShooting(Vector3 worldPos)
{
var bulletSpawner = new BulletSpawner();
_spawnerDisposable = Observable.Interval(System.TimeSpan.FromSeconds(_fireInterval))
.Subscribe(_ =>
{
var spawnPos = new Vector3(worldPos.x, worldPos.y + 0.5f, worldPos.z);
bulletSpawner.Spawn(spawnPos);
}
);
}
}
・MonoBehaviourの場合
public class BulletShooterMono : MonoBehaviour
{
readonly float _fireInterval = 2.0f; // 2秒に一回発射する
public void StartShooting(Vector3 worldPos)
{
var bulletSpawner = new BulletSpawner();
Observable.Interval(System.TimeSpan.FromSeconds(_fireInterval))
.Subscribe(_ =>
{
var spawnPos = new Vector3(worldPos.x, worldPos.y + 0.5f, worldPos.z);
bulletSpawner.Spawn(spawnPos);
}
).AddTo(this); // AddTo(this)を追加した
// このGameObject の OnDestroy に連動して自動で Dispose()させる
}
}
注意
learn.microsoft や Effective C# を見る限り、「継承されるかもしれない通常のクラスの場合」の例が、一般的なDisposeパターンのようです。個人的には継承はあまり使わないので、seadledクラスの例のやり方で十分そうです。
また、アンマネージリソースについてはUnityの開発においては考慮する必要がないと思われます。
Effective C# には
データベース接続やGDI+オブジェクト、COMオブジェクト、あるいはその他のシステムオブジェクト
のことをアンマネージリソースといっているようです。
この本では、アンマネージ型もしくはIDisposableをメンバに含む型においてIDisposableを実装する必要があると書いてありました。
今回の文脈に沿った話かわからないのですが、アンマネージリソースの定義についてはたくさん議論されているようです。
参考:マネージリソースとアンマネージリソースの定義
まとめ
いろいろ調べましたが、Unityでゲーム開発を行う際にはそこまで注意深く実装する必要なさそうということがわかりました。
IDisposableをフィールドに持っていれば、IDisposableを実装する必要があるというわけです。
参考記事