C#
Unity
Oculus

VR空間でユニティちゃんからTwitterの通知をもらう

Oculus Rift Advent Calendar 2017の7日目の記事です。

自分はエンジニアではないのですが、趣味でOculus Rift用のTwitterクライアント的なアレを開発してまして、「ツイッター、ユニティちゃんが通知してくれたら、素敵やん?」と思った結果が以下になります

フォローされたりツイートがいいねされたりすると喜んで教えてくれ、逆にいいねを外されたりすると悲しみながら伝えてくれます。
ユニティちゃんも好きですが、SDユニティちゃんが可愛くて好きです。

はじめに

実装としては自前のTwitterAPIラッパーを使い、TwitterのUser Streamに接続しています。
世の中ではTwitterが有料APIを発表したりUser Streamの廃止がアナウンスされていたりしていますが、魂はまだUser Streamを求めています。

なお、動作確認はUnity 2017.1.0f3で行っており、中途半端に古くて申し訳無さがあります。

やりかた

2017-12-04_02h07_41.png
こんな風に書くのが良いのかどうかよくわかりませんが、全体のクラス関係はこのような感じです。

  • TweetEventHandlerがUser Streamへの接続を行い、
  • 返ってきたレスポンスに応じてイベントメッセージをNotificationHandlerに渡す
  • NotificationHandlerはイベントメッセージに応じてNotificationPanelとNotificationUnitychanのpublicメソッドを叩く

という流れでユニティちゃんから通知をもらいます。ちなみにフキダシをユニティちゃんに直接紐付けなかったのは、ユニティちゃんの動きに沿ってフキダシが動いたりするのが読みづらかったからです。

User Streamに接続する

GitHubのReadMeに書いてあるとおりです。

TweetEventHandler
Twity.Stream stream;

void StartMyStream()
{
    stream = new Twity.Stream(Twity.StreamType.User);
    Dictionary<string, string> streamParameters = new Dictionary<string, string>();
    StartCoroutine(stream.On(streamParameters, OnStream);
}

private void OnStream(string response, Twitter.StreamMessageType messageType)
{
 // 後述
}

User Streamから流れてきたメッセージを判別する

ここもGitHubのReadMeに書いてあるとおりなんですが一応説明すると、User Streamのコールバックにはレスポンス本文(string)とメッセージの種類(Twitter.StreamMessageType)が引数として渡されます。
https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/streaming-message-types

TwitterStreamType
public enum StreamMessageType
    {
        Tweet,                  // A Tweet has been posted.
        StatusDeletionNotice,   // A given Tweet has been deleted.
        LocationDeletionNotice, // Geolocated data must be stripped from a range of Tweets.
        LimitNotice,            // A filtered stream has matched more Tweets than its current rate limit allows to be delivered.
        WithheldContentNotice,  // Either the indicated Tweet or indicated user has had their content withheld.
        DisconnectMessage,      // Streams may be shut down for a variety of reasons.
        StallWarning,           // current health of the connection (required "stall_warnings" parameter)
        FriendsList,            // Upon establishing a User Stream connection
        FriendsListStr,
        DirectMessage,          // send or receive Direct Messages
        StreamEvent,            // Notifications about non-Tweet events. Check "event_name".
        None                    // Error or no response
    }

主に返ってくるのはFriendsListTweetStreamEventの3種類です。

  • FriendsList:Userstreamの接続が成功したときに返ってきます。型はTwitter.FriendsList
  • Tweet:フォローの誰かがつぶやいたときに返ってきます。型はTwitter.Tweet
  • StreamEvent:自分が誰かをフォローしたり誰かにフォローされたりツイートがいいねされたり、いろんなシチュエーションで返ってきます。型はTwitter.StreamEvent

このTwitter.StreamMessageTypeに応じて返ってきたstringをJsonUtilityでオブジェクトにします。

TweetEventHandler
void StartMyStream()
{
    // 先述
}

private void OnStream(string response, Twity.StreamMessageType messageType)
{
    try
    {
        // Tweetが送られてきたときはそれをパネルにして表示する
        if(messageType == Twity.StreamMessageType.Tweet)
        {
            Twity.Tweet tweet = JsonUtility.FromJson<Twity.Tweet>(response);
            // 詳しく述べませんが、ツイートを表示する処理
            if (!TweetPanelManager.isCurrentDisplayed(tweet))
            {
                TweetPanelManager.AddToCurrentList(tweet);
                GenerateTweetCard(tweet);
            }
        }

        // StreamEventが送られてきたときはNotificationHandlerに送る
        else if (messageType == Twity.StreamMessageType.StreamEvent)
        {
            Twity.StreamEvent streamEvent = JsonUtility.FromJson<Twity.StreamEvent>(response);
            GameObject.Find("NotificationHandler").GetComponent<NotificationHandler>().ShowNotification(streamEvent);
        }

        // FriendsListが送られてきたときは特に何もしない(ちゃんと返ってきたかどうかだけ確認する)
        else if (messageType == Twity.StreamMessageType.FriendsList)
        {
            Twity.FriendsList friendsList = JsonUtility.FromJson<Twity.FriendsList>(response);
        }
    }
    catch (System.Exception e)
    {
        Debug.Log(e.ToString());
    }
}

NortificationHandlerというクラスで通知に関する諸々を制御しているので、そこのpublicメソッドであるShowNotificationTwitter.StreamEvent streamEventを渡します。

StreamEventに応じてユニティちゃんに通知をもらう

通知は「フキダシ」と「ユニティちゃんの動き」の2つで表現します。どの種類のStreamEventを渡されたかによって、フキダシは表示するテキストが変わり、ユニティちゃんは喜ぶか悲しむかが変わります。

StreamEventの種類も https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/streaming-message-types に載っています。いっぱいありますが、今回はfavoriteunfavoritefollowの3つを考えます。

  • favorite:自分が誰かのツイートをいいねした、または誰かが自分のツイートをいいねした
  • unfavorite:自分が誰かのツイートをからいいねを外した、または誰かが自分のツイートからいいねをはずした
  • follow:自分が誰かをフォローした、または誰かが自分をフォローした

ちなみにunfollowは自分が誰かのフォローを外した場合だけが取得でき、自分へのフォローが外されたことはなぜか取得できません。また、リツイート関係も取得できません。

フキダシのテキスト

フキダシはそれ用のPrefabを作っておいて、通知があるたびにそれを生成する形にしています。

balloon.png
こんなん。適当につくりました。

NotificationHandler
public void ShowNotification(StreamEvent streamEvent)
{
    GameObject notificationPanel = Instantiate(
        notificationPanelPrefab, // フキダシっぽいパネルをPrefab化しておく。NotificationPanelというクラスをAttachしておく
        transform.localPosition, // NotificationHandlerはフキダシ出したい場所あたりに配置しておく
        transform.localRotation
    );

    //フキダシの初期化処理
    notificationPanel.transform.SetParent(transform);
    notificationPanel.transform.Translate(new Vector3(0.16f, 1f, 0));
    notificationPanel.GetComponent<NotificationPanel>().streamEvent = streamEvent;
    notificationPanel.GetComponent<NotificationPanel>().Init(); 

    // ユニティちゃん(後述)
    // ...
}

ちなみに、StreamEventは「自分が誰かをフォローした」というような自分の行動も通知として流れ込んできます。自分の行動は通知してもらってもしかたないので除外する必要があります。

{
"event_name":"EVENT_NAME", 
"created_at": "Sat Sep 4 16:10:54 +0000 2010",
"target": TARGET_USER, // event_nameに記載される行為の対象となった人。followならフォローされた人
"source": SOURCE_USER, // event_nameに記載される行為を行った主体。followならフォローを行った本人
"target_object": TARGET_OBJECT // event_nameに記載される行為の対象物。favoriteならTweetが入る。followならnull
}

StreamEventの中身自体はこうなっているので、sourceが自分の場合は何も通知しないようにします。
(さらにちなみにですが、TwitterのUser Streamで返ってくるメッセージ自体はevent_nameではなくeventというキー名になっています。しかしC#ではeventが予約語のため、このライブラリではevent_nameという名前に置換して使っています)

NotificationPanel
public Twity.StreamEvent streamEvent;
public void Init()
{
    bool isMyAction = streamEvent.source.id == "自分のid" ? true : false; // 自分の行動かどうかの判別

    if (streamEvent.event_name == null) return;
    if (isMyAction) return; // 自分の行動だった場合は何もしない

    Tween(true); // フキダシが出てくるときの拡大の動き。ご自由に
    transform.Find("Panel/Text").GetComponent<Text>().text = notificationText(streamEvent); // GameObjectの親子関係は適宜直してください
}

private string notificationText(Twity.StreamEvent streamEvent)
{
    return String.Format(
        eventNameDictionary[streamEvent.event_name],
        streamEvent.source.name,
        streamEvent.source.screen_name
        );
}
private Dictionary<string, string> eventNameDictionary = new Dictionary<string, string>()
{
    {"favorite", "あなたのツイートが{0}(@{1})さんからいいねされたよ!"},
    {"unfavorite", "あなたのツイートがいいねからはずされちゃった…"},
    {"follow", "{0}(@{1})さんにフォローされたよ!"}
};

これでユニティちゃんが通知してくれるフキダシができました(上の動画ではフォローしてくれた相手などは表示していませんでしたが、このコードだと表示されます)。

ユニティちゃんの動き

ユニティちゃんにはNotificationUnitychanというよくわからない名前のクラスをattachしています。
動きとしては簡単(というかデフォルトの)ジャンプと悲しみの動作に絞って、StreamEventがfavoritefollowのときはジャンプして喜び、unfavoriteのときは悲しむようにしました。それぞれの場合に"Jump"、"Sad"というStateをNotificationHandlerからNotificationUnitychanに送ります。

NotificationHandler
public void ShowNotification(Twity.StreamEvent streamEvent)
{
    // フキダシ(先述)
    // ...

    // ユニティちゃん
    unitychan.GetComponent<NotificationUnitychan>().Response(CheckUnitychanResponse(streamEvent));
}

// streamEventに応じて"Jump"か"Sad"のどちらかを返す
private string CheckUnitychanResponse(Twity.StreamEvent streamEvent)
{
    List<string> eventNameForUnitychanJump = new List<string>() { "favorite", "follow" };
    List<string> eventNameForUnitychanSad = new List<string>() { "unfavorite" };

    if (eventNameForUnitychanJump.IndexOf(streamEvent.event_name) != -1)
    {
        return "Jump";
    }
    else if (eventNameForUnitychanSad.IndexOf(streamEvent.event_name) != -1)
    {
        return "Sad";
    }
    else
    {
        return null;
    }
}

ユニティちゃん側の制御はたぶん普通のやりかた。

NotificationUnitychan
private Animator anim;
private AnimatorStateInfo currentBaseState;

static int idleState = Animator.StringToHash("Base Layer.Idle");
static int locoState = Animator.StringToHash("Base Layer.Locomotion");
static int jumpState = Animator.StringToHash("Base Layer.Jump");
static int sadState = Animator.StringToHash("Base Layer.Sad");

private void Start()
{
    anim = GetComponent<Animator>();
    anim.speed = animSpeed;
}

private void Update()
{
    currentBaseState = anim.GetCurrentAnimatorStateInfo(0);

    if (currentBaseState.fullPathHash == jumpState)
    {
        anim.SetBool("Jump", false);
    }
    else if (currentBaseState.fullPathHash == sadState)
    {
        anim.SetBool("Sad", false);
    }    
}

public void Response(string responseType)
{
    if (!anim.IsInTransition(0))
    {
        anim.SetBool(responseType, true);
    }
}

2017-12-06_00h32_32.png
Animator自体はユニティちゃんに初めからついてくるものをちょこちょこいじっている程度です。そのあたりの知見は無です。

まとめ

こんな感じでユニティちゃんから通知をもらえるようになりました。正直Oculus Riftというか単なるUnityの記事なんですが、ただ画面上で通知をもらうのに比べると、目の前でユニティちゃんが喜んだりしてくれるのは圧倒的な"良さ"があります。
VR空間上でのUIは色々と試行錯誤がなされている感じですが、キャラクターを媒介としたUIも良いものだなと思いました。誰かの本で読みましたが、「人間は人間のためのインターフェースとして残っていく」ってやつは本当にありえると思います。

なお、VR用のTwitterクライアントは細々と作り続けている趣味プロジェクトなので、もしご指摘ご意見などあればどしどしいただけると嬉しいです。


*この記事はユニティちゃんライセンス条項の元に作成されています。