UnityEditorを使って2D格闘(2D Fighting game)作るときのモーション遷移図作成の半自動化に挑戦しよう<その4>

  • 1
    いいね
  • 2
    コメント

続き物の第4話

この連載は、下記の遷移図作成をスクリプトでバッチ処理しよう、というものだ。
話しに付いてこれなくなった方は この連載が終わって元の流れに戻るまでの間は しばらくお待ちいただきたい。
201701230746a34b.png
これを、ボタン1発で

201701230746a35.png
こうするスクリプト言語を開発しようという話しだ。前回から読んできているものとする。

第1話 http://qiita.com/muzudho1/items/9c50b8b894e3cf4c90da
第2話 http://qiita.com/muzudho1/items/8455c0c7a26d5788b541
第3話 http://qiita.com/muzudho1/items/a1669f4d1721428790c1

ソース公開中 Open source

201701211744gif33.gif
201701201027gif26.gif

https://github.com/muzudho/KifuwarabeFighter2

プログラムを創造しよう

例えば 次のようなプログラムで 自動で線が引かれるとすれば、作業が楽になるのではないだろうか?

TRANSITION ADD
FROM "Base Layer.SMove"
TO "Base Layer.SAtkLP"

そして、ステート名を 名指しするのではなく 条件で指定することができれば なお 生産性が上がるだろう。

TRANSITION ADD
FROM "Base Layer.SMove"
TO ATTR (BusyX Block)

命名センス

このプログラム言語は なんと名前を付けようか。 遷移図を作る様子を 星をつなげる星座に倣って Stella とかどうだろうか? ただそれだと プログラム名だと認識しずらいので SQL から2文字もらって StellaQL(ステラキューエル)としよう。

あっ、略称は SQL になってしまう。

例題

次の属性(ATTRとか、荷札とかいってるやつ)があるとする。

Punch パンチ
Kick キック
L ライト(小とか弱とか)
M ミディアム(中)
H ハード(大とか強とか)
BusyX レバーの横倒しが利かなくなる
BusyY レバーの縦倒しが利かなくなる
Stand 立ち中
Jump ジャンプ中
Block ブロック姿勢
Damage ダメージ中

組み合わせる

例えば、弱パンチであれば

( L Punch )

丸かっこで L と Punch をひとくくりにして、キーワード と呼ぶことにする。
( L Punch ) というキーワードを指定すれば、立ち弱パンチ、ジャンプ弱パンチ がヒットする段取りだ。

後々、もっとモーションを増やして盛り込んだあとでは、しゃがみ弱パンチ や ダッシュ弱パンチ もヒットすることになるだろう。
ただ、使いづらそうだ。もっと実用的な指定のしかたは こうだ。

[ ( Stand Punch ) ( Stand Kick ) ]

今度は キーワード が2つある。立ちパンチと、立ちキック という指定だ。
これで 立ち弱パンチ、立ち中パンチ、立ち強パンチ、立ち弱キック、立ち中キック、立ち強キック が当てはまるだろう。
これら キーワード を 角カッコ で囲んだものを、 キーワード・リスト と呼ぶことにする。

どうだろう、見やすい言語になる予感はしないだろうか?

次に、強パンチ、強キックは除く、と 例外を指定したい場合は こうだ。

( [ ( Stand Punch ) ( Stand Kick ) ] { H } )

息を吹きかける形の 中かっこ { } は、取り除く という意味になるキーワードだ。これを NG・キーワード と呼ぶと 慣れ親しみやすいのではないか。 ここで もし、

 [ ( Stand Punch ) ( Stand Kick )  { H } ]

と書いてしまうと、

立ちパンチ を拾ってきて、
立ちキック を拾ってきて、
強以外の全て を拾ってきて、

の3つが順繰りに並ぶことになり、最後の1つがでかすぎるので ブロックや ダメージも拾ってきてくることになってしまう。そこで { H } は キーワード・リストの外に出して、

# [ ( Stand Punch ) ( Stand Kick ) ] { H }

おっと、頭に # を付けると コメント行ということにしよう。

( [ ( Stand Punch ) ( Stand Kick ) ] { H } )

全体は ( )、[ ]、{ } の いずれかのカッコで囲むという 縛りにしておこう。

すると

TRANSITION ADD
FROM "Base Layer.SMove"
TO ATTR ( [ ( Stand Punch ) ( Stand Kick ) ] { H } )

という ステラキューエル のコードを見れば、
SMoveステートから、立ちパンチ、立ちキックのうち 強 を除いたものへ、
白い矢印を引っ張る

という意味になる。

これで問題無ければ 実装に入りたい。まあ 誰もいないので 自分ひとりだから さくさく決定して 前に進むんだが……。もちろん ここで聞きたいのは「こういう場合はどうするのか?」という自分に無く他者には有る見識であって、「ではそれも含めて解決方法を考えよう」という問題解決を行えるわけだ。「不安に思う」「やらない方がよい」「わたしの気にめさない」「最近の流行りから逸れており興味が持たれない」とかそういうメタ的な主張ではない。

ここまでが 言語設計。 次は 解法 だ。
ミステリーで言うところの、探偵が 犯人はこの中にいます。なぜならあのときコレができたのは1人…… とか反論を塗りつぶしていっている時間が 言語設計 で、犯人が名乗り出て トリックを語りだす時間 が 解法 な。 後ほど、このページに追記していきたい。

後置き逆ポーランド記法が使えないか?

長くなるが、動きを見せよう。目で見た方が分かりやすいだろう。 ヨッシーのたまご に似ている。

以下、後置き逆ポーランド記法 改造バージョン だ。

(1)

( [ ( Stand Punch ) ( Stand Kick ) ] { H } )

(2)

(
[ ( Stand Punch ) ( Stand Kick ) ] { H } )

(3)

(
[
( Stand Punch ) ( Stand Kick ) ] { H } )

(4)

(
[
(
Stand Punch ) ( Stand Kick ) ] { H } )

(5)

(
[
(
Stand
Punch ) ( Stand Kick ) ] { H } )

(6)

(
[
(
Stand
Punch
) ( Stand Kick ) ] { H } )
# あっ、) だ! 戻ろう!

(7)

    (
    [
    (
    Stand
--> Punch
    ) ( Stand Kick ) ] { H } )

(8)

    (
    [
    (
--> Stand
    Punch
    ) ( Stand Kick ) ] { H } )

(9)

    (
    [
--> (
    Stand
    Punch
    ) ( Stand Kick ) ] { H } )
# あっ、)  に対応する  (  を見つけた! じゃあ Stand & Punch で検索しよう!

(10)

    (
    [
--> 1 ( Stand Kick ) ] { H } )
# 計算結果は、部室のロッカーの 1 番に入れておいた。計算を続ける。

(11)

    (
    [
    1
    ( Stand Kick ) ] { H } )

(12)

    (
    [
    1
    (
    Stand Kick ) ] { H } )

(13)

    (
    [
    1
    (
    Stand
    Kick ) ] { H } )

(14)

    (
    [
    1
    (
    Stand
    Kick
    ) ] { H } )
# あっ、) だ! 戻ろう!

(15)

    (
    [
    1
    (
    Stand
--> Kick
    ) ] { H } )

(16)

    (
    [
    1
    (
--> Stand
    Kick
    ) ] { H } )

(17)

    (
    [
    1
--> (
    Stand
    Kick
    ) ] { H } )
# あっ、)  に対応する  (  を見つけた! じゃあ Stand & Kick で検索しよう!

(18)

    (
    [
    1
--> 2 ] { H } )
# 計算結果は、部室のロッカーの 2 番に入れておいた。計算を続ける。

(19)

    (
    [
    1
    2
    ] { H } )
# あっ、] だ! 戻ろう!

(20)

    (
    [
    1
--> 2
    ] { H } )

(21)

    (
    [
--> 1
    2
    ] { H } )

(22)

    (
--> [
    1
    2
    ] { H } )
# あっ、]  に対応する  [  を見つけた! じゃあ 部室のロッカーの 1 と 2 に入っている靴は
# いっしょに混ぜて 重複は trancate(捨てる)しよう。
# その結果は 部室のロッカーの 3 に入れる。

(23)

    (
--> 3 { H } )

(24)

    (
    3
    { H } )

(25)

    (
    3
    {
    H } )

(26)

    (
    3
    {
    H
    } )
# あっ、} だ! 戻ろう!

(27)

    (
    3
    {
--> H
    } )

(28)

    (
    3
--> {
    H
    } )
# あっ、}  に対応する  {  を見つけた! じゃあ H & ……、1個だけか。1個で検索しよう!
# そして、その結果に当てはまらなかったものは 全部 部室のロッカーの 4 番に入れておこう。
# 計算を続ける。

(29)

    (
    3
--> 4 )

(30)

    (
    3
    4
    )
# あっ、) だ! 戻ろう!

(31)

    (
    3
--> 4
    )

(32)

    (
--> 3
    4
    )

(33)

--> (
    3
    4
    )
# あっ、)  に対応する  (  を見つけた! じゃあ 部室のロッカーの 3 と 4 に入っている靴は
# 全部のロッカーに重複している靴だけ残して、そうでない靴は trancate(捨てる)しよう。
# その結果は 部室のロッカーの 5 に入れる。

(34)

--> 5

(35)

    5
# (^▽^)あっ、計算が終わってるぜ。答えは 部室のロッカーの 5 番に入ってるからな。

これでいけるのだろうか。途中でかなり処理の重い全検索が見えた気がするが、心の目をつぶって 前に進むことにしよう。

後ほど、このページに追記していきたい。

絵を描いたので貼り付けておく

201701240856a15a7b13.png

ATTR は Attribute のことで 属性検索 だ。
グーグル検索で 「漫画 1位 面白い」 とか入れている段階だ。

今回の話しでは、Stand や Punch といった属性がそれにあたる。

ELEM は Element のことで 要素集計 だ。
例えば 該当するページが200件あって、ロッカーの1番に入っている、といった感じだ。

Unityのアニメーターで言うと、ステート名 がロッカーにいっぱい入っているわけだ。

で、( )、[ ]、{ } のカッコを頼りに 該当するステート名の数を減らしていく。

( ) なら 数は減るし、
[ ] なら 維持されるし、
{ } は いったん、とてもでかいものになる。

1つの計算式の中で、大きく分けて まったく違う2つの計算に分かれていることに注意されたい。

ATTRは 指示用

201701240856a15a7b14b.png
あの店はチャーハンがうまい! と評判になれば 主人が がくっ と腰を落としそうな 品揃えの図だ。

使いやすさを考慮して ATTR を用意し、そして使いこなせれば、多くの商品を ちょうどいい感じに ひとくくりにできるだろう。

ATTR部での( )むすび

201701241344a2.png
味噌でもあり、ラーメンでもあるのは 味噌ラーメンと 大盛味噌ラーメンだ。

ATTR部での[ ]むすび

201701241344a3.png
ラーメンの指す範囲がでかすぎる。例えば客が「主人! ラーメンか、味噌をくれ」と言っていれば、大盛塩ラーメン を出してもいいわけだ。ラーメンなのだから。

ATTR部での{ }むすび

201701241344a4.png
まるでヒットマンに狙われたように、味噌 と ラーメンは死んでしまった。だが待ってほしい、その他 残りたくさんの … で省略されたメニューも ここには残っている。酢豚も餃子もあるのだ。

まだ後ほど、このページに追記していきたい。

ぶっきらぼうに実装するなら

ステート名の数 × ロッカーの数 だけの大きさのテーブルを用意すれば マシンの力に頼る技 で解決する。

(1)
201701241344a5.png
大盛でない塩ラーメンか 味噌ラーメンを頼みたいときの表現だ。

201701241344a5b1.png
塩ラーメンを注文すると、並みか 大盛か 主人は気になるだろう。

201701241344a5b2.png
だが優柔不断な客は 券売機の前で こうとも言った。「味噌ラーメンでもいいです」

201701241344a5b3.png
右の表 3 と書かれた列を見て欲しい。塩か味噌か、並みか大盛か。4つの選択肢がある。

201701241344a5b4.png
大盛は嫌だ。主人にとって 有益な情報だ。

201701241344a5b5.png
その結果、並みの味噌ラーメンか、並みの塩ラーメン の選択肢が残った。主人は 味噌ラーメンと 塩ラーメンを 出すだろう。

メモリを省エネ化したり、高速化したり、式を最適化して短手数にしたり、あるいは 何も手を施さないことで 実装はできるだろう。

これだけ実装すれば 問題は まるっと 解決できるのか、それを検討中だ。
後ほど、このページに追記していきたい。

プログラムを調整しよう

使う単語を SQL に似せておくと 覚えやすいだろう。

追加

TRANSITION INSERT
SET Duration 0 ExitTime 1
FROM “Base Layer.SMove”
TO ATTR (BusyX Block)

新規追加と同時にデータを設定する場合。
プロパティの設定は単に 項目名 値 項目名 値 ……の繰り返しにすることにした。型は int、float、bool の3つだけを想定している。

更新

TRANSITION UPDATE
SET Duration 0.25 ExitTime 0.75
FROM “Base Layer.SMove”
TO ATTR (BusyX Block)

更新の場合。無いトランジションには設定できないのが INSERT との違い。

削除

TRANSITION DELETE
FROM “Base Layer.SMove”
TO ATTR (BusyX Block)

削除する場合。

データ表示

TRANSITION SELECT
FROM “Base Layer.SMove”
TO ATTR (BusyX Block)

列を指定できるようにしてもいいのかもしれないが、とりあえず プロパティー全部 ログ出力してはどうか。

次は ひとまず前に進むことにする。今想定に上がっていない問題は StellaQL では解決できない。設計はたいへん きつい。

続きは 後ほど、このページに追記していきたい。

さあ、実装だ

NUnitを使おう

少し作っては実行して動いているのを確認してからまた作る、というのを繰り返すと 進捗を進めやすい。それをするには NUnit がちょうどいい。
201701241656a2.png
[Window] - [Editor Tests Runner] と進み、Assets/Editor フォルダーの下に C#スクリプトを作るといい。

201701241656a3.png
StellaQL を使えるようにするまでの準備が膨大なので Qiitaに説明記事を書くのは端折る。完成させる方を急ぐ。ユニット・テスト用に用意したのがこのコードだ。

        string expression = "( [ ( Alpha Cee ) ( Beta Dee ) ] { Eee } )";

        List<int> result = Util_StellaQL.Execute(expression);

とりあえず Execute( ~ ) メソッドで 字句解析 をやってみよう。

字句解析

201701241656a4.png
なぜ if文でまとめるといった もっとマシな書き方をしないのか、という目が 常に気になるのが記事の怖さだ。switch 文、楽だし……。

怠けて書いても
201701241656a5.png
配列の中に 字句(トークン)が分解されて 入っている。
先に字句解析(分解しておくこと)しておくと、単語もさくっと取り出せるし、空白も消し飛ばしたし、あとで カーソル移動 が楽だろう。

記事は引き続き どんどん書いていく。

構文解析

文章をスキャン(行き来)して プログラムに扱いやすいデータに変換する。
201701241916a9.png
スキャンのプログラムは これだけ。

NUnit で動かすと……。
201701241916a6.png
go が1トークン左へ、back が1トークン右へ、だ。

201701241916a7.png
まあ、動いているんじゃないだろうか。

201701241916a8.png
無駄のない動きにするのは 完成したあと 必要だったらしよう。

文字列→列挙型 変換

( [ ( Alpha Cee ) ( Beta Dee ) ] { Eee } )

この Alpha や Cee を、列挙型の AstateIndex.Alpha や AstateIndex.Cee に変換したい。

てっとりばやく 文字列を、列挙型の要素に総当たりでぶつけてしまおう。
201701250543a10b.png
列挙型の型は こうやってメソッドに渡せる。この列挙型の内容は ステート名 を全部手打ちしたもので、ステート名と1対1対応 するものとする。

List<int> result = Util_StellaQL.Execute(expression, typeof(AstateIndex));

201701250543a11b.png
メソッドの引数は このように受け取り、

public static List<int> Execute(string expression, Type enumration)

201701250543a12b.png
このように書けば 文字列 "Num" に対応した列挙型の要素 AstateIndex.Num を返す。該当しなかった場合例外を投げる。

object enumElement = Enum.Parse(enumration, "Num"); // 変換できなかったら例外を投げる
Debug.Log("enumElement = " + enumElement.ToString() + " typeof = " + enumElement.GetType());

201701250543a13b.png
これで、文字列と 列挙型は変換できた。

続きは 後ほど、このページに追記していきたい。

補集合を取ろう

補集合とは何かを説明する。

これ、全集合

201701240856a15a2.png
英語で言うと Universe。

これ、集合

201701240856a15a6.png
英語で言うと set。

これ、補集合

201701240856a15a2b.png
英語で言うと Complete。

では、補集合を取るプログラムをコーディングしてみよう。

これ、全集合

201701250543a14b.png
めんどくさいので、サンプル・プログラムは簡単にする。 全部で Zero、A、B、C、D、E が入っていることを意味している。
enum型の頭に[Flags]とアノテーションを付け、中身は 0、1、ときてそのあとは 1<<1、1<<2、1<<3 と書いていく。

        [Flags]
        public enum Attr
        {
            Zero = 0, // (0) 最初の要素は 0 であることが必要。あとで計算に使う。
            Alpha = 1, // (1)
            Beta = 1 << 1, // (2)
            Cee = 1 << 2, // (4)
            Dee = 1 << 3, // (8)
            Eee = 1 << 4, // (16)
        }

先頭を Zero = 0 にするのが工夫だ。ほんとうは かっこうを付けて Phi = 0 にして かっこいー! とやろうと思ったり EmptySet = 0 にして ほんとうはこうだろう、とやろうと思ったりしたが 何それ、と言われると説明がめんどうなので Zero = 0 とした。

これ、集合

201701250543a15b.png
B と D が入っていることにする。

これ、補集合

201701250543a15c.png
集合 と 全集合(列挙型) を指定することで、補集合が取れるようにした。なんか こんなライブラリ既にありそうだが 探すのがめんどうなので 自分で作ってしまう。多分、未来社会を覗けば 手を伸ばすところに掃除機があるのに、掃除機を作ってしまう人というのもいるのだろう。

下の方に ユニットテストが書かれているが、全集合(Zero、A、B、C、D、E)において集合(B、D)の補集合は(Zero、A、C、E)だ、ということが書いている。
実行してみよう。
201701250543a16b.png
うまくいったようだ。

201701250543a17b.png
プログラムの考え方、コードの書き方が ずぼらなのは 諦めてもらいたい。これでも動く。

        /// <summary>
        /// 補集合
        /// </summary>
        public static List<int> Complement(List<int> set, Type enumration)
        {
            List<int> complement = new List<int>();
            {
                // 列挙型の中身をリストに移動。
                foreach (int elem in Enum.GetValues(enumration)) { complement.Add(elem); }
                // 後ろから指定の要素を削除する。
                for (int iComp = complement.Count - 1; -1 < iComp; iComp--)
                {
                    if (set.Contains(complement[iComp]))
                    {
                        Debug.Log("Remove[" + iComp + "] ("+ complement[iComp]+")");
                        complement.RemoveAt(iComp);
                    }
                    else
                    {
                        Debug.Log("Tick[" + iComp + "] (" + complement[iComp] + ")");
                    }
                }
            }
            return complement;
        }

これで補集合が取れるようになったので、NOTオペレーション を実装できるようになった。

続きは 後ほど、このページに追記していきたい。

ATTR部 ( )演算、[ ]演算、{ }演算 ユニットテスト

さあ、( )演算、[ ]演算、{ }演算 の実装開始だ。
それぞれ Keyword演算、KeywordList演算、NGKeyword演算 と名付ける。 “オペレーション” は7文字もあって長いので “演算” と呼ぶことにする。

まず、ユニット・テストのコードを乗せる。さっきの 補集合 の使いまわしだ。

( )演算 ユニットテスト

201701250543a18.png

    /// <summary>
    /// ( ) 演算をテスト
    /// </summary>
    [Test]
    public void OperationKeyword()
    {
        List<int> set = new List<int>() { (int)AstateDatabase.Attr.Beta, (int)AstateDatabase.Attr.Dee };
        List<int> result = StellaQLScanner.Keyword_to_locker(set, typeof(AstateDatabase.Attr));

        int i = 0;
        foreach (int attr in result) { Debug.Log("Attr[" + i + "]: " + (AstateDatabase.Attr)attr + " (" + attr + ")"); i++; }

        Assert.AreEqual(1, result.Count);
        if (1 == result.Count)
        {
            Assert.AreEqual((int)AstateDatabase.Attr.Beta | (int)AstateDatabase.Attr.Dee, result[0]);
        }
    }

全集合(Zero、A、B、C、D、E)において集合(B、D)というキーワードは( B と D がくっついたもの )だ、ということが書いている。

[ ]演算 ユニットテスト

201701250543a19.png

    /// <summary>
    /// [ ] 演算をテスト
    /// </summary>
    [Test]
    public void OperationKeywordList()
    {
        List<int> set = new List<int>() { (int)AstateDatabase.Attr.Beta, (int)AstateDatabase.Attr.Dee };
        List<int> result = StellaQLScanner.KeywordList_to_locker(set, typeof(AstateDatabase.Attr));

        int i = 0;
        foreach (int attr in result) { Debug.Log("Attr[" + i + "]: " + (AstateDatabase.Attr)attr + " (" + attr + ")"); i++; }

        Assert.AreEqual(2, result.Count);
        if (2 == result.Count)
        {
            Assert.AreEqual((int)AstateDatabase.Attr.Beta, result[0]);
            Assert.AreEqual((int)AstateDatabase.Attr.Dee, result[1]);
        }
    }

全集合(Zero、A、B、C、D、E)において集合(B、D)というキーワード・リストは( B と D )だ、ということが書いている。

{ }演算 ユニットテスト

201701250543a20.png

    /// <summary>
    /// { } 演算をテスト
    /// </summary>
    [Test]
    public void OperationNGKeywordList()
    {
        List<int> set = new List<int>() { (int)AstateDatabase.Attr.Beta, (int)AstateDatabase.Attr.Dee };
        List<int> result = StellaQLScanner.NGKeywordList_to_locker(set, typeof(AstateDatabase.Attr));

        int i = 0;
        foreach (int attr in result) { Debug.Log("Attr[" + i + "]: " + (AstateDatabase.Attr)attr + " (" + attr + ")"); i++; }

        Assert.AreEqual(4, result.Count);
        if (4 == result.Count)
        {
            Assert.AreEqual((int)AstateDatabase.Attr.Zero, result[0]);
            Assert.AreEqual((int)AstateDatabase.Attr.Alpha, result[1]);
            Assert.AreEqual((int)AstateDatabase.Attr.Cee, result[2]);
            Assert.AreEqual((int)AstateDatabase.Attr.Eee, result[3]);
        }
    }

全集合(Zero、A、B、C、D、E)において集合(B、D)というNGキーワードは(Zero、A、C、E)だ、ということが書いている。補集合と同じだ。

ところで なんでコードを乗せているのに Visual Studio 2015 のスクリーンショットも貼ってあるのかというと、絵がないと 文字ばっかりの文章を手に取らない人 のために貼っている。2度手間でつらい。

さあ、ユニットテストを実行しよう

端折るが、3つとも通っている。
201701250543a21b.png

では、それぞれのコードの内容を見ていく。

ATTR部 ( )演算、[ ]演算、{ }演算の実装

201701250543a22b.png
実は ユニット・テストのコードより 本体の方が短い。

        public static List<int> Keyword_to_locker(List<int> set, Type enumration)
        { // 列挙型要素を OR 結合して持つ。
            List<int> attrs = new List<int>();
            int sum = (int)Enum.GetValues(enumration).GetValue(0);//最初の要素は 0 にしておくこと。 列挙型だが、int 型に変換。
            foreach (object elem in set) { sum |= (int)elem; }// OR結合
            attrs.Add(sum); // 列挙型の要素を結合したものを int型として入れておく。
            return attrs;
        }

        public static List<int> KeywordList_to_locker(List<int> set, Type enumration)
        { // 列挙型要素を 1つ1つ ばらばらに持つ。
            List<int> attrs = new List<int>();
            foreach (int elem in set) { attrs.Add(elem); }// 列挙型の要素を1つ1つ入れていく。
            return attrs;
        }

        public static List<int> NGKeywordList_to_locker(List<int> set, Type enumration)
        {
            return Complement(set, enumration); // 補集合を返すだけ☆
        }

これで、部室のロッカーに 属性を入れる、というATTR部分は実装できた。次は 集計 のELEM部分のプログラミングだ。

続きは 後ほど、このページに追記していきたい。

属性フィルタリングしよう

属性フィルタリングとは何かを説明する。まずは各部名称から。

Universe (ユニバース)

201701261054a26.png
これユニバース。全集合とか全部とか そういう意味。その他全部も、ゼロも入っている。

Element (エレメント)

201701261054a26b2.png
1個1個はエレメント。要素とかメンバーとか元とか 呼び方はいっぱいある。

Attribute (アトリビュート)

201701261054a26b3.png
これアトリビュート。属性とかタグとか荷札とか 呼び方はいっぱいある。

フィルタリング 属性 And 属性 (アンド)

例えば、「塩」属性と「ラーメン」属性を And して、フィルタリングすると……。
201701261054a26b6.png
前に見たような気がする説明だが、今回は 専門用語 を使っている点が違う。 前回は And、Or、Not を使うと読者が逃げると思ったので 括弧を使う書き方にすることで 避けた。

これからは And、Or、Not を使って説明できるようにするために もう1回説明する。なぜ And、Or、Not を使うのかと言うと、「丸かっこ開く、A、B、丸かっこ閉じる」としゃべるより、「A アンド B」としゃべる方が、会話しやすいからだ。

フィルタリング 属性 Or 属性 (オア)

201701261054a26b7.png
どんどん行こう。

フィルタリング Not 属性 (ノット)

201701261054a26b8.png
どんどん行こう。

フィルタリング Not 属性 And Not 属性

201701261054a26b9.png
この店のラーメンは何味なのだ。

で、フィルタリングとは何かというと……

フィルタリング

201701261054a26b10.png

エレメントの どれを残すか、という操作を フィルタリング という。
属性をうまく組み合わせて、どんな残し方でもできるようにしておくのが親切な設計だろう。

中には 無属性 のエレメントがたくさんあったり、名指しで指定したい場合もある。
その場合は「属性検索」以外の方法が ふさわしい。属性検索でカバーできることにも限界がある。

フィルタリングを実装しよう

アトリビュート

201701261054a27b.png
とりあえず さっきと同じで。

ユニバース

201701261054a28.png
アルファベット順で A~Zの26匹の動物を並べた。属性は 名前に含まれているアルファベットを当てた。分かりやすいだろう。

ユニットテストを書こう

201701261054a29.png
これ ANDフィルター

201701261054a30.png
これ ORフィルター

201701261054a31.png
これ NOTフィルター

コードを抜粋しようと思ったんだが 長いので、Git Hub から拾ってもらいたい。

テストを1個しかやっていないものの、とりあえずこれで ユニットテストして 成功したら 先に進もう。

ユニットテストしよう

201701261054a32b.png
はい、動いた。

では それぞれの実装を見てみよう。

AND属性フィルター実装

201701261054a33.png

OR属性フィルター実装

201701261054a34.png

NOT AND NOT……属性フィルター実装

201701261054a35.png

これで、属性を使って エレメントを絞り込む機能はできたわけだ。つまり もう属性を使って トランジションをつなげたいステート を狙い撃ちする下地はできている。

次は これを テキストで書いておいて、ボタンを押すと Unityで順次 実行される形にしていきたい。

続きは 後ほど、このページに追記していきたい。

ステート・フルネーム正規表現フィルター

あっ! ついでに 作っておこう。
Unity公式の呼び方は分からないが、仮に "Layer Base.Alpaca" といったステートを指定する文字列を ステート・フルネーム と呼ぶとしよう。
これを正規表現でパターン・マッチングできれば便利だろう。正規表現って何かは ここでは説明しない。

テストケースを作ろう

201701261533a38.png
フルネームではなく、ステート名だけの方に N が含まれているものを選ぶとしよう。N の大文字小文字は区別しない。

テストを実行した。

201701261533a39b.png
はい、オッケ!

実装は次の通り

201701261533a40.png
作業もパターン化してきている。

続きは 後ほど、このページに追記していきたい。

要素フィルター

あっ! 要素(エレメント)フィルター を作っていなかったので 作ることにする。
要素フィルターを説明する。

要素フィルター AND

201701261639a43.png
今度は 要素同士を比べる。

要素フィルター OR

201701261639a42.png
どんどん行こう。

要素フィルター NOT (never)

201701261639a44.png

これが ELEM部 の計算だ。だいぶ前に説明した。

テストケースを作ろう

201701261639a45.png
これ、要素フィルターAND

201701261639a46.png
要素フィルターOR

201701261639a47.png
要素フィルターNOT 下の方切れてるけど。

ささ、テストケースを実行しよう

201701261639a48b.png
はい、通った。 いくつか名前変えた。

まだまだ続く。
続きは 後ほど、このページに追記していきたい。

構文パーサーを作ろう

また 各部名称を説明する。

201701262143a49b.png
もっといろいろあるが、とりあえず よく使うのがこれ。見落としたのが「0.0」で、ワード、シンボル、ワードの3つでできている。

201701262143a49c.png
なんか いっぱいあるな……。

201701262143a49d.png
ストリング(string)は 文字が順番に並んでいる列のことで、先頭から何文字目、といった具合に指定ができるやつだ。

201701262143a49e.png
きりがないので ここらへんで……。

201701262143a49f.png
まず、パーサー(parser)を使って トークン(token)に分割する。トークンにしておくとあとで便利だ。

201701262143a50.png
201701262143a50b.png
この文字列ばっかり入れておくオブジェクトに トークンを入れておこう。

201701262143a51.png
構文(Syntax)は とりあえず 4つ あるとしよう。構文とは何かというと……。

201701262143a51b.png
構文がなくなると よく分かるな。

ユニットテストを書こう

201701262143a52a.png
query文字列を Parser でばらばらにして、全部のトークンを1個所にまとめている。

201701262143a52b.png
全部同じパターン。

201701262143a52c.png
あとで楽。

201701262143a52d.png
構文はなくなった。残ったのはデータだけだ。

ユニットテストを実行しよう

201701262143a53b.png
はい、通った。

パーサーの内容を解説していきたい。

パーサーを実装しよう。

たくさん あるので、INSERT だけ解説する。
201701262143a54a.png
構文に沿って、ストリングの先頭から順番に ちょん切っていっている。
Fixed~ というところに固定の文字、Var~ というところに変わる文字がくるだろう。 C# の命名規則から外れているが 読みやすいのでそうした。

201701262143a54b.png
コンピューターでの処理に向くように、順番が いったりきたりしない構文にしている。 VarSpaces、FixedWord、VarWord、VarValue、VarStringliteral、VarParentesis といった名前から トークンが予想できないだろうか。

それぞれ見ていこう。

201701262143a55a2.png
1つ1つのメソッドは短い。Fixed~ ならマッチしているか確かめ、Var~ ならパターンマッチをしている。

201701262143a55a3.png
プロパティに設定する値は int、float、bool を想定しているが、VarValue は浮動小数点のドットに対応したもの。 VarStringliteral (ストリング・リテラル)では「\"」といったエスケープに対応している。

201701262143a55a4.png
属性検索の ( ) [ ] { } がある部分はパースが大変なので 開きカッコに対応する閉じ括弧が あと何が残っているかスタックに積みながら パースしている。

続きは 後ほど、このページに追記していきたい。

ATTR部を解析しよう

([(Alpaca Bear)(Cat Dog)]{Elephant})

これを、

(
[
(
Alpaca
Bear
)
(
Cat
Dog
)
]
{
Elephant
}
)

こうして、

0: () Bear Alpaca
1: () Dog Cat
2: [] 1 0
3: {} Elephant
4: () 3 2

こういうふうに ばらそう。あとで楽だ。スキャンの都合で括弧の中の並びは逆順になる。

テストケースを作ろう

201701271004a56.png
テストケースを見ると 文字列を配列に入れようとしていることが分かるのではないか。

201701271004a57.png
データを配列に入れることで、後の作業で 構文解析しなくて済んで 楽だ。

ユニットテストしてみよう。

201701271004a58b.png
はい、通った。

実装を見てみよう。

ATTR部解析を実装しよう

201701271004a59.png
頭からリストに入れているだけ。

201701271004a60.png
閉じ括弧を見つけたら 後ろにさかのぼってリストに入れている。

解析部は だいたい終わってきて データの扱いだけになったのではないか。
続きは 後ほど、このページに追記していきたい。

ここで Ease(イーズ)プログラミング

実行速度を高速にするより、作るのが楽(Ease)な方を選ぶスタイルを イーズ(Ease)プログラミング と呼ぶ。
キータイピングが少なくて楽とか、考えることが少なくて楽とか、いろいろだ。

不具合が1個あって 探すのが難しかったので、List を諦めて HashSet に えいやっ と置き換えた。

リスト(List)とセット(Set)の違い

201701271004a61.png
リストには どっちが前で後ろで、先頭から何番目に何があって、ということが記憶されていて コンピューターには素早く扱いやすい形だ。

セットでは 中に入っているのか、入ってないのか ぐらいしか分からない。1個1個 中身を取り出すときは 順番はてきとうだ。考えることが少なくて済む

クエリ―を実装しよう

今まで

([(Alpha Cee)(Beta)]{Eee})

というのを 必死こいて パースしていた。そしてパースできて、次の形にできた。

List<List<string>> tokenLockers = new List<List<string>>(){ // 元は "([(Alpha Cee)(Beta)]{Eee})"
    new List<string>() { "Cee", "Alpha", },
    new List<string>() { "Beta", },
    new List<string>() { "1","0",},
    new List<string>() { "Eee", },
    new List<string>() { "3","2",},
};
List<string> tokenLockersOperation = new List<string>() { "(", "(", "[", "{", "(", };

じゃあ、これを使って、アルファベット26文字の頭文字をもった26匹の動物の名前データベースから

Alpaca
Cat
Rabbit

というデータを引っ張ってこれたら、それは クエリ― のできあがりだ。
そして できた。

テストケースを見てみよう

201701271004a62a.png
バグを探すのに悩まされて 全面改装したが……、今まで ロッカーと呼んでいたのが もっと真面目な名前で言うと クエリ―(Query) だ。

そのあと メソッドを1個実行して、 その結果を長々と アサート(診断、Assert)している。

201701271004a62b.png
最後に Alpaca、Cat、Rabbit が入っていることを アサートしている。

ユニットテストを実行してみよう

201701271004a63b.png
できてる。

では その実装を解説する。

クエリーを実装しよう

201701271004a64.png
4時間ぐらい うんうん分からんと 悩んでいた。

201701271004a64b.png
{Elephant} みたいに書いたところなんだが、Elephant を取り出すときは OR 演算のようにし、中括弧を外すときに NOT するのが正解だった。これを 2回 NOT して0件になってしまっていた。見つけるのに時間がかかった。

次は、 クエリ― を Unity 上で実行できる仕組みを考えたい。
続きは 後ほど、このページに追記していきたい。

STATE SELECT 文を先に作ろう

少しずつ作ってテストしながら進めていくスタイルなので、テストできる手段を先に作りたい。 先に STATE SELECT 文 を作る。

STATE SELECT
WHERE ATTR ([(Alpha Cee)(Beta)]{Eee})

を実行すると、

Alpaca
Cat
Rabbit

(のレコードのインデックス)が返ってくるものとする。

テストケースを作ろう

201701271004a65.png
だいぶ クエリーの形が見えてきた。

テストケースを実行しよう。

201701271004a66b.png
通っている。C#の命名規則から外れるが 名前に数字を付けてソートした。

STATE SELECT クエリーを実装しよう

201701271004a67.png
今までの礎(いしずえ)を使うだけなので すっきりしたもんだ。

どんどん続く。
続きは 後ほど、このページに追記していきたい。

TRANSITION SELECT クエリーを作ろう

TRANSITION SELECT
FROM "Base Layer.Zebra"
TO ATTR ([(Alpha Cee)(Beta)]{Eee})

と書いて、

# FROM
Zebra

# TO
Alpaca
Cat
Rabbit

という結果が出てくれば、Zebraステート から、Alpaca、Cat、Rabbitステートに線をつなぐ、という意味になる。

テストケースを書こう

201701271004a68.png
こういう感じ。

ユニットテストを実行しよう

201701271004a69b.png
できている。

TRANSITION SELECT クエリーの実装を解説しよう

201701271004a70.png
部品化が進んできている。部品の中身を見てみよう。

パーサーでトークンを作ったあと、あとはトークンを元に レコード・インデックスの集合を作っている。

201701271004a71.png
フルネームで指定しているか、 ATTR ( ~ ) の形式で指定しているかで 2分している。どちらにしても レコード・インデックスの集合を返している。

続きは 後ほど、このページに追記していきたい。

一行コメントを実装しよう

# コメントA
STATE SELECT

# コメントB
WHERE ATTR ([(Alpha Cee)(Beta)]{Eee})

# コメントC

と書いたら「#」で始まる行は 読み飛ばされるとしよう。

# No! ダメ
STATE SELECT # no comment. ここにコメントは書けない
WHERE ATTR ([(Alpha Cee)(Beta)]{Eee})

行の途中からコメントにするのは今回 作らない。
SQLで 一行コメントは「--」、複数行コメントは「/」から「/」だが、行頭「#」でコメントにする方が作るのが楽なんで。

例えば 次のように

# No!
Set name1 -0.1 name2 -2 name3 --Ho! Ho!
1.5 name4 10

# No!
{alpaca/* bear*/ cat}

コメントが打てれば便利なんだが、とりあえずパス。オープンソースだし欲しかったら作ってくれ。

テストケースを作ろう

201701271004a72.png
どんどん手抜きしないと 終わらない。

ユニットテストを実行しよう

201701271004a73b.png
オッケ!

一行コメントの実装を解説しよう

201701271004a74.png
スプリットで 行分割して、「#」で始まる行は 空行に置換して、最後は 空行を全部すっとばすだけ。簡単!

続きは 後ほど、このページに追記していきたい。

続く

201701271004a76b.png

201701272205gif35.gif

201701272205gif36.gif

ひとまず、コマンドライン窓を用意した。続きは 次の記事で書いていきたい。
続き<その5>: http://qiita.com/muzudho1/items/50806e7d790034d975a6