ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

Unity の uGUI でウィンドウのフェードアウト処理

ゲームを作るなら細かい演出は大事! フェードアウトは大事!

Unity で、GameObject のフェードアウト処理をおこないたくてネットで探したんですが、
GameObject 単体のフェードアウト処理になっているものが多かったので、
All Children GameObject (すべての子オブジェクト)に対しても同時におこなえるフェードアウト処理をおこなう方法について書きます!

なお、シーン間のフェードアウト・イン処理をおこなう方法としては、
こちらの 『Simple Fade Scene Transition System』 というアセットがおすすめです。
今回のコードを書くにあたって参考にした部分が多いです。

ポイント1: CanvasGroup

こちらの Q&A が参考になりました。 : How to fade a game object will all its children?

GameObject の Image に対して処理をおこなう方法ですと、
子オブジェクトのアルファ値はそのままになってしまいます。

Canvas Group を目的の GameObject にコンポーネントとして付与することで、
Children GameObject を含めたアルファ値の操作を楽におこなうことが出来ます。

f:id:nekonenene:20190519180615p:plain
Canvas Group Component は Alpha を値として持つ

ポイント2: EventSystem

また、フェードアウト中はユーザーの操作を受け付けたくありません。

タッチ操作やクリック操作を無効にする必要があります。
そのためには、操作の受け付け担当である EventSystem さんを一時的に無効にしてあげるのが一番です。

EventSystem eventSystem = GameObject.FindObjectOfType<EventSystem>();
eventSystem.enabled = false;

でおこなえます。true に戻してあげるのを忘れないよう注意しましょう!

(参考: UnityのEventSystemを操る - あさちゅんのゲームブログ

そして出来上がったコード

出来上がったコードがこちらです。

対象の GameObject に、独自 Component である Fadeout Component と、前述した CanvasGroup Component を一時的にアタッチし、
フェードアウトが終わったら削除し、 GameObject に対して SetActive(false) を実行しています。

Destroy(this); のあとのコードが実行されるのが自分的には不思議なんですが、ここなんでなのでしょう……わかる方いらっしゃいましたら教えてください)

なお、独自 Component を作ってアタッチしているのは、 StartCoroutine メソッドなどの実行に Context (現在の状態)を渡す必要があるからです。
(参考: coroutineとasync/awaitのあれこれ
もちろん、 new GameObject(); で GameObject を作って、そこに Fadeout Component をアタッチするという方法でも可能です。

呼び出し側のコード

呼び出し側は以下のようなコードです。
ブログ用にいろいろ省略していますが、 OnClickCloseButton メソッドを Button の OnClick 用のメソッドとして使い、
OpenSettingsWindow メソッドや CloseSettingsWindow は別クラスから呼び出されることもあります。

using UnityEngine;
using Util;

public class SettingsWindowController : MonoBehaviour {
    GameObject settingsWindowPanel;

    void Start() {
        settingsWindowPanel = this.gameObject;
        settingsWindowPanel.SetActive(false);
    }

    public void OpenSettingsWindow() {
        settingsWindowPanel.SetActive(true);
    }

    public void OnClickCloseButton() {
        CloseSettingsWindow();
    }

    public void CloseSettingsWindow() {
        Fade.FadeoutObject(settingsWindowPanel, 0.3f);
    }
}

できあがり

無事に、ウィンドウやその中のテキストなどがいっしょにフェードアウトしてくれました!

f:id:nekonenene:20190519184434g:plain
Fade out the window on close

きっと似たようなコードでフェードイン処理も作れる気がするので、
次はフェードイン処理も作っておきたいな、と思いました。

この記事が Unity を使っている方のお役に立てましたら幸いです。

materialize-css のJSライブラリを TypeScript + React 環境で読み込む

Materialize 、モダンっぽい Web サイトを作れるので、
個人的には Bootstrap よりは最近使うようになりつつあります。

「Bootstrap より軽いし……」と書こうとして実際にいま測ってみたらそうでもなかったですが(笑)

1. Bootstrap vs. Materialize (ファイルサイズ比較)

1-1. Bootstrap

  • bootstrap.min.js (v4.1.3) : 20.56KB
  • bootstrap.min.js (v4.1.3) : 13.76KB
  • jquery-3.3.1.slim.min.js : 23.47KB
  • popper.min.js (v1.14.3) : 6.8KB

Total: 64.59KB

1-2. Materialize

Total: 59.53KB

Materialize の JavaScriptjQuery を内包しているので、そのぶん大きくなっていますね。
読み込むファイル数は少ないので、そのぶんコストは低いですが、思ったより容量に差はなかったです。

閑話休題。本題にいきましょう。

2. Materialize の JavaScript ライブラリを TypeScript で使いたい

例えば materialize-cssNavbar Component の Dropdown を使いたいときには、
Dropdown のドキュメントにある通り、

// ドキュメント通りに dropdown-trigger class を設定し、以下の script を末尾などに記述
document.addEventListener('DOMContentLoaded', function() {
  var elems = document.querySelectorAll('.dropdown-trigger');
  M.Dropdown.init(elems, {});
});

// jQuery を呼び出している場合は以下の 1 行で済む
$('.dropdown-trigger').dropdown();

上のどちらかの方法でドロップダウンをアクティブにしなくてはいけません。

HTML の最後に script タグを使って、

<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script>
  document.addEventListener('DOMContentLoaded', function() {
    var elems = document.querySelectorAll('.dropdown-trigger');
    M.Dropdown.init(elems, options);
  });
</script>

こんな感じで書いてあげてもいいわけですが、
せっかくなら ts ファイルや tsx ファイル内で呼び出してあげたい。その方法です。

3. 答え

3-1. npm install -D @types/materialize-css

前置きが長くなりましたが、以下のようにライブラリをダウンロードすればOKです。

npm i -S materialize-css
npm i -D @types/materialize-css

materialize-css だけでなく、
@types/materialize-css を入れているのが一番のポイントです。
これがないと TypeScript でのコンパイルに失敗してしまいます。

なお、古い記事だと npm i -S materialize-css@next で入れている記事がありますが、
2019年5月現在では、 npm i -S materialize-css で入るのは 1.0.0 、
npm i -S materialize-css@next で入るのは 1.0.0-rc.2 と、
next を付けるほうがむしろ古いバージョンが入ってしまうのでお気をつけください。

f:id:nekonenene:20190506005825p:plain
https://www.npmjs.com/package/materialize-css?activeTab=versions より

3-2. import M from 'materialize-css'

あとはいつも通りに、 tsx ファイルや ts ファイルの先頭で import してあげれば OK です。

import M from 'materialize-css';

2. の項で書いた例のように、

var elems = document.querySelectorAll('.dropdown-trigger');
M.Dropdown.init(elems, {});

を適当な場所で書くことにより Dropdown の初期化をおこなうこともできますが、
ドキュメントの例にあるようなクラス名(dropdown-trigger)をそのまま使う場合でしたら、

M.AutoInit();

でカンタンに済ませることも可能です。(参考: https://materializecss.com/auto-init.html

どのコンポーネント、そして class 名だと初期化されるかは元のコードを読むといいでしょう。

https://github.com/Dogfalo/materialize/blob/efa0aee/js/global.js#L106-L135

3-3. React で扱う場合の Tips

M.AutoInit();componentDidMount 内で呼び出すと良いでしょう。
(※ constructor 内で呼び出すと失敗します。constructor は HTML render がおこなわれる前に走るためです)

public componentDidMount(): void {
  M.AutoInit();
}

M.AutoInit(); は便利ですが、すべてのコンポーネントに関する init がおこなわれるわけではありません。
例えば M.updateTextFields(); もその一つです。(参考: 『Text Inputs』 の『Prefilling Text Inputs』の項)
これは、Text Input Form にすでに文字が入力されている場合には、ラベルを上に移動しておいてくれるメソッドです。

https://github.com/Dogfalo/materialize/blob/efa0aee/js/forms.js#L3-L21

使用しないとこうなる:

f:id:nekonenene:20190506021047p:plain

使用するとページ表示時にこうなってくれる:

f:id:nekonenene:20190506021123p:plain

これは M.AutoInit(); から呼び出されないことがコードからわかりますので
入力フォームのあるページでは M.updateTextFields(); を使ってあげるといいです。

私の書いているコードでは、input フォームの defaultValue を非同期通信で取ってくるために
render のされ直しが発生してしまうため、
componentDidMount 内ではなく componentDidUpdate 内で呼び出してあげています。

public componentDidUpdate(): void {
  M.updateTextFields();
}

4. おしまい

React を今まで避けて生きてきたんですが、
かんたんな Go (Gin) + React のアプリケーションを作ってみようとさわってみたら
ちょっとずつわかってきました。

Redux には触れていないですが、今のところバケツリレーも大規模になっていないので、
今回は使わないでもいけそうです。

ついでに TypeScript も導入し、数年ぶりにさわっていますが、
慣れていない Go + 慣れていない React + 慣れていない TypeScript の組み合わせでちょこちょこ苦しんでいます(笑)
型チェックのために書く量は増えるし、参考資料そのままで書けない場合もあり大変です。

とても勉強になっているので、もう少しがんばってみます!

セッションと Cookie について今さら理解する

Webサイトのログインで必要な考えとなる
セッション周りを全然わかっていなかったので、いろいろ自分用にメモ。

1. 動機

今まで Rails や周辺ライブラリがよしなにやってくれていたものを、
Go言語で Web アプリを作ろうとしたら自分でちゃんと実装しないとだったので、
ログイン機構(特に devise の current_user メソッド)を作るためにセッションを理解する。

2. 役立つ記事

この 3 つを読んでおけば、セッションと、そのための Cookie について理解が整う。

3. Tips

3-1. net/http が SetCookie メソッドを持つ

Go言語の場合は net/http パッケージが SetCookie 関数を持つのでそれを使うと楽。

なお第一引数には、HTTP Response を組み立てる ResponseWriter 構造体を用いる。
Cookie をクライアント(Webブラウザ)に Set するのは、 HTTP Response の Header を使っておこなうため。

3-2. Value に入れる値は URL Encode されたもの?

今使っている Web フレームワーク gin のここのコードで、

Value: url.QueryEscape(value),

となっていたため、 URL Encode が必要なのか確認してみました。
stackoverflow のこの回答では、「そうだよ。すべきだ」って回答が一番上に来てますが、他の回答も見ると議論の余地があることがわかります。

There is some confusion over encoding of a cookie value. The commonly held belief is that cookie values must be URL-encoded, but this is a fallacy even though it is the de facto implementation. The original specification indicates that only three types of characters must be encoded: semicolon, comma, and white space. The specification indicates that URL encoding may be used but stops short of requiring it. The RFC makes no mention of encoding whatsoever. Still, almost all implementations perform some sort of URL encoding on cookie values. In the case of name=value formats, the name and value are typically encoded separately while the equals sign is left as is.

(『HTTP cookies explained』の『Cookie encoding』の項より引用)

日本語に訳すと、『CookieValueエンコードについてはいくらかの混乱が起こっています。一般的に Value は URL-encoded が必要と信じられていますが、実際の実装でそうなっている物はあれどこれは誤解です。元の仕様では、セミコロン、カンマ、および空白の3種類の文字のみをエンコードする必要があることが示されています。これは URL Encode を使用できることは示しますが、要求しているわけではありません。RFCでもエンコーディングについて一切言及していません。(以下略)』

そして他の回答では

RFC6265:           "for example Base64"
PHP:               URL encode
Go:                raw
Node.js + Express: URL encode

と、各言語によって実装が異なることを示してくれていました。

PHP を例に取ると、PHP の setcookie 関数は、
Value として与えられた引数を URL Encode して実際は保存します。
PHP 5 以降で実装された setrawcookie 関数を使うと、URL Encode をおこなわず保存します。
(参考: https://www.php.net/manual/ja/function.setcookie.php

RFC6265 内では

To maximize compatibility with user agents, servers that wish to store arbitrary data in a cookie-value SHOULD encode that data, for example, using Base64 [RFC4648].

と書かれてはいますが、たしかにエンコードすべきと言っているまでで、Base64 は例示してるだけですね。
(なんでユーザーエージェント文字列との互換性を持ちたいのかはわからない……)

3-3. SameSite の設定はデフォルトでいいかも

Go言語 v1.11 以降は SameSite ってパラメーターを Cookie に設定できるんですけど、
「厳しいほうがいいだろう」と思って SameSite: http.SameSiteStrictMode に設定したら、OAuth でコールバックを受け取ってからの Cookie 読み込みができない罠にハマりそうだったので、
SameSite: http.SameSiteDefaultMode でとりあえずは良さそうです。

実装がどう違うかはいつか読みます……。

4. おわりに

忘れないように書いておきたかった内容なのでゴチャゴチャしていますが、
セッションへの理解がだいぶ進んで、まともな Web プログラマーに近付けた気がします。

作ってる練習用Webアプリのセッション管理部分はこんな感じ ↓ になりましたー。

https://github.com/nekonenene/gin_quiz_app/tree/master/session