※ この記事はUnityの例外処理をどう行うべきか個人的に調査・検討した結果をまとめたものです。公式見解のまとめ等ではないためご注意ください。
例外処理の基本方針
Unity開発における例外処理の基本方針ですが、結論、以下のようにするとよさそうです。
やるべきことは大きく分けて2パターン
- 業務エラー(ユーザーで対処できる例外)対応
- 方針: Catchして悪影響がでないように処理した上でゲーム続行できるようにする。
- システム/アプリケーションエラー(ユーザーには対処できない例外)対応
- 方針: 共通例外処理に流し、ログを取るなどした後でアプリを落とす。
前者は例えば地下鉄で操作したらサーバー接続に失敗、といったケース。
後者は例えば、ある手順を踏むとNullReferenceExceptionが発生、といったケースが当てはまります。
基本的に.NETの例外処理 シリーズの考え方をそのまま使わせて頂いているため、詳細はそちらをご参照ください(丸投げ)。
業務エラー(ユーザーで対処できる例外)の場合のサンプル
まずユーザーで対処できる例外の場合。
こちらは各々のメソッドでtry-catchを利用し、処理が止まってしまわないよう気をつけます。
using System.Net;
using System.IO;
public class WwwClient
{
public WwwClientGetResult GetSample(string url)
{
var result = new WwwClientGetResult();
try
{
// 接続に失敗するとWebExceptionが飛ぶ
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream stream = response.GetResponseStream();
var html = new StreamReader(stream).ReadToEnd();
result.IsSuccess = true;
result.Html = html;
return result;
}
catch (WebException e)
{
// 失敗したらここでキャッチして呼び出し元にExceptionが伝播しないようにする。
result.IsSuccess = false;
result.Html = "";
return result;
}
}
}
public class WwwClientGetResult
{
public bool IsSuccess;
public string Html;
}
もっと細かくハンドリングしたい方は公式をご参照ください。
ちなみに呼び出し元では次のような感じのコードになります。
using UnityEngine;
public class Sample : MonoBehaviour
{
void Start()
{
var url = "http://www.example.com/";
WwwClient client = new WwwClient();
var result = client.GetSample(url);
// 呼び出し後、結果を受けてif/switchで処理を分ける。
if (result.IsSuccess == true)
{
Debug.Log("成功時の処理");
// ※ result.Htmlをパースするとかなんとか。
}
else
{
Debug.Log("失敗時の処理");
// ※ 「サーバーとの接続に失敗しました。時間をおいてリトライしてください」的なメッセージを表示する処理を入れる
}
}
}
「ユーザーが対処できない例外」の場合のサンプル
一方のユーザーが対処できない例外ですが、これが起きている状態は継続不能の非常事態と考えられます。速やかにアプリを終了させ、それ以上に被害が拡大するのを防がなければいけません。
そこで、例外は個別にCatchせず上まで素通りさせます。
using System;
public class Sample2
{
void Execute()
{
// 個別のメソッドではcatchしない。それが正しい。
var omg = new Dangerous();
omg.Explosion();
}
}
なにもしないのが正しいという、珍しいパターンです。
ただし、Unityはキャッチできない例外が飛んできても自動的にアプリを落としてくれません。
そのためLogCallbackに自作クラスをアタッチし、キャッチできない例外が飛んできたときはそちらで処理します。
(若干トリッキーなので、もっといい方法をご存知の方はご教示ください!)
using UnityEngine;
public class MyUncaughtExceptionHundler : MonoBehaviour
{
void OnEnable()
{
Application.logMessageReceived += HandleException;
}
void OnDisable()
{
Application.logMessageReceived -= HandleException;
}
void HandleException(string logString, string stackTrace, LogType type)
{
if (type == LogType.Exception)
{
// ※ ここでは何もしていないが、可能ならロギングもしておくと吉。
// ※ ケースバイケースだが、非アクティブ化やDestroy()等も必要になるかもしれない。
// エラーダイアログを表示させる
var dialog = GameObject.Find("ErrorDialog");
dialog.GetComponent<Canvas>().enabled = true;
}
}
}
ここでは手作りのエラーダイアログを出すようにしています。
ゲームの作りによっては、シーンを切り替えたりするなど、他の方法のほうが適切な場合もあると思います。
(※ そしてもちろん、実際のゲームではもう少し見た目に気を配るべきです)
エラーダイアログのボタンが押されたらアプリを落とします。次のようなスクリプトをあらかじめアタッチしておきます。
using UnityEngine;
using UnityEditor;
public class Terminate : MonoBehaviour
{
public void QuitApplication()
{
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#else
// ※ エディタでは無視されます
Application.Quit();
#endif
}
}
基本的な例外処理の方針は、以上で終了です。
が、若干特例があります。
特例1: リソース解放が必要になるメソッド
アンマネージドリソースを使うメソッドに関しては、解放を確実に行わないといけないため、usingまたはtry-finallyを利用します。ファイル操作やDB操作等ですね。
usingを使う場合
public static void Write(string filePath, string contents)
{
using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (var writer = new StreamWriter(stream))
{
writer.Write(contents);
}
}
}
try-finallyを使う場合
public static void Write(string filePath, string contents)
{
var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
try
{
var writer = new StreamWriter(stream);
try
{
writer.Write(contents);
}
finally
{
if (writer != null)
{
writer.Close();
}
}
}
finally
{
if (stream != null)
{
stream.Close();
}
}
}
両方とも行えることは同じのはずですが、usingのほうが綺麗に書けますので、特にこだわりがなければusingでいいのかなと思います。
特例2:UniRx
UniRxの場合はCatchメソッドが専用に用意されているため、そちらを利用します。
IObservable<T> WithErrorHandling<T>(IObservable<WWW> source)
{
IObservable<T> asyncRequest = null;
asyncRequest = source.Timeout(TimeSpan.FromSeconds(30))
.Select(www => JsonConvert.DeserializeObject<T>(www.text))
.Catch((TimeoutException error) => Observable.Throw<T>(error))
.Catch((WWWErrorException error) => Observable.Throw<T>(error))
.Catch((Exception error) => Observable.Throw<T>(error));
return asyncRequest.PublishLast().RefCount();
}
(※ @neueccさんのスライドの記述そのままです)
参考
- https://blogs.msdn.microsoft.com/nakama/2008/12/29/net-part-1/
- http://qiita.com/fujioko/items/aa7f131d815efbd3b3e4
- http://baba-s.hatenablog.com/entry/2014/02/24/000000
- http://www.slideshare.net/neuecc/history-practices-for-unirx-unirx
以上。