Interactive UI with UniRx
-
Upload
yuto-iwashita -
Category
Technology
-
view
7.641 -
download
6
Transcript of Interactive UI with UniRx
Interactive UI With UniRx
• 株式会社トライフォート
• Unity/JSエンジニア
• 24歳
岩下 侑冬 Yuto Iwashita
@y120sb
?UI今日話すこと
MVなんちゃらの話ではない
• パターンの話は先人の記事を読もう
• MVVMとかMVPとか
最近のUIに求められるもの
• リッチなアニメーション
• 手触り感
• パフォーマンス
UIの実装はラクじゃない
• ビジネスロジックなんて大した問題ではない
• (個人的に)アプリの実装コストの7割はUI
• ドラッグ、アニメーション、etc…
ラクしたい
UniRxでできます!
UniRxが解決する2つの問題
ロジック的ややこしさ
と
手触り感の表現
ロジック的ややこしさ
ロジック的ややこしさ
• 仕様が複雑で実装も複雑になるやつ
• そういうUIをユーザーに分からせるには 親切さも表現しなくてはならない ➔ 色んな親切機能を追加する羽目に
例
トゥモローアイランドhttp://tomorrowisland.trifort.jp/
• 弊社ソーシャルゲームタイトル第一弾
• 箱庭 × MO
• 開発期間1年くらい、 Unityエンジニアは最大で5人
自分だけの箱庭を作ってみたり
仲間と一緒に 冒険したり
サービス終了するけど…
• アプリを通してUniRxガッツリ!
• クローズしてもUniRxは便利
アイテム合成UI
• D&D
• ドラッグ通過判定
• 所要時間の表示
• アニメーション
複雑なUI
• D&D
• ドラッグ通過判定
• 所要時間の表示
• アニメーション
複雑なUI
全部UniRxの守備範囲
すべてをObservableへ
• IObservable<T>だらけにするとロジックからUI、アニメーションへの繋ぎが簡単になる
• 書く人によって違う非同期処理の書き方も 統一できる(C# event, callback, coroutine)
所要時間の表示//ドラッグ開始 OnLongTapDragStart
.Select(tuple => tuple.Item2.Model.Recipe) //スロットにレシピが投入されるストリーム .SelectMany(recipe => OnThrowIn
.Select(slots => slots.Where(s => s.Thrown).Count())
.Select(count => new { recipe, count }) //ドラッグ開始時にレシピ1枚の場合の所要時間を表示するため .StartWith(new { recipe, count = 1 }))
//所要時間に変換 .Select(anon => anon.recipe.CalculateNeedTime(anon.count)) //ドラッグ終了まで待ち受け .TakeUntil(OnLongTapDragEnd) //繰り返し .Repeat().Subscribe(time => needTime.text = time)
DragStart
SelectMany
Select
TakeUntil
Repeat
アニメーション/// <summary>/// 箱のなかに吸い込まれる /// </summary>public IObservable<Unit> IntoBox (SlotBox box) {
const float duration = 0.45f;
//拡縮var tween = itemIconTransform.DOScale(Vector3.zero, duration);
//移動 itemIconTransform.DOMove(box.Position, duration);
//TweenerをIObservable<Tweener>に変換return tween.AsObservable();
}
UniRxが解決する2つの問題
ロジック的ややこしさ
と
手触り感の表現
手触り感の表現
手触り感
• 最近の流行り(だと思う)
• 触ったら動く
• 動いたらそれが連鎖していく
例
Paperhttps://www.facebook.com/paper
• Facebook製
• ぬるぬる動く、触れる
• 見やすくはない
• 指の動きと連動して徐々に変化するUI
• どうつくる?
動きの関係性を定義する
1) アクションから発生する動きを見つける
2) 動きを細かく分解する
3) 分解した動きをストリームとして記述する
4) 動きを全て合成し、定義する
動きの関係性を定義する
1) アクションから発生する動きを見つける
指が動く 要素が動く
2) 動きを細かく分解する
• 指がY方向に動く 拡縮する
• 指がX方向に動く X方向に動く
2) 動きを細かく分解する
• 指がY方向に動く 拡縮する
• 指がX方向に動く X方向に動く
• Y方向に拡縮する Y方向の位置がずれる ずれを戻す
• X方向に拡縮する X方向の位置がずれる ずれを戻す
3) 分解した動きを記述する
• UniRxのストリームとして動きを記述する
指がX方向に動く ⇣
X方向に動く
//IObservable<Vector2> var dragMoveStream = onDrag
.Select(e => container.InverseTransformVector(e.delta).x)
.Select(deltaX => new Vector2(deltaX, 0f));
指がY方向に動く ⇣
拡縮する
//IObservable<Vector3> var scaleStream = onDrag
.Select(e => parent.localScale.y * ToScreenPosition(e.position).y / ToScreenPosition(e.position - e.delta).y)
.Select(scale => Vector3.one * scale);
Y方向に拡縮する、ずれる ⇣
Y方向のずれを戻す
//IObservable<Vector2>var cancelSlipYStream = scaleStream
.Select(scale => defaultHeight * scale.x)
.Select(y => new Vector2(0f, y - parent.anchoredPosition.y));
X方向に拡縮する、ずれる ⇣
X方向のずれを戻す
//IObservable<Vector2> var cancelSlipXStream = onDrag
.Select(e => container.InverseTransformPoint(e.position))
.Zip(scaleStream.Select(scale => parent.localScale.x / scale.x), (position, comparsionScale) => new { position, comparsionScale })
.Select(anon => (anon.comparsionScale - DefaultScale) * (-parent.anchoredPosition.x + anon.position.x))
.Select(deltaX => new Vector2(deltaX, 0f));
//移動 Observable.Merge(dragMoveStream, cancelSlipYStream, cancelSlipXStream)
.Subscribe(move => parent.anchoredPosition += move)
.AddTo(this);
//拡縮scaleStream
.Subscribe(scale => parent.localScale = scale)
.AddTo(this);
4) 動きを全て合成し、定義する
Demo
関係性の定義によるUIの実装
• 原因(指の動き)と起こる結果(拡縮、移動)を 細かく分解し、そのまま記述する
• 最後にUniRxで合成して適用する
Settings編
Settingsの動きの関係性
• 指が動く 触れた要素が動く
• 要素が動く 真上にある要素が遅れて動く
• 要素が動く ➔ 真下にある要素が遅れて動く
Settingsの動きの関係性
• 指が動く 触れた要素が動く
• 要素が動く 直後にある要素が遅れて動く
• 要素が動く ➔ 直前にある要素が遅れて動く
ループしてしまう
Settingsの動きの関係性• 指が動く 触れた要素が動く 前後の要素に動きが伝わる
• 上から動きが伝わってくる ➔ 下に伝える
• 下から動きが伝わってくる ➔ 上に伝える
伝播してきた動きは逆向きへ伝える
ToPrev
つられて動く
動く要素
ToPrev
つられて動く
public class ListItem : MonoBehaviour { //上に動きを伝えるパイプ public IObservable<Vector2> ToPrev { get { return toPrev.AsObservable(); } } //下に動きを伝えるパイプ public IObservable<Vector2> ToNext { get { return toNext.AsObservable(); } }
• まず上下の要素を繋ぐパイプを用意する
リスト要素のクラスを作成する
上から動きが伝わってくる ⇣
下に伝える
var fromPrev = myTransform.parent.GetChild(siblingIndex - 1) .GetComponent<ListItem>() .ToNext .Delay(TimeSpan.FromMilliseconds(10));
fromPrev.Subscribe(toNext).AddTo(this);
下から動きが伝わってくる ⇣
上に伝える
var fromNext = myTransform.parent.GetChild(siblingIndex + 1) .GetComponent<ListItem>() .ToPrev .Delay(TimeSpan.FromMilliseconds(10));
fromNext.Subscribe(toPrev).AddTo(this);
指が動く ⇣
上下に伝わる
//ドラッグによる移動IObservable<Vector2> fromDrag = this.OnDragAsObservable()
.Select(e => myTransform.parent.InverseTransformVector(e.delta))
.Select(delta => new Vector2(delta.x, 0));
fromDrag.Subscribe(toPrev).AddTo(this);fromDrag.Subscribe(toNext).AddTo(this);
動きをまとめ、関係性として定義
Observable.Merge(fromPrev, fromNext, fromDrag) .Subscribe(delta => myTransform.anchoredPosition += delta) .AddTo(this);
Demo
注意とまとめ
• RxはUIでも便利。使えそうな所を見つけたらガンガンつかっていこう。
• 無理して使わない。
• 複雑な動きも分解してみると大したことないかも。
Thanks!