Unityでデスクトップマスコットを作ってみた

# はじめに
はじめまして。木更津高専 Advent Calendar 2019 11日目担当の、わくと です。
初めてのQiita記事です。がんばります！
# 概要
コーディング中・ブラウジング中に画面の右下あたりが寂しくなることはないですか？
そんなときにぴったりな、いつでも見守ってくれるデスクトップマスコットを作りました！
# 開発環境
- Windows10
- Unity2019.2.14f1

# 使用したツール・モデル
- [MMD4Mecanim](http://stereoarts.jp/)
- [戌神ころね（ホロライブ）](https://3d.nicovideo.jp/works/td63650)

# デモ
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ころねさんのデスクトップマスコット作ってみました😇<a href="https://twitter.com/hashtag/%E6%88%8C%E7%A5%9E%E3%81%93%E3%82%8D%E3%81%ADMMD?src=hash&amp;ref_src=twsrc%5Etfw">#戌神ころねMMD</a> <a href="https://t.co/CFdLYOP3rS">pic.twitter.com/CFdLYOP3rS</a></p>&mdash; わくと (@otukaw) <a href="https://twitter.com/otukaw/status/1204551965012676608?ref_src=twsrc%5Etfw">December 11, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
まばたきとマウス追従をしています。
# 実装
ボーン・モーフなどはアニメーションなどを使わず、すべてスクリプトで動かしています。
また、ウィンドウの透過などの処理はWindowsAPIを叩いています。

### モデル取り込み
まず、MMDのモデルをMMD4Mecanimを使ってインポートします。[この記事](https://qiita.com/hiroyuki_hon/items/931c79164b0ffe19517f)がわかりやすかったです。
取り込む前にreadmeをしっかり確認します。
### 表情
表情のスクリプトです。長いので簡略化してあります。

```csharp:MorphController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MorphController : MonoBehaviour
{
    MMD4MecanimModel model;
    MMD4MecanimModel.Morph[] morph;

    Dictionary<string, int> morphIndex = new Dictionary<string, int>(); // モーフのインデックス番号
    HashSet<int> changedMorphIndex = new HashSet<int>();

    void Start() {
        model = GetComponent<MMD4MecanimModel>();
        morph = model.morphList;
        int i = 0;
        foreach (var tmp in morph) {
            morphIndex.Add(tmp.morphData.nameJp, i++);
        }
    }

    void Update() {
        int screenX = Screen.width;
        int screenY = Screen.height;
        Vector3 mouse = Input.mousePosition;
        mouse.x = mouse.x / screenX / 2 - 1;
        mouse.y = mouse.y / screenY / 2 - 1;
        setMorph("瞳＿上", mouse.y);
        setMorph("瞳＿下", -mouse.y);
        setMorph("瞳＿左", mouse.x);
        setMorph("瞳＿右", -mouse.x);
    }

    // Morphの名前を渡すとvalueをセットしてくれる関数
    void setMorph(string name, float value) {
        model.morphList[morphIndex[name]].weight = value;
        changedMorphIndex.Add(morphIndex[name]);
    }
}
```

マウスの座標を、中心が(0,0)、縦方向・横方向それぞれを-1.0～1.0の範囲になるように正規化して、そのまま目のモーフに代入しています。
実際は眉毛など他の場所も操作していますが、省略します。
このスクリプトをモデルにアタッチします。
### 体関節
表情とほぼ一緒ですが、ボーンを動かすため回転情報を渡している点が違います。

```csharp:MorphController.cs
// 表情のスクリプトのつづき

Dictionary<string, int> boneIndex = new Dictionary<string, int>(); // ボーンのインデックス番号
HashSet<int> changedBoneIndex = new HashSet<int>();
float mag = 6.0f; // 回転情報を渡すときの倍率

void Start() {
    // 略
    foreach (var tmp in model.boneList) {
        boneIndex.Add(tmp.boneData.nameJp, tmp.boneID);
    }
}

void Update() {
    // 略
    setBone("頭", Mathf.Abs(mag * -mouse.y) <= 20f ? mag * -mouse.y : 20f * -Mathf.Sign(mouse.y),
                  Mathf.Abs(mag * -mouse.x) <= 10f ? mag * -mouse.x : 10f * -Mathf.Sign(mouse.x),
                  Mathf.Abs(mag / 2 * mouse.x) <= 5f ? mag / 2 * mouse.x : 5f * Mathf.Sign(mouse.x));

    setBone("腰", Mathf.Abs(mag / 2 * -mouse.y) <= 3f ? mag / 2 * -mouse.y : 3f * -Mathf.Sign(mouse.y),
                  Mathf.Abs(mag / 2 * -mouse.x) <= 10f ? mag / 2 * -mouse.x : 10f * -Mathf.Sign(mouse.x),
                  Mathf.Abs(mag / 2 * mouse.x) <= 5f ? mag / 2 * mouse.x : 5f * Mathf.Sign(mouse.x));
}

// ボーンの名前を渡すと値をセットしてくれる関数
void setBone(string name, float x, float y, float z) {
    Vector3 value = new Vector3(x, y, z); 
    model.boneList[boneIndex[name]].userEulerAngles = value;
    changedBoneIndex.Add(boneIndex[name]);
}
```

回転しすぎないようにするために三項演算子を使っているため少し複雑に見えますが、やっていることはマウスの座標に倍率をかけて代入しているだけです。

### なでなで
個人的に考えるのが一番大変だったところです。
なでなで処理は頭の上でのマウスの移動距離を使っています。

```csharp:NadeNadeController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NadeNadeController : MonoBehaviour
{
    public static bool isNadeNade = false;  // なごみ状態にあるか

    public static float mouseMov = 0;   // なでなで中のマウス移動距離
    bool hasNadeing = false;            // なでなでしているか
    int noNadeframe = 0;                // なでなでしてないフレーム数

    public int nadeRate = 10;   // なでなでを検知するしきい値
    public int noNadeRate = 5;  // なでなでしていない状態を検知するしきい値

    Vector2 mousePos;       // 現在のフレームのマウス座標
    Vector2 prevMousePos;   // 一つ前のフレームのマウス座標

    void Update()
    {
        if(noNadeframe * Time.deltaTime > 0.5f) {
            isNadeNade = false;
            mouseMov = 0;
        }

        if(!hasNadeing) {
            noNadeframe++;
        }

        if(mouseMov >= 1000) 
            isNadeNade = true;
        else
            isNadeNade = false;
    }

    private void OnMouseEnter() {
        hasNadeing = true;
        noNadeframe = 0;
        prevMousePos = Input.mousePosition;
    }

    private void OnMouseOver() {
        mousePos = Input.mousePosition;
        float distance = Mathf.Abs(Vector2.Distance(mousePos, prevMousePos)); // マウスの移動した距離
        mouseMov += distance < Screen.width / nadeRate ? 0 : distance;
        if(distance < Screen.width / noNadeRate) {
            noNadeframe++;
        } else {
            noNadeframe = 0;
        }
        prevMousePos = mousePos;
    }

    private void OnMouseExit() {
        hasNadeing = false;
        noNadeframe = 0;
    }
}
```

特に難しいことはしていませんが、一定時間以上マウスが動かないでいると普通の表情に戻ります。
当たり判定は下の画像のようになっています。
![当たり判定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265415/c355c1ea-1354-cae6-5c36-6ecff27298a5.png)


# おわりに・感想
頑張ればがんばった分だけ可愛くなるので作っていて楽しかったです。
簡単に作れるので、自分の推しで作ってみるのはいかがでしょうか？
まだまだ、待機モーションなど付け足したい機能があるので、これからも開発を続けていこうと思います！
なにかあったら[Twitter](https://twitter.com/otukaw)に連絡してもらえると助かります。

（自分の書いたコードを公開するって結構恥ずかしいんですね…）
# 参考サイト
https://qiita.com/hiroyuki_hon/items/931c79164b0ffe19517f
https://qiita.com/mkt_/items/82f4057f51b1657c971e
https://qiita.com/gatosyocora/items/7cbe14914f8e603f2eab
http://chokuto.ifdef.jp/urawaza/api/
# 著作権表記
©2019 cover corp.