// zlib/libpng License | |
// | |
// Copyright (c) 2021 TSUCHIYA Tsukasa | |
// Inspired By ImtStateMachine(Sinoa) https://github.com/Sinoa/IceMilkTea/blob/develop/Packages/IceMilkTea/Runtime/Core/UnitCode/PureCsharp/StateMachine.cs | |
// | |
// This software is provided 'as-is', without any express or implied warranty. | |
// In no event will the authors be held liable for any damages arising from the use of this software. | |
// Permission is granted to anyone to use this software for any purpose, | |
// including commercial applications, and to alter it and redistribute it freely, | |
// subject to the following restrictions: | |
// | |
// 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. | |
// If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. | |
// 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. | |
// 3. This notice may not be removed or altered from any source distribution. | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Runtime.ExceptionServices; | |
namespace Tsukasa.Core | |
{ | |
/// <summary> | |
/// ステートマシンの更新処理中に発生した、未処理の例外をどう振る舞うかを表現した列挙型です | |
/// </summary> | |
public enum StateMachineUnhandledExceptionMode | |
{ | |
/// <summary> | |
/// Update関数内で発生した例外をそのまま例外として発生させます。 | |
/// </summary> | |
ThrowException, | |
/// <summary> | |
/// OnUnhandledException ハンドラに転送されます。 | |
/// </summary> | |
CatchException, | |
} | |
/// <summary> | |
/// 司式ステートマシン | |
/// プッシュダウンオートマトン形式のステートマシン実装 | |
/// </summary> | |
/// <typeparam name="TContext">このステートマシンで制御するクラス</typeparam> | |
class StateMachine<TContext> | |
{ | |
/// <summary> | |
/// ステートのスタック | |
/// </summary> | |
protected Stack<State> stateStack; | |
/// <summary> | |
/// サスペンドフラグ。Trueが設定されていると、スタックの巡回を中断して処理を終える。 | |
/// </summary> | |
private bool suspend; | |
/// <summary> | |
/// 未処理の例外が発生した際の振る舞いの設定取得をします | |
/// </summary> | |
public StateMachineUnhandledExceptionMode UnhandledExceptionMode { get; set; } | |
/// <summary> | |
/// ステートマシンが保持しているコンテキスト | |
/// </summary> | |
public TContext Context { get; private set; } | |
/// <summary> | |
/// 現在のスタックしているステートの数 | |
/// TODO:このプロパティ不要か? | |
/// </summary> | |
public int StackCount => stateStack.Count; | |
/// <summary> | |
/// ステートマシンの Update() 中に未処理の例外が発生した時のイベントハンドラです。 | |
/// ただし UnhandledExceptionMode プロパティに CatchException が設定されている必要があります。 | |
/// false が返されると、例外が未処理と判断され Update() 関数が例外を送出します。 | |
/// </summary> | |
public event Func<Exception, bool> UnhandledException; | |
/// <summary> | |
/// 各ステートの規定クラス | |
/// </summary> | |
public abstract class State | |
{ | |
// メンバ変数定義 | |
private StateMachine<TContext> stateMachine; | |
/// <summary> | |
/// このステートが所属するステートマシン | |
/// ※Stateのコンストラクタ実行時には初期化されていないので注意 | |
/// </summary> | |
protected internal StateMachine<TContext> StateMachine | |
{ | |
get | |
{ | |
return stateMachine ?? throw new NullReferenceException("ステートがステートマシンに登録されるより前にステートマシンあるいはコンテキストが参照されました"); | |
} | |
set { stateMachine = value; } | |
} | |
/// <summary> | |
/// このステートが所属するステートマシンを所持しているオブジェクト | |
/// ※Stateのコンストラクタ実行時には初期化されていないので注意 | |
/// </summary> | |
protected internal TContext Context => StateMachine.Context; | |
/// <summary> | |
/// ステートの更新を処理する | |
/// </summary> | |
protected internal abstract void Update(); | |
} | |
/// <summary> | |
/// ステート巡回から脱出する特殊なステート | |
/// </summary> | |
public class Suspend : StateMachine<TContext>.State | |
{ | |
protected internal override void Update() | |
{ | |
//サスペンドフラグを立てる | |
StateMachine.suspend = true; | |
} | |
} | |
/// <summary> | |
/// コンストラクタ | |
/// </summary> | |
/// <param name="context">このステートマシンを所有しているオブジェクト</param> | |
public StateMachine(TContext context) | |
{ | |
// 渡されたコンテキストがnullの場合 | |
if (context is null) | |
{ | |
// nullは許されない | |
throw new ArgumentNullException(nameof(context)); | |
} | |
// メンバの初期化 | |
Context = context; | |
stateStack = new Stack<State>(); | |
UnhandledExceptionMode = StateMachineUnhandledExceptionMode.ThrowException; | |
} | |
/// <summary> | |
/// ステートをスタックする | |
/// </summary> | |
/// <param name="state">スタックするステート</param> | |
public void PushState(State state) | |
{ | |
//ステートマシンコンテキストを設定する | |
state.StateMachine = this; | |
//ステートをスタックにプッシュする | |
stateStack.Push(state); | |
} | |
/// <summary> | |
/// 複数のステートをスタックする | |
/// </summary> | |
/// <param name="stateList">ステートリスト</param> | |
public void PushStateList(IEnumerable<State> stateList) | |
{ | |
foreach (var state in stateList.Reverse()) | |
{ | |
PushState(state); | |
} | |
} | |
/// <summary> | |
/// スタックされたステート群を実行する | |
/// </summary> | |
public void Update() | |
{ | |
try | |
{ | |
//サスペンドフラグを初期化 | |
suspend = false; | |
//ステートがスタックされている限り巡回を続ける | |
while (stateStack.Count > 0) | |
{ | |
//スタックの先頭からステートを除去し、そのステートの処理を実行する。 | |
stateStack.Pop().Update(); | |
//サスペンドフラグが立っている(=Update処理を脱出する)なら巡回を終了 | |
if (suspend) break; | |
} | |
} | |
catch (Exception exception) | |
{ | |
//例外発生時のエラーハンドリング | |
DoHandleException(exception); | |
return; | |
} | |
} | |
/// <summary> | |
/// ステートスタックに積まれているすべてのステートを捨てます。 | |
/// TODO:動作未検証 | |
/// </summary> | |
public void ClearStack() | |
{ | |
// スタックを空にする | |
stateStack.Clear(); | |
} | |
#region 内部ロジック系 | |
/// <summary> | |
/// 発生した未処理の例外をハンドリングします | |
/// </summary> | |
/// <param name="exception">発生した未処理の例外</param> | |
/// <exception cref="ArgumentNullException">exception が null です</exception> | |
private void DoHandleException(Exception exception) | |
{ | |
// nullを渡されたら | |
if (exception == null) | |
{ | |
// 何をハンドリングすればよいのか | |
throw new ArgumentNullException(nameof(exception)); | |
} | |
// もし、例外を拾うモード かつ ハンドラが設定されているなら | |
if (UnhandledExceptionMode == StateMachineUnhandledExceptionMode.CatchException && UnhandledException != null) | |
{ | |
// イベントを呼び出して、正しくハンドリングされたのなら | |
if (UnhandledException(exception)) | |
{ | |
// そのまま終了 | |
return; | |
} | |
} | |
// 上記のモード以外(つまり ThrowException)か、例外がハンドリングされなかった(false を返された)のなら例外をキャプチャして発生させる | |
ExceptionDispatchInfo.Capture(exception).Throw(); | |
} | |
#endregion | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment