この記事は Unity #3 Advent Calendar 2019 ・ mast Advent Calendar 2019 の8日目の記事です!
ごめんなさい!!!3日も遅れてしまいました...(一番下の展示した話は絶賛執筆中
TL;DR
11月の頭に学園祭で下のようなVRゲームを展示した。
【5C棟511でプレイ!】
— RE:REAL@雙峰祭5C511 (@rereal2019) November 1, 2019
初めまして!
RE:REALでは雙峰祭内の芸術祭1日目2日目に5C棟511教室にてゲーム作品を展示します🎮
実際の511教室を、VR空間上に再現!教室で普段できないことができます✨
みんなのプレイ結果が #playrereal にあがるのでチェックしてね✌︎
↓開発中の映像( ∩'-'📷⊂ ) pic.twitter.com/geJsZvXJJA
その技術まとめログを残して置きたくまとめた。
Gitのコミットログをたどってトピックを時系列で書いてく。
構想
まずは、 作りたいもの・テーマ・要件 を決めるところから始めた。
2019年7月の時点で、作りたいものは「ゲーム」ということだけが決まっていた。
あとは、どんなテーマでどんなゲームを作るかを考えて行くだけであるが、そのまま夏休みを消化してしまった。
9月の終わり、「このままでは学園祭間に合わねぇ」となり、構想を急ピッチで固めることとなった。スマホやゲーム端末でどこでも簡単にゲームをできるからこそ、学園祭の「その日」「その場所」でしかできないものを目指すことにした。
作るゲームは「VRスプラトゥーン(仮)」、その名の通りVRでスプラトゥーンをするというゲームである。
アイデアが思いついてしたのは、まずは**「先人はいないか」** **「二番煎じではないのか」**をサーベイしていくことに、そんな中見つけたのは以下のインターフェースである。
#スプラトゥーン の新しいインタフェース作った pic.twitter.com/rqedtePWAY
— kougaku (@kougaku) June 20, 2016
ツイートにあるインターフェースの詳細は こちら
これは新しいインターフェースであり、ゲームシステム自体は任天堂の端末で動いてるので、ゲームシステムから自分で実装していけば、二番煎じならない と踏み開発にとりかかることにした。
部屋が借りらたので、現実と全く同じようにモデリングした空間をステージにすることにした。
現実と 同じ見た目 の空間をステージにすることで、 「その日」「その場所」でしかできない という目指すべき点を満たした。同時に、メディアアートとしてのコンセプトが完成した。
目に映っているのは現実であるが、実際には現実では無く
体験している本人からは、現実の物理的な世界でできないことが、
目に映っている現実と同じように見える情報的な世界で、実現できる
ゲームを作りたいといっても、制作するゲームは展示のためで、プレイしてもらう以上、自己満足は避けるべきである。
開発において、自己満足ルートに入らないように、先に要件を練って決めておくのは大切だった。
Unityで簡単お手軽VR
2019/9/29
Oculus無ェ、Viveも無ェ、お金もそれほど持って無ェ
ですが、VRゲームを作っていく。Oculus RiftもGoもQuestもHTC Viveもありませんが、スマートフォンがあれば十分。
UnityのPlayer Settings > (Android)XR Settings > Virtual Reality Supported
にチェックを入れて、下のリストにVRプラットフォームを追加するだけ
今回は、Cardboard
を使いました。
これで、Unityのカメラに映っている空間が自動で左目用、右目用に分かれてレンダリングされ、カメラの向きがジャイロセンサーに追従するようになった。
↑ これはスマホの画面です。
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
エレコム VR リモコン Bluetooth [VR/AR ヴルームSDK対応] ブラック JC-VRR02VBK
※PRとかステマとかではなく 普通に良かった
Unityへの導入は上のQiitaの記事を参考にした。
実際にAndroid端末でVroomコントローラーを使うのには、Androidの位置情報のパーミッションを許可する必要があった。
部屋の計測
2019/10/15
実際の部屋がステージなので、モデリングするために計測をした。サクサクっと部屋の計測をしたかったのでレーザー距離計を導入した。1.測りたい起点にレーザー距離計を当てる
2.ボタンを押してレーザーポイントが測りたい終点を確認
3.もう一度ボタンを押すとディスプレイに距離がでる
これだけ部屋の大体の大きさがわかった。
BOSCH(ボッシュ) レーザー距離計 GLM30 【正規品】
大体の形で距離を測っておいて、測れない部分は全体の長さから調整して割り出した。
部屋のモデリング
2019/10/21
Autodesk Fusion 360
を使って、実際に測った寸法どおりにモデリングした。壁や床の色はテクスチャでつけるのでモデリングでは形だけを作っていく。細かいパーツはとりあえず後回しで、80%でも良いからゲームをプレイできるように、大まかなに部屋を作っていく。
素材の購入
2019/10/22
概形のモデリングを終え、Unity内に壁と天井と床を作成したが、壁だけだとどうにも寂しい。教室の最低限の要素として、黒板
と蛍光灯
は必要ではありそう。だが、計測してないしモデリングの手間を考えると、教室のアセットを買うべきだと思って購入。このアセットから黒板と蛍光灯のモデルを拝借し、サンプルシーンから部屋のライティングがどうなっているかも確認した。
ライティングがよくわからない
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プロジェクト」早速DL。https://t.co/0IvHjiUQWV
— 龍 lilea (@lileaLab) July 9, 2019
これはすごい。
ため息が出る美しさ。
しかもプロジェクトデータも公開されるとか! pic.twitter.com/nDhpL56xKK
実際に作成してできたのはこんな感じの教室
まだまだ改善の余地はあるが、まだゲームのシステムには手をつけていないので作り込みはシステムができてから...
InkPainterの導入
2019/10/22
スプラトゥーンはインクを飛ばしてステージを汚していくゲームですが、どうすればUnityで実装できるでしょうか...。Unityならアセットに頼って行くのがよさそうなのでアセットを探します。制作するゲームでは、いーす さんが作成した InkPainter を使います。
InkPainterはテクスチャが設定されてないと塗れない
2019/10/22
インクを付けたいMesh
にはMesh Collider
とInkPainter.cs
アタッチする。MousePainter.cs
をMain Camera
にアタッチするとマウスでクリックしたところにインクが付く(マウスから直線上にRayを飛ばして、最初にオブジェクトと交差する交点にインクが付く
しかし、テクスチャが未設定だとインクが付かないので、Albedo
にテクスチャをセットしておく。
InkPainterはモデルがUV展開されてないと塗れない
2019/10/22
ふぇ〜、壁をクリックした瞬間全部塗られてしまった...
この時点で「テクスチャがめちゃくちゃ小さくて、一瞬でテクスチャを全部塗りつぶしてしまった?」とか考えて、こねこねした。
この記事をみるとどうやらモデルが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
じゃないといけないので注意。
モデルが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展開したときの縮尺が統一されていないからである。
モデルの縮尺を合わせるコストの方が高そうなので、InkPainter側でモデルごとにブラシサイズに係数をかけられるようにした。
※brush.Scale
は全体で設定されている値
/*-------- 省略 -------- */
/// <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にアタッチする。
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
を変更する必要がある。
CollisionPainter.cs
にはOnCollisionStay
が実装されているので、Sphereが壁や床にあたっている間に呼ばれる。
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 | オブジェクトを破壊するまでのディレイ時間 |
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
に、「オリジナルのテクスチャ」と「塗ったあとのテクスチャ」を比較して、変化があったピクセルの数を計算するプログラムを追加した。
InkPainter.csにもともとあったSaveRenderTextureToPNG
関数を参考にプログラムを作成。
PaintSet#mainTexture (Texture)
がオリジナルの画像でPaintSet#paintMainTexture (RenderTexture)
がインクの付いた画像になっている。Texture
とRenderTexture
の内容は単純比較できなさそうなので、どちらもColor[]
に変換して単純に色を比較した。この処理はむちゃくちゃ重いです。
Texture から Texture2D への変換
http://nakamura001.hatenablog.com/entry/20171012/1507810457
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
が塗りの色なのでこれを変える。インクボールの色はマテリアルでセットしているのでそれに同じ色を当ててあげる。
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
実際には下のQiitaの記事のプログラムを参考に、スコアが計算されたらカメラに映っている画像をキューブマップに落とし込んで、Shaderを使ってEquirectangularな画像を生成/保存できるようにした。
Unityでカメラのequirectangular画像を作成してみる
https://qiita.com/mechamogera/items/0b47e5947f1eee2467da
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 Settings
のAlways Included Shaders
にプロジェクトに追加したConversion/CubemapToEquirectangular
を追加しないとAndroid上で動作しないので注意
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
Unity Native Gallery Plugin
を使うとユーザー領域にデータを保存できるようになるので導入、GitHubのページからNativeGallery.unitypackage
をダウンロードしてインポートで導入完了。あとは、保存するプログラムを書き換えるだけで終わった。
【Unity】iOS の写真や Andoid のギャラリーに画像や動画を保存できる「Unity Native Gallery Plugin」紹介
http://baba-s.hatenablog.com/entry/2017/12/26/210500
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
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
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にも優しい。
// ----- 省略 ------
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
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の書き方で記録できる。
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
になるとゲームが始められる。(文章で書くと説明が長いのでこのシステムは良くないと今思っている...
メモリ使用が800MBを超えるとアプリが落ちる
2019/11/1
アプリを終了せずともゲームをスタート/ストップできるようになったのだが、どうも3,4回目をプレイして塗りを計算したあたりでアプリが落ちる。コードを見る限り、プレイ回数に依存して配列外アクセス、nullアクセスして、NullReferenceException
とかではなさそうだった(今思えばログ見ればよかったのでは...。とりあえずこういった問題には、Profilerを使う。Android StudioにはAndroid Profilerがあるので、
デバイスをmacにつないだままプレイしてCPUやメモリの状態を観察すると、ゲームが終了するごとに約200MBぐらい増えている。
800MBあたりを越えた時点でアプリが終了してメモリがパージされている。これはつまりメモリリークってやつですね。ゲームが1回終わるごとにアプリをリセットすれば良いが、忘れたりでもしたらプレイ結果が消えるので、これは解決するべき問題。(人間がシステムに対して行わなければいけないことは最小限にしたい
メモリリークに対してあまり詳しくないのでサーベイ。調べると、Texture系はnewして使わなくなったらDestroyしようということなのでプログラムにDestroyを追加
Destroy(_renderTexture);
Destroy(newTex);
Destroy(texture2D);
cubemapは1280x1280x6のテクスチャ、renderTextureは4096x2048でどちらもRGB24なので、これだけで約53MBもあるわけだ...
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
[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を回すだけ。
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にして返すエンドポイントのプログラム
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をパースしてゲームに組み込みます。
[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#で書く
// 立ち上がり検知
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;
/*-------- 省略 -------- */
}
UnityXRで動き回るにはカメラの親オブジェクトを移動させる必要がある
2019/11/1
ジョイスティックの上入力と下入力の機能に空きがあるので、前と後ろに移動できるようにしてみる。
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を動かすことにした。
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/
プレイヤーネームの登録を作成
2019/11/3
デプロイしたサイトに新規ゲームのためにプレイヤーの名前を登録できるフォームのあるページを作成した。念を入れてプレイ時間とインクの塗りをリセットするか?を変えられるようなInputも作成した。プレイヤーには名前を入れるだけのフォームに見えるように、フォームのSubmitボタンを上にして、下の方にオプションの項目を並べている。
ユーザーネームを登録できるエンドポイントはFunctionに実装しておく。もともとあったデータをDatebaseから取得して処理を進めたかったので、await
を使えるようにfunctions.https.onRequest
の引数の関数には、asyncをつけている。
async/await 入門(JavaScript)
https://qiita.com/soarflat/items/1a9613e023200bbebcb3
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
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で解決した。
exports.getRank = functions.https.onRequest((req, res) => {
cors(req, res, async() => {
/*-------- CROSを許可したい処理 -------- */
});
});
Ajaxを使ってランキングを表示する部分(Javascriptの書き方が混ざっていてやべぇなw
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の部分
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]反省
絶賛執筆中
最後に
UnityとFirebaseは神だった