diff --git a/src/UniTask/Assets/Scenes/MiddlewareSample.cs b/src/UniTask/Assets/Scenes/MiddlewareSample.cs new file mode 100644 index 0000000..e922eaf --- /dev/null +++ b/src/UniTask/Assets/Scenes/MiddlewareSample.cs @@ -0,0 +1,471 @@ +using Cysharp.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; +using UnityEngine.SceneManagement; +using UnityEngine.UI; + +namespace Cysharp.Threading.Tasks.Sample +{ + //public class Sample2 + //{ + // public Sample2() + // { + // // デコレーターの詰まったClientを生成(これは一度作ったらフィールドに保存可) + // var client = new NetworkClient("http://localhost", TimeSpan.FromSeconds(10), + // new QueueRequestDecorator(), + // new LoggingDecorator(), + // new AppendTokenDecorator(), + // new SetupHeaderDecorator()); + + + // await client.PostAsync("/User/Register", new { Id = 100 }); + + + // } + //} + + + public class ReturnToTitleDecorator : IAsyncDecorator + { + public async UniTask SendAsync(RequestContext context, CancellationToken cancellationToken, Func> next) + { + try + { + return await next(context, cancellationToken); + } + catch (Exception ex) + { + if (ex is OperationCanceledException) + { + // キャンセルはきっと想定されている処理なのでそのまんまスルー(呼び出し側でOperationCanceledExceptionとして飛んでいく) + throw; + } + + if (ex is UnityWebRequestException uwe) + { + // ステータスコードを使って、タイトルに戻す例外です、とかリトライさせる例外です、とかハンドリングさせると便利 + // if (uwe.ResponseCode) { }... + } + + // サーバー例外のMessageを直接出すなんて乱暴なことはデバッグ時だけですよ勿論。 + var result = await MessageDialog.ShowAsync(ex.Message); + + // OK か Cancelかで分岐するなら。今回はボタン一個、OKのみの想定なので無視 + // if (result == DialogResult.Ok) { }... + + // シーン呼び出しはawaitしないこと!awaitして正常終了しちゃうと、この通信の呼び出し元に処理が戻って続行してしまいます + // のでForget。 + SceneManager.LoadSceneAsync("TitleScene").ToUniTask().Forget(); + + + // そしてOperationCanceledExceptionを投げて、この通信の呼び出し元の処理はキャンセル扱いにして終了させる + throw new OperationCanceledException(); + } + } + } + + public enum DialogResult + { + Ok, + Cancel + } + + public static class MessageDialog + { + public static async UniTask ShowAsync(string message) + { + // (例えば)Prefabで作っておいたダイアログを生成する + var view = await Resources.LoadAsync("Prefabs/Dialog"); + + // Ok, Cancelボタンのどちらかが押されるのを待機 + return await (view as GameObject).GetComponent().ClickResult; + } + } + + public class MessageDialogView : MonoBehaviour + { + [SerializeField] Button okButton = default; + [SerializeField] Button closeButton = default; + + UniTaskCompletionSource taskCompletion; + + // これでどちらかが押されるまで無限に待つを表現 + public UniTask ClickResult => taskCompletion.Task; + + private void Start() + { + taskCompletion = new UniTaskCompletionSource(); + + okButton.onClick.AddListener(() => + { + taskCompletion.TrySetResult(DialogResult.Ok); + }); + + closeButton.onClick.AddListener(() => + { + taskCompletion.TrySetResult(DialogResult.Cancel); + }); + } + + // もしボタンが押されずに消滅した場合にネンノタメ。 + private void OnDestroy() + { + taskCompletion.TrySetResult(DialogResult.Cancel); + } + } + + public class MockDecorator : IAsyncDecorator + { + Dictionary mock; + + // Pathと型を1:1にして事前定義したオブジェクトを返す辞書を渡す + public MockDecorator(Dictionary mock) + { + this.mock = mock; + } + + public UniTask SendAsync(RequestContext context, CancellationToken cancellationToken, Func> next) + { + if (mock.TryGetValue(context.Path, out var value)) + { + // 一致したものがあればそれを返す(実際の通信は行わない) + return new UniTask(new ResponseContext(value)); + } + else + { + return next(context, cancellationToken); + } + } + } + + public class LoggingDecorator : IAsyncDecorator + { + public async UniTask SendAsync(RequestContext context, CancellationToken cancellationToken, Func> next) + { + var sw = Stopwatch.StartNew(); + try + { + UnityEngine.Debug.Log("Start Network Request:" + context.Path); + + var response = await next(context, cancellationToken); + + UnityEngine.Debug.Log($"Complete Network Request: {context.Path} , Elapsed: {sw.Elapsed}, Size: {response.GetRawData().Length}"); + + return response; + } + catch (Exception ex) + { + if (ex is OperationCanceledException) + { + UnityEngine.Debug.Log("Request Canceled:" + context.Path); + } + else if (ex is TimeoutException) + { + UnityEngine.Debug.Log("Request Timeout:" + context.Path); + } + else if (ex is UnityWebRequestException webex) + { + if (webex.IsHttpError) + { + UnityEngine.Debug.Log($"Request HttpError: {context.Path} Code:{webex.ResponseCode} Message:{webex.Message}"); + } + else if (webex.IsNetworkError) + { + UnityEngine.Debug.Log($"Request NetworkError: {context.Path} Code:{webex.ResponseCode} Message:{webex.Message}"); + } + } + throw; + } + finally + { + /* log other */ + } + } + } + + public class SetupHeaderDecorator : IAsyncDecorator + { + public async UniTask SendAsync(RequestContext context, CancellationToken cancellationToken, Func> next) + { + context.RequestHeaders["x-app-timestamp"] = context.Timestamp.ToString(); + context.RequestHeaders["x-user-id"] = "132141411"; // どこかから持ってくる + context.RequestHeaders["x-access-token"] = "fafafawfafewaea"; // どこかから持ってくる2 + + var respsonse = await next(context, cancellationToken); + + var nextToken = respsonse.ResponseHeaders["token"]; + // UserProfile.Token = nextToken; // どこかにセットするということにする + + return respsonse; + } + } + + + public class AppendTokenDecorator : IAsyncDecorator + { + public async UniTask SendAsync(RequestContext context, CancellationToken cancellationToken, Func> next) + { + string token = "token"; // どっかから取ってくるということにする + RETRY: + try + { + context.RequestHeaders["x-accesss-token"] = token; + return await next(context, cancellationToken); + } + catch (UnityWebRequestException ex) + { + // 例えば700はTokenを再取得してください的な意味だったとする + if (ex.ResponseCode == 700) + { + // 別口でTokenを取得します的な処理 + var newToken = await new NetworkClient(context.BasePath, context.Timeout).PostAsync("/Auth/GetToken", "access_token", cancellationToken); + context.Reset(this); + goto RETRY; + } + + goto RETRY; + } + } + } + + public class QueueRequestDecorator : IAsyncDecorator + { + readonly Queue<(UniTaskCompletionSource, RequestContext, CancellationToken, Func>)> q = new Queue<(UniTaskCompletionSource, RequestContext, CancellationToken, Func>)>(); + bool running; + + public async UniTask SendAsync(RequestContext context, CancellationToken cancellationToken, Func> next) + { + if (q.Count == 0) + { + return await next(context, cancellationToken); + } + else + { + var completionSource = new UniTaskCompletionSource(); + q.Enqueue((completionSource, context, cancellationToken, next)); + if (!running) + { + Run().Forget(); + } + return await completionSource.Task; + } + } + + async UniTaskVoid Run() + { + running = true; + try + { + while (q.Count != 0) + { + var (tcs, context, cancellationToken, next) = q.Dequeue(); + try + { + var response = await next(context, cancellationToken); + tcs.TrySetResult(response); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + } + } + finally + { + running = false; + } + } + } + + + public class RequestContext + { + int decoratorIndex; + readonly IAsyncDecorator[] decorators; + Dictionary headers; + + public string BasePath { get; } + public string Path { get; } + public object Value { get; } + public TimeSpan Timeout { get; } + public DateTimeOffset Timestamp { get; private set; } + + public IDictionary RequestHeaders + { + get + { + if (headers == null) + { + headers = new Dictionary(); + } + return headers; + } + } + + public RequestContext(string basePath, string path, object value, TimeSpan timeout, IAsyncDecorator[] filters) + { + this.decoratorIndex = -1; + this.decorators = filters; + this.BasePath = basePath; + this.Path = path; + this.Value = value; + this.Timeout = timeout; + this.Timestamp = DateTimeOffset.UtcNow; + } + + internal Dictionary GetRawHeaders() => headers; + internal IAsyncDecorator GetNextDecorator() => decorators[++decoratorIndex]; + + public void Reset(IAsyncDecorator currentFilter) + { + decoratorIndex = Array.IndexOf(decorators, currentFilter); + if (headers != null) + { + headers.Clear(); + } + Timestamp = DateTimeOffset.UtcNow; + } + } + + public class ResponseContext + { + bool hasValue; + object value; + readonly byte[] bytes; + + public long StatusCode { get; } + public Dictionary ResponseHeaders { get; } + + public ResponseContext(object value, Dictionary header = null) + { + this.hasValue = true; + this.value = value; + this.StatusCode = 200; + this.ResponseHeaders = (header ?? new Dictionary()); + } + + public ResponseContext(byte[] bytes, long statusCode, Dictionary responseHeaders) + { + this.hasValue = false; + this.bytes = bytes; + this.StatusCode = statusCode; + this.ResponseHeaders = responseHeaders; + } + + public byte[] GetRawData() => bytes; + + public T GetResponseAs() + { + if (hasValue) + { + return (T)value; + } + + value = JsonUtility.FromJson(Encoding.UTF8.GetString(bytes)); + hasValue = true; + return (T)value; + } + } + + public interface IAsyncDecorator + { + UniTask SendAsync(RequestContext context, CancellationToken cancellationToken, Func> next); + } + + + public class NetworkClient : IAsyncDecorator + { + readonly Func> next; + readonly IAsyncDecorator[] decorators; + readonly TimeSpan timeout; + readonly IProgress progress; + readonly string basePath; + + public NetworkClient(string basePath, TimeSpan timeout, params IAsyncDecorator[] decorators) + : this(basePath, timeout, null, decorators) + { + } + + public NetworkClient(string basePath, TimeSpan timeout, IProgress progress, params IAsyncDecorator[] decorators) + { + this.next = InvokeRecursive; // setup delegate + + this.basePath = basePath; + this.timeout = timeout; + this.progress = progress; + this.decorators = new IAsyncDecorator[decorators.Length + 1]; + Array.Copy(decorators, this.decorators, decorators.Length); + this.decorators[this.decorators.Length - 1] = this; + } + + public async UniTask PostAsync(string path, T value, CancellationToken cancellationToken = default) + { + var request = new RequestContext(basePath, path, value, timeout, decorators); + var response = await InvokeRecursive(request, cancellationToken); + return response.GetResponseAs(); + } + + + UniTask InvokeRecursive(RequestContext context, CancellationToken cancellationToken) + { + return context.GetNextDecorator().SendAsync(context, cancellationToken, next); // マジカル再帰処理 + } + + async UniTask IAsyncDecorator.SendAsync(RequestContext context, CancellationToken cancellationToken, Func> _) + { + // Postしか興味ないからPostにしかしないよ! + // パフォーマンスを最大限にしたい場合はuploadHandler, downloadHandlerをカスタマイズすること + + // JSONでbodyに送るというパラメータで送るという雑設定。 + var data = JsonUtility.ToJson(context.Value); + var formData = new Dictionary { { "body", data } }; + + using (var req = UnityWebRequest.Post(basePath + context.Path, formData)) + { + var header = context.GetRawHeaders(); + if (header != null) + { + foreach (var item in header) + { + req.SetRequestHeader(item.Key, item.Value); + } + } + + // Timeout処理はCancellationTokenSourceのCancelAfterSlim(UniTask拡張)を使ってサクッと処理 + var linkToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + linkToken.CancelAfterSlim(timeout); + try + { + // 完了待ちや終了処理はUniTaskの拡張自体に丸投げ + await req.SendWebRequest().ToUniTask(progress: progress, cancellationToken: linkToken.Token); + } + catch (OperationCanceledException) + { + // 元キャンセレーションソースがキャンセルしてなければTimeoutによるものと判定 + if (!cancellationToken.IsCancellationRequested) + { + throw new TimeoutException(); + } + } + finally + { + // Timeoutに引っかからなかった場合にてるのでCancelAfterSlimの裏で回ってるループをこれで終わらせとく + if (!linkToken.IsCancellationRequested) + { + linkToken.Cancel(); + } + } + + // UnityWebRequestを先にDisposeしちゃうので先に必要なものを取得しておく(性能的には無駄なのでパフォーマンスを最大限にしたい場合は更に一工夫を) + return new ResponseContext(req.downloadHandler.data, req.responseCode, req.GetResponseHeaders()); + } + } + } +} \ No newline at end of file diff --git a/src/UniTask/Assets/Scenes/MiddlewareSample.cs.meta b/src/UniTask/Assets/Scenes/MiddlewareSample.cs.meta new file mode 100644 index 0000000..b1b9d49 --- /dev/null +++ b/src/UniTask/Assets/Scenes/MiddlewareSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7fc39a4b35a8db44592cddc0b365942f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/UniTask/Assets/Scenes/SandboxMain.cs b/src/UniTask/Assets/Scenes/SandboxMain.cs index a8cc69d..59fb8a1 100644 --- a/src/UniTask/Assets/Scenes/SandboxMain.cs +++ b/src/UniTask/Assets/Scenes/SandboxMain.cs @@ -17,6 +17,8 @@ using UnityEngine.UI; using UnityEngine.SceneManagement; using UnityEngine.Rendering; using System.IO; +using System.Linq.Expressions; +using Cysharp.Threading.Tasks.Sample; // using DG.Tweening; @@ -493,9 +495,11 @@ public class SandboxMain : MonoBehaviour async UniTaskVoid Start() { - RunStandardTaskAsync(); + //Expression.Lambda>(null).Compile(true); - UnityEngine.Debug.Log("UniTaskPlayerLoop ready? " + PlayerLoopHelper.IsInjectedUniTaskPlayerLoop()); + //RunStandardTaskAsync(); + + //UnityEngine.Debug.Log("UniTaskPlayerLoop ready? " + PlayerLoopHelper.IsInjectedUniTaskPlayerLoop()); //var url = "http://google.com/404"; //var webRequestAsyncOperation = UnityWebRequest.Get(url).SendWebRequest(); @@ -524,8 +528,23 @@ public class SandboxMain : MonoBehaviour //UniTask.Delay(TimeSpan.FromSeconds(3)). - //okButton.onClick.AddListener(UniTask.UnityAction(async () => - //{ + + + okButton.onClick.AddListener(UniTask.UnityAction(async () => + { + + var client = new NetworkClient("http://localhost:5000", TimeSpan.FromSeconds(2), + new QueueRequestDecorator(), + new LoggingDecorator()); + //new AppendTokenDecorator(), + //new SetupHeaderDecorator()); + + + await client.PostAsync("", new { Id = 100 }); + + + })); + // _ = ExecuteAsync(); // await UniTask.Yield(); diff --git a/src/UniTask/ProjectSettings/ProjectSettings.asset b/src/UniTask/ProjectSettings/ProjectSettings.asset index 3fd3e00..c1a7968 100644 --- a/src/UniTask/ProjectSettings/ProjectSettings.asset +++ b/src/UniTask/ProjectSettings/ProjectSettings.asset @@ -111,6 +111,8 @@ PlayerSettings: switchNVNShaderPoolsGranularity: 33554432 switchNVNDefaultPoolsGranularity: 16777216 switchNVNOtherPoolsGranularity: 16777216 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 vulkanNumSwapchainBuffers: 3 vulkanEnableSetSRGBWrite: 0 m_SupportedAspectRatios: @@ -191,22 +193,6 @@ PlayerSettings: uIStatusBarHidden: 1 uIExitOnSuspend: 0 uIStatusBarStyle: 0 - iPhoneSplashScreen: {fileID: 0} - iPhoneHighResSplashScreen: {fileID: 0} - iPhoneTallHighResSplashScreen: {fileID: 0} - iPhone47inSplashScreen: {fileID: 0} - iPhone55inPortraitSplashScreen: {fileID: 0} - iPhone55inLandscapeSplashScreen: {fileID: 0} - iPhone58inPortraitSplashScreen: {fileID: 0} - iPhone58inLandscapeSplashScreen: {fileID: 0} - iPadPortraitSplashScreen: {fileID: 0} - iPadHighResPortraitSplashScreen: {fileID: 0} - iPadLandscapeSplashScreen: {fileID: 0} - iPadHighResLandscapeSplashScreen: {fileID: 0} - iPhone65inPortraitSplashScreen: {fileID: 0} - iPhone65inLandscapeSplashScreen: {fileID: 0} - iPhone61inPortraitSplashScreen: {fileID: 0} - iPhone61inLandscapeSplashScreen: {fileID: 0} appleTVSplashScreen: {fileID: 0} appleTVSplashScreen2x: {fileID: 0} tvOSSmallIconLayers: [] @@ -566,7 +552,8 @@ PlayerSettings: scriptingRuntimeVersion: 1 gcIncremental: 0 gcWBarrierValidation: 0 - apiCompatibilityLevelPerPlatform: {} + apiCompatibilityLevelPerPlatform: + Standalone: 6 m_RenderingPath: 1 m_MobileRenderingPath: 1 metroPackageName: Template_2D @@ -621,6 +608,7 @@ PlayerSettings: XboxOnePersistentLocalStorageSize: 0 XboxOneXTitleMemory: 8 XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: vrEditorSettings: daydream: daydreamIconForeground: {fileID: 0}