iTweenでノベルアプリを作ろう！

Unityに**[iTween](https://www.assetstore.unity3d.com/en/#!/content/84)**というTween Assetがある。
今回はiTweenを使用して、ノベルアプリっぽいものを作ってみた☆

# iTweenについて
[iTween公式サイト](http://itween.pixelplacement.com/documentation.php)
[easing demo(動きのデモ)](http://www.robertpenner.com/easing/easing_demo.html)

iTweenは**無料**で豊富なTween機能を持っている便利なAssetです。
使い方はこんな感じ。

```csharp:
void Main()
{
    iTween.MoveTo(gameObject, iTween.Hash(
        "position",         Vector3.one,
        "time",             1f,
        "delay",            0f,
        "looptype",         iTween.LoopType.none,
        "easetype",         iTween.EaseType.easeInQuad,
        "islocal",          true,
        "oncomplete",       "OnMoveComplete",
        "oncompletetarget", gameObject));
}

void OnMoveComplete() { }
```

これで指定のGameObjectが移動します。
設定は全て文字列で指定するのでとても手軽です♪

# iTweenでノベルアプリを作ってみた
https://github.com/okamura0510/NovelApp
![mov.gif](https://qiita-image-store.s3.amazonaws.com/0/96554/7243aceb-b904-ebe3-4488-598f47fbe9d8.gif)

#### **Unity側**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/96554/7ba60274-e978-e4a8-a38a-7d12ab0069cd.png)

#### **NovelScript.xls**
![image2.png](https://qiita-image-store.s3.amazonaws.com/0/96554/8108669e-7c4f-ccb8-b48e-85088f22f1c9.png)

#### **プログラム**
```csharp:NovelApp.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using NPOI.SS.UserModel;
using NPOI.HSSF.UserModel;

/// <summary>
/// ノベルアプリ
/// </summary>
public class NovelApp : MonoBehaviour
{
    /// <summary>
    /// インスタンス
    /// </summary>
    public static NovelApp Instance { get; private set; }

    /// <summary>
    /// キャラ画像
    /// </summary>
    public Image Chara;
    /// <summary>
    /// メッセージテキスト
    /// </summary>
    public Text MessageText;

    /// <summary>
    /// コマンドデータ
    /// </summary>
    private Queue<CommandData> commandData = new Queue<CommandData>();
    /// <summary>
    /// コマンドリスト
    /// </summary>
    private List<Command> commands = new List<Command>();

    /// <summary>
    /// コマンドリスト
    /// </summary>
    public List<Command> Commands { get { return commands; } set { commands = value; } }

    /// <summary>
    /// 開始処理
    /// </summary>
    void Start()
    {
        Instance = this;

        // Excelからコマンド読み込み
        string path = Application.dataPath + "/!NovelApp/Editor/NovelScript.xls";
        using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            IWorkbook book = new HSSFWorkbook(fs);
            ISheet sheet = book.GetSheetAt(0);
            for (int i = 1; i <= sheet.LastRowNum; i++)
            {
                IRow row = sheet.GetRow(i);
                if (row == null) { continue; }

                // コマンド取得
                string id = row.GetCell(0).ToString();
                string param = row.GetCell(1).ToString();
                if (id == Finish.Id) { break; }

                // コマンドデータ保存
                var data = new CommandData() { Id = id, Param = param.Split(',') };
                commandData.Enqueue(data);
            }
        }
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    void Update()
    {
        // ウェイト中はコマンド実行停止
        if (Wait.IsRunning) { return; }

        // コマンド実行
        if (commandData.Count > 0)
        {
            var data    = commandData.Dequeue();
            var command = CreateCommand(data.Id, data.Param);
            command.Exec();
            commands.Add(command);
        }
    }

    /// <summary>
    /// コマンド作成
    /// </summary>
    /// <param name="id">ID</param>
    /// <param name="param">パラメータ</param>
    /// <returns>コマンド</returns>
    private Command CreateCommand(string id, string[] param)
    {
        // コマンド作成
        Command command = null;
        switch(id)
        {
            case Wait.Id:
                command = gameObject.AddComponent<Wait>();  
                break;
            case ShowChara.Id:
                command = gameObject.AddComponent<ShowChara>();
                break;
            case ShowMessage.Id:
                command = gameObject.AddComponent<ShowMessage>();
                break;
            case Move.Id:
                command = gameObject.AddComponent<Move>();
                break;
            case Rotate.Id:
                command = gameObject.AddComponent<Rotate>();
                break;
            case Punch.Id:
                command = gameObject.AddComponent<Punch>();
                break;
        }

        // コマンド初期化
        command.Init(id, param);
        return command;
    }
}
```
```csharp:CommandData.cs
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// コマンドデータ
/// </summary>
public class CommandData
{
    /// <summary>
    /// ID
    /// </summary>
    public string Id { get; set; }
    /// <summary>
    /// パラメータ
    /// </summary>
    public string[] Param { get; set; }
}
```
```csharp:Command.cs
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// コマンド基底クラス
/// </summary>
public class Command : MonoBehaviour
{
    /// <summary>
    /// ID
    /// </summary>
    public string id = "";
    /// <summary>
    /// パラメータ
    /// </summary>
    public string[] param = null;

    /// <summary>
    /// 初期化
    /// </summary>
    /// <param name="id">ID</param>
    /// <param name="param">パラメータ</param>
    public virtual void Init(string id, string[] param)
    {
        this.id    = id;
        this.param = param;
    }
    
    /// <summary>
    /// 実行
    /// </summary>
    public virtual void Exec() { }

    /// <summary>
    /// 終了
    /// </summary>
    public virtual void Finish()
    {
        NovelApp.Instance.Commands.Remove(this);
        Destroy(this);
    }
}
```
```csharp:Wait.cs
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// ウェイトコマンド
/// </summary>
public class Wait : Command
{
    public const string Id = "Wait";

    /// <summary>
    /// ウェイト実行中フラグ
    /// </summary>
    public static bool IsRunning
    {
        get
        {
            // コマンド内にウェイトコマンドが存在するか？
            foreach (var command in NovelApp.Instance.Commands)
            {
                if (command is Wait) { return true; }
            }
            return false;
        }
    }

    /// <summary>
    /// ウェイト時間
    /// </summary>
    private float time = 0f;

    /// <summary>
    /// 初期化
    /// </summary>
    public override void Init(string id, string[] p)
    {
        base.Init(id, p);

        time = float.Parse(param[0]);
    }

    /// <summary>
    /// 実行
    /// </summary>
    public override void Exec()
    {
        // 一定時間なにもしない(空回し)
        iTween.ValueTo(gameObject, iTween.Hash(
            "from",             0f,
            "to",               1f,
            "time",             time,
            "onupdate",         "OnWaitUpdate",
            "oncomplete",       "OnWaitComplete",
            "oncompletetarget", gameObject));
    }

    /// <summary>
    /// ウェイト更新
    /// </summary>
    /// <param name="value">変化値</param>
    void OnWaitUpdate(float value) { }

    /// <summary>
    /// ウェイト終了
    /// </summary>
    void OnWaitComplete()
    {
        Finish();
    }
}
```
```csharp:ShowChara.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// キャラ表示コマンド
/// </summary>
public class ShowChara : Command
{
    public const string Id = "ShowChara";

    /// <summary>
    /// キャラ画像
    /// </summary>
    private Image chara = null;
    /// <summary>
    /// ウェイト時間
    /// </summary>
    private string fileName = "";
    /// <summary>
    /// 表示タイプ。0：瞬時、1：フェイドイン
    /// </summary>
    private int showType = 0;

    /// <summary>
    /// 初期化
    /// </summary>
    public override void Init(string id, string[] p)
    {
        base.Init(id, p);

        chara    = NovelApp.Instance.Chara;
        fileName = param[0];
        showType = int.Parse(param[1]);
    }

    /// <summary>
    /// 実行
    /// </summary>
    public override void Exec()
    {
        // キャラ画像読み込み
        NovelApp.Instance.Chara.sprite = Resources.Load<Sprite>(fileName);

        // 表示
        if (showType == 0)
        {
            // 瞬時(終了)
            OnShowCharaComplete();
        }
        else
        {
            // フェイドイン
            chara.color = new Color(chara.color.r, chara.color.g, chara.color.b, 0f);
            iTween.ValueTo(gameObject, iTween.Hash(
                "from",             0f,
                "to",               1f,
                "time",             1f,
                "onupdate",         "OnShowCharaUpdate",
                "oncomplete",       "OnShowCharaComplete",
                "oncompletetarget", gameObject));
        }
    }

    /// <summary>
    /// 表示更新
    /// </summary>
    /// <param name="value">変化値</param>
    void OnShowCharaUpdate(float value)
    {
        // アルファ変化
        chara.color = new Color(chara.color.r, chara.color.g, chara.color.b, value);
    }

    /// <summary>
    /// 表示終了
    /// </summary>
    void OnShowCharaComplete()
    {
        Finish();
    }
}
```
```csharp:ShowMessage.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// メッセージ表示コマンド
/// </summary>
public class ShowMessage : Command
{
    public const string Id = "ShowMessage";

    /// <summary>
    /// メッセージテキスト
    /// </summary>
    private Text messageText = null;
    /// <summary>
    /// メッセージ
    /// </summary>
    private string message = "";

    /// <summary>
    /// 初期化
    /// </summary>
    public override void Init(string id, string[] p)
    {
        base.Init(id, p);

        messageText = NovelApp.Instance.MessageText;
        message     = param[0];
    }

    /// <summary>
    /// 実行
    /// </summary>
    public override void Exec()
    {
        messageText.text = message.Replace("<br>", "\n");
        Finish();
    }
}
```
```csharp:Move.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// 移動コマンド
/// </summary>
public class Move : Command
{
    public const string Id = "Move";

    /// <summary>
    /// キャラ画像
    /// </summary>
    private Image chara = null;
    /// <summary>
    /// ポジション
    /// </summary>
    private Vector3 position = Vector3.zero;
    /// <summary>
    /// 時間
    /// </summary>
    private float time = 0f;
    /// <summary>
    /// 初回ウェイト
    /// </summary>
    private float delay = 0f;
    /// <summary>
    /// ループタイプ
    /// </summary>
    private iTween.LoopType looptype = iTween.LoopType.none;
    /// <summary>
    /// イースタイプ
    /// </summary>
    private iTween.EaseType easetype = iTween.EaseType.linear;

    /// <summary>
    /// 初期化
    /// </summary>
    public override void Init(string id, string[] p)
    {
        base.Init(id, p);

        var vec  = param[0].Split('/').Select((s) => float.Parse(s)).ToArray();
        chara    = NovelApp.Instance.Chara;
        position = new Vector3(vec[0], vec[1], vec[2]);
        time     = float.Parse(param[1]);
        delay    = float.Parse(param[2]);
        looptype = (iTween.LoopType)Enum.Parse(typeof(iTween.LoopType), param[3]);
        easetype = (iTween.EaseType)Enum.Parse(typeof(iTween.EaseType), param[4]);
    }

    /// <summary>
    /// 実行
    /// </summary>
    public override void Exec()
    {
        iTween.MoveTo(chara.gameObject, iTween.Hash(
            "position",         position,
            "time",             time,
            "delay",            delay,
            "looptype",         looptype,
            "easetype",         easetype,
            "islocal",          true,
            "oncomplete",       "OnMoveComplete",
            "oncompletetarget", gameObject));
    }

    /// <summary>
    /// 移動終了
    /// </summary>
    void OnMoveComplete()
    {
        Finish();
    }
}
```
```csharp:Rotate.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// 回転コマンド
/// </summary>
public class Rotate : Command
{
    public const string Id = "Rotate";

    /// <summary>
    /// キャラ画像
    /// </summary>
    private Image chara = null;
    /// <summary>
    /// 角度
    /// </summary>
    private Vector3 rotation = Vector3.zero;
    /// <summary>
    /// 時間
    /// </summary>
    private float time = 0f;
    /// <summary>
    /// 初回ウェイト
    /// </summary>
    private float delay = 0f;
    /// <summary>
    /// ループタイプ
    /// </summary>
    private iTween.LoopType looptype = iTween.LoopType.none;
    /// <summary>
    /// イースタイプ
    /// </summary>
    private iTween.EaseType easetype = iTween.EaseType.linear;

    /// <summary>
    /// 初期化
    /// </summary>
    public override void Init(string id, string[] p)
    {
        base.Init(id, p);

        var vec  = param[0].Split('/').Select((s) => float.Parse(s)).ToArray();
        chara    = NovelApp.Instance.Chara;
        rotation = new Vector3(vec[0], vec[1], vec[2]);
        time     = float.Parse(param[1]);
        delay    = float.Parse(param[2]);
        looptype = (iTween.LoopType)Enum.Parse(typeof(iTween.LoopType), param[3]);
        easetype = (iTween.EaseType)Enum.Parse(typeof(iTween.EaseType), param[4]);
    }

    /// <summary>
    /// 実行
    /// </summary>
    public override void Exec()
    {
        iTween.RotateTo(chara.gameObject, iTween.Hash(
            "rotation",         rotation,
            "time",             time,
            "delay",            delay,
            "looptype",         looptype,
            "easetype",         easetype,
            "islocal",          true,
            "oncomplete",       "OnRotateComplete",
            "oncompletetarget", gameObject));
    }

    /// <summary>
    /// 回転終了
    /// </summary>
    void OnRotateComplete()
    {
        Finish();
    }
}
```
```csharp:Punch.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// パンチコマンド
/// </summary>
public class Punch : Command
{
    public const string Id = "Punch";

    /// <summary>
    /// キャラ画像
    /// </summary>
    private Image chara = null;
    /// <summary>
    /// ダメージ量
    /// </summary>
    private Vector3 amount = Vector3.zero;
    /// <summary>
    /// 時間
    /// </summary>
    private float time = 0f;
    /// <summary>
    /// 初回ウェイト
    /// </summary>
    private float delay = 0f;
    /// <summary>
    /// ループタイプ
    /// </summary>
    private iTween.LoopType looptype = iTween.LoopType.none;
    /// <summary>
    /// イースタイプ
    /// </summary>
    private iTween.EaseType easetype = iTween.EaseType.linear;

    /// <summary>
    /// 初期化
    /// </summary>
    public override void Init(string id, string[] p)
    {
        base.Init(id, p);

        var vec  = param[0].Split('/').Select((s) => float.Parse(s)).ToArray();
        chara    = NovelApp.Instance.Chara;
        amount   = new Vector3(vec[0], vec[1], vec[2]);
        time     = float.Parse(param[1]);
        delay    = float.Parse(param[2]);
        looptype = (iTween.LoopType)Enum.Parse(typeof(iTween.LoopType), param[3]);
        easetype = (iTween.EaseType)Enum.Parse(typeof(iTween.EaseType), param[4]);
    }

    /// <summary>
    /// 実行
    /// </summary>
    public override void Exec()
    {
        iTween.PunchScale(chara.gameObject, iTween.Hash(
            "amount",           amount,
            "time",             time,
            "delay",            delay,
            "looptype",         looptype,
            "easetype",         easetype,
            "islocal",          true,
            "oncomplete",       "OnPunchComplete",
            "oncompletetarget", gameObject));
    }

    /// <summary>
    /// パンチ終了
    /// </summary>
    void OnPunchComplete()
    {
        Finish();
    }
}
```
```csharp:Finish.cs
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// 終了コマンド
/// </summary>
public class Finish : Command
{
    public const string Id = "Finish";
}
```

# 解説
## あゆめぐ
猫です。不思議な生き物です。

## NovelScript.xls
ノベルスクリプト。ここに記述されてる内容を1行1行読み込んでコマンド処理しています。
UnityでExcelファイルを読み込むのは[NPOI](http://npoi.codeplex.com/)使うと楽です([G2U](https://www.assetstore.unity3d.com/en/#!/content/11818)で管理すると尚いいかも)。

## NovelApp.cs
メインプログラム。NovelScript.xlsからコマンドを読み込んで、コマンドクラスにパース後、順次処理しています。
コマンドクラスはMonoBehaviourを継承しているので、現在実行中のコマンドデータがエディタ上から確認出来ます。

## コマンドクラス
コマンド毎にiTweenの機能を使用しています。
特に`iTween.ValueTo`は便利です。一定時間に何らかの処理を行う場合、大抵これで対応出来ます。
あと`iTween.Punch***`。名前がまんまｗ

## 注意点
iTweenは便利なんですが、若干パフォーマンスが悪いです([比較](http://www.daikonforge.com/forums/threads/dftween-performance-test-results.1867/))。
予想ですが、iTweenは実行時にGameObjectにスクリプトが追加されます。このスクリプトが処理完了後に毎回破棄されているので重いのではないかと思ってます。

# 最後に
なんかちょっとiTweenの説明するつもりが意外と規模が大きくなってしまった。。。
もはやiTweenの説明の域を超えてるというね(笑)

Tween Assetは他にもたくさんあるのですが、やっぱり**無料で手軽に使える**っていうのが長く使われてる理由かな。

シンプル イズ ベスト(この記事とは関係ない)