- 投稿日:2020-06-03T22:03:49+09:00
UnityでSwiftの自作「Static Library」を使う
環境
- Unity 2018.4.23f1
- Xcode 11.5
- Swift 5
- iOS 13.4.1(iPhone 11)
手順の概要
「Unity」から直接「Swift」のコードを呼び出すことはできないようです。。。orz
その為、下記のような形で、「Objective-C++」を経由する事で、「Swift」の関数を呼び出します。設定手順
「Swift」の「Static Library」を作成
「Xcode」で、プロジェクトを作成
項目 値 Product Name 任意 Team 任意 Organization Name 任意 Organaization Identifier 任意 Language Objective-C
- 任意のディレクトリにプロジェクトを作成
呼び出される「Swift」ファイルの作成
- 「プロジェクト」→「コンテキストメニュー」→「New File」を押下
- 「iOS」→「Source」→「Swift File」を選択して、「Next」を押下
- 任意の名前を設定して、「Create」を押下
- 「Create Briding Header」を押下
- 作成した「Swift」ファイルを選択して、呼び出す関数を作成
// // SwiftTest.swift // SwiftStaticLibrary // // Created by Kumatta_ss on 2020/05/31. // Copyright © 2020 Kumatta_ss. All rights reserved. // import Foundation class SwiftTest: NSObject { /// 呼び出しのみ @objc static func swiftCallTest() { NSLog("swiftCallTest OK!") } /// 引数のみ /// - Parameters: /// - val1: 引数1 @objc static func swiftCallTestArgument(val1: String) { NSLog("swiftCallTestArgument OK! Argument:" + val1) } /// 引数、戻り値あり /// - Parameters: /// - val1: 引数1 /// - Returns:戻り値 @objc static func swiftCallTestArgumentReturn(val1: String) -> String { NSLog("swiftCallTestArgumentReturn OK!") return "Return swiftCallTestArgumentReturn Argument:" + val1; } }外部公開用の「Objective-C++」を作成する
- 「Command + B」で、Buildする
※Swiftのヘッダーファイルを生成させる- 初期で作成されている、「Objective-C」のファイルの拡張子を、「m」を、「mm」にする
※「Objective-C++」に変更- 変更したファイルに、Swiftを呼び出す関数を作成
// Swiftの関数宣言ヘッダー(名称は、「{プロジェクト名}-Swift.h」形式) #import <SwiftStaticLibrary-Swift.h> // 外部に公開用の関数宣言 extern "C" { void CallTest(); void CallTestArgument(const char *val1); const char* CallTestArgumentReturn(const char *val1); } // 呼び出しのみ void CallTest() { NSLog(@"CallTest"); [SwiftTest swiftCallTest]; } // 引数のみ void CallTestArgument(const char *val1) { NSLog(@"CallTestArgument"); [SwiftTest swiftCallTestArgumentWithVal1:@(val1)]; } // 引数、戻り値あり const char* CallTestArgumentReturn(const char *val1) { NSLog(@"CallTestArgumentReturn"); NSString *result = [SwiftTest swiftCallTestArgumentReturnWithVal1:@(val1)]; const char *resultEncodeVal = [result cStringUsingEncoding:NSUTF8StringEncoding]; char *returnVal = (char*)malloc(strlen(resultEncodeVal) + 1); strcpy(returnVal, resultEncodeVal); return returnVal; }ライブラリの生成
- ビルドすると「Products」ディレクトリ配下に、「a」ファイルが作成される
ファイルを選択した状態で、右のメニューの「Indentity andType」→「Full Path」の矢印を押下すると、格納ディレクトリを開きますUnityで「Sttaic Library」を使用する
ライブラリの配置
- 格納用ディレクトリを、下記の構成で作成
Assets ├─ Efitor ├─ Plugins | └─ iOS └─ Scripts
- 「Assets/Plugins/iOS」に生成された、「a」ファイルを格納
- 格納した「a」ファイル選択して、「Inspector」の下記の項目を設定して、「Apply」を押下
項目 値 Select platforms for plugin -> Include Platfprms 「iOS」のみにチェック Platform settings デフォルト 呼び出しクラスの作成
- 呼び出し確認用のクラスを、「Assets/Scripts」に作成
using System.Runtime.InteropServices; using UnityEngine; public class UnityCallTest : MonoBehaviour { [DllImport("__Internal")] private static extern void CallTest(); [DllImport("__Internal")] private static extern void CallTestArgument(string val1); [DllImport("__Internal")] private static extern string CallTestArgumentReturn(string val1); // Start is called before the first frame update void Start() { CallTest(); CallTestArgument("1.Unityからの呼び出しだ!"); string result = CallTestArgumentReturn("2.Unityからの呼び出しだ!"); Debug.Log("============================ Unity Result:" + result); } // Update is called once per frame void Update() { } }
- 「Hierarchy」に、任意のGameObjectを作成して、作成したスクリプトをアタッチ
ビルド設定
- 「Player setting」で、任意の設定をする
※今回は下記の赤い部分のみを変更しました
- 「Assets/Editor」配下に、拡張エディタを作成
そのままだと、ビルドしたXcodeプロジェクトに、毎回設定をしないといけないので、拡張エディタを作成using System.IO; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; public class TestPostProcessBuild { [PostProcessBuild] public static void BuildTest(BuildTarget buildTarget, string projectPath) { // iOSの場合のみ if (BuildTarget.iOS == buildTarget) { string projectFilePath = PBXProject.GetPBXProjectPath(projectPath); var proj = new PBXProject(); proj.ReadFromFile(projectFilePath); string target = proj.TargetGuidByName(PBXProject.GetUnityTargetName()); // BridgingHeaderを作成・設定 string projSwiftBridgingFile = "Classes/Unity-iPhone-Bridging-Header.h"; string swiftBridgingFile = Path.Combine(projectPath, projSwiftBridgingFile); var fs = File.Create(swiftBridgingFile); fs.Close(); string swiftBridgingFileGuid = proj.AddFile(swiftBridgingFile, projSwiftBridgingFile, PBXSourceTree.Source); proj.AddFileToBuild(target, swiftBridgingFileGuid); proj.SetBuildProperty(target, "SWIFT_OBJC_BRIDGING_HEADER", projSwiftBridgingFile); // Xcodeのビルド設定 proj.SetBuildProperty(target, "CLANG_ENABLE_MODULES", "YES"); proj.AddBuildProperty(target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks"); proj.SetBuildProperty(target, "SWIFT_VERSION", "5.0"); proj.SetBuildProperty(target, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES"); proj.SetBuildProperty(target, "SWIFT_INSTALL_OBJC_HEADER", "YES"); proj.SetBuildProperty(target, "SWIFT_PRECOMPILE_BRIDGING_HEADER", "YES"); proj.SetBuildProperty(target, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "$(PRODUCT_NAME)-Swift.h"); proj.AddBuildProperty(target, "LIBRARY_SEARCH_PATHS", "/usr/lib/swift/"); proj.AddFileToBuild(target, proj.AddFile("usr/lib/swift/libswiftFoundation.tbd", "Frameworks/libswiftFoundation.tbd", PBXSourceTree.Sdk)); proj.WriteToFile(projectFilePath); } } }ビルドして実機で実行
- ログに実行結果が出力されると思います
あとがき
今回、Swiftで作成されたライブラリを、Unityで呼び出す必要があり、
「Objective-C」、「Swift」を触った事ない状態から、色々試した結果です。ビルドのプロパティについては、「Xcode」でネイティブアプリとして、
「Objective-C」と、「Objective-C + Swift」の二つを作成、解析して、必要な設定を割り出した物になります。問題や追加の説明が欲しいなどが、あったらコメントなどで、教えていただけたら嬉しいです。
- 投稿日:2020-06-03T21:20:28+09:00
[Unity] Depthについてあれこれ
はじめに
これからまた情報を付加させていくつもりですが,現時点でも何かの参考になればと思っております.
間違っている点や補足などがありましたら教えてください.パースペクティブ射影変換(ProjectionMatrix)
DirectX
(x, y, z, 1) \begin{pmatrix} \frac{H}{W}\cot(\frac{\theta}{2}) & 0 & 0 & 0 \\ 0 & \cot(\frac{\theta}{2}) & 0 & 0 \\ 0 & 0 & fz \cdot \frac{1}{fz - nz} & 1 \\ 0 & 0 & fz \cdot \frac{-nz}{fz - nz} & 0 \\ \end{pmatrix}上の式に関してz成分に関して計算してみると,
\frac{1}{z} (\frac{fz\cdot z}{fz-nz} - \frac{fz \cdot nz}{fz - nz}) \\ \frac{fz}{z} \frac{z - nz}{fz - nz}Depth
具体的に数値を当てはめてみると
fz = 10 \\ fz = 1 \\ \frac{10}{9} - \frac{10}{9z} \\そして,グラフにしてみるとこんな感じになります(横軸がビュー空間のdepth, 縦が変換後のdepth)
実際にNearClip(z = 1)を代入してみると値が0,FarClip(z = 10)を代入してみると値が1になっていることからDepthの範囲が0 ~ 1に収収まっていることが確認出来ました.
x,y値
yについて値を具体的に考えてみます.
$\frac{\theta}{2} = 30$とし,$near=2.0$, $far=10.0$としたとき,それぞれの地点でのyのtopの高さは$y_{NearTop} = \frac{2}{\sqrt 3}$, $y_{FarTop} = \frac{10}{\sqrt 3}$ となります.(tanを使用することで計算出来ます)それに対して上に示した行列を作用させると,
Near_Topについて
y'_{NearTop} = \frac{ \frac{2}{\sqrt 3} \cot{\frac {\pi}{6}}}{nz} \\ y'_{NearTop} = \frac{2}{nz} \\ y'_{NearTop} = 1.0
Far_Topについて
y'_{FarTop} = \frac{ \frac{10}{\sqrt 3} \cot{\frac {\pi}{6}}}{fz} \\ y'_{NearTop} = \frac{10}{fz} \\ y'_{FarTop} = 1.0よってどちらも1.0になっています.他のbottom, right, leftについても同様に計算していけば正規化(-1.0 ~ 1.0)させれていることが確認できるかと思います.
またw除算する前のタイミングでは-w ~ w
となってます.OpenGL
(x, y, z, 1) \begin{pmatrix} \frac{2near}{right - left} & 0 & 0 & 0 \\ 0 & \frac{2near}{top - bottom} & 0 & 0 \\ 0 & 0 & -\frac{fz +nz}{fz - nz} & -1 \\ 0 & 0 & -2 \cdot \frac{fz \cdot nz}{fz - nz} & 0 \\ \end{pmatrix}上の式に関してz成分に関して計算してみると
(\frac{z \cdot (fz + nz) - 2fzfn}{fz-nz}) \frac{1}{z}Depth値
具体的に数値を当てはめてみると
fz = 10 \\ fz = 1 \\ \frac{11}{9} - \frac{20}{9z} \\実際にNearClip(z = 1)を代入してみると値が-1,FarClip(z = 1)を代入してみると値が1になっていることからDepthの範囲が0 ~ 1に収収まっていることが確認出来ました.
ここまでで分かったこととしてはDirectXではDepthの値が0.0 ~ 1.0, OpenGLではDepthの値が-1.0 ~ 1.0, といういことです.
LinearDepth
UnityObjectToClipPos
UnityObjectToClipPos
ではx,yは-w ~ w
, zは0 ~ w
(GraphicsAPIによっては-w ~ w
)の範囲までの計算が行われ,w除算が行われていない状態です.ComputeScreenPos
入ってくる値としては
UnityObjectToClipPos
によって変換されたw除算される前の値です.
x, yについて0 ~ w
に変換している.z, wに関しては前の値が再代入されています.inline float4 ComputeNonStereoScreenPos(float4 pos) { float4 o = pos * 0.5f; o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w; o.zw = pos.zw; return o; }_CameraDepthTexture
0.0 ~ 1.0までの値が格納されているTexture
https://soramamenatan.hatenablog.com/entry/2019/11/10/133420dCOMPUTE_EYEDEPTH
#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos(v.vertex).z inline float3 UnityObjectToViewPos(in float3 pos) { return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz; }通常Unityは左手座標系(z軸の奥の方がプラス)なのですが,カメラ座標系にしたときは右手座標系(z軸の手前がプラス)になります.その為,今回-1.0をかけている理由としてはDepthの値(正)を必要とするためです.
参考:https://tech.drecom.co.jp/knowhow-about-unity-coordinate-system/
線形にしたい場合
fixed4 frag(v2f i) : SV_Target { half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); depth = Linear01Depth(depth); return depth; }GrapchisAPIによる違い
https://docs.unity3d.com/ja/current/Manual/SL-PlatformDifferences.html6
- 投稿日:2020-06-03T18:51:07+09:00
【Unity】汎用Animatorを色々なキャラクターに適用させる方法
想定環境
- Unity5以上
概要
Unity標準のAnimator、分かりにくくないですか?
ステートベースで直感的にアニメーション遷移を作れる!最高!と思いきや、以下のような、グロいAnimatorを作ってしまうのは割とあるあるだと思います。しかも、Animatorは基本的に一つのキャラに一つずつ必要になります。ポチポチとステートを作り、そのステートにアニメーションを当てはめ、という作業を全キャラ分・・・。軽く死ねますね。
というわけで、複数キャラに跨って使いまわせる汎用Animatorを作るテクニックの紹介です。(私の知っている限りUnity5系のころには標準搭載している機能なのですがあまり知られていないっぽい?ので記事化してみました。)
Animator Override Controllerを使おう!
はい、実はこんな便利なものがあります。使い方は公式リファレンスが十分丁寧に教えてくれているのでリファレンス見てね、で終わりなんですが、一応知見をここにまとめておきます。
名前の通り、こいつは「あるAnimatorをベースにして、そのステート内で再生するアニメーションを任意のアニメーションで上書きすることができる」というものです。
実際に作りながら解説してみます。
1.ベースとなるAnimatorを普通に作る
まずベースとなるAnimatorを用意します。
キャラによっては歩きモーションや攻撃モーションを複数持つことも考えられるので、A,B,Cといったようにとりあえず用意だけしておきます。
このAnimatorは、あくまでベースなので、実際にAnimationをあてがう必要はありません。ステートマシンの遷移部分だけ作っておけばよいのです。2. Animator Override Controllerを作る
ベースを作ったので、次に実際のキャラクターに使用するAnimatorを作ります。
公式リファレンスに記述してあるように、Create -> Animator Override Controller を選択します。すると、Animatorに似ているようで微妙に違うアイコンのアセットが作られます。これは実際のキャラに使用するAnimatorとなるので、きちんと命名しましょう。今回はゾンビに使いたいので、ZombieAnimatorとしました。
3. Inspectorで設定する
Inspectorを見てみましょう。
このような画面になっていると思います。
Controllerの部分に、ベースとして用意しておいたAnimatorをドラッグアンドロップなどで参照します。
するとこのような画面になります。Original の列には、ベースとなる Animator のステートに参照されている Animation 名が表示されます。
Override の列に、その Animation と対応する Animation をドラッグアンドドロップなどで参照できます。対応する Animation が無ければ、None のままで構いません。
以上です。
これで、ステートマシンを使いまわしつつ、別々のアニメーションをするキャラクターを簡単に量産できます。
はい、ちゃんと女ゾンビも男ゾンビも同じステートマシンで動いてますね。
ちなみに導入の際の注意ですが、さすがにステートマシンの遷移を Override側で変更などはできないので、遷移の体系が違うものを作る場合は、その都度 Animator を新規作成する必要があります。
以上、小ネタでした。
宣伝
本稿は現在絶賛公開中のスマホゲーム『たのしいさんすう』の開発で実際に使っているテクニックを紹介しています。ぜひぜひ、お手に取ってたくさんのゾンビたちと戯れていただければ幸いです。
【たのしいさんすう】
iOS: https://apps.apple.com/us/app/zombie-mathters/id1504347831?l=ja&ls=1
Android: https://play.google.com/store/apps/details?id=com.gamebox.sansuu
ディレクター様: https://twitter.com/gamebox7777
筆者: https://twitter.com/john95206
- 投稿日:2020-06-03T18:29:39+09:00
Unityでマウスの動きに合わせてGameObjectを動かす方法
はじめに
こんな感じのものを作ります
ステップ
- 動かしたいGameObjectを作成します
- 「MouseChase.cs」の名前でC#スクリプトを生成します
- 2のスクリプトを1のGameObjectにアタッチします
スクリプト
MouseChase.csusing UnityEngine; public class MouseChase : MonoBehaviour { /// <summary> /// Update /// </summary> void Update() { transform.position = Input.mousePosition; } }補足
transform.position = Input.mousePosition;上記の箇所ですが、以下のように書くこともできます
gameObject.transform.position = Input.mousePosition;違いは参考のURLにて詳しく書いてますので、参考にしてみてください
上の記述のほうが早いようです参考
gameObject.GetComponent() と transform の違い(または Unity における省略記法について)
- 投稿日:2020-06-03T16:26:39+09:00
ゲームのUIでボタン音を操作やカーソル位置に応じて変える
単調なボタン音をメニューの状況に合わせて変える
ゲームにおいて、「ボタンを選択した」ときに音を鳴らすことを考えます。最近のゲームでは、選択したボタンの位置によって音を微妙に変え、「終端」や「右端」「左端」を感覚的にわかりやすくしている事例も多くあります。
本投稿ではゲーム開発において、どの位置のボタンを選択しているかによって音を変える演出を作ります。
音の制御には、CRI ADX2を使用します。また、実装例はUnityで紹介しますが、UE4+ADX2でもブループリントを使って同じことが可能です。前提となる知識
この記事は統合型サウンドミドルウェア「CRI ADX2」及び付属のサウンドオーサリングツールAtom Craftの触り方をある程度理解している方向けの解説です。
初めてADX2を使う、という方は各エンジンへの導入の記事を先に参照ください。Unity: Unityのサウンド機能をADX2で強化する
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377UE4: ADX2 for UE4の導入で、一歩上のサウンド表現を(導入編)
https://qiita.com/SigRem/items/4250925f6d66a4fd287aボタンのセレクト音が変わるシチュエーション
言葉で説明するのは冗長なので、こちらの動画をご覧ください。
UIのサウンド実装テスト(再) 端のボタン選択してる時だけピッチがちょい上がる pic.twitter.com/sv0DoMhfQX
— スーパーミソダンゴ (@Takaaki_Ichijo) June 3, 2020このように、ボタンを選択したときに音を鳴らす場合、「どのボタンを選択しているか」で音の鳴り方を変えます。
音素材の生成には、Tsugi社のDSP-Sci-Fiを使用しています。
Atom Craftでの設定
まずは、「通常状態」と「ピッチが上がった状態」をキューに設定する必要があります。
今回は、ADX2の「AISAC」機能を使って実装を行います。Atom Craftを立ち上げ、まずはプロジェクトツリーのAISACコントロールで、今回使うAISAC値を操作するコントロールIDを用意します。
例では、ButtonPitchという名前にしました。次に、ボタン音のキューを作成したあと、キューにAISACを追加します。
ダイアログでは、先ほど作成したAISACコントロールIDと、グラフタイプに「ピッチ」を指定します。
AISACは、ADX2で音を鳴らすときに「0からの1のパラメータ値」をゲームから渡し、値によって音を変化させる機能です。
パラメータの値によってどのくらい音が変化するかは、Atom Craft内のグラフで指定します。今回はピッチを変化させています。
このスクリーンショットの下に描かれている青い線がASIACグラフです。
キュー「Select」にはAisac_0という名前のAISAC設定がり、その中身はピッチの変更設定で、ゲームから渡される0から1までの値に応じてピッチをどう変化させるかを設定しています。AISACビューの白い縦線が、ゲームから渡される値を示しています。ButtonPitchの値が0.501fだったとき、を次の図は示しています。
グラフが凹の形になっているのは、0方向を左端、1方向を右端としてとらえ、それぞれのAISAC値だったときにピッチを上げる、という仕組みにしているためです。
右と左で音の変化が全く同じ場合は、たとえば0.500以下が無加工の音で、0.501以上のときにピッチを上げる、という2値の設定でも構いません。AISACビューの隣に表示している「ポイントリスト」は、AISAC内の点の位置を数値で調整できるウィンドウです。
左から順番にAISACIDには数字が降ってあり、その位置を表形式で確認できます。
マウスで適当に点を打ったあと、こまかい調整はポイントリストで数値での調整がお勧めです。ボタンが端に来たことを感知する
次にUnity側の準備です。ボタンが端に来たことを検知して、AISACに適切な値を渡します。
今回は非常に単純な実装として、enumでボタン位置を管理しておく仕組みで実装します。
uGUI Buttonのコンポーネントと同じゲームオブジェクトに、次のスクリプトを配置します。ボタンがキーナビゲーションにおいて端に来ているかどうかは、Buttonクラスの基底クラスSelectableのFindSelectableOnLeft、FindSelectableOnRightメソッドでチェックできます。
隣に選択可能なSelectableがあるか調べて、なければnullを返します。UIButtonPlaySe.csusing UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class ButtonPlaySe : MonoBehaviour, ISelectHandler { public CriAtomSource seAtomSource; private Selectable buttonSelectable; private void Awake() { buttonSelectable = GetComponent<Selectable>(); } public void OnSelect(BaseEventData eventData) { float aisacControl = 0.5f; if (buttonSelectable.FindSelectableOnLeft() == null) { aisacControl = 0.05f; } else if (buttonSelectable.FindSelectableOnRight() == null) { aisacControl = 0.95f; } seAtomSource.SetAisacControl("ButtonPitch", aisacControl); seAtomSource.Play(); } }seAtomSourceフィールドには、シーンにあるSE再生用のCriAtomSourceの参照をインスペクターで指定します。
ButtonPositionの列挙体によって、並べてあるボタンが右端・中・左端の3種類から指定できます。スクリプトはISelectHandlerインターフェースを実装しており、キーボードやパッド入力で当該のボタンが選択されると、OnSelectコールバックが呼ばれます。そのタイミングでAISAC値を渡ししています。
右端の場合は0.05f, 左端は0.95f,それ以外は中央といった具合です。
この方法では、シーンの上であらかじめボタンに「端かそうでないか」の指定が必要です。
動的にボタンが増えたり減ったりする場合は、こうした固定での指定ができませんので、なんらかの検出の仕組みが必要です。
たとえばX方向におけるボタンの位置を画面解像度から取得し、ある程度画面の端に近いボタンであれば端にいると判定する、などの方法がとれます。
おそらくEventSystems.BaseInputModule.DetermineMoveDirectionを使えばボタンのナビゲーションで次に遷移できるかできないか検出できると思うのですが、手間が多そうなので簡単な方法をとりました。Selectable.FindSelectableOnLeftメソッドでボタンがどの位置にあるか自動的に判定し、音を変化させることができます。FindSelectableOnUp、FindSelectableOnDownもありますので、上下での変化にも対応できます。
応用編
AISACはピッチ以外にも操作が可能ですので、端のボタンを選択したときにだけエコーなどのエフェクトがかかる、といった演出も作れます。
また、音をパラメータで加工するのではなく、完全に別素材の音を鳴らしたい場合は、ADX2の「セレクタ」や「ゲーム変数」機能で実装できます。セレクタラベル
ゲームの状況に合わせてサウンドエフェクトのかかり具合を変える
https://qiita.com/Takaaki_Ichijo/items/957dd09112b9939ff083ゲーム変数
ゲームの状況に合わせて音のエフェクトの掛かり具合を変える その2
https://qiita.com/Takaaki_Ichijo/items/4777ba953e7992a872e4
- 投稿日:2020-06-03T09:11:18+09:00
Re: C#(Unity)でHTTP/3通信してみる その参 ~Unityから使ってみる~
Re: C#(Unity)でHTTP/3通信してみる その壱 ~OSSの選定からビルドまで~ では quiche の .lib/.dll を作成する方法を紹介しました。
続く Re: C#(Unity)でHTTP/3通信してみる その弐 ~Windowsでquicheのサンプルを動かしてみる~ では Windows 環境で quiche のサンプルを使って HTTP/3 通信の実装を試してみました。
今回はいよいよ最終回、 C# (Unity) から HTTP/3 通信してみるところまでを解説しようと思います。本題に入る前に …… http3sharp について
今回の記事で紹介している実装及び Unity サンプルを http3sharp (Ver: 0.1.0) として以下のリポジトリにて公開しました。
https://github.com/TakeharaR/http3sharp
CloudFlare の QUIC,HTTP/3 ライブラリ quiche のラッパ層である qwfs と、この qwfs を用いた Unity 向け HTTP/3 クライアントライブラリである http3sharp から成ります。
実験的実装なのでエラー処理などの例外系がかなり甘いですが、 Unity で HTTP/3 通信試してみたいぜ、という方は是非触ってみてください。当記事における解説の前提条件
- 今回の記事では以下の点について重点的に説明します
- HTTP/3,QUIC ライブラリである quiche の組み込み方法・注意事項
- HTTP/3,QUIC 通信を行うライブラリ実装時の注意事項
- また、以下の内容については紹介しない、または軽く触れるに留めます
- Unity からの Native ライブラリの使い方、及びマーシャリング
- 最適化 (http3sharp の実装も現状最適化は施していません)
本記事の流れ
以下の流れで紹介します。
- quiche アドバンスド (その弐 で触れなかった quiche の設定や関数、その他補足に関する内容)
- 実装方針についての紹介
- 実装でハマった問題の共有(未解決問題含む)
- http3sharp を用いて Unity 上で HTTP/3 通信を行うサンプルの紹介
個人的な備忘録も兼ねているのでかなりの長文になってしまいましたが、必要な個所だけ掻い摘んで参考にして貰えたら幸いです。
バージョン
今回の記事は HTTP/3, QUIC ともに draft version 27 に準拠した仕様で記載を行っています(最新バージョンは 28 です)。
その他、利用・動作確認している関連モジュール・エンジンのバージョンは以下の通りです。
- Unity : 2019.3.14f
- quiche : 0.4.0
- boringssl : quiche の submodule バージョンに準ずる
- aioquic : a38756bab2cd9039e89a06a1f442f330d349eea0 (master)
- http3sharp : 0.1.0
注意事項
その弐 同様に今回の記事も HTTP/3, QUIC の仕様をある程度理解している前提で進めます。
多少は補足しますが、理解が追いつかない個所がある場合は以下のいずれかで補完することをお勧めします。
- flano_yuki さんが書いている http3-note や ASnoKaze blog の各解説記事
- 筆者の著作物である 『くいっく』HTTP/3編 (宣伝)
- HTTP/3, QUIC の draft - 27
quiche アドバンスド
前書きから長くなってしまいましたがいよいよ本題です。
この項では、その弐 の example で解説では触れなかった quiche のより一層踏み込んだ設定項目について解説します。qlog の出力
qlog は HTTP3, QUIC の標準ログ形式です。
現在は DQUIC and HTTP/3 event definitions for qlog 及び Main logging schema for qlog にて仕様の策定が進められています。
また、 qlog を使って HTTP3, QUIC の通信結果の可視を行ってくれる qviz というツールも存在しています。
qviz は https://github.com/quiclog/qvis で公開されているソースコードを自分でビルドするか、 https://qvis.edm.uhasselt.be/#/files にて公開されているサービスかのいずれかにより利用可能です。以下のような感じで多重化通信を可視化してくれる等、これがない開発は考えられないくらいに便利な代物なので HTTP/3, QUIC のライブラリを利用する際には対応を確認するのをお勧めします。
qlog/qviz のより詳細な内容が気になる方は qlog/qvizでQUICの可視化 及び 「Debugging Modern Web Protocols with qlog」の紹介 にて neko-suki さんが解説してくださっているので、そちらを参照ください。
さて、そんな便利 qlog ですが、 その壱 でも触れたように quiche も勿論対応しています1。
ただ、デフォルトのビルドでは qlog の出力は無効になっているので、Cargo.toml
を修正して有効にしてあげる必要があります。
と言っても、[dependencies]
に依存関係が設定されているので、[features]
を以下に書き換えるだけで OK です。default = ["boringssl-vendored", "qlog"]上記でビルドした quiche をリンクした上で、
quiche_conn_set_qlog_path
を呼び出すことにより qlog が出力されるようになります。// Enables qlog to the specified file path. Returns true on success. bool quiche_conn_set_qlog_path(quiche_conn *conn, const char *path, const char *log_title, const char *log_desc);引数に
quiche_conn
という quiche の QUIC ハンドルを取ることから分かるように、出力される単位は QUIC のコネクション単位です。
path
に既に存在するパスを指定してもファイルを上書き生成してくれます。quiche のビルドについては その壱 を参照してください。
輻輳制御について
その壱 でも触れたように、 quiche は輻輳制御アルゴリズムとして NewReno 及び CUBIC に対応しています。
quiche_config_set_cc_algorithm
にて指定可能です。enum quiche_cc_algorithm { QUICHE_CC_RENO = 0, QUICHE_CC_CUBIC = 1, }; // Sets the congestion control algorithm used. void quiche_config_set_cc_algorithm(quiche_config *config, enum quiche_cc_algorithm algo);さらに、最近 HyStart++ にも対応しました。
HyStart++ は RTT を閾値として TCP のスロースタートを改善するアルゴリズムです。
詳細を知りたい方は、日本語の解説では flano_yuki さんの TCP Slow Startを改善する HyStart++について - ASnoKaze blog を、英語であれば本家 CloudFlare の記事である CUBIC and HyStart++ Support in quiche を参考にしてみると良いと思います。HyStart++ を有効にするには
quiche_config_enable_hystart
を呼び出します。// Configures whether to use HyStart++. void quiche_config_enable_hystart(quiche_config *config, bool v);信頼されたルート証明書の一覧について
quiche が依存している BoringSSL は OpenSSL の動きに準拠しています。
よって、何も指定しない場合は システムのデフォルト の信頼されたルート証明書の一覧が使われます。// SSL_CTX_set_default_verify_paths loads the OpenSSL system-default trust // anchors into |ctx|'s store. It returns one on success and zero on failure. OPENSSL_EXPORT int SSL_CTX_set_default_verify_paths(SSL_CTX *ctx);更に、信頼されたルート証明書の一覧のファイルパスを指定する機能が https://github.com/cloudflare/quiche/issues/418 で追加されました。
システムデフォルトオンリーの動作はデバッグ時や開発時に不便なことがあるので嬉しい限りです。
と喜んだのも束の間、この機能は C ラッパ側にはまだ反映されていないようです。
そのうち追加されると思うので、今回はスルーしてしまおうと思います。
必要な場合は自分で簡単にラッパ層に追加可能だと思うので、自己対応しちゃっても良いと思います。コネクションクローズとライフタイム
quiche では QUIC コネクションは
quiche_conn
で、 HTTP/3 コネクションはquiche_h3_conn
によって管理されます。// A QUIC connection. typedef struct Connection quiche_conn; // A QUIC connection. typedef struct Http3Connection quiche_h3_conn;これらのコネクションに紐づける形でリクエストのストリームを
quiche_h3_send_request
により生成します。// Sends an HTTP/3 request. int64_t quiche_h3_send_request(quiche_h3_conn *conn, quiche_conn *quic_conn, quiche_h3_header *headers, size_t headers_len, bool fin);
quiche_conn
及びquiche_h3_conn
はコネクション切断時に各々の終了関数でクローズする必要があります。
対して、ストリームの明示的な解放は不要で、quiche_h3_conn_poll
で取得したステータスがQUICHE_H3_EVENT_FINISHED
になると自動的に解放されるようです。
コネクションがクローズされた際にはquiche_conn
及びquiche_h3_conn
は使いまわすことはできず、新しく生成し直す(socket も開き直す)必要があります。タイムアウトについて
その弐 ではタイムアウトに関連する処理をばっさりカットして解説を行いました。
しかし、タイムアウトは製品の実運用時にはとても重要な要素です。
よって、今回ではしっかり確認していきたいと思います。まずは quiche のタイムアウト関連の関数を確認してみます。
// Sets the `max_idle_timeout` transport parameter. void quiche_config_set_max_idle_timeout(quiche_config *config, uint64_t v); // Returns the amount of time until the next timeout event, in nanoseconds. uint64_t quiche_conn_timeout_as_nanos(quiche_conn *conn); // Returns the amount of time until the next timeout event, in milliseconds. uint64_t quiche_conn_timeout_as_millis(quiche_conn *conn); // Processes a timeout event. void quiche_conn_on_timeout(quiche_conn *conn);これだけだとよく分からないので、 Rust 側のリファレンスや nginx での組み込み方法2、実動作を確認をしてみた所、大体以下のような仕様のようです。
quiche_config_set_max_idle_timeout
でタイムアウト(ミリ秒)を指定する
- QUIC トランスポートパラメータである
max_idle_timeout
を設定するquiche_conn_timeout_as_nanos
/quiche_conn_timeout_as_millis
で 0 が返ってきたらタイムアウトしたと判断してquiche_conn_on_timeout
を呼び出す- 更に
quiche_conn_is_closed
を呼んでコネクションのクローズ処理を行うquiche_conn_timeout_as_nanos
/quiche_conn_timeout_as_millis
はquiche_conn_close
を呼び出した後にも 0 を返す ※ポイント1max_idle_timeout
に 0 を指定することでタイムアウトが発生しなくなる ※ポイント2ポイント1, ポイント2 それぞれにハマり所があるので少し補足します。
ポイント1 :
quiche_conn_close
呼び出し時の動作中断等の処理でクライアントから明示的にコネクションを切断したい場合には
quiche_conn_close
を呼び出します。
しかし、quiche_conn_close
を呼び出した後にquiche_conn_timeout_as_nanos
/quiche_conn_timeout_as_millis
を呼び出すと何故か 0 が返ってきます。
さらにこのタイミングできちんとquiche_conn_on_timeout
を呼び出してあげないとquiche_conn_is_closed
が true を返してくれず、切断判定ができません。
また、ソケットエラーで通信が失敗した際等もquiche_conn_timeout_as_nanos
/quiche_conn_timeout_as_millis
が 0 を返すタイミングがありました。上記の流れについては quiche のソースコードまで確認できていないので、認識や理解が怪しい部分があります。
現状動作検証の結果ベースで実装していますが、このままだと事故りそうなので、しっかりと仕様を理解した後に当記事へ追記し、 http3sharp 側も修正しようと思います。ポイント2 :
max_idle_timeout
への 0 指定時の動作とユーザタイムアウトの実装QUIC のトランスポートパラメーターである
max_idle_timeout
には、 0 を指定するとタイムアウトが無制限になるという仕様があります。
quiche もこの仕様に準拠している為、quiche_config_set_max_idle_timeout
に 0 を設定するとタイムアウトが発生しなくなります。以下は QUIC の draft にある
max_idle_timeout
の定義です。max_idle_timeout (0x01): The max idle timeout is a value in milliseconds that is encoded as an integer; see (Section 10.2). Idle timeout is disabled when both endpoints omit this transport parameter or specify a value of 0.実際の運用でタイムアウトが発生しないのは困るので、例えば、ゲームにおいては
quiche_config_set_max_idle_timeout
には 5 ~ 10 秒程度の値を設定したいケースが発生しそうです。
しかし、 quiche では、リクエストを発行していない状態でquiche_config_set_max_idle_timeout
に設定した時間が経過するとタイムアウトが発生してしまいます(ハンドシェイク時は除く)。
これは、 quiche が PING フレームを用いたコネクションの維持3を実装していないことに起因します。つまり、現状の quiche では、HTTP/1.1 時代の Keep-Alive のように、リクエストを発行していない状態でコネクションを維持させておくことができません。
対策として以下の実装を行う事でこの問題を回避できそうです。
- 自分で PING フレームを送る実装を行う
quiche_config_set_max_idle_timeout
には 0 を指定し、リクエスト通信中には受信データが 0 の期間が一定時間経過した場合にタイムアウトとしてクライアント側からコネクションを切断するhttp3sharp では現状いずれの実装も入れていませんが、将来的には 2 の実装を導入することになると思います。
ただ、 HTTP/3 には 0-RTT があるのでコネクション切れてもいいやん、という説もあるので入れないかもしれません4。stream の取り扱い
quiche_h3_send_request
で HTTP/3 のリクエストストリームを生成した後は、quiche_h3_conn_poll
でステータスを監視する必要があります。
その弐 のサンプル実装時にも言及しましたが、quiche_h3_conn_poll
は処理を行う必要のあるストリームの ID を返します。
なので、 Stream クラスを作成し、その中でquiche_h3_conn_poll
を呼んでも、自分 以外 のストリーム ID に紐づいたイベントが返ってくることがあり、うまくいきません(最初この設計にしてハマりました……)。
Stream クラスではなく、より上層でquiche_h3_conn_poll
で取得したストリーム ID に対して処理を呼び出す必要がある点に注意してください。quiche のエラーについて
quiche のエラーは
quiche_error
及びquiche_h3_error
にて定義されています。
しかし、「どの関数でどの値が返るのか」、「ある値が返った場合に何をしたらいいのか」が明記されていないので、若干処理に困るケースがありました。
動作未確認のものは多いですが、 qwfs にて quiche エラーコンバート関数を作成しているので参考にしてみてください。
Connection::ConvertQuicheErrorToStatus
及びStream::ConvertQuicheH3ErrorToStatus
にて処理しています。実装方針について
quiche についての理解が深まったので、次は自分で作るライブラリの実装方針を考えてみます。
とは言え、ここで細かい実装について説明し出すとより一層長大な解説になってしまうので、今回の記事では実装方針の紹介に留めます。
実装が気になる方はHttp3Sharp.cs
に最低限のリファレンスは記載しておいたので、そこから見ていってみてください。コネクションとリクエストの管理を分ける
まず第一に、多重化を踏まえて HTTP/1.1 までと考え方を変える必要があります。
HTTP/1.1 までは Keep-Alive でコネクションを使いまわすことはあっても、 1 リクエスト 1 コネクションで設計を行う事が普通でした。
しかし、HTTP/3 や QUIC では、コネクションそのものの管理とリクエストの管理は別になります(これは HTTP/2 からの変更でもあります)。
リクエストは失敗したけどコネクションは生きている、という状況もあり得ないとは言い切れません。
この為、コネクション全体のステータスとリクエスト単位のステータス・結果を別に取得できるような設計にする必要があります。
そこで、 http3sharp ではインスタンス全体のステータスHttp3Sharp.Status
と、個別のリクエスト結果Http3Sharp.ResponseParamaters
内のステータスを別々にアプリ側に渡す仕様としました。
また、中断Http3Sharp.Abort
時にも、コネクションを一度破棄した後に自動で再度ハンドシェイクを行う実装として、次のリクエストをなるべく早く送信可能できるようにしています。インスタンスの管理
多重化はホストとポートを合わせた
Authority
単位で行われます。
よって、 http3sharp のインスタンス管理もこのAuthority
単位で行うのが取り回しが良さそうです。
ということで、コンストラクタでホスト名とポート、それからコネクションのオプション設定を受け取る設計としましたpublic Http3Sharp(string hostName, string port, ConnectionOptions options)Authority 単位でも別インスタンスにしたい場合もあるかもなので、固有 ID を生成する関数を用意して、その ID でインスタンスを管理する実装の方がいいかもしれません。
しかし、同一ホストへファイルの受信と API 通信を行う、というような場合はポートを分けたりサブドメインを使う場合が多いと思われますので今回はこの対応は行いませんでした。膨大なリクエスト数の想定
Unity でのゲーム開発はリソース数が膨大になりがちで、データの一括ダウンロード時に 4 ~ 5 桁のリクエストが発行されることはざらにあります。
この為、数千数万のリクエストが発行されたとしても、その処理のオーバーヘッドはなるべく少ない設計にしておきたいところです。
これを踏まえ http3sharp では以下の設計としました。
- C# 側ではなるべくステータスや管理リストは保持せずに Native 側に追いやる
- リクエスト単位で処理経過・結果を舐めるような処理はなるべくしたくないので、C# - Native 間ではコールバック形式で結果を取得する
- C# の for はとても遅いのでなるべく呼ばない設計としておく
- ファイル I/O 等重い処理があり実行ループが処理落ちすると通信バッファを拾いきれず、通信のパフォーマンスが落ちることがあるので、実行ループ部は非同期に処理する
- libev は Android や iOS での動作が不安なので使わない
- C++ のスレッド実装はマルチプラットフォーム対応が面倒なので、とりあえずは C# のスレッドに逃げたい
- 非同期のロックの回数を減らす為にユーザが呼び出す関数はなるべく少なくする
ファイル I/O の負荷分散等できることはまだまだありますが、細部の実装については後から改善できるので、上記を大方針として実装を行いました。
現状マーシャリングとかその他のメモリ管理もかなり無駄のある実装ですが、この辺も将来的にぼちぼち直していければ、という感じです。
string とかもやばいですね。以下、スレッド側の処理です。
private void UpdateQwfs() { while (true) { if (_cToken.Token.IsCancellationRequested) { break; } QwfsResult result; if (1 == Interlocked.Exchange(ref _requestRetry, 0)) { lock (_lockObject) { result = qwfsRetry(_hostId); } Debug.Assert(QwfsResult.Ok == result); } if (1 == Interlocked.Exchange(ref _requestAbort, 0)) { lock (_lockObject) { result = qwfsAbort(_hostId); } Debug.Assert(QwfsResult.Ok == result); } lock (_lockObject) { result = qwfsUpdate(_hostId); // ここの結果は色々返るが status 側で見るのでスルー(要改善) } // todo : 処理負荷を見た wait Thread.Sleep(1); } _cToken.Dispose(); }基本、ロックして Native 実装側の qwfs 内部を更新する
qwfsUpdate
を呼び出しているだけです。
Sleep 処理とか雑過ぎるのでさっさと直したいところですね。以下はアプリ側が呼び出す、レスポンスとステータス、プログレスを取得する関数です。
public List<Http3Sharp.ResponseParamaters> Update(out Http3Sharp.Status status, out ulong progress, out ulong totalWriteSize) { // Update スレッドの監視とか本当はした方がいいがとりあえずなし status = Http3Sharp.Status.Wait; progress = 0; totalWriteSize = 0; _responseForEachHost[_hostId].Clear(); lock (_lockObject) { var result = qwfsGetProgress(_hostId, out progress, out totalWriteSize); Debug.Assert(QwfsResult.Ok == result); result = qwfsIssueCallbacks(_hostId); Debug.Assert(QwfsResult.Ok == result); result = qwfsGetStatus(_hostId, out status); Debug.Assert(QwfsResult.Ok == result); } return _responseForEachHost[_hostId]; }レスポンスとステータス、プログレスは別々の関数で取得する方が自然ですが、先ほど書いたように lock の回数を減らす為にまとめて取得する仕様にしています。
個の実装だと qwfs の処理負荷が高い時にアプリ側ががっつり固まってしまうので本来であればMonitor.TryEnter
のようなものを使った方が良いですが、とりあえず雑に実装してあります。リトライについて
コネクション全体の失敗時には、内部的に失敗したリクエストのリストを保持して、リトライ関数を呼び出すだけで失敗したリクエスト全てを再送信する実装としました。
これもリクエスト数が多い場合に C# 側での処理負荷が上がるのを防ぎたい、リストの二重管理をしたくないという動機からの実装です。
反面、現状では個別のリクエストの失敗に対するリトライについては、前述したHttp3Sharp.Update
の戻り値のリストを見てあげないといけない実装です。めんどうです。
改善しようと思います。リクエストとレスポンスの管理
これまで説明してきたように、
しかし、あまりに仕様を簡略化し過ぎてしまった為に、現状出したリクエストに対してHttp3Sharp.Update
で返ってきたレスポンスの紐づけができません。
この仕様では、API 通信を複数同時に行い、個別の対応を行いたい場合等で面倒なケースが発生します。
QUIC DATAGRAM に対応する事になった際にも相性が悪そうです。
そこで、将来的にはHttp3Sharp.RequestParamaters
にレスポンス取得用のコールバック関数を登録し、Http3Sharp.Update
内でそれを呼ぶ形に変更するのが良さそうです。やらないことリスト
まずは動かすのが今回の主眼なので、以下の内容は後回しとしました。
- 細かいエラーハンドリング
- socket やコネクションのきちんとしたクローズ・後始末
- メモリや処理負荷の最適化関連の実装 (設計面ではある程度考慮)
- テスト関連
実装でハマった問題の共有(未解決問題含む)
実装を進める中で、 QUIC ならではの内容を含む問題をいくつか踏んだので共有します。
解決に苦戦し(解消できてないものもあります……)、本記事の公開に影響を与えたレベルのものもあるので、皆さんが HTTP/3 の通信実装をする際の参考になれば幸いです。今回 http3sharp の実装では、
- テストサーバとして aioquic の
examples/http3_server.py
をメインに使用- 上記の動作がおかしい場合のセカンドとして CloudFlare の
blog.cloudflare.com
にアクセスさせて頂くという形で動作確認を行いながら実装作業を進めました。
まだまだどこの OSS も枯れていないので、複数の動作検証環境を用意しておくと問題発生時に切り分けが容易になるのでお勧めです。それでは、個別に問題を見ていきましょう。
Unity Editor で再生 → 終了 → 再生すると Unity Editor ごと落ちる
Unity Editor 内の plugin フォルダに quiche.dll を配置し、
quiche_enable_debug_logging
で quiche 内のデバッグログの出力設定を行ってUnity Editor の 再生 → 終了 → 再生 と実行すると Unity Editor がクラッシュします。
一度目の Unity Editor 終了時に quiche 内のstatic mut
な変数が解放されるのが原因のようです。
Rust をよく理解できていないので何故解放されてるかも理解できていませんが、 Unity Editor の plugin フォルダに .dll を配置すると Editor 再生後は .dll を握りっぱなしになるので、そのあたりの兼ね合いだとは思います。
LoadLibrary
で動的に dll をロード・アンロードする実装に変更すれば当問題は回避できると思うので、将来的に試して見ようと思います。その他 Rust と Unity Editor の相性はあまりよろしくないようのか、Visual Studio のデバッガを繋いだ状態でうかつなことをしたり、通信中に Editor 再生を停止すると Editor がそっとお亡くなりになることが多々あります。。
根本的な原因の理解ができていないので、まずは Rust 側の知見を集めるべきかな、と感じています。129 個目の以降のリクエストが発行できない
開発も終盤、いよいよ多重化の動作確認を作成する段で、
quiche_config_set_initial_max_streams_bidi
でinitial_max_streams_bidi
の値を 129 以上に設定しても、129 個目のリクエストの生成を行うquiche_h3_send_request
で必ず処理が失敗してしまう
という問題を踏みました。
この問題の解決には今回一番苦戦したので、少し長くなりますが詳細を紹介しようと思います。まず、
quiche_h3_send_request
の失敗時の関数の戻り値を確認すると -12 が返ってきており、// The peer violated the local stream limits. QUICHE_ERR_STREAM_LIMIT = -12,どうやらストリームの生成上限に引っかかっているようです。
QUIC のストリームの上限値に関する仕様は複雑で、正しく理解するには
initial_max_streams_bidi
及びMAX_STREAMS
フレームの仕様を把握する必要があります。
まず、これらの値はどちらも peer(通信相手) の双方向ストリームの作成上限数5 ですが、以下の違いがあります。
initial_max_streams_bidi
- トランスポートパラメータに含める初期の上限値
MAX_STREAMS
フレーム
initial_max_streams_bidi
の値を更新する必要な際になった際に都度送信する上限値更に、これらの値は 現在通信中のストリーム数 ベースではなく、クローズ済みのストリームも含む累計値の上限値 であることにも注意が必要です。
詳細は QUIC draft の 4.5. Controlling Concurrency 及び 19.11. MAX_STREAMS Frames を参照してください。ちなみに、
quiche_config_set_initial_max_streams_bidi
で設定する値も peer(通信相手) の双方向ストリームの作成上限数 です。
私も完全に勘違いしていましたが、クライアント側が作成可能なリクエストの上限数ではないことに注意してください。さて、
initial_max_streams_bidi
及びMAX_STREAMS
フレームの仕様を理解したところで、具体的に何が起きていたかを解説します。
- aioquic からは
initial_max_streams_bidi
として 128 が降ってきていた- quiche は 128 個目のリクエストまでは正常に処理を行っていたが、
MAX_STREAMS
による上限値の更新がなかったためにストリーム生成数の上限に達してquiche_h3_send_request
で失敗を返すようになった動作をまとめると非常にシンプルですが、ここに二つほど問題が潜んでいました。
- サーバから
MAX_STREAMS
フレームが送信されてきていない- クライアントから
STREAMS_BLOCKED
フレームを送信していない1 については、aioquic のコードを見る限りは未実装のようです。
_handle_max_streams_bidi_frame
という関数でMAX_STREAMS
フレームを作成するようですが、この関数が呼び出されていないようでした。
サンプルサーバなので実装を省いた可能性もあります。2 については、まず
STREAMS_BLOCKED
フレームについて少し補足します。
STREAMS_BLOCKED
はストリームの累計生成数が peer から指定されたMAX_STREAMS
に到達してしまった際に更新を要請するフレームです。
詳細は QUIC draft の 19.14. STREAMS_BLOCKED Frames を参照してください。
今回発生している状況もまさしくこのシチュエーションですが、quiche_h3_send_request
で上限数を超えていた場合にもSTREAMS_BLOCKED
フレームを送信せずに処理を継続してしまっているように見受けられます。aioquic, quiche 共に実装の理解は非常に浅いので、私の勘違いもあるかもしれません。
もう少し詳細を追ってみて、問題が確定したら報告しようと思います。多重化通信ができない
Unity のサンプルを作成している途中で、何やら 1 リクエストずつ処理が進んでいる気配を感じ取りました。
怪しいときはすぐに qlog/qviz で確認!ということで見てみたところ、以下のように多重化がされていないことがすぐに判明しました。qlog/qviz は神。
色のついた棒が通信の開始と終了を表しているので、上記の画像だと順次リクエストが処理されていることになります。
同じ実装で CloudFlare に接続すると以下のようにきちんと多重化されています。
ということで、これは aioquic のサーバサンプルの問題のようです(その他の実験公開されている HTTP/3 サーバでも同様の動きを示すものが少しだけありました)。
サンプルコードの設定等によって回避可能かまでは追っていませんので、もしかしたらデフォルトの仕様かもしれません。
追加調査したらここに結果を記載しようと思います。Rust のデバッグ
ハマった問題の共有ではないですが、ハマった時に非常に便利だったので Rust のデバッグについても少し触れます。
その弐 で記載したように、Visual Studio 2019 を使っていると Unity にデバッガをアタッチした状態でそのまま quiche の中にもぐることができます。
しかし、 quiche のデフォルトの設定では debug ビルドでも最適化が働いており、変数や関数そのものが削除されていたりすることが多くあります。
こんな感じでそもそもステップインできなかったりします。かなしい。そこで、ビルド時に最適化オプションを変更してあげるとデバッグ可能になります。
最適化レベルは[profile.dev]
や[profile.release]
のopt-level
で設定できます。
参考 : https://doc.rust-jp.rs/book/second-edition/ch14-01-release-profiles.html
結果、かなり細かく見れるようになりました!http3sharp を用いて Unity 上で HTTP/3 通信を行うサンプルの紹介
数々の問題を乗り越え、ようやく Unity での動作サンプルの解説までたどり着くことができました。
今回 http3sharp のサンプルとして以下を用意しています。
- SingleRequestSample
- リクエストを一つ送信し、受信したボディを画面にテキストで表示するサンプル
- MultiRequestSample
- 多重化を用いて同時に複数のリクエストを送信するサンプル
どちらのサンプルも基本的には aioquic のサンプルサーバをローカルに立てて動作確認を行ってますが、パラメータを適切に設定してあげれば外部のサーバとも通信可能です。
QUIC base-drafts の Implementations に掲載されている実験用サーバにも是非接続してみてください。
※多重化のサンプルの方は割と負荷を掛けるので、ローカルでないサーバと通信する場合は同時接続数を減らす等利用にご注意くださいSingleRequestSample
SingleRequestSample は任意の宛先に HTTP/3 リクエストを送信し、受信したレスポンスのボディを表示するサンプルです。
[GameObject]Http3SharpHost
にて宛先や証明書の検証の有無等のパラメータを設定可能です。
特に説明が必要なパラメータもないので、以下で実装の軽い解説をしたいと思います。
Http3SharpManager
Native ライブラリのインスタンス管理の都合上、 http3sharp では初期化・終了関数を必ず一度ずつ呼び出す必要があります。
- 初期化関数 :
Http3Sharp.Initialize
- 終了関数 :
Http3Sharp.Uninitialize
当サンプルでは上記関数を呼び出す Http3SharpManager 実装し、これを
DontDestroyOnLoad
指定することで、シーン遷移時ではなくアプリ終了時にHttp3Sharp.Uninitialize
を一度呼び出す一般的な作りとしています。Http3SharpSampleCore
http3sharp の実処理を呼び出し・管理するクラスです。
各サンプルのUpdate
でHttp3SharpSampleCore.Http3.Update
を呼び出すことにより http3sharp のステータス及び完了したリクエストの取得を行います。
実装はシンプルなのであまり解説する個所もないですが、ステータス管理とプログレスについて補足します。
- ステータス管理について
- リトライ
- http3sharp ではリトライ中かどうかというステータスを持っていない為、
Http3.Retry
を呼び出した どうかのフラグを持たせる、若干煩雑な実装になっています- 実際のゲームではリトライボタンとスタートボタンは共通か、別途ウィンドウ表示することがほとんどなのであまり問題にならないかと思います
- アボート
- http3sharp は
Http3.Abort
が呼び出されると内部的に保持しているリクエストを全て破棄し、コネクション切断後に再度コネクションを張り直しますHttp3Sharp.Status.Aborting
中にHttp3.Abort
を呼び出しても何も起きませんが、分かり易さの為に呼び出せない実装としてあります- プログレスについて
- http3sharp では
Http3.Update
呼び出し間でダウンロードしたデータ量と、今まで通信したデータ量を取得できます- 今まで通信したデータ量は、
Http3.Status.Wait
に戻った状態でリクエストを発行すると初期化されますちなみに、このクラスは
MultiRequestSample
でも併せて利用しています。Http3SharpSampleSingle
リクエストの作成とボディの表示を行うクラスです。
失敗時は Core 側でエラー原因の表示まで行うので、成功時の処理しか実装していません。
……特に解説することがありませんのでサクッと次にいきましょう。MultiRequestSample
MultiRequestSample は、任意の宛先に複数の HTTP/3 リクエストを多重化して送信し、その進捗を表示するサンプルです。
[GameObject]Http3SharpHost
にて宛先や証明書の検証の有無等のパラメータを設定可能です。
1k ~ 1M のサイズのデータのリクエスト(※)をDownload File Num
個行い、Work Dir
に 0 から連番(拡張子無し)で保存します。
※ aioquic のサンプルサーバのGET/NNNN
機能(NNNN で指定した数Z
が返ってくる)を利用しています
Max Multiple Download Num
はinitial_max_streams_bidi
とは別に http3sharp で独自に管理する同時にリクエスト可能な最大値です。
initial_max_streams_bidi
とは異なり、累計ではなく現存リクエストしている数を指定できます。前述したようにメインの処理は
Http3SharpSampleCore
に任せている為、実装面の解説は割愛します。結び
以上で「Re: C#(Unity)でHTTP/3通信してみる その参 ~Unityから使ってみる~」は終了です。
HTTP/3 を利用してみるにはまだ時期尚早感はありますが、これを見て「自分も使ってみたい」という方が一人でも増えると嬉しく思います。また、 その壱 から合わせると結構な長文となってしまいました……。
ここまでお付き合い頂きました方、本当にありがとうございます。
今回公開した http3sharp は今後チマチマ改善していこうと思っていますので、使ってみた方がいましたら是非フィードバックをお願いします。
https://github.com/TakeharaR/http3sharp に Issue を作るでも、 Twitter やここでコメントを頂く形でも構いません。次回はちょっと毛色を変えて音声認識エンジンである Julius 触ってみた記事でも書こうかなと思っています。
それではまた。
最近 qlog 0.3.0 にも対応したようです → https://github.com/cloudflare/quiche/pull/526 ↩
QUIC にはコネクションを維持する為に PING フレームというフレームを 送信しても良い という仕様があります。詳細は QUIC draft の 19.2. PING Frame を参照してください。 ↩
インスタンスの再生成等の負荷を考えると PING フレームでの維持の方がゲーム的には都合が良さそうではあります。 ↩
正確にはタイプ指定により単方向ストリームの上限数も変更可能です ↩
- 投稿日:2020-06-03T01:07:56+09:00
【Unity 2019】画面が激しくテアリング(波打ち)する問題の回避方法
Unity 2017.2 から Unity 2019.2 に乗り換えました
プレハブの編集機能が神です。
しかし、Windows ビルドでフルスクリーンモードの時に「テアリング」という画面が波打つ現象がやたらと気になるようになりました。VSync がオフだからでは?
いや、VSync は両バージョンでオフだったんですけど、Unity 2017 の時はこんなに酷くなかった。
何かあったに違いない…。原因と対策
Unity 2019 からはフルスクリーンモード時のデフォルトの画面転送方式が変わったようです。
(BitBlt → DXGI flip model swapchain)どうもこの DXGI flip model swapchain でテアリングが起きてしまっているようです。
Player 設定をよく見たら以前の BitBlt 方式に戻すオプションが追加されていました。
ここをオフにしたらテアリングはなくなりました。めでたしめでたし。VSync をオンにするのではダメ?
自分は2Dアクションゲームを作っていますが、VSync がオンだとびびるほど操作感がもっさりします。
もっさり感は静止画や動画の比較では伝わらないのが残念…。今回の対策でテアリングも気にならなくなったので、個人的にはゲーム側の VSync 設定はオフ一択です。
(ただ、念のためオン・オフを切り替えるオプションは実装しようと思います)