16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ドワンゴAdvent Calendar 2020

Day 12

Yoga Layout を使ってゲームのUI構築に flexbox を取り入れる

Posted at

この記事は『ドワンゴ Advent Calendar 2020』12日目の記事です。
本日、2020年12月12日はニコニコ動画の14歳の誕生日です。……14歳!?!?!?

そんな誕生日ネタとは一切関係ない1、(ブラウザ)ゲームのUIの話です。

3行でおk

  • ゲームのメニュー等のUIの構築は結構たいへん
  • HTML+CSSみたいに勝手にいい感じに並んで欲しい
  • ReactNativeが使っている Yoga Layout が良さそうなので試してみた!

Webとゲーム

僕はお仕事でWebフロントをがちゃがちゃといじることが多いです。
Webの画面構築に使われるものといえばHTML/CSSが基本ですが、強いインタラクションが必要となるコンテンツの「プレーヤー2」は必ずしもその限りではありません。

例えば、ニコニ立体 の3Dモデルプレーヤーは Unity で開発されていたり、ニコニコQ のクイズプレーヤーはドワンゴが開発する2Dゲームエンジンである Akashic Engine で開発されています。
sss.jpg
「強いインタラクションを持つ=ゲームに近い」と言うことでき、機能要件的にこういったゲーム開発に関連する技術をWebサービスに採用することも手段になりえます。

ゲームにおけるUIレイアウト

Webの場合は「うーん、<div> ならべて display: flex で!!」のような形でレイアウトを実現することができますが、ゲームの画面構築にCSSは当然つかうことができないため、この点がゲーム技術をWebに取り入れる際のWebエンジニアにとって1つのハードルになります。

Unityの場合であれば、高度なレベルエディタが付属しているため、エディタ上で見た目通りに部品を設置したり、公式やサードパーティから提供されているレイアウトエンジンを用いることで比較的容易にUIを組むことができます。3

一方で、Akashic Engine や CreateJS、Pixi.js などといった、比較的軽量な描画ライブラリの場合、コード上で表示座標やサイズを明示する必要があります。

例)AkashicEngineの場合
const sprite = new g.FilledRect({
  scene,
  cssColor: '#ff0000',
  // ↓表示される位置やサイズを指定する必要がある
  x: 50,
  y: 100,
  width: 160,
  height: 90,
});

Webで言えば、すべての要素を position: absolute でレイアウトするような気持ちですね。どこでも配置モード。

要素数が少ないうちはコレでも問題はありませんが、たくさんの画面要素がある場合や、今後の機能追加で画面構成を変更したい!となった場合に、後からこのコードを修正して適切なレイアウトを実装するのは困難です。

実際にゲームをつくる場合は、デザイナーさんにPSDファイルやSketchファイルから座標情報を機械的に作り出せるようにレイヤー名や構造に気を使ってもらうなどの手段もあります。ですが、ユーザーの状態や入力値によって要素数が可変になる画面等はデザインツール上で作業を完結させることが難しく、最終的にはどうしてもエンジニアが頑張る必要があります。

そこで、ゲームエンジンとは別のレイアウトエンジンを用いる方法を探していたところ、Yoga Layoutを採用するのが良さそうと感じたため紹介します。

Yoga Layout

Yoga は Facebook が公開しているマルチプラットフォームのレイアウトエンジンです。
ReactNative や Facebook Litho の内部などで使われており、CSSのFlexboxのようなレイアウト計算ができます。

// https://yogalayout.com/getting-started/standalone より引用
import yoga, {Node} from 'yoga-layout';

const root = Node.create();
root.setWidth(500);
root.setHeight(300);
root.setJustifyContent(yoga.JUSTIFY_CENTER);

const node1 = Node.create();
node1.setWidth(100);
node1.setHeight(100);

const node2 = Node.create();
node2.setWidth(100);
node2.setHeight(100);

root.insertChild(node1, 0);
root.insertChild(node2, 1);

root.calculateLayout(500, 300, yoga.DIRECTION_LTR);

console.log(root.getComputedLayout());
// -> {left: 0, top: 0, width: 500, height: 300}
console.log(node1.getComputedLayout());
// -> {left: 150, top: 0, width: 100, height: 100}
console.log(node2.getComputedLayout());
// -> {left: 250, top: 0, width: 100, height: 100}

各NodeにサイズやFlexboxのパラメータや親子関係を設定し、最後に calculateLayout することで、各Nodeの座標とサイズが自動的に計算されます。
先ほどのコードの結果を図示するとこういう感じです。
こういう図

Yoga Layoutを使うと良いこと

CSSで使われているFlexboxと同様のレイアウト計算ができるため、HTML/CSSを使ったことがあるWebエンジニアであればスタイルの指定が容易です。

また、複数の画面サイズへの対応が必要になった場合や、画面内の要素数が不定な場合など、事前に座標を決め打つことが難しいケースでも、よくわからない計算式を書いたりする必要がなく、後々のUI改修が必要になった場合にどこをいじればよいのかわからないといったことをある程度予防できます。

const menuItems = [...]; // いくつ項目があるか不定
const screenWidth = 640; // この辺りの数字も後で変えるかも…?
const itemWidth = 100;
const margin = 15;

// こういう書き方をすると後でよくわからない…!
const leftBase = (screenWidth - itemWidth * menuItems.length - (margin * menuItems.length - 1)) / 2;
menuItems.forEach((item, i) => {
  item.x = leftBase + i * (itemWidth + margin);
});

// Yoga Layoutならこう書けるため、意味が残しやすい
const root = Node.create();
root.setWidth(screenWidth);
root.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);

menuItems.forEach((item, i) => {
  const child = Node.create();
  child.setWidth(itemWidth);
  if (i > 0) child.setMargin(Yoga.EDGE_LEFT, margin);
});

余談:使おうとしたときに困ったこと(2020/11/29時点)

npmで公開されている yoga-layout はインストールプロセスの中でビルド処理が行われるのですが、Nodeのバージョンが新しいとビルドができません(◞‸◟)
そのため、事前にビルド済みのものが内包されている yoga-layout-prebuilt を使おうとしたのですが、以下のIssueで上がっているエラーがでてしまい、そちらもうまくいきませんでした。

生成後の nbind.js の一部が未定義変数でエラーになってしまっているため、一旦の回避策として 生成後のnbind.jsにパッチを当てる というアレな fork をしています。ちゃんと解決したいな……

実際に使ってみた例の紹介

Yoga Layout の検証として、Akashic Engine の描画エンティティの位置・サイズを設定できるライブラリを作ってみました。

仕組み的には単純に Yoga Layout をラップしたもので、Facebook の Litho をオマージュしたような書き味で、宣言的(?)にスタイルを指定できる形になっています。

簡単な利用イメージ
import { View } from '@rutan/akashic-flexbox';

const root = View.create({scene})
               .direction('row') // fiex-direction: row; 相当
               .width(500)
               .height(240)
               .padding(15);

const entity = root.build(scene);
scene.append(entity);

もうちょっと現実に近い使い方はリポジトリの demo フォルダに入っています。
https://github.com/rutan/akashic-flexbox/blob/main/demo/

デモの画面
上記のようなレイアウトが 150行程度のコード で記述できます。またコードの内容自体も CSS がわかる方であれば比較的理解が容易かなと思います。

今後のこと

今回はざっくりUI構築ができる部分しか作っていませんが、実際のアプリケーション開発を考えると他にも考えることがありそうです。

  • 初回描画後のレイアウト計算
    • 例えばユーザーがボタンを押したら要素が増える!などの場合は、任意のタイミングでレイアウトの再計算&反映を行いたい
  • アニメーションとの連携
    • ゲーム系であればUIが動かないということはありえないはず……
    • akashic-timeline などのアニメーションライブラリとの連携ができるようにしたい

また、書き味についてもまだまだ考えられることはありそうです。
いっそ、ReactのようにJSXで指定できたら便利かもしれませんが、仮想DOMつくる?みたいになってくるとさすがに話が大げさになってくるかな……?

まとめ

レイアウト計算をする術が提供されていない環境において、Yoga Layoutを利用することでWebエンジニアやWebデザイナーには馴染みが深いCSSのようなレイアウト指定を低コストに取り入れることができます。

プロダクションで実際に使う場合は(特にモバイルなど)ファイルサイズ感などの考慮も必要ではありますが4、表示要素数が多い場合やサイズが不定の場合には強い効果を発揮できるかとおもいます。

おまけ

……という話を先日、社内のエンジニアLTでしました。ドワンゴエンジニアLTとは、毎週月曜日に業務のことや趣味でやっていることについてみんなでLTをする会です。

かつては社内の一角のスペースにリアルに人が集まってLTを行っていましたが、ドワンゴは今年より全社的にリモートワークに移行したため、現在はGoogle meetを利用したLT会を行っています。

そんなわけでVirtualCastを利用したバーチャル登壇(?)をしてみました。
事前の仕込み5とかはちょっと大変ですが、LTはエンタメでもあり、自分自身の社内PR活動とも言えるため、こういうチャレンジは色々していきたいですし、ノウハウが溜まったらどこかにまとめられたらいいなと思っています。

明日の記事は @saka1_p さんです。

  1. 何も考えずに土曜日にしよ~と思って選んで後から気づいた

  2. ニコニコのサービス的には「プレイヤー」ではなく「プレーヤー」と表記することになっているらしいです

  3. 僕にはできません……(◞‸◟)

  4. 今回つくったものをproductionビルドすると約400KB程度でした。許容できないこともないみたいなラインですね……

  5. ぼくの部屋のスペックが低いため、VRをするときは事前に部屋の物を動かす必要があります

16
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?