Unityのエディター拡張で駅すぱあとを動かしてみる

[ヴァル研究所 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/val) の11日目はUnityに関するお話です。

Super Mario Runなどで話題のUnity上で駅すぱあとの経路探索をやってしまう方法です。
しかも、Unityも駅すぱあとも無料で始められるので、初めての方にもお勧めです。

# 事前準備(Unity編)

## Unityを用意する

まずはUnityをWebサイトからダウンロードします。
ダウンロードは[こちら(Unity Technologies社)](https://unity3d.com/jp/get-unity/update)から行うことが出来ます。
ちなみに、WindowsとMacOSXのどちらでも動きます。

## Unityをインストールする

ダウンロードしたファイルを実行してインストールします。
今回はUnityのエディター本体だけで十分ですが、将来的にiOSやAndroidなどでも動かしたい場合は対応するプラットフォームのコンポーネントも入れておくと良いでしょう。

# 事前準備(駅すぱあと編)

## 駅すぱあとWebサービスの利用登録を行う

「駅すぱあとWebサービス」は、駅すぱあとの検索エンジン等をWebAPIとして活用出来るサービスです。
有料のスタンダードプランと無料のフリープランがありますが、今回利用する駅すぱあとWebサービスはフリープランでかまいません。

[駅すぱあとWebサービスお申し込みページはこちらから](https://ekiworld.net/service/lp/webservice/)

なお、ご利用ドメインの入力を求められますが、Unityのエディターから利用する場合はドメイン指定が不要ですので、ドメインが分からない場合は「localhost」などを入れておけば大丈夫です。

## 駅すぱあとWebサービスのアクセスキーが届くのを待つ

アクセスキーが発行されるまでしばらくかかります。

その間にUnityを動かしてみたり、[アセットストア(Unity Technologies社)](https://www.assetstore.unity3d.com/jp/#!/)でUnity Technologies社のサンプルプロジェクトをダウンロードして動かしてみたりして、Unityに慣れておくことをオススメします。

[サンプルプロジェクト一覧(Unity Technologies社)](https://unity3d.com/jp/learn/resources/downloads)

# 実装

## Unityを起動する

Unityを起動します。
なお、初めて使う場合はUnityのアカウント作成が必要になります。
画面の指示にしたがって登録を完了させてください。

## プロジェクトを作成する

今回は分かりやすいように空のプロジェクトを作成します。
プロジェクト名は特に制限がありませんので、分かりやすいものを指定すれば大丈夫です。
また、エディター拡張のみ利用しますので、3Dと2Dのどちらでもかまいません。

![プロジェクト作成!](https://qiita-image-store.s3.amazonaws.com/0/153241/3df29998-e0ba-4ccb-3d06-47f396253bd0.png)

ちなみに、既にUnityを使いこなせている方は自分のプロジェクトに入れてみるのも良いでしょう。
※このページに記載のC#スクリプトは転載可ですので、GitHubなどで共有いただいても問題ありません。

## C#スクリプトを作成する

1. プロジェクトウィンドウで /Assets/EkispertWebService/Editor/ を作成します。
1. Create → C# Script を選択してスクリプトファイルを作成します。
1. スクリプトに Ekispert という名前を付けます。

これで Ekispert.cs ファイルが出来上がりました。

![スクリプトファイルの作成](https://qiita-image-store.s3.amazonaws.com/0/153241/df12d2c4-26f6-8314-a1ba-b1159e532ecc.png)

## スクリプトを編集する

先ほど作成したスクリプトファイルをダブルクリックするとMonoDevelopが開くはずです。
下記のスクリプトを Ekispert.cs のスクリプトに置き換えて保存してみてください。

```C#
using UnityEngine;
using UnityEditor;
using System.Collections;

public class Ekispert : EditorWindow
{
    private string[] DateYearItems;
    private int DateYearIndex = -1;
    private string[] DateMonthItems;
    private int DateMonthIndex = -1;
    private string[] DateDayItems;
    private int DateDayIndex = -1;

    private string[] DateHourItems;
    private int DateHourIndex = -1;
    private string[] DateMinuteItems;
    private int DateMinuteIndex = -1;

    private string StationName1 = "";
    private string StationName2 = "";
    private string StationName3 = "";
    private bool Plane = true;
    private bool Shinkansen = true;
    private bool LimitedExpress = true;
    private bool Bus = true;
    private int SearchTypeIndex = 0;
    private string[] SearchTypeItems = { "出発時刻探索", "到着時刻探索", "終電探索", "始発探索" };

    private string Key = "";
    private string ApiUrl = "http://api.ekispert.jp/";

    [@MenuItem("Window/駅すぱあと")]
    public static void ShowWindow()
    {
        EditorWindow.GetWindow(typeof(Ekispert));
    }

    void OnGUI()
    {
        GUILayout.Label("駅すぱあとWebサービス", EditorStyles.boldLabel);

        EditorGUILayout.BeginHorizontal();
        GUILayout.Label("探索種別", EditorStyles.label);
        SearchTypeIndex = EditorGUILayout.Popup(SearchTypeIndex, SearchTypeItems);
        EditorGUILayout.EndHorizontal();

        EditorGUILayout.BeginHorizontal();
        GUILayout.Label("日付", EditorStyles.label);
        int index;
        // 年
        ArrayList YearArray = new ArrayList();
        index = 0;
        for (int i = System.DateTime.Now.Year - 1; i <= System.DateTime.Now.Year + 1; i++)
        {
            if (DateYearIndex == -1 && i == System.DateTime.Now.Year)
            {
                DateYearIndex = index;
            }
            YearArray.Add(string.Format("{0}年", i));
            index++;
        }
        DateYearIndex = EditorGUILayout.Popup(DateYearIndex, (string[])YearArray.ToArray(typeof(string)));
        // 月
        index = 0;
        ArrayList MonthArray = new ArrayList();
        for (int i = 1; i <= 12; i++)
        {
            if (DateMonthIndex == -1 && i == System.DateTime.Now.Month)
            {
                DateMonthIndex = index;
            }
            MonthArray.Add(string.Format("{0}月", i));
            index++;
        }
        DateMonthIndex = EditorGUILayout.Popup(DateMonthIndex, (string[])MonthArray.ToArray(typeof(string)));
        // 日
        index = 0;
        ArrayList DayArray = new ArrayList();
        int LastDay = 31;
        if (DateMonthIndex + 1 == 4 || DateMonthIndex + 1 == 6 || DateMonthIndex + 1 == 9 || DateMonthIndex + 1 == 11)
        {
            LastDay = 30;
        }
        else if (DateMonthIndex + 1 == 2)
        {
            LastDay = 28;
            if ((System.DateTime.Now.Year - 1 + DateYearIndex) % 4 == 0)
            {
                if (!((System.DateTime.Now.Year - 1 + DateYearIndex) % 100 == 0 && (System.DateTime.Now.Year - 1 + DateYearIndex) % 400 != 0))
                {
                    LastDay = 29;
                }
            }
        }
        for (int i = 1; i <= LastDay; i++)
        {
            if (DateDayIndex == -1 && i == System.DateTime.Now.Day)
            {
                DateDayIndex = index;
            }
            DayArray.Add(string.Format("{0}日", i));
            index++;
        }
        DateDayIndex = EditorGUILayout.Popup(DateDayIndex, (string[])DayArray.ToArray(typeof(string)));
        EditorGUILayout.EndHorizontal();

        if (SearchTypeIndex == 0 || SearchTypeIndex == 1)
        {
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label("時刻", EditorStyles.label);
            // 時
            ArrayList HourArray = new ArrayList();
            for (int i = 0; i <= 23; i++)
            {
                if (DateHourIndex == -1 && i == System.DateTime.Now.Hour)
                {
                    DateHourIndex = i;
                }
                HourArray.Add(string.Format("{0}時", i));
            }
            DateHourIndex = EditorGUILayout.Popup(DateHourIndex, (string[])HourArray.ToArray(typeof(string)));
            // 分
            ArrayList MinuteArray = new ArrayList();
            for (int i = 0; i <= 59; i++)
            {
                if (DateMinuteIndex == -1 && i == System.DateTime.Now.Minute)
                {
                    DateMinuteIndex = i;
                }
                MinuteArray.Add(string.Format("{00}分", i));
            }
            DateMinuteIndex = EditorGUILayout.Popup(DateMinuteIndex, (string[])MinuteArray.ToArray(typeof(string)));
            EditorGUILayout.EndHorizontal();
        }

        StationName1 = EditorGUILayout.TextField("出発地", StationName1);
        StationName2 = EditorGUILayout.TextField("目的地", StationName2);
        StationName3 = EditorGUILayout.TextField("経由地", StationName3);

        Plane = EditorGUILayout.Toggle("飛行機", Plane);
        Shinkansen = EditorGUILayout.Toggle("新幹線", Shinkansen);
        LimitedExpress = EditorGUILayout.Toggle("特急", LimitedExpress);
        Bus = EditorGUILayout.Toggle("バス", Bus);

        Key = EditorGUILayout.TextField("アクセスキー", Key);

        if (Key == "")
        {
            EditorGUILayout.HelpBox("アクセスキーを入力してください", MessageType.Warning);
        }
        else
        {
            if (StationName1 == "" || StationName2 == "")
            {
                EditorGUILayout.HelpBox("出発地と目的地を入力して探索を行ってください", MessageType.Info);
            }
            else
            {
                if (GUILayout.Button("探索結果を表示", EditorStyles.miniButton))
                {
                    if (StationName1 != "" && StationName2 != "")
                    {
                        Application.OpenURL(getEWSUrl());
                    }
                }
            }
        }
    }

    private string getEWSUrl()
    {
        string url = ApiUrl + "v1/xml/search/course/light";
        url += "?key=" + Key;
        url += "&from=" + WWW.EscapeURL(StationName1);
        url += "&to=" + WWW.EscapeURL(StationName2);
        if (StationName3 != "")
        {
            url += "&via=" + WWW.EscapeURL(StationName3);
        }
        url += "&date=" + (System.DateTime.Now.Year - 1 + DateYearIndex) + (DateMonthIndex < 9 ? "0" : "") + (DateMonthIndex + 1) + (DateDayIndex < 9 ? "0" : "") + (DateDayIndex + 1);
        switch (SearchTypeIndex)
        {
            case 0:
                url += "&searchType=departure";
                url += "&time=" + (DateHourIndex < 10 ? "0" : "") + DateHourIndex + (DateMinuteIndex < 10 ? "0" : "") + DateMinuteIndex;
                break;
            case 1:
                url += "&searchType=arrival";
                url += "&time=" + (DateHourIndex < 10 ? "0" : "") + DateHourIndex + (DateMinuteIndex < 10 ? "0" : "") + DateMinuteIndex;
                break;
            case 2:
                url += "&searchType=lastTrain";
                break;
            case 3:
                url += "&searchType=firstTrain";
                break;
        }
        url += "&plane=" + (Plane ? "true" : "false");
        url += "&shinkansen=" + (Shinkansen ? "true" : "false");
        url += "&limitedExpress=" + (LimitedExpress ? "true" : "false");
        url += "&bus=" + (Bus ? "true" : "false");
        url += "&redirect=true";
        return url;
    }
}
```

## 動作確認してみる

Window メニューの中に「駅すぱあと」が表示されるようになりました。
早速カーソルを合わせて選択してみます。

![メニュー](https://qiita-image-store.s3.amazonaws.com/0/153241/6ed029da-3948-4ed0-8e3b-4329a13ed57a.png)

すると、どうでしょう、駅すぱあとウィンドウが開きます。

![駅すぱあとウィンドウ](https://qiita-image-store.s3.amazonaws.com/0/153241/37f2f4d1-e12e-abbc-7cf7-7ff56a80c007.png)

出発地、目的地、送られてきた駅すぱあとWebサービスのアクセスキーを入力すると「探索結果を表示」のボタンが出ますので、押してみます。

![経路探索](https://qiita-image-store.s3.amazonaws.com/0/153241/b58d58e6-5a46-0b4c-f2bb-9c99f3c69115.png)

ブラウザが立ちあがって、経路が表示されましたか？

![探索結果](https://qiita-image-store.s3.amazonaws.com/0/153241/31d57130-250e-6113-6d0f-fba2720181ef.png)

# まとめ

Unityのエディターに統合することで簡単に移動経路が表示出来るようになりました。
「開発に夢中になって帰宅時間が分からない」とか「終電あるの？」とか、色々な場面で活用できると思います。
また、今回利用した駅すぱあとWebサービスフリープランのAPIは「駅すぱあと for web URL生成」です。
[フリープランドキュメント](http://docs.ekispert.com/v1/le/)
スクリプトを書き換えて「常に終電で経路を表示してみる」とか、アクセスキーの入力が面倒なので埋め込んでみるとか、カスタマイズすることも可能です。

*ご注意* アクセスキーは大切に扱ってください。埋め込んだ状態でGitHubなどで公開は行わないようにお願いします。