Altseed + WPFでツール開発(入門編)

こちら[Amusement Creators Advent Calender](https://adventar.org/calendars/3442)の14日目の記事です。

# はじめに
みなさん、初めまして。Amusement CreatorsのGON(ごん)です。
普段は、2D横スクロールゲームを開発しております。(冬コミ間に合うか？？
今回は、ACACということで無い文章力を振り絞って書こうと思います。

# みなさんはツール開発しますか？
個人的にゲーム開発には、 **システム/リソース/調整** の大きな要素があると思います。
で、ちょっとクオリティの高いゲームを目指すと必ず詰まるのが、リソースの管理、配置の調整では無いでしょうか。(そもそも素材なんてねぇよというのは今回は無しで)
そこで、「**ツール開発**することで一気にリソース管理・調整の効率をあげましょう!!」というのが今回のお話です。

## ツールを作る利点/欠点
パッと思いつくだけ書いてみました。
### 利点
* パラメータ入力が容易に
* プレビューしながら調整ができる
* プログラマ以外の人間が調整・配置が可能に
    * 多人数で開発するのに向いてる
* 既存のツールを使うよりカスタマイズ性が高い
    * 既存のツールは、汎用性をある程度必要としているため、痒いところに手が届かないor機能が多すぎて習得に時間がかかる

### 欠点
* 割に合わない(データが少ないならコードで配置した方がかえって楽かも)
* 急な仕様変更に弱い(仕様変更する奴が悪い説もある。ある程度はシステムの最適化で吸収できる→後述)

# Altseedについて
とてもすごいゲームエンジンです。
[ACAC1日目](https://qiita.com/sosei_gocho/items/e6b4769b5d3d88576e81#%E3%82%B2%E3%83%BC%E3%83%A0%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3%E3%81%A8altseed)で良い感じに説明してくれてますね。そちらを見ましょう。アドベントカレンダーの有効利用です()

そして、Altseedは通常、初期化すると勝手にウィンドウを作ってくれるのですが、外部ウィンドウでAltseedの機能を使うこともできます。
そうすれば、既存のGUIフレームワークと組み合わせて、いろんなことができちゃうってわけですwktk

# WPFに組み込む利点
* **Binding**めっちゃ便利
* HTMLの感覚でフレキシブルな配置・デザインが可能
* 高DPI対応 

ざっとこんな感じです。Bindingマジ便利。  
ただAltseedの画面を外部のウィンドウに表示するだけなら、Windows Formsを使った方が楽です。(HWNDに紐つけるため)
しかし、Bindingを使うことで、Altseed側とWPF側でのデータのやり取りが容易になるので、ツールを開発するならWPFが良いと思います。

# Altseed+WPF初級編
色々説明するより、実際に作ってみた方が早いので、以下そのレシピです。
## まず、ファイル配置
WPFのプロジェクトを作りましょう。
![1.png](https://qiita-image-store.s3.amazonaws.com/0/326376/1779ceec-a89a-36df-9ccf-190df6be52ea.png)
こんな感じに`App.xaml`と最初に表示するウィンドウとなる`MainWindow.xaml`があると思います。
そして、NuGetなりでAltseedを参照しましょう。

通常、メインループはWPF側でよしなにしてくれるので、気にする必要はないのですが、毎フレーム更新処理を行うゲーム特有の性質もあり、メインループは、こちらで実装した方が良さそうです。

ということで、エントリポイントとなっている`App.xaml`を消去し、`Program.cs`を追加しましょう。

```cs:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfAltseed
{
   static class Program
   {
       [STAThread]
       static void Main(string[] args)
       {
           // 外部のウィンドウを作成する。
           bool closed = false;
           MainWindow window = new MainWindow();
           window.Closed += (sender, e) =>
           {
               closed = true;
           };
           window.Show();
           // 外部のウィンドウを利用してAltseedを初期化する。
           asd.Engine.InitializeByExternalWindow(window.altseed.Child.Handle, System.IntPtr.Zero, (int)window.altseed.RenderSize.Width, (int)window.altseed.RenderSize.Height, new asd.EngineOption());

           while (asd.Engine.DoEvents())
           {
               // 外部のウィンドウの処理を進める。
               MainWindow.DoEvents();
               // 外部のウィンドウが閉じられたらAltseed用のゲームループも抜ける。
               if (closed)
               {
                   break;
               }

               asd.Engine.Update();
           }

           asd.Engine.Terminate();
       }
   }
}
```

ほぼ[ここ](https://altseed.github.io/Doc/HowToUseApplication/Window.html)とやっているとは、変わりません。  
要は、ウィンドウが閉じられた時にメインループから抜け出すことと、WPF側のイベントを処理できることを満たしていれば良いです。
## WPF側の構築
WPFは、デザインを記述するXAML部分と挙動を記述するC#のソースコード部分に分かれます。
### XAML
```xml:MainWindow.xaml
<Window x:Class="WpfAltseed.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       xmlns:local="clr-namespace:WpfAltseed"
       mc:Ignorable="d"
       Title="MainWindow" Height="450" Width="800">
   <Grid>
       <Grid.ColumnDefinitions>
           <ColumnDefinition Width="51*"/>
           <ColumnDefinition Width="37*"/>
       </Grid.ColumnDefinitions>
       <TextBox x:Name="textBox" TextWrapping="Wrap" AcceptsReturn="True" Grid.Column="1" Margin="0,0,0,-1" />
       <WindowsFormsHost x:Name="altseed"/>
   </Grid>
</Window>
```
ここで重要なのが`WindowsFormsHost`です。  
Altseedの機能を外部ウィンドウで使いたい時、唯一必要なのが`HWND`というWindowsAPIのウィンドウを管理するオブジェクトです。  
Windows Formsの場合、この`HWND`は各コントロールで個別に持っているのですが、WPFはウィンドウにつき一つです。
これが原因となって、WPFのみウィンドウではAltseedをウィンドウのある特定の領域のみに描画させることができません。
しかし、WPFとWindows Formsは相互連携機能を持っています。
つまり、Windows FromsからWPFを呼び出すこともその逆もできるってわけです。
今回は、`WindowsFormsHost`を使うことで、WPFのウィンドウの中にWindows Formsを召喚させます。そうすることで、ウィンドウの特定領域のみAltseedを適応できます。

あと適当に`TextBox`を配置しておきましょう。
### C# 
```c#:MainWindow.xaml.cs
using System.Windows;
using System.Windows.Threading;

namespace WpfAltseed
{
   /// <summary>
   /// MainWindow.xaml の相互作用ロジック
   /// </summary>
   public partial class MainWindow : Window
   {
       public MainWindow()
       {
           InitializeComponent();
           altseed.Child = new System.Windows.Forms.Control();
       }

       public static void DoEvents()
       {
           DispatcherFrame frame = new DispatcherFrame();
           Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
               new DispatcherOperationCallback(ExitFrames), frame);
           Dispatcher.PushFrame(frame);
       }

       public static object ExitFrames(object f)
       {
           ((DispatcherFrame)f).Continue = false;
           return null;
       }
   }
}
```

`altseed.Child = new System.Windows.Forms.Control();`でWindows Formsのコントロールのインスタンスを`WindowsFormsHost`に登録しておきます。

そして、ウィンドウのイベントを処理するために`DoEvents()` `ExitFrames(object f)`関数を作りましょう。  
あまり中身は気にしなくて良いと思います。

## Altseed側の構築
今回は、適当に文字を表示することにしました。  
ということで、`Program.cs`のメインループ直前に以下を追加

```cs:Program.cs
// フォントを生成する。
var font = asd.Engine.Graphics.CreateDynamicFont("", 30, new asd.Color(255, 255, 255), 0, new asd.Color());

// 文字描画オブジェクトを生成する。
 asd.TextObject2D obj = new asd.TextObject2D();

// 描画に使うフォントを設定する。
obj.Font = font;

// 描画位置を指定する。
obj.Position = new asd.Vector2DF(100, 100);

// 描画する文字列を指定する。
obj.Text = "C++完全に理解した";

// 文字描画オブジェクトのインスタンスをエンジンへ追加する。
asd.Engine.AddObject2D(obj);
```
ほぼ[サンプル](https://altseed.github.io/Doc/HowToUseBasic/TextObject2D.html)そのままです。
正直なんでも良い。

## バインディングしよう
ここ**重要!!** です。って言っても2行加えるだけですが。  
めっちゃ簡単。  

まず、`Program.cs`でさっき付け足した直後に以下を追加

```cs:Program.cs
window.DataContext = obj;
```
で、`MainWindow.xaml`の`TextBox`部分を書き換える。

```xml:MainWindow.xaml
<TextBox x:Name="textBox" TextWrapping="Wrap" AcceptsReturn="True" Grid.Column="1" Margin="0,0,0,-1" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"/>
```
重要なのは、`Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"`です。  
`{Binding Path}`で`DataContext`に登録したオブジェクトの`Path`プロパティをバインディングできます。
今回は、`DataContext`に`asd.TextObject2D`の`obj`を代入し、`TextBox`の`Text`プロパティを`{Binding Text}`とすることで、`TextBox`の`Text`プロパティと`obj.text`を常に同期することができます。

## できたね!!

[![Image from Gyazo](https://i.gyazo.com/a0976d9b11e0b0de24cc77bfaa8ff770.gif)](https://gyazo.com/a0976d9b11e0b0de24cc77bfaa8ff770)

上の工程を一通りやるとこんな感じなると思います。
しっかりと、テキストボックスの内容がAltseed側に反映されてますね。
これを応用すれば、WPF側でデータを入力させてAltseed側で即時反映できます。  
また、WPFのBindingには型チェックの機能があります。
例えば、バインディング先がint型だった場合、数値以外が入力されるとテキストボックスが赤くなります。  
これで、データの誤入力が減ると思います。
# Altseed + WPFををする上での注意点
なくはないです。まあしょうがないです。

* Altseedの各種入力機能は使えない(WPFとAltseedを別ウィンドウにすれば問題ないらしい)
* Windowsのみ
* [空域問題](https://docs.microsoft.com/ja-jp/dotnet/framework/wpf/advanced/wpf-and-win32-interoperation)(要は、`WindowsFormsHost`領域ではWPFのイベントは取れません。残念...)

まあ、大きな問題にはならないと思います。

# 終わりに
**Altseed + WPFでツール開発(入門編)** は、いかがだったでしょうか。
これで、簡単にAltseedが既存のGUIフレームワークと融合できることをわかってもらえたかと思います。

また今回は、ツール開発の具体的な導入法をメインで紹介しました。
個人的には、次回以降？でもっと踏み込んだ内容が書けたら良いと思っています。
割とこの周辺技術、ネタには困らないので。

## 次回予告??
ということで次回予告です。書けたら良いですね

* Altseed + WPFでツール開発(応用編) ~属性を使ってUI自動生成~
* Altseed + RoslynPadで簡単スクリプト
* Avaloniaで夢のクロスプラットホームツール開発

## 参考文献

* [Altseed](https://altseed.github.io/)  
* [コントロールを明示的に更新する - WPFアプリケーションでのApplication.DoEvents の実装 (WPFプログラミング)](https://www.ipentec.com/document/csharp-wpf-implement-application-doevents)  
* [空域問題](https://docs.microsoft.com/ja-jp/dotnet/framework/wpf/advanced/wpf-and-win32-interoperation)