AtsEXを使ってBVEの補助表示を作ろう

**BveEX対応は気が向いたらやります**

新高校部長になりました宮島です。
今回は、AtsEXを駆使してBVEの補助表示を触ろうかと思います。
また、今回は長くなりそうなので **AtsEXやC#の基本的な扱いはできる**という前提で話を進めていきます。
WindowsPCが壊れていて、Macで書いているので間違ってたら都度修正します。
それでは早速...

# 前提知識
まずは、制作にあたっての前提知識をおさらいしておきましょう。
DX9やBVEの知識がある方は読み飛ばしてください。
### ・DirectXの仕様について
DirectXの基本的なスタンスは、「毎フレームごとに描画する」というものです。
ScratchやUnityのように、画像を一回配置すれば変更があるまで置かれるみたいなことはありません。
また、今回はUIなのであまり深掘りしませんが、ワールド座標を指定しなければ描画位置を変更することはできません。

### ・BVE側の実装について
BVE側では、３つのレイヤーが用意されています。一つは今回使うUIのレイヤー、もう一つは運転台パネルのレイヤー、最後に3Dモデルが配置できる路線のレイヤー(ここだけ3D)です。
今回はUIのレイヤーにパッチを当てる形式で実装します。

### ・実装の方針について
コンストラクタで3Dモデルを作成、（厳密には違いますが）毎フレーム発火されるイベントに登録し、その中でBVEにパッチを当てる描画系の処理を描きます。

<br>
大方の面倒な作業はBVEとAtsEXで肩代わりしてくれていますので、これぐらいの認識でいいでしょうか。<br>
次の項目へ進みましょう。  

# 参照の追加
NugetよりTypeWrapping,FastMember,HarmonyPatch,SlimDXを追加して以下のものをスクリプトに追加。
```C#
using FastMember;
using TypeWrapping;
using ObjectiveHarmonyPatch;
using SlimDX;
using SlimDX.Direct3D9;
```
これでひとまず必要なものは揃いました。AtsEX系(BveTypesとか)は書いてくうちに入ると思います。

# モデルの初期化
とりあえず、画面の中心に画像を一枚描画しましょう。
フィールドに以下のように記述します。
```C#
Model model;
```
次に、コンストラクタに以下のように記述します。
```C#
string texFilePath = Path.Combine(Path.GetDirectoryName(Location),@"Path_to_your_imageFile");
RectangleF rectangleF = new RectangleF(150, 150, 300, 300);
model = Model.CreateRectangleWithTexture(rectangleF, 0, 0, texFilePath);//四角形の3Dモデル
```
各関数、変数の説明から。
### Location
一行目のLocationは、AtsEX側で用意されている変数です。
このアセンブリファイル(PluginUsingで宣言されているファイル)までのパスを取得できます。
Path.Combine()の第２引数は、このアセンブリファイルから描画したい画像までの相対パスです。
### RectangleF
System.Drawingで宣言されているもので、長方形を作成できます。
第一引数には、x軸の中心点(Unityで言う所のx軸のピポット)を、
第二引数は、それのY軸を、
第三引数には、x軸方向の幅(横幅)の長さを、
第四引数は、それのy軸（縦幅）を、それぞれ記入します。
各引数はピクセル数での表記となっているので、intです。
勿論、これに引数を入れて
```C#
int width = Direct3DProvider.Instance.PresentParameters.BackBufferWidth;//ウインドウの横幅
int height = Direct3DProvider.Instance.PresentParameters.BackBufferHeight;//その縦幅
RectangleF rectangleF = new RectangleF(width / 2, height / 2, width, height);
```
こういう事もできます。（ただ、この場合だと解像度の初期値で固定になります）

### CreateRectangleWithTexture
長ったらしい関数名ですが、読んで字の如く **「四角い2Dモデルを作る」** ものになります。
これはBVE側で用意されているものになります。本来なら面倒臭い手順を踏まなければいけないのですが、気にする必要はありません。
mackoyさんとRock_Onさんに感謝...
第一引数にはRectangleF型（さっき作ったもの）を、
第二引数にはz軸の位置（UIなので今回は0で構いません）を、
第三引数には透過色(ここに何かしらの色を設定するとその色を透過できる)を、
第四引数にはテクスチャのパスを記述します。
以下のように記述することになります。
```C# 
model = Model.CreateRectangleWithTexture(rectangleF, 0, 0, texFilePath);
//ここでのmodelは最初の項目で初期化した変数
```
# イベントの設定
先ほどの通り、毎フレーム別処理で描画する感じになるので、そのイベントを設定します。
ラムダ式で記述してもいいですが、今回はメソッドを分けてみました。
先ほどのmodelの初期化の後に以下のように書きます。（ここはラムダ式の記述でも共通）
```C#
ClassMemberSet assistantDrawerMembers = BveHacker.BveTypes.GetClassInfoOf<AssistantDrawer>();
FastMethod drawMethod = assistantDrawerMembers.GetSourceMethodOf(nameof(AssistantDrawer.Draw));
drawPatch = HarmonyPatch.Patch(Name, drawMethod.Source, PatchType.Prefix);
```
パッチを当ててるらしいですが、詳しいことはわかりません（本当はよろしくないんですが）
次に、これの後にイベントの登録（またはそのラムダ式）を書きます。
```C#
drawPatch.Invoked += DrawPatch_Invoked;
```
次に、フィールドに
```C#
private PatchInvokationResult DrawPatch_Invoked(object sender, PatchInvokedEventArgs e)
{
    return PatchInvokationResult.DoNothing(e);
}
```
こちらを。
描画処理はこの中に書いていくので以後「DrawPatch_Invoked()」と呼びます。

# 描画処理
では、メインの描画処理について書いていきましょう。
ワールド座標の設定から入りましょう。
DrawPatch_Invoked()の中に記述します。
第一引数は画面上のx座標の位置（中心が0）
第二引数は同様に画面上のy座標の位置
第三引数はz軸の位置（今回はUI要素なので0でOK）
を表します。
（中心が原点、x軸の正方向は右側、y軸の正方向は上）
例えば、画面の左上1/4のところにあるようなものは
```C#
Device device = Direct3DProvider.Instance.Device;
int width = Direct3DProvider.Instance.PresentParameters.BackBufferWidth;
int height = Direct3DProvider.Instance.PresentParameters.BackBufferHeight;
device.SetTransform(TransformState.World, Matrix.Translation(-width/4, height/4, 0));
```
です。(widthとheightはRectAngleFの項目で説明したので省略)
Device device = ~~　はこの後必要になりますので記述しといてください。

次に、描画。
```C#
model.Draw(Direct3DProvider.Instance,false);
```
これで描画処理は終了です。

# 開放
Model型ではDispose()メソッドを呼ぶことができます。
Dispose()メソッドで呼びましょう。
```C#
model.Dispose();
```
これだけ。

# 総括
簡単にでしたが、DirectXで補助表示を作成する方法についてまとめてみました。
以下は全コードです。物足りない方はおまけで用例をのっけたのでご参考まで。
``` MapPlugin.cs
using System;
using AtsEx.PluginHost.Plugins;
using BveTypes.ClassWrappers;
using FastMember;
using TypeWrapping;
using ObjectiveHarmonyPatch;
using AtsEx.PluginHost.Native;
using AtsEx.PluginHost;


namespace Sample
{
    internal class Sample
    {
        Model model;
        public MapPluginMain(PluginBuilder builder) : base(builder)
        {
            string texFilePath = Path.Combine(Path.GetDirectoryName(Location),@"Path_to_your_imageFile");
            RectangleF rectangleF = new RectangleF(150, 150, 300, 300);
            model = Model.CreateRectangleWithTexture(rectangleF, 0, 0, texFilePath);
            ClassMemberSet assistantDrawerMembers = BveHacker.BveTypes.GetClassInfoOf<AssistantDrawer>();
            FastMethod drawMethod = assistantDrawerMembers.GetSourceMethodOf(nameof(AssistantDrawer.Draw));
            drawPatch = HarmonyPatch.Patch(Name, drawMethod.Source, PatchType.Prefix);
            drawPatch.Invoked += DrawPatch_Invoked;
        }
        private PatchInvokationResult DrawPatch_Invoked(object sender, PatchInvokedEventArgs e)
        {
            Device device = Direct3DProvider.Instance.Device;
            int width = Direct3DProvider.Instance.PresentParameters.BackBufferWidth;
            int height = Direct3DProvider.Instance.PresentParameters.BackBufferHeight;
            device.SetTransform(TransformState.World, Matrix.Translation(-width/4, height/4, 0));
            model.Draw(Direct3DProvider.Instance,false);
            return PatchInvokationResult.DoNothing(e);
        }
        public override void Dispose()
        {
            model.Dispose();
        }
        public override TickResult Tick(TimeSpan elapsed)
        {
            return new MapPluginTickResult();
        }
    }
}
```
これで多分動くと思います。動かなかったらご連絡ください。

# おまけ
普通にわかると思いますが、**初期化さえすれば**配列型とかも使えます。便利。
（Unityは初期化も代わりにやってくれるので、初期化作業を知らない人もいるみたいなので一応...）
あと、他のスクリプトに機能を分散させることも可能です。
そこらを踏まえて以上のコードを書き直してみますと以下の2ファイルのようになります。
（これだと簡素化する効果は薄いですが、UIの数が多くなればなるほど効果が高まります。）
```MapPlugin.cs
using System;
using AtsEx.PluginHost.Plugins;
using BveTypes.ClassWrappers;
using FastMember;
using TypeWrapping;
using ObjectiveHarmonyPatch;
using AtsEx.PluginHost.Native;
using AtsEx.PluginHost;
namespace Sample
{
    internal class Main
    {
        UIDrawer uiDrawer;
        public MapPluginMain(PluginBuilder builder) : base(builder)
        {
            uiDrawer = new uiDrawer();
            uiDrawer.CreateModel(Location);
            drawPatch.Invoked += DrawPatch_Invoked;
        }
        private PatchInvokationResult DrawPatch_Invoked(object sender, PatchInvokedEventArgs e)
        {
            uiDrawer.Draw();
            return PatchInvokationResult.DoNothing(e);
        }
        public override void Dispose()
        {
            uiDrawer.Dispose();
        }
        public override TickResult Tick(TimeSpan elapsed)
        {
            return new MapPluginTickResult();
        }
    }
}
```
```UIDrawer.cs
using System.Drawing;
using System.IO;
using AtsEx.PluginHost.Plugins;
using BveTypes.ClassWrappers;
using SlimDX;
using SlimDX.Direct3D9;

namespace Sample
{
    internal class UIDrawer
    {
        Model[] models = new Model[10];//適当に10個
        
        public void CreateModel(string Location)
        {
            for(int i = 0; i < models.length; i++)
            {
                models[i] = CreateModel(@"picture\" + i + ".png",0,0,150,-225);
                //まとめるときは数字で規則性をつける
            }
            //このように複数回呼び出すときはメソッドにしてしまうのが吉
            Model CreateModel(string path, float x, float y, float sizex, float sizey)
            {
                string texFilePath = Path.Combine(Path.GetDirectoryName(Location), path);
                RectangleF rectangleF = new RectangleF(x, y, sizex, -sizey);
                Model resultModel = Model.CreateRectangleWithTexture(rectangleF, 0, 0, texFilePath);
                return resultModel;
            }
        }
        public void Draw()
        {
            int width = Direct3DProvider.Instance.PresentParameters.BackBufferWidth;
            int height = Direct3DProvider.Instance.PresentParameters.BackBufferHeight;
            Device device = Direct3DProvider.Instance.Device;
            for(int i = 0; i<models.length; i++)
            {
                device.SetTransform(TransformState.World, Matrix.Translation(-width/4+50i, height/4+10i, 0));
                models[i].Draw(Direct3DProvider.Instance, false);
            }
        }
        public void Dispose()
        {
            foreach(Model model in models)
            {
                model.Dispose();
            }
        }
    }
}
```
こんなところでしょうか。
他のAtsEX機能についても後々まとめますので、これからもよろしくお願いしますm(_ _)m

**明日はこちら** => ~~まだ決まってねぇ~~