47
29

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.

Unity #3Advent Calendar 2019

Day 8

Unityを使って学園祭でVRスプラトゥーンを自作して展示した話

Last updated at Posted at 2019-12-11

この記事は Unity #3 Advent Calendar 2019mast Advent Calendar 2019 の8日目の記事です!

ごめんなさい:bow_tone1:!!!3日も遅れてしまいました...(一番下の展示した話は絶賛執筆中

TL;DR

11月の頭に学園祭で下のようなVRゲームを展示した。

その技術まとめログを残して置きたくまとめた。
Gitのコミットログをたどってトピックを時系列で書いてく。

構想

まずは、 作りたいものテーマ要件 を決めるところから始めた。
2019年7月の時点で、作りたいものは「ゲーム」ということだけが決まっていた。
あとは、どんなテーマでどんなゲームを作るかを考えて行くだけであるが、そのまま夏休みを消化してしまった。

9月の終わり、「このままでは学園祭間に合わねぇ」となり、構想を急ピッチで固めることとなった。スマホやゲーム端末でどこでも簡単にゲームをできるからこそ、学園祭の「その日」「その場所」でしかできないものを目指すことにした。

作るゲームは「VRスプラトゥーン(仮)」、その名の通りVRでスプラトゥーンをするというゲームである。
アイデアが思いついてしたのは、まずは**「先人はいないか」** **「二番煎じではないのか」**をサーベイしていくことに、そんな中見つけたのは以下のインターフェースである。

ツイートにあるインターフェースの詳細は こちら

これは新しいインターフェースであり、ゲームシステム自体は任天堂の端末で動いてるので、ゲームシステムから自分で実装していけば、二番煎じならない と踏み開発にとりかかることにした。

部屋が借りらたので、現実と全く同じようにモデリングした空間をステージにすることにした。
現実と 同じ見た目 の空間をステージにすることで、 「その日」「その場所」でしかできない という目指すべき点を満たした。同時に、メディアアートとしてのコンセプトが完成した。

目に映っているのは現実であるが、実際には現実では無く
体験している本人からは、現実の物理的な世界でできないことが、
目に映っている現実と同じように見える情報的な世界で、実現できる

あとは、実際に以下のような要件を考えていくことにした。
image.png

ゲームを作りたいといっても、制作するゲームは展示のためで、プレイしてもらう以上、自己満足は避けるべきである。
開発において、自己満足ルートに入らないように、先に要件を練って決めておくのは大切だった。

Unityで簡単お手軽VR

2019/9/29
Oculus無ェ、Viveも無ェ、お金もそれほど持って無ェ ですが、VRゲームを作っていく。Oculus RiftもGoもQuestもHTC Viveもありませんが、スマートフォンがあれば十分。

UnityのPlayer Settings > (Android)XR Settings > Virtual Reality Supportedにチェックを入れて、下のリストにVRプラットフォームを追加するだけ
image.png
今回は、Cardboard を使いました。

これで、Unityのカメラに映っている空間が自動で左目用、右目用に分かれてレンダリングされ、カメラの向きがジャイロセンサーに追従するようになった。
output00.gif
↑ これはスマホの画面です。

Google VR
https://docs.unity3d.com/ja/current/Manual/googlevr_sdk_overview.html

スマホ用のVRコントローラー

2019/10/3
基本、VRヘッドセットには専用のコントローラーがあるが、スマートフォンにはないのでスマートフォン用のVRコントローラーを探す(後々思えばDaydreamでも良かったなぁ)。Wiiリモコンだと持ちやすいし、傾きも取れるし、BluetoothでつながるがAndroid 8.0ではデータを取れない。そんな中ベストなソリューションとして見つけたのが、エレコムの ヴルームSDK専用コントローラSDKもあれば、Qiitaでの記事もある。これは買いだと思いAmazonでポチった。

【Vroom SDK】5230円で構築したオレオレVR開発環境が割とよかった話
https://qiita.com/drumath2237/items/479eda46afaadfb55ae4

UnityでVroom開発
https://qiita.com/TakaoWing/items/17758537d2a848f24191
image.png
エレコム VR リモコン Bluetooth [VR/AR ヴルームSDK対応] ブラック JC-VRR02VBK
※PRとかステマとかではなく 普通に良かった

Unityへの導入は上のQiitaの記事を参考にした。
実際にAndroid端末でVroomコントローラーを使うのには、Androidの位置情報のパーミッションを許可する必要があった。
image.png

部屋の計測

2019/10/15
実際の部屋がステージなので、モデリングするために計測をした。サクサクっと部屋の計測をしたかったのでレーザー距離計を導入した。1.測りたい起点にレーザー距離計を当てる 2.ボタンを押してレーザーポイントが測りたい終点を確認 3.もう一度ボタンを押すとディスプレイに距離がでる これだけ部屋の大体の大きさがわかった。

image.png
BOSCH(ボッシュ) レーザー距離計 GLM30 【正規品】

大体の形で距離を測っておいて、測れない部分は全体の長さから調整して割り出した。
image.png

部屋のモデリング

2019/10/21
Autodesk Fusion 360を使って、実際に測った寸法どおりにモデリングした。壁や床の色はテクスチャでつけるのでモデリングでは形だけを作っていく。細かいパーツはとりあえず後回しで、80%でも良いからゲームをプレイできるように、大まかなに部屋を作っていく。

image.png
※ 最初は窓も省略していた

素材の購入

2019/10/22
概形のモデリングを終え、Unity内に壁と天井と床を作成したが、壁だけだとどうにも寂しい。教室の最低限の要素として、黒板蛍光灯は必要ではありそう。だが、計測してないしモデリングの手間を考えると、教室のアセットを買うべきだと思って購入。このアセットから黒板と蛍光灯のモデルを拝借し、サンプルシーンから部屋のライティングがどうなっているかも確認した。

image.png
Japanese School Classroom

ライティングがよくわからない

2019/10/22
CGの華はやっぱりレンダリング!
よりリアルを目指すならPhysically based renderingだけど、Unityでのレンダリングは初めてで右も左もわからない。レイトレーシングを自分で書いたことがあっても、UnityにはUnityのお作法があるので、下の記事を参考に部屋のライティングを作り込んでいく。(Unityでのレンダリングについて、他にも資料/本がありましたらコメントにお願いします。

【Unity】入門ライティング設定!
https://qiita.com/Nekomasu/items/8845d076c4356809f0ff

【Unity】ライティングを理解するためにコーネルボックスを作って遊ぶ
http://tsubakit1.hateblo.jp/entry/2016/03/14/230904

【Unity】良い感じに見える(屋内向け)ライティングの設定手順
http://tsubakit1.hateblo.jp/entry/2018/01/04/235520

【Unity】"家具を動かせる" 部屋(ステージ)の見栄えを、出来る限り良い感じにする
http://tsubakit1.hateblo.jp/entry/2018/01/17/001302

Unityで、PBRなライティング環境をセットアップしてみよう
http://techblog.sega.jp/entry/2019/04/25/100000

理想は、Unity Japan Officeプロジェクト

実際に作成してできたのはこんな感じの教室
image.png
まだまだ改善の余地はあるが、まだゲームのシステムには手をつけていないので作り込みはシステムができてから...

InkPainterの導入

2019/10/22
スプラトゥーンはインクを飛ばしてステージを汚していくゲームですが、どうすればUnityで実装できるでしょうか...。Unityならアセットに頼って行くのがよさそうなのでアセットを探します。制作するゲームでは、いーす さんが作成した InkPainter を使います。

image.png
InkPainter

InkPainterはテクスチャが設定されてないと塗れない

2019/10/22
インクを付けたいMeshにはMesh ColliderInkPainter.csアタッチする。MousePainter.csMain Cameraにアタッチするとマウスでクリックしたところにインクが付く(マウスから直線上にRayを飛ばして、最初にオブジェクトと交差する交点にインクが付く
しかし、テクスチャが未設定だとインクが付かないので、Albedoにテクスチャをセットしておく。
image.png

InkPainterはモデルがUV展開されてないと塗れない

2019/10/22
ふぇ〜、壁をクリックした瞬間全部塗られてしまった...
この時点で「テクスチャがめちゃくちゃ小さくて、一瞬でテクスチャを全部塗りつぶしてしまった?」とか考えて、こねこねした。
image.png
この記事をみるとどうやらモデルがUV展開されていないと一回で塗り潰れると書いてある。
石膏像にInk Painterでペイントする【Unity】
http://bibinbaleo.hatenablog.com/entry/2017/12/10/164535

Autodesk Mayaを使ってUV展開した。この際、展開するUV座標の範囲はX:0.0-1.0 Y:0.0-1.0じゃないといけないので注意。
image.png

この時点の進捗
output01.gif

モデルがRead/Write Enabledになってないと塗れない

2019/10/25
とりあえず、このままAndroid用にプロジェクトに書き出す。そうするとUnity Editor上では動作していたが、Android上では最初はインクを塗れなかった。

Splatoonの塗りみたいのを再現したい その5
http://esprog.hatenablog.com/entry/2016/05/08/212355
この記事をみてみると

メッシュのインポート設定でRead/Write Enabledをチェックしないと正常に動作しない場合がある

とあり、次の記事をリンクしている。

RaycastHitのTexcoordが常に0ベクトルを返すのですよ
http://esprog.hatenablog.com/entry/2016/05/06/161729

Texcoordを取得してその位置にインクを塗る、といった処理をしているのですが、見ての通り正常に描画されない場合、Texcoordが常に0ベクトルを返しています。
解決するのはものすごく簡単です。メッシュのインポート設定でRead/Write Enabledにチェックを入れるだけです。

とりあえず、 メッシュのインポート設定でRead/Write Enabledにチェックを入れる ことで塗れない問題は解決した。

InkPainterでペイントのサイズを補正できるようにした

2019/10/25
一通りインクを塗れるようにして、Android用に書き出して試し塗りをしてみると、床/天井と壁でブラシのサイズが違うことに気づいた。原因はわかっている。モデルをUV展開したときの縮尺が統一されていないからである。
output02.gif
モデルの縮尺を合わせるコストの方が高そうなので、InkPainter側でモデルごとにブラシサイズに係数をかけられるようにした。
brush.Scaleは全体で設定されている値

InkCanvas.cs
/*-------- 省略 -------- */
/// <summary>
/// To set the data needed to paint shader.
/// </summary>
/// <param name="brush">Brush data.</param>
/// <param name="uv">UV coordinates for the hit location.</param>
private void SetPaintMainData(Brush brush, Vector2 uv, float paintScale) // <-- 引数を追加
{
	paintMainMaterial.SetVector(paintUVPropertyID, uv);
	paintMainMaterial.SetTexture(brushTexturePropertyID, brush.BrushTexture);
	paintMainMaterial.SetFloat(brushScalePropertyID, brush.Scale * paintScale); // <-- 係数をかける
	paintMainMaterial.SetFloat(brushRotatePropertyID, brush.RotateAngle);
	paintMainMaterial.SetVector(brushColorPropertyID, brush.Color);

/*-------- 省略 -------- */

Sphereを飛ばして塗れるように

2019/10/28
【Unity】オブジェクトを放物線を描くように目的地まで射出する
https://qiita.com/udo_nba/items/a71e11c8dd039171f86c

【Vroom SDK】5230円で構築したオレオレVR開発環境が割とよかった話
https://qiita.com/drumath2237/items/479eda46afaadfb55ae4

を参考にVirtualController.csを作成し、コントローラー代わりのGameObjectにアタッチする。

VirtualController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VirtualController : MonoBehaviour
{
    
    public GameObject camera;

    public GameObject ThrowingObject;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
    
#if UNITY_IPHONE || UNITY_ANDROID
        transform.rotation = VvrController.Orientation();
        transform.transform.Rotate(new Vector3(0, 1, 0), -90);
#endif

        if (Input.GetMouseButtonDown(0) || VvrController.Trigger())
        {
            // マウス左クリックでボールを射出する
            ThrowingBall();
        }

        //transform.forward
    }

    private void ThrowingBall(){
        GameObject ball = Instantiate(ThrowingObject, this.transform.position, Quaternion.identity);

        Rigidbody rid = ball.GetComponent<Rigidbody>();
        //Vector3 a = new Vector3(10f, 10f, 10f);
        rid.AddForce(this.transform.right * rid.mass * 2.0f, ForceMode.Impulse);
    }
}

Vroomコントローラーのトリガーが引かれたら、シーン内にインスタンス化してあるインクボールがコピーされて、コントローラーの向いている方向へ発射するプログラムになってる。
ThrowingObjectはインクボールでRigitBody,Sphere Collider,CollisionPainter.csをアタッチしてある。

Unityの当たり判定は難しい

2019/10/28
どうしても玉が壁や床に当たらないことがあった。

Unityで速い攻撃モーションの時に当たり判定がされない問題の対応をする
https://gametukurikata.com/mesh/trailcollider

【Unity】RigidbodyのCollision Detection(衝突の検知)を変えて実験
https://ekulabo.com/rigidbody-collision-detection

衝突判定のあれこれ
https://qiita.com/moscoara_nico/items/8eee1de552601a8a8f1f

Unity-今更ながらRigidBodyのCollision Detectionについて表にまとめてみた
https://qiita.com/take_shi/items/fea7304fe9868a74d561

どうやら、速度が速い場合にはデフォルトの判定は効かないらしく Collision Detection を変更する必要がある。
image.png

CollisionPainter.csにはOnCollisionStayが実装されているので、Sphereが壁や床にあたっている間に呼ばれる。

CollisionPainter.cs
public void OnCollisionStay(Collision collision)
{
	if(waitCount < wait)
		return;
	waitCount = 0;

	foreach(var p in collision.contacts)
	{
		var canvas = p.otherCollider.GetComponent<InkCanvas>();
		if((canvas != null) && isPaintable)
			canvas.Paint(brush, p.point);
	}
}

玉が当たったら破棄

2019/10/28
インクボールが壁に当たっても玉が存在し続けるのはおかしいのでインクボールはオブジェクトにあたったら削除する。削除にはDestroy関数を使う。OnCollisionEnterなので、あたった時に関数が呼ばれるが、 CollisionPainter.csではOnCollisionStayを塗りの判定に使っていのですぐに削除するとインクがつかない。オブジェクトを破壊するまでのディレイ時間を設定しておけば塗りの判定のあとにちょうどオブジェクトが消える算段である。

Object.Destroy
https://docs.unity3d.com/ja/current/ScriptReference/Object.Destroy.html

public static void Destroy (Object obj, float t= 0.0F);

パラメータ
obj 破壊するオブジェクト
t オブジェクトを破壊するまでのディレイ時間
DestroySphere.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroySphere : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    void OnCollisionEnter(Collision collision)
    {
        //Debug.Log("当たった!");
        Destroy(this.gameObject,0.5);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

塗り面積を計算してスコアにする

2019/10/29
本家は対戦ゲームで塗った面積比で勝敗が決まる。作成したゲームは対戦ではなく、1人でプレイするので塗った面積をスコアをする必要があった。なので、InkPainter.csに、「オリジナルのテクスチャ」と「塗ったあとのテクスチャ」を比較して、変化があったピクセルの数を計算するプログラムを追加した。

output03.gif

InkPainter.csにもともとあったSaveRenderTextureToPNG関数を参考にプログラムを作成。
PaintSet#mainTexture (Texture)がオリジナルの画像でPaintSet#paintMainTexture (RenderTexture)がインクの付いた画像になっている。TextureRenderTextureの内容は単純比較できなさそうなので、どちらもColor[]に変換して単純に色を比較した。この処理はむちゃくちゃ重いです。

Texture から Texture2D への変換
http://nakamura001.hatenablog.com/entry/20171012/1507810457

InkPainter.cs
public int CompareRenderTexture(){
	var ps = paintSet[0];
	int diff_count = 0;
	if(ps != null){
		var renderTexture = ps.paintMainTexture;
		var newTex = new Texture2D(renderTexture.width, renderTexture.height);
		RenderTexture.active = renderTexture;
		newTex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
		newTex.Apply();

		Texture mainTexture = ps.mainTexture; // Material のメインテクスチャを取得
		Texture2D texture2D = new Texture2D(mainTexture.width, mainTexture.height, TextureFormat.RGBA32, false);

		RenderTexture currentRT = RenderTexture.active;

		RenderTexture _renderTexture = new RenderTexture(mainTexture.width, mainTexture.height, 32);
		// mainTexture のピクセル情報を renderTexture にコピー
		Graphics.Blit(mainTexture, _renderTexture);

		// renderTexture のピクセル情報を元に texture2D のピクセル情報を作成
		RenderTexture.active = _renderTexture;
		texture2D.ReadPixels(new Rect(0, 0, _renderTexture.width, _renderTexture.height), 0, 0);
		texture2D.Apply();

		Color[] origin_pixels = texture2D.GetPixels();

		RenderTexture.active = currentRT;

		Color[] paintedTex = newTex.GetPixels();

		for(int i = 0;i < origin_pixels.Length;i++){
			if(origin_pixels[i] != paintedTex[i])
				diff_count++;
		}

		Debug.Log(diff_count);
	}
	return diff_count;
}

Unityの重力を変える

2019/10/29
インクボールの速度が遅いと、玉が重力によって壁に到達する前に落ちてしまう。だからといって玉の速度が速いとあたり判定に失敗することがあるので、ちょうど良い放物線描くようにしたいと思った。物体が下まで落ちるのに関係する係数は重力加速度です。高校物理で習いました。

物体の速度は時間に比例する
$$ v = gt $$
物体の移動距離は時間の2乗に比例する
$$ y = \frac{1}{2}gt^2 $$

落下運動を調べる ~重力加速度~
https://www.nhk.or.jp/kokokoza/tv/butsurikiso/archive/resume006.html

【Unity】 重力を変更する
https://qiita.com/t_Kaku_7/items/fdf5bab18b65f6f9dcb4

上のQiitaの記事を参考に、重力加速度を-2にすることでちょうど良い放物線でインクボールが飛びました。

色をランダムに塗れるようにする

2019/10/29
インクの色が1つしかないと絵的に寂しいのでランダムでインクボールの色が変わるようにした。CollisionPainter#Brush#Colorが塗りの色なのでこれを変える。インクボールの色はマテリアルでセットしているのでそれに同じ色を当ててあげる。

VirtualController.cs
private void ThrowingBall(){
    GameObject ball = Instantiate(ThrowingObject, this.transform.position, Quaternion.identity);


    CollisionPainter paint = ball.GetComponent<CollisionPainter>();
    
    Debug.Log(paint.brush.Color);

    paint.brush.Color = def_color[(int)Random.Range(0, 6 + 1)];
    ball.GetComponent<Renderer>().material.color = paint.brush.Color;
    
    Rigidbody rid = ball.GetComponent<Rigidbody>();
    //Vector3 a = new Vector3(10f, 10f, 10f);
    rid.AddForce(this.transform.right * rid.mass * 4.0f, ForceMode.Impulse);
}

CubeMapを360度画像に変換する

2019/10/30
インクを塗った教室を共有できるように、VR空間を画像にする必要が出てきた。教室のある面だけを写してスクリーンショットを撮るか、テクスチャをサーバーに転送して専用のビュアーとかで見れるようにするか、とか色々アイデアを考えたが、プレイヤーがどこをよく塗ってどう共有したいかがわからない以上は、全部を画像に落とし込みたかった。

サーベイの中参考になったのは下の記事で、ツールかCubeMapを使うのが良さそうだった。

SphericalImageCam Unityエディタ内で超綺麗な360度全天周パノラマ映像を撮影する「日本作者さん」のスクリプト
http://www.asset-sale.net/entry/SphericalImageCam

Unityの基本機能だけを使ってSceneのキューブマップ(全天球画像)を作る
https://qiita.com/ELIXIR/items/c71ee67eb259bfa7d2c7

Unity でステレオ VR 動画を作成する (ほぼ完全 (?) 版)
https://qiita.com/tan-y/items/941de5c8bc3309f835d5

Unity Recorder の使い方
https://qiita.com/tan-y/items/644760a18484cbe71d43

2019-11-03-12-33-06-score-3609.jpeg
↑変換した画像

実際には下のQiitaの記事のプログラムを参考に、スコアが計算されたらカメラに映っている画像をキューブマップに落とし込んで、Shaderを使ってEquirectangularな画像を生成/保存できるようにした。

Unityでカメラのequirectangular画像を作成してみる
https://qiita.com/mechamogera/items/0b47e5947f1eee2467da

VirtualController.cs
private IEnumerator CalcResult() {
    int total_score = 0;
    for(int i = 0;i < inkcanvas.Length;i++){
        total_score += inkcanvas[i].CompareRenderTexture();
        yield return null;
    }
    // スコア計算
    textMesh2.text = $"{total_score:D}";
    yield return null;

    // ここで360度画像を生成
    // カメラの位置とか調製した方がいいかな?
    model.SetActive(false);
    Camera cam = camera.GetComponent<Camera>();
    Material convMaterial;

    int outputWidth = 4096;
    int outputHeight = 2048;
    int cubeWidth = 1280;

    Texture2D equirectangularTexture;

    Cubemap cubemap = new Cubemap(cubeWidth, TextureFormat.RGBA32, false);

    string path = Application.dataPath + @"/all" + "_painted.png";

    Shader conversionShader = Shader.Find("Conversion/CubemapToEquirectangular");
    convMaterial = new Material(conversionShader);
    
    RenderTexture currentRT = RenderTexture.active;

    RenderTexture renderTexture = new RenderTexture(outputWidth, outputHeight, 24);
    equirectangularTexture = new Texture2D(outputWidth, outputHeight, TextureFormat.RGB24, false);

    cam.RenderToCubemap(cubemap);

    Graphics.Blit(cubemap, renderTexture, convMaterial);
    //Graphics.Blit(cubemap, renderTexture);

    // renderTexture のピクセル情報を元に texture2D のピクセル情報を作成
    RenderTexture.active = renderTexture;
    equirectangularTexture.ReadPixels(new Rect(0, 0, outputWidth, outputHeight), 0, 0, false);
    equirectangularTexture.Apply();

    RenderTexture.active = currentRT;

    byte[] pngData = equirectangularTexture.EncodeToPNG();
    if(pngData != null)
    {
        File.WriteAllBytes(path, pngData);
    }
    Debug.Log(path);
    // https://qiita.com/ELIXIR/items/c71ee67eb259bfa7d2c7
    // https://qiita.com/mechamogera/items/0b47e5947f1eee2467da

    model.SetActive(true);

    yield return null;
}

Project Settings > Graphics > Built-in Shader SettingsAlways Included Shadersにプロジェクトに追加したConversion/CubemapToEquirectangularを追加しないとAndroid上で動作しないので注意

image.png

Androidスマートフォンに画像を保存

2019/10/30
現状だと、File.WriteAllBytesで指定しているパスは、Application.dataPathなので当然、写真アプリからはみることができない。写真アプリから見れれば最悪手動で共有をできるし、データ管理もしやすいので、写真アプリから見れるところに生成した画像を保存できるようにした。

Application.dataPath
https://docs.unity3d.com/ja/current/ScriptReference/Application-dataPath.html

【Unity3D】ファイル保存パス
https://qiita.com/bokkuri_orz/items/c37b2fd543458a189d4d

image.png

Unity Native Gallery Pluginを使うとユーザー領域にデータを保存できるようになるので導入、GitHubのページからNativeGallery.unitypackageをダウンロードしてインポートで導入完了。あとは、保存するプログラムを書き換えるだけで終わった。
【Unity】iOS の写真や Andoid のギャラリーに画像や動画を保存できる「Unity Native Gallery Plugin」紹介
http://baba-s.hatenablog.com/entry/2017/12/26/210500

VirtualController.cs
byte[] pngData = equirectangularTexture.EncodeToPNG();
if(pngData != null)
{
#if UNITY_IPHONE || UNITY_ANDROID
    NativeGallery.SaveImageToGallery(equirectangularTexture,"UnityGames",DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss")+".png");
#else
    File.WriteAllBytes(path, pngData);
#endif
}
Debug.Log(path);

共有システムにFirebaseを導入

2019/10/31
スコアの算出と共有用の画像の生成ができたら、その2つを共有できるシステムを組むことに。共有まわりのシステムにはFirebaseを使った。単に、Firebaseを使ってみたく導入を決めた。

Firebaseとは何かは適当な記事を参照してほしい。
Firebaseの始め方
https://qiita.com/kohashi/items/43ea22f61ade45972881

  • Unityからデータを受け取ったり共有ツイートをしたりするコントローラー的役割にFunctions
  • 画像データそのもの自体を保存しておくのにStorage
  • ユーザー名、スコア、画像ファイル名、ツイートURLなどを管理するのにDatabase
  • このゲームのWebサイトのために、Hosting
    これらの4つの機能を使って作っていくことにした。

下の記事を参考にFirebaseをinitした。

Firebase で Cloud Functions を簡単にはじめよう
https://qiita.com/tdkn/items/2ed2b01f2656fc50da8c

Cloud Functions for Firebaseが最高だった話
https://qiita.com/HALU5071/items/e43729ac5b06b0506fbe

multipart/form-dataをアップロードに対応

2019/10/31
サーバーにファイルを上げるとなるとPOSTメソッド+multipart/form-dataでアップロードすることになる。POSTメソッドを叩けるエンドポイントをFunctionsに実装した。基本は公式のドキュメントを見ながら、実装例を参考にしていく。ポストしたデータはStorageに保存されるようになっている。

Cloud Functions > ドキュメント > ガイド HTTP関数
https://cloud.google.com/functions/docs/writing/http

Cloud Functionsから、Storageにアップロードする
https://shuheitagawa.com/upload-to-storage-via-cloudfunctions/

FirebaseのHostingとCloud Functionsを利用してStorageへファイルをアップロードするデモアプリケーション
https://qiita.com/rubytomato@github/items/11c7f3fcaf60f5ce3365

index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const path = require('path');
const os = require('os');
const fs = require('fs');
const Busboy = require('busboy');

admin.initializeApp(functions.config().firebase);

//-------省略--------

exports.scoreUpload = functions.https.onRequest((req, res) => {
    if (req.method === 'POST') {
        const busboy = new Busboy({ headers: req.headers });
        // This object will accumulate all the uploaded files, keyed by their name.
        const uploads = {}
        const allowMimeTypes = ['image/png', 'image/jpg'];
        const tmpdir = os.tmpdir();

        const bucket = admin.storage().bucket();

        // This callback will be invoked for each file uploaded.
        busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {

            if (!allowMimeTypes.includes(mimetype.toLocaleLowerCase())) {
                //console.warn('disallow mimetype: ' + mimetype);
                return;
            }

            // Note that os.tmpdir() is an in-memory file system, so should
            // only be used for files small enough to fit in memory.
            const filepath = path.join(tmpdir, filename)
            uploads[fieldname] = filepath;
            file.pipe(fs.createWriteStream(filepath));
            
            // ここでアップロード処理
            file.on('end', () => {
                bucket.upload(filepath,{ destination: `${filename}`,metadata: { cacheControl: 'public,max-age=31536000',contentType: mimetype } })
                .then(() => {
                    console.log('file upload success: ' + filepath);
                    return new Promise((resolve, reject) => {
                        fs.unlink(filepath, (err) => {
                            if (err) {
                                reject(err);
                            } else {
                                resolve();
                            }
                        });
                    });
                })
                .catch(err => {
                    console.error(err);
                    // TODO error handling
                });
            });
        });

        busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
            console.log('Field [' + fieldname + ']: value: ' + val);
        });

        // This callback will be invoked after all uploaded files are saved.
        busboy.on('finish', () => {
            res.end();
        });

        // The raw bytes of the upload will be in req.rawBody. Send it to
        // busboy, and get a callback when it's finished.
        busboy.end(req.rawBody);
    } else {
        // Client error - only support POST.
        res.status(405).end();
    }
});

Unityから画像をアップロード

2019/10/31
作成したFunctionsへPOSTメソッドを使って画像とスコアをアップロードした。標準でネットワーク機能も備わっているのでUnityは本当に楽

Unityで画像をPOSTし,PHPでサーバに保存する
https://qiita.com/s_ktmr/items/6fcc770ea89fe885b8bd

UnityWebRequest
https://docs.unity3d.com/ja/current/ScriptReference/Networking.UnityWebRequest.html

VirtualController.cs
IEnumerator UploadFile(byte[] pngData,int score) {
    string fileName = DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss")+".png";

    // formにバイナリデータを追加
    WWWForm form = new WWWForm ();
    form.AddBinaryData ("file", pngData, fileName, "image/png");
    form.AddField( "score", $"{score}" );
    // HTTPリクエストを送る
    UnityWebRequest request = UnityWebRequest.Post ("https://*********.cloudfunctions.net/scoreUpload", form);
    yield return request.Send();

    if (request.isHttpError) {
        // POSTに失敗した場合,エラーログを出力
        Debug.Log (request.error);
        textMesh.text = "Ready";
    } else {
        // POSTに成功した場合,レスポンスコードを出力
        Debug.Log (request.responseCode);
        textMesh.text = "Ready";
    }
    yield return null;
}

アップロードを高速化

2019/10/31
どうしても端末でスコアを計算して、2-3MBのPNG画像をアップロードするのは時間がかかりすぎるので、高速化したかった。PNGは可逆圧縮なのでファイルサイズは大きくなる、別にTwitterに投稿する用の画像ならJPEGでも全然良いので、サーバーにアップロードするファイルはJPEGでエンコード。JPEGにするとファイルサイズが600KBぐらいになったのでStorageにも優しい。

VirtualController.cs
// ----- 省略 ------
StartCoroutine(UploadFile(equirectangularTexture.EncodeToJPG(),total_score));
// ----- 省略 ------
IEnumerator UploadFile(byte[] pngData,int score) {
    string fileName = DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss")+"-score-" + score + ".jpg";
 
    // formにバイナリデータを追加
    WWWForm form = new WWWForm ();
    form.AddBinaryData ("file", pngData, fileName, "image/jpg");
// ----- 省略 ------

FirebaseからTwitterを投稿

2019/10/31
Unityからアップロードした画像をFirebaseを介してTwitterへ投稿する。Node.js + Twitterで検索したら次の記事が出てきたので助かった。multipart/form-dataのアップロードに対応したときに、アップロードしたファイルを一時的なパスに保存してあるので、アップロードする画像のパスにそれを指定すればいい。

Node.jsでTwitterに画像投稿するメモ
https://qiita.com/n0bisuke/items/6b269f61152e9f336c35

index.js
const data = fs.readFileSync('/path/to/file');
const mes = `RE:REALをプレイしました! \nスコア:${mScore} \n5C511でプレイ! \n\n #playrereal #雙峰祭 #5C511`; //投稿するメッセージ

(async () => {
    //画像のアップロード
    const media = await client.post('media/upload', {media: data});
    console.log(media);

    //Twitterに投稿
    const status = {
        status: mes,
        media_ids: media.media_id_string // Pass the media id string
    }
    const response = await client.post('statuses/update', status);
    console.log(response);
    console.log(response.entities["media"][0]["media_url"]);
    res.end();
})();

上のプログラムをデプロイしたが、Firebaseで403エラーが帰ってきた...
どうやら従量課金制じゃないとTwitterAPIを叩けないらしいので従量課金制に変更した。学園祭2日で大量の処理が走るわけではないので

FirebaseでTwitterのAPIが叩きたかった
https://blog.nagatech.work/post/twitterbot/361

無料プランだと外部のHTTPリクエスト(ここではTwitterのAPIを叩くこと)は作れない
従量課金制のBlazeプランに入ると使える

Firebase FunctionでDatabaseへの書き込み

2019/11/1
今後、ユーザーによるプレイのランキングページなどを作ることを見越してゲームのプレイデータを記録できるようにした。FunctionにDatabaseの書き込みを実装した。

ホーム >Firebase Realtime Database データの保存
https://firebase.google.com/docs/database/admin/save-data?hl=ja

Firebase Cloud Functions, Realtime DatabaseでCRUD REST WebAPIを作る
https://qiita.com/devnokiyo/items/26de016b0baf60b26c90

上の記事を参考に、Tweetのレスポンスとともにデータを記録。pushでJavascriptのObjectの書き方で記録できる。

index.js
const ref = admin.database().ref('record');
ref.push({
    id: response.id_str,
    user_name: "noname",
    score: mScore,
    tweet_id: response.id_str,
    tweet_url: "https://twitter.com/rereal2019/status/" + response.id_str,
    tweet_media_uri: response.entities["media"][0]["media_url"],
    media_uri: mFilename,
    created_time: parseInt( new Date() /1000 )
}).then(data => {
    return res.end();
})
.catch(error => {
    // TODO ERRORハンドリング
    return res.end();
});

ゲーム内のスタートを改善

2019/11/1
今までは、Readyと表示された状態でコントローラーのボタンを押したらゲームをスタートできたが、これだと展示の際にコントローラーを渡したら誤って押してしまい突然ゲームが始まることがあると思った。なので、簡単なチュートリアルを兼ねて、箱にインクボールを当てるとスタートというシステムに改良した。箱には、OnCollisionStayを実装したクラスをアタッチしており当たったインクボールの数をカウントする。箱の上部には当てるべきインクボールが数が表示されており、インクボールが当たれば減る。その数が0になればOKと表示され、すべてがOKになるとゲームが始められる。(文章で書くと説明が長いのでこのシステムは良くないと今思っている...

output04.gif

メモリ使用が800MBを超えるとアプリが落ちる

2019/11/1
アプリを終了せずともゲームをスタート/ストップできるようになったのだが、どうも3,4回目をプレイして塗りを計算したあたりでアプリが落ちる。コードを見る限り、プレイ回数に依存して配列外アクセス、nullアクセスして、NullReferenceExceptionとかではなさそうだった(今思えばログ見ればよかったのでは...。とりあえずこういった問題には、Profilerを使う。Android StudioにはAndroid Profilerがあるので、

image.png

デバイスをmacにつないだままプレイしてCPUやメモリの状態を観察すると、ゲームが終了するごとに約200MBぐらい増えている。
image.png

800MBあたりを越えた時点でアプリが終了してメモリがパージされている。これはつまりメモリリークってやつですね。ゲームが1回終わるごとにアプリをリセットすれば良いが、忘れたりでもしたらプレイ結果が消えるので、これは解決するべき問題。(人間がシステムに対して行わなければいけないことは最小限にしたい
image.png

メモリリークに対してあまり詳しくないのでサーベイ。調べると、Texture系はnewして使わなくなったらDestroyしようということなのでプログラムにDestroyを追加

InkCanvas.cs
Destroy(_renderTexture);
Destroy(newTex);
Destroy(texture2D);

cubemapは1280x1280x6のテクスチャ、renderTextureは4096x2048でどちらもRGB24なので、これだけで約53MBもあるわけだ...

VirtualController.cs
Destroy(cubemap);
Destroy(renderTexture);
Destroy(convMaterial);

Unityでメモリ節約
https://qiita.com/consolesoup/items/769fdf9e5748aa3e7f05

【Unity】開発中のアプリがメモリリークで強制終了するようになった時に対応したこと
http://baba-s.hatenablog.com/entry/2017/06/29/100000

Unity RenderTextureのメモリ確保と解放タイミングの落とし穴
https://www.shibuya24.info/entry/rendertexture_mem_alloc

Unity コンポーネントがDestroyされても、オブジェクトがGCで回収されないかもしれない話
https://qiita.com/toRisouP/items/4574a30622f43ddbde79

UnityでTextureを明示的に破棄
https://qiita.com/tempura/items/b87eb07568d974664671

玉が一定範囲を越えたら消えるように

2019/11/1
窓にインクをつけるかつけないかを悩んで、つけないことにしたので窓に当たり判定をつけなかった。だから、インクボールが窓の外に出たらインスタンスが残ってしまう。重力が設定されているので、大量にインスタンスが残るとゲーム自体のパフォーマンスが悪くなってしまうので、原点からの距離を測って一定範囲外にでたら消すようにした。

unity > 物体がある範囲外に移動した時に消えるようにする > Destroy(gameObject);
https://qiita.com/7of9/items/f9843c6668eb269678de

DestroySphere.cs
    [SerializeField]
    public float DestroySphereRange = 20.0f;

    // Update is called once per frame
    void Update()
    {
        float dist = Vector3.Distance(new Vector3(0, 0, 0),transform.position);
        if (dist > DestroySphereRange) {
            Destroy(this.gameObject);
        }
    }

塗りをリセット

2019/11/1
InkPainterでインクの塗りをプログラムから消すには、InkCanvas#ResetPainitを呼べば良い。なのでInkCanvasを全部取得してforeachを回すだけ。

VirtualController.cs
public void canvasReset(){
    foreach(var canvas in FindObjectsOfType<InkCanvas>())
	canvas.ResetPaint();
}

UnityでJSONを扱う

2019/11/1
現在プレイしているユーザー名は何か、次のゲームはコンティニューなのかどうか、プレイ時間は何秒かといったゲームに関わるパラメータは動的に変更できるようにするべきなので、ゲーム開始前にパラメータをFirebaseにとりにいくようにした。パラメータはChromeからサクッと手動で追加してプログラムを追加した。

ホーム > Firebase Realtime Database データの取得
https://firebase.google.com/docs/database/admin/retrieve-data?hl=ja

↓Datebaseに記録したパラメータを取得してJSONにして返すエンドポイントのプログラム

index.js
exports.nextConfig = functions.https.onRequest((req, res) => {
 
    const db = admin.database().ref('next_config');
    db.once("value").then(snapshot => {
        const result = {
            is_continue : snapshot.child("is_continue").val(),
            next_user_name : snapshot.child("next_user_name").val()
        }
        res.send(JSON.stringify(result));
    })  
    .catch(error => {
        // TODO ERRORハンドリング
        return res.end();
    });
});

これで、https://*********.cloudfunctions.net/nextConfigにGETメソッドでアクセスすれば、パラメータがJSONになってダウンロードできます。

JsonUtility をつかって Unity で JSON を取り扱う方法
https://qiita.com/sea_mountain/items/6513b330983ffa003959

この記事を参考にJSONをパースしてゲームに組み込みます。

VirtualController.cs
[Serializable]
public class NextConfig
{
    public bool is_continue;
    public string next_user_name;
}

IEnumerator getConfing() {
    UnityWebRequest request = UnityWebRequest.Get("https://*********.cloudfunctions.net/nextConfig"); 
    // リクエスト送信
    yield return request.Send();

    // 通信エラーチェック
    if (request.isHttpError) {
        Debug.Log(request.error);
    } else {
        if (request.responseCode == 200) {
            // UTF8文字列として取得する
            string text = request.downloadHandler.text;
            //Debug.Log(text);
            NextConfig item = JsonUtility.FromJson<NextConfig>(text);
            //Debug.Log("" + item.is_continue);
            //Debug.Log("" + item.next_user_name);
            if(item.is_continue == false){
                canvasReset();
            }
            isConfig = true;
        }
    }
}

インクボールの色を変えれるようにする

2019/11/1
今まではインクボールの色がランダムで出ていたが、プレイヤーが好きな色を使えると良いと思いジョイスティックで色を選択できるようにした。ジョイスティックがある方向に倒れているかの状態しかAPIからは取得できないので、矢印キーを1回押す感覚で左右に倒すことはできない(Unityでいう、GetKeyはあるが、GetKeyDownがない感じ)。なのでジョイスティックを倒したというイベントを1回限りで取れるようにした。

FPGA開発でVerilogHDLを書いた気持ちで、立ち上がり検出をC#で書く

VirtualController.cs
    // 立ち上がり検知
    private bool prev_left = false;
    private bool curt_left = false;
/*-------- 省略 -------- */
    void Update(){
/*-------- 省略 -------- */
        // 左方向検知
        curt_left = VvrController.JoystickAction() == VvrJoystickAction.Left;
        if (Input.GetKeyDown(KeyCode.LeftArrow) || ((prev_left == false) && (curt_left == true))){
            colorPalette = ((colorPalette + 8) - 1) % 8;
            //Debug.Log(""+colorPalette);
            setPalette();
        }
        prev_left = curt_left;
/*-------- 省略 -------- */
}

これでジョイスティックを傾けると色が変わるようになった。
output05.gif

UnityXRで動き回るにはカメラの親オブジェクトを移動させる必要がある

2019/11/1
ジョイスティックの上入力と下入力の機能に空きがあるので、前と後ろに移動できるようにしてみる。

output06.gif

Unity標準のVR機能(UnityEngine.XR)メモ
# Virtual Reality Supportedをオンにすると何が起きるの?
https://framesynthesis.jp/tech/unity/xr/#virtual-reality-supported%E3%82%92%E3%82%AA%E3%83%B3%E3%81%AB%E3%81%99%E3%82%8B%E3%81%A8%E4%BD%95%E3%81%8C%E8%B5%B7%E3%81%8D%E3%82%8B%E3%81%AE

また、実行時にカメラのPositionとRotationがトラッキングの動きで上書きされるようになります。たとえばカメラをスクリプト等で移動しているような場合、スクリプトでのTransformの更新が上書きされて無効化されてしまいます。対策としては、カメラを別のゲームオブジェクトの子オブジェクトにして親のほうをスクリプトで動かしてください。

MainCameraのPositionを動かしても無意味なので、上の記事通り、MainCameraに親オブジェクトを作成して、そのPositionを動かすことにした。

image.png

VirtualController.cs
void Update(){
/*-------- 省略 -------- */
        // 上方向検知
        // https://framesynthesis.jp/tech/unity/xr/
        // 親オブジェクトを動かすしかない
        if (Input.GetKey(KeyCode.UpArrow) || (VvrController.JoystickAction() == VvrJoystickAction.Up)){
            //if(isPlaying)
            Vector3 pos = cameraMover.transform.position;
            pos.x += 0.1f;
            if(pos.x >= -1.8f){
                pos.x = -1.8f;
            }
            cameraMover.transform.position = pos;
        }

        // 下方向検知
        if (Input.GetKey(KeyCode.DownArrow) || (VvrController.JoystickAction() == VvrJoystickAction.Down)){
            Vector3 pos = cameraMover.transform.position;
            pos.x -= 0.1f;
            if(pos.x <= -5.6f){
                pos.x = -5.6f;
            }
            cameraMover.transform.position = pos;
        }
/*-------- 省略 -------- */
}

Unityで音をならす

2019/11/3
ゲームの見た目はだいたいできてきたので、BGMとSEを追加。SEは重複ありなので、PlayではなくPlayOneShot関数を使った。プログラムから音を鳴らしていくのに下のQiitaの記事がとても参考になった。

[Unity] Unity×音についてざっくりまとめ
https://qiita.com/lycoris102/items/5d5359b2015a8fdebaaa

【Unity】音声(BGM・SE)の再生・ループ・フェードアウトなどの設定方法を徹底解説!
https://xr-hub.com/archives/18550

その5 SEの同時発生数問題を考えてみる
http://marupeke296.com/UNI_SND_No5_NumberOfSE.html

【Unity】重複ありのBGMの鳴らし方「Play()」「PlayOneShot()」
https://squmarigames.com/2018/12/19/unity-beginner-audiosource/

ゲームのサイトを作成

2019/11/3
firebase initしたときにHostingも選択しているので、プロジェクトフォルダ下のpublicディレクトリをサクッと編集して、firebase deployコマンドを打ち込むだけ。しかし、これだとFunctionの方もデプロイが走るので下のコマンドでデプロイする。

firebase deploy --only hosting

実際にデプロイしたサイト
https://sohosai2019-rereal.firebaseapp.com/

image.png

プレイヤーネームの登録を作成

2019/11/3
デプロイしたサイトに新規ゲームのためにプレイヤーの名前を登録できるフォームのあるページを作成した。念を入れてプレイ時間とインクの塗りをリセットするか?を変えられるようなInputも作成した。プレイヤーには名前を入れるだけのフォームに見えるように、フォームのSubmitボタンを上にして、下の方にオプションの項目を並べている。

↓実際のサイト
image.png

ユーザーネームを登録できるエンドポイントはFunctionに実装しておく。もともとあったデータをDatebaseから取得して処理を進めたかったので、awaitを使えるようにfunctions.https.onRequestの引数の関数には、asyncをつけている。

async/await 入門(JavaScript)
https://qiita.com/soarflat/items/1a9613e023200bbebcb3

index.js
exports.newGame = functions.https.onRequest(async(request, response) => {
    if (request.method === 'POST') {

        console.log(request.body.iscontinue);
        console.log(request.body.username);
        console.log(request.body.options);
        const db = admin.database().ref('next_config');

        const old_user_name = (await admin.database().ref('next_config/next_user_name').once('value')).val();

        if (typeof request.body.iscontinue !== 'undefined') {
            //onのとき
            //usernameが0なら更新しない、あれば更新
            db.set(
                {
                    next_user_name: (request.body.username.length != 0)? request.body.username : old_user_name,
                    is_continue: true,
                    play_time : Number(request.body.options)
                }
            );

        }else{
            //offのとき
            //usernameが0はnoname、あれば更新
            db.set(
                {
                    next_user_name: (request.body.username.length != 0)? request.body.username : "noname",
                    is_continue: false,
                    play_time : Number(request.body.options)
                }
            );

        }
        // ルールとかにすると良い
        response.redirect('https://sohosai2019-rereal.firebaseapp.com/new.html')
    }
});

ランキング機能を追加

2019/11/3
スコアがでるゲームにはインセンティブとしてランキングが必要なので、Twitterに投稿する文でこのスコアが何位なのかわかるようにした。ランキングは、自身のスコアより高いスコアのデータの個数+1が自身の順位なので、orderByChildでスコアをソート、startAtで自身より高いスコアのデータのみにフィルタリング、numChildrenでデータの個数を取得で順位を算出した。

ホーム > Firebase Realtime Database
データの取得 指定した子キーでの並べ替え
https://firebase.google.com/docs/database/admin/retrieve-data?hl=ja#%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%9F%E5%AD%90%E3%82%AD%E3%83%BC%E3%81%A7%E3%81%AE%E4%B8%A6%E3%81%B9%E6%9B%BF%E3%81%88

index.js
let rank_res = 0;
const db = await admin.database().ref('records').orderByChild("score").startAt(Number(fields["score"])).once("value").then(snapshot => {
    rank_res = snapshot.numChildren() + 1;
    //console.log("in:"+rank_res);
});
            
console.log(rank_res);

const data = fs.readFileSync(uploads['file']);
const mes = ((fields["user_name"] != "noname")?`${fields["user_name"]} さんが \n`:"")+`RE:REALをプレイしました! \nスコア:${Number(fields["score"])} \n現在の順位は${rank_res}位! \n\n雙峰祭5C511でプレイ! \n\n #playrereal #雙峰祭 #5C511`; //投稿するメッセージ

ランキングページを作成するために、FirebaseでCORSを許可する

2019/11/3
https://*********.cloudfunctions.net/getRankをPOSTメソッドで叩くと、JSONでランキングのデータがダウンロードできるようにした。FunctionにgetRankのエンドポイントを実装して、ページにはAjaxを使ってPOSTを叩くプログラムを実装した。まぁ簡単にはいかず、firebaseapp.comからcloudfunctions.netのドメインのAPIを叩くのだから、当然No 'Access-Control-Allow-Origin' header is present on the requested resource. とChromeに言われる。

Cloud Functions for Firebase でCORSを許可する方法
https://qiita.com/seya/items/0f12bd09c8e856123bc3

npm install cors もしくは yarn add cors でインストールして、CORSを許可したい関数を以下のようにwrapします。

上の記事を参考に、npm install corsして処理をwrapで解決した。

index.js
exports.getRank = functions.https.onRequest((req, res) => {
    cors(req, res, async() => {
/*-------- CROSを許可したい処理 -------- */
    });
});

Ajaxを使ってランキングを表示する部分(Javascriptの書き方が混ざっていてやべぇなw

rank.html
var xhr = new XMLHttpRequest();
 
 xhr.open('POST', 'https://*********.cloudfunctions.net/getRank');
 xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
 xhr.send( 'limit=100' );
 xhr.onreadystatechange = function() {
 
 if(xhr.readyState === 4 && xhr.status === 200) {

     //console.log( xhr.responseText );

    let _parse = JSON.parse( xhr.responseText );

    let rank = 1;
    _parse.forEach(element => {
      console.log(element);

      let _div = document.createElement('div');
            _div.className = "mdl-card mdl-shadow--2dp";
            _div.setAttribute("style", "width:512px;");
            _div.innerHTML = "<div class=\"mdl-card__title\">"+
                              "<h2 class=\"mdl-card__title-text\">"+rank+"位</h2>"+
                            "</div>"+
                            "<div class=\"mdl-card__media\">"+
                              "<img src=\""+element.tweet_media_uri+"\" height=\"150\" alt=\"\""+
                               "class=\"test-img\">"+
                            "</div>"+
                            "<div class=\"mdl-card__supporting-text\">"+
                                "<h5>"+element.user_name+"</h5>"+
                                "<h5>スコア:"+element.score+"</h5>"+
                            "</div>"+
                            "<div class=\"mdl-card__actions\">"+
                               "<a href=\""+element.tweet_url+"\" target=\"_blank\">ツイートをみる</a>"+
                            "</div>"+
                          "</br>";
            document.getElementById("rank_column").appendChild(_div);
            document.getElementById("rank_column").appendChild(document.createElement('br'));

            rank++;

    });
   
 }
}

Firebase Functionの部分

index.js
exports.getRank = functions.https.onRequest((req, res) => {
    cors(req, res, async() => {
        let result = [];
        if (req.method === 'POST') {
            //console.log(req.body.limit);
            await admin.database().ref('records').orderByChild("score").limitToLast(Number(req.body.limit)).once("value").then(snapshot => {
                    snapshot.forEach(e =>{
                        result.push({
                            id: e.child("id").val(),
                            user_name: e.child("user_name").val(),
                            score: e.child("score").val(),
                            tweet_id: e.child("tweet_id").val(),
                            tweet_url: e.child("tweet_url").val(),
                            tweet_media_uri: e.child("tweet_media_uri").val(),
                            created_time: e.child("created_time").val()
                        });
                    });
                }
            );
        }
        res.send(JSON.stringify(result.reverse()));
    });
});

【JavaScript入門】FormやAjaxのPOST送信・取得の方法まとめ!
AjaxによるPOST
https://www.sejuku.net/blog/53627

FirebaseのCloud FunctionsでCORSが~とかAccess-Control-Allow-Originが~と言われたらこれ
https://qiita.com/qrusadorz/items/40234ac0b5c5c2315cad

[WIP]展示

2019/11/3-4
11月3日 11:00-16:00、11月4日 10:00-12:00 14:45-16:00の8.25時間で、総プレイ数は60回といった感じだった。普通に学園祭は人が来るので、またゲームを展示して行けると良かったりと思った。結局、当日までコーディングをしていたが、自分としては書き出したAndroidアプリが途中でクラッシュすることもFirebase上でバグを踏むこともなく動いたのにはとても満足した。必要だと思ったことは当日色々思ったがメモするのを忘れてしまったので、随時展示について細かいことをここに書いて行きます。今のところ、ゲームに関するインストラクションを作るべきなのと、水分が必要なの点が大事だとここに書いておきます。

[WIP]反省

絶賛執筆中

最後に

basic2.png

UnityFirebaseだった

47
29
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
47
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?