FINAL STAGE制作記録

GW期間中から21日間でFINAL STAGEというゲームを作りました。

ゲームを公開したツイートはこちらです。

ゲーム制作をしたという報告の記事はこちら nanka.hateblo.jp

制作期間は作業ログと動画を毎日記録していました。あまり他人に見せれるような実装やコードではないんですが、備忘録という事でそこから抜粋しつつ21日間どのように制作を進めていたのかを記事として残します。

解説記事ではなく、あくまで制作記録です。

1日目

作るゲームの決定

今回ゲーム制作をするきっかけになった、社内ゲームジャムのテーマが「打ち上げる」でした。

お題が何にしても、地球防衛軍ライクな3Dのシューティングゲームにしようというのは決めていたので、テーマにこじつけるために「ラスボスを唯一倒せるミサイルを打ち上げるまで耐えるゲーム」を作ることにしました。

TPSカメラの作成

TPSカメラはCinemachineの機能に完全に乗っかることでノーコードで作成できました!

良い解説記事があったのでこちらをご参照ください。 nekojara.city

プレイヤーを作成

プレイヤーの挙動はSphereのRigidBodyを利用しました。 f:id:kaneta1011:20210626195535p:plain

キーボードの入力情報を元に、Rigidbody.AddForceを用いて弾を転がして、見た目だけ別に差し替えるというシンプルな方法でホバークラフトっぽい操作感にしました。

var horizontal = Input.GetAxisRaw("Horizontal");
var vertical = Input.GetAxisRaw("Vertical");
var direction = new Vector3(horizontal, 0, vertical).normalized;

var targetAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg + camera.eulerAngles.y;

if (direction.magnitude >= 0.1f) {
    var moveDir = Quaternion.Euler(0, targetAngle, 0) * Vector3.forward;
    rb.AddForce(moveDir * 30.0f, ForceMode.Acceleration);
}

if (rb.velocity.magnitude > 20.0f) {
    rb.velocity = rb.velocity.normalized * 20.0f;
}

二個目のifブロックで最大速度の制限を行っていますが、この方法だと落下速度まで制限されてしまうので、本当は落下と入力による移動速度は別々で管理しておいた方が良さそうです。

個人的に落下にも速度制限が付いた方がホバークラフトっぽかったのでそのままにしました。(時間が無かったともいう...)

Unity - Scripting API: Rigidbody.AddForce

また、SmoothDampAngleを利用して、入力に応じて自機の見た目に傾きを与えています。

var angle = Mathf.SmoothDampAngle(car.rotation.eulerAngles.z, -horizontal * 10.5f, ref turnSmoothVelocity, turnSmoothTime);
var angle2 = Mathf.SmoothDampAngle(car.rotation.eulerAngles.x, vertical * 10.5f, ref turnSmoothVelocity2, turnSmoothTime);
car.rotation = Quaternion.Euler(angle2, camera.eulerAngles.y, angle);

Unity - Scripting API: Mathf.SmoothDampAngle

自機から射撃する弾関連の素材作成

弾のモデリング

弾はHoudiniを用いてモデリングしました。

f:id:kaneta1011:20210626201329p:plain

弾の大きさや長さ、分割数などを変更できるデジタルアセットにしておいたので、後ほど弾丸の種類を増やしたりするのにとても便利でした。

画像素材作成

着弾時のパーティクル用画像と、レーザーガン用のマズルフラッシュ画像をSubstance Designerで作成しました

これくらいPhotoshopGimpで作れという話ですが、SDを利用した方が個人的に速く作れそうなので...

f:id:kaneta1011:20210626202828p:plain
着弾時のスパーク
f:id:kaneta1011:20210626203554p:plain
マズルフラッシュ

弾の挙動作成

弾自体は発射時の銃の姿勢から一直線に飛んでいくだけとして、着弾時に特殊な処理をしているので紹介します。

カメラ揺れ

最終的には発射時に変更されましたが、1日目では着弾時にカメラ揺れを発生させていました。

こちらもTPSカメラと同様Cinemachineの機能を利用することでかなり簡単に実現できました。

良い解説記事があったのでこちらをご参照ください light11.hatenadiary.com

爆発

着弾時に周囲のRigidbodyを吹き飛ばすような爆発演出を行っています。

これを実現するために、Physics.OverlapSphereとRigidbody.AddExplosionForceという二つの関数を利用しました。

void Explosion(Vector3 position) {
    Vector3 explosionPos = position;
    Collider[] colliders = Physics.OverlapSphere(explosionPos, 6.0f);
    foreach (Collider hit in colliders)
    {
        Rigidbody rb = hit.GetComponent<Rigidbody>();

        if (rb != null)
            rb.AddExplosionForce(30000.0f, explosionPos, 6.0f, 0.1f);
    }
}

Unity - Scripting API: Physics.OverlapSphere Unity - Scripting API: Rigidbody.AddExplosionForce

1日目の動画

youtu.be

2日目

マシンガンを作成

1日目にレーザーガンを作成したので、最もベースとなる武器のマシンガンを作成しました。

銃や弾の挙動自体はレーザーガンとほぼ一緒だったので、ほとんど変更するところはありませんでしたが、マズルフラッシュのみ新しく作成しました。

f:id:kaneta1011:20210626204429p:plain
マズルフラッシュのデジタルアセット

マズルフラッシュは1日目に作った弾丸のデジタルアセットを利用して、円状に配置することで作成できました。

こちらも、火花の数や角度などをパラメーターで設定できるようにしたので、バリエーションの量産が容易に行えました。

ゲームでは3パターンのマズルフラッシュをパーティクルシステムでランダムに表示しています。

f:id:kaneta1011:20210626204648p:plain
マズルフラッシュをゲームで表示している様子

敵を作成

二種類の武器ができて試し打ちがしたくなったので敵を追加しました。

敵のモデルはUnityのプリミティブで用意されているSphereとCubeを組み合わせたシンプルなものです。最終的にHoudiniでかっこいい敵を作りたいなと思っていたんですが、意外とこのシンプルな形状が気に入ったので最後までこの姿になりました。

f:id:kaneta1011:20210626205223p:plain
敵の形状

敵の挙動もRigidbodyで制御しています。

見づらいコードで恐縮ですが、挙動自体はとてもシンプルで4つの処理を行っています。

transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation((target.position + Vector3.up * 4.0f) - transform.position), Time.deltaTime * rotateSpeed);
var force = transform.forward * speed;
var dot = Vector3.Dot(force.normalized, rb.velocity.normalized);
dot = dot * 0.5f + 0.5f;
force += -rb.velocity * (1.0f - dot) * brake;
rb.AddForce(9.81f * rb.mass * Vector3.up + force);
  • ターゲットの方向を徐々に向くようにする
  • 敵は正面方向へ移動し続ける
  • 重力を打ち消す力を与え続ける
  • 移動しようとしている方向と現在移動している方向が逆の場合はブレーキを掛ける

2日目の動画

youtu.be

3日目

敵をブラッシュアップ

マテリアル設定

SphereとCubeにそれぞれ別のマテリアルを設定して、敵っぽい見た目にすることにした。

ノーマルマップはUnityのデフォルトプロジェクトのもともと入っていたものを利用。

f:id:kaneta1011:20210627153432p:plain
敵のマテリアル

死亡時に爆発エフェクトを追加

こちらのUnity Assetに含まれる爆発エフェクトを利用して、敵が死亡した時に爆発するようにした。

assetstore.unity.com

f:id:kaneta1011:20210627151041p:plain
敵が死ぬときに爆発

ガトリングガン追加

マシンガンの仕組みを流用して、発射位置の追加と発射レートを変更してガトリングガンを追加

f:id:kaneta1011:20210627145634p:plain
ガトリングガンを発射している様子

ホーミングガン追加

敵のロックオン

毎フレーム敵をプレイヤーからの距離順でソートして以下3体の敵を自動追尾するようにした

  • 一番近い敵
  • 近い順で25%の場所にいる敵
  • 近い順で50%の場所にいる敵

敵をソートする処理

var enemies = GameObject.FindObjectsOfType<AttackTransformEnemy>();
var cameraPos = CameraManager.Instance.MainCamera.transform.position;

sortedEnemies = enemies.Where( v => {
    return v.IsLive;
}).OrderBy(v => {
    var direction = v.transform.position - cameraPos;
    return direction.sqrMagnitude;
}).ToArray();

敵を選択する処理

public AttackTransformEnemy GetEnemyOrderDistance(float normalizedDistance) {
    var length = sortedEnemies.Length;
    if (length == 0) return null;
    var index = (int)Mathf.Lerp(0, length - 1, Mathf.Clamp01(normalizedDistance));
    return sortedEnemies[index];
}

追尾処理

ホーミングレーザーは派手に飛ばしたかったので、発射箇所を6つ設定しました。

他の銃は、カメラが向いてる方向に弾を飛ばすのに対して、ホーミングレーザーは斜め上固定で発射します。

ホーミング処理は、敵の挙動と同じように徐々にターゲットの方向へ向くように回転して、常に直進するようにしています。

動いている敵にあたるように回転速度を調整すると、発射したレーザーがすぐに敵に向かっていってしまい、綺麗に発射しているのように見えなかったので、発射後徐々に回転速度を上げるようにしました。

この方法だと至近距離で弾を発射した時に当たらなくなるので、ターゲットに近づくほど回転速度を上げる調整もしています。

var targetTransform = target.transform;
var rotateSpeedTime = Mathf.Clamp01(1.0f - rotateSpeedTransitionTime / rotateSpeedTransitionDuration);
var rotateSpeed = Mathf.Lerp(startedRotateSpeed, endedRotateSpeed, rotateSpeedTime);
rotateSpeedTransitionTime -= Time.deltaTime + Time.deltaTime * Mathf.Exp(-Vector3.Distance(transform.position, targetTransform.position));
movingRotation = Quaternion.Lerp(movingRotation, Quaternion.LookRotation(targetTransform.position - transform.position), Time.deltaTime * rotateSpeed);

弾の軌跡はTrail Rendererで実装。

パワーアップアイテム追加

こちらのサイトのアイコンをお借りして、パワーアップアイテムの実装を行った

game-icons.net

同じタイミングで、弾薬システムも追加したので、このパワーアップアイテムを獲得することで、ホーミングレーザーなどの特殊武器を利用できるようにした。

f:id:kaneta1011:20210627160734p:plain
パワーアップアイテム

3日目の動画

youtu.be

4日目

マザーシップのモデリング

ラスボスとなるマザーシップをHoudiniでモデリングした

外側の独特な模様はボロノイで分割後、mirrorノードを利用して模様を対称的にしている。

ベースは球なのであまり時間を掛けずにモデリングできた。

f:id:kaneta1011:20210627161021p:plain
マザーシップのノード
f:id:kaneta1011:20210627161117p:plain
マザーシップをゲーム上に表示した様子

残り弾薬のUI作成

Line Renderer等を用いて、残り弾薬のUIを作成。

UnityのAnimationを用いて、表示・非表示のアニメーションも作成。

弾薬の有無に応じて、アニメーションさせてかっこよくしたかった。

f:id:kaneta1011:20210627161835p:plain
残り弾薬のUI

4日目の動画

youtu.be

5日目

武器とプレイヤーのモデリング

武器とプレイヤーもHoudiniでモデリングすることにした。

手動で書いたカーブを入力すると、いい感じに武器っぽい形のメッシュを生成するジェネレーターを作成したので、全種類の武器とプレイヤーのモデルを一瞬で量産することができた。

仕組みはとてもシンプルで以下の手順で生成している

  • 手動で書いたカーブに厚みを持たせてメッシュ化(ベースメッシュ)
  • ベースメッシュの各頂点にベースメッシュを配置してフラクタル形状にする
  • 各頂点に配置したベースメッシュ同士が重なっているところに凹みを追加

f:id:kaneta1011:20210627162430p:plain
武器ジェネレーター

f:id:kaneta1011:20210627163333p:plain
自機をゲームで表示した様子

特殊武器シェーダーを作成

マシンガン以外の武器に利用するシェーダーをShaderGraphで作成しました。

武器とパワーアップアイテムの関連付けをしやすくするための色付けと武器獲得時、消滅時のディゾルブを実装しています。

f:id:kaneta1011:20210627172124p:plain
武器シェーダーのノード

f:id:kaneta1011:20210627172328p:plain
武器の色付きオーラ

f:id:kaneta1011:20210627172354p:plain
武器獲得・消滅時のディゾル

自機がかっこよく出来上がったのでうれしくなってスクショを沢山撮影した

f:id:kaneta1011:20210627172558p:plain
スクショ1
f:id:kaneta1011:20210627172615p:plain
スクショ2
f:id:kaneta1011:20210627172630p:plain
スクショ3

武器のアニメーション

UnityのAnimationを用いて、弾発射時の反動アニメーションを作成

反動アニメーションがあるだけでとてもいい感じに遊べるようになってテンションが上がっていた。

5日目の動画

youtu.be

6日目

マザーシップ破壊演出

制限時間を実装したので、マザーシップが破壊される演出を作成しました。

ミサイル

ミサイルのメッシュは以下のUnity Assetを利用させて頂きました。

assetstore.unity.com

ミサイルの挙動は到着時刻を指定する形で実装しました。

好みの軌跡を描けるように、ターゲットまでの距離が一定になるまでは直進、その後はホーミングするという実装を行いました。

var targetTransform = target;
var distance = ToTargetXZVector.magnitude;
if (distance < startFollowRange) {
    followRotateSpeed += followAddRotateSpeed * Time.deltaTime;
    movingRotation = Quaternion.Lerp(movingRotation, Quaternion.LookRotation(targetTransform.position - transform.position), Time.deltaTime * followRotateSpeed);
}
// 現在のターゲットまでの距離と到着までの残り時間からスピードを算出
var speed = ToTargetVector.magnitude / arrivalTime;

var velocity = movingRotation * Vector3.forward * speed;
transform.position += velocity * Time.deltaTime;
transform.rotation = movingRotation;

if (arrivalTime <= 0.0f) {
    isEnd = true;
    onEnd.Invoke();
}

arrivalTime -= Time.deltaTime;

f:id:kaneta1011:20210627173702p:plain
マザーシップにミサイルが当たる直前

カメラの挙動

カメラは例によってCinemachineの機能を利用しています。

Timelineを使ってみようかなとも思ったんですが、あまり複雑なカメラワークにする予定は無かったので、Blend List Cameraを利用してカメラワークを切り替えました。

Blend List Cameraについてはよい解説記事があったのでこちらをご参照ください。 light11.hatenadiary.com

ミッションクリア演出

こちらも残り弾薬のUIと同じように、uGUIで実装し、挙動はUnityのAnimationで付けました。

f:id:kaneta1011:20210627173931p:plain
ミッションクリア演出

6日目の動画

youtu.be

7日目

チュートリアルステージのモデリング

チュートリアルステージもHoudiniでモデリングしました。

遠景が街のように見えればよかったので、Grid上に様々な大きさのCubeを散らばせただけの手抜き制作です。

f:id:kaneta1011:20210627174229p:plain
チュートリアルステージのノード

自分の能力が足りずに、マテリアルをいい感じに設定することができなかったので、下手にテクスチャをタイリングするぐらいなら何もしない方が良いだろうと思い、ノーマルマップだけ設定して、ライティングにビジュアルをお任せする形にしました。

次回ゲームを作るときには、ちゃんとマテリアルを設定できるようになりたい。

f:id:kaneta1011:20210627174301p:plain
チュートリアルステージをセットアップしてゲーム上で表示している様子

7日目の動画

youtu.be

8日目

チュートリアルからタイトル表示へのカメラワークを作成

CinemachineのDollyカメラを利用してタイトル表示のカメラワークを作成。

Dollyカメラについてはこちらの記事が詳しいのでご参照ください。

light11.hatenadiary.com

f:id:kaneta1011:20210627192743p:plain
タイトル表示のカメラワーク

URPの1オブジェクト4ライト制限を回避するためにステージのメッシュを分割

チュートリアル開始時にトンネルの中からゲームが始まるんですが、この時点ではステージ全体がワンメッシュになっていたため、URPの1オブジェクト4ライトの制限を受けていて、トンネル内部のポイントライトが正しく反映されない状態になっていました。(最終的にステージはMixed Lightにしたのでもしかしたらこの対応は不要だったかもしれない)

f:id:kaneta1011:20210627192828p:plain
トンネル

自分の知識不足かもしれませんが、Houdiniで書き出したモデルをUnityで読み込んだときにGameObjectを分割されている状態にするためには、Houdini側でGroup化しておく必要がありそうでした。

いろいろ調べたところ、以下の手順で求めているGroup生成ができることが分かりました。

まずはDivideノードのBricker Polygonsを利用してメッシュを特定の範囲で分割します。

support.borndigital.co.jp

f:id:kaneta1011:20210627192849p:plain
DivideノードのBricker Polygons

これではまだ頂点で分割されただけなので、以下の手順でそれぞれをGroupにします。

Attribute Wrangleを用いてプリミティブにGroupの情報を設定します。

今回は位置のZ座標をfloorでID化してGroup情報としました。

f:id:kaneta1011:20210627192931p:plain
Z座標でグルーピングした結果をプリミティブのアトリビュートに入れる

Partitionノードを用いて、アトリビュートからGroupを作成します。

f:id:kaneta1011:20210627193016p:plain
Partitionノードでグループ作成

以上の手順でトンネルを一定の長さで分割しGroup化することができました。

これをUnityで読み込むとあらかじめGameObjectが分割された状態で読み込まれました。

f:id:kaneta1011:20210627193058p:plain
頂点が分割されて、それぞれがグループ化されている

8日目の動画

youtu.be

9日目

シナリオのテキストを考える

地球防衛軍みたいな熱い感じのテキストを表示したかったので、過去にプレイしたシリーズを思い出しながら頑張って考えました!

テキストの再生システムを作成

マスターデータの用意

考えたシナリオテキストを、スプレッドシートに必要な情報と合わせて記入。

最終的にテキスト一つに対するデータは以下になりました。

  • 発言者名の色
  • 発言者名
  • 文言
  • 表示を開始する時間
  • 表示する時間
  • 文言表示時に特殊な処理をするためのタグ

スプレッドシートには文言のふりがなも含まれていますが、これは文言を表示する時間を計算するためのものです。

計算方法は、文言のふりがなの文字数で決定しています。

とても単純ですが、結構それっぽい時間文言が表示されるようになりましたし、自分で設定する必要がなくなったのでとても時短になりました。

文言のひらがな化には以下のWeb APIを利用させて頂いています。

yomi-tan.jp

f:id:kaneta1011:20210627194638p:plain
アイテムチュートリアルの文言例

Unity側でシナリオデータをAsset化する

Scriptable Objectを利用してシナリオデータをAsset化しました。

f:id:kaneta1011:20210627200839p:plain
シナリオデータのScriptableObject

Editor拡張を書いてワンボタンでスプレッドシートのデータをScriptable Objectに反映することができるようになっています。

public override void OnInspectorGUI()
{
    base.OnInspectorGUI();
    if (GUILayout.Button("Download CSV")) {
        var novel = target as NovelObject;
        GoogleSpreadSheetUtility.DownloadCSV(novel.SheetID, novel.SheetName, (request) => {
            var lines = CSVToLineDataArray(request.downloadHandler.text.Replace("\"", ""));
            novel.Lines = lines;
            EditorUtility.SetDirty(novel);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        });
    }
}

public class GoogleSpreadSheetUtility
{
    public static void DownloadCSV(string id, string name, Action<UnityWebRequest> onSuccess = null, Action<UnityWebRequest> onError = null) {

        UnityWebRequest request = UnityWebRequest.Get("https://docs.google.com/spreadsheets/d/"+id+"/gviz/tq?tqx=out:csv&sheet="+name);
        request.SendWebRequest();

        EditorApplication.CallbackFunction updateFunc = null;
        updateFunc = () =>
        {
            if (request.isDone)
            {
                if (request.result == UnityWebRequest.Result.ProtocolError || request.result == UnityWebRequest.Result.ConnectionError || !string.IsNullOrEmpty(request.error)) {
                    onError?.Invoke(request);
                }
                else {
                    onSuccess?.Invoke(request);
                }
                EditorApplication.update -= updateFunc;
            }
        };

        EditorApplication.update += updateFunc;
    }
}

最初はScriptedImporterを利用して、CSVをAsset化していたんですが、毎回毎回スプレッドシートからCSVをダウンロードしてUnityにインポートするという手間が大変過ぎたので、このような形になりました。

これのおかげで、シナリオを反映させる手間がかなり効率化されました。

f:id:kaneta1011:20210627203501p:plain
シナリオテキスト

街を攻撃する敵を作成

街を攻撃している小型戦闘機はHoudiniで街のメッシュに対してスキャッターしたポイントクラウドを100個程生成して、そこを攻撃させている

地面から高くなるほど点群の密度が高くなるようにしている。(高い場所の方が目立つので)

f:id:kaneta1011:20210627203535p:plain
敵の攻撃対象になる点群

この点群をCSVで出力して、UnityでAssetとして認識させるためにScriptedImporterを利用して実装しました。

ランタイム時にこのデータを参照して、街を攻撃する敵を配置している。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.AssetImporters;
using System.IO;

[ScriptedImporter(1, "points")]
public class PointsImporter : ScriptedImporter
{
    [SerializeField]
    private float unit = 100.0f;
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var text = File.ReadAllText(ctx.assetPath);
        StringReader reader = new StringReader(text);

        var csvDatas = new List<string[]>();
 
        while (reader.Peek() != -1) {
            string line = reader.ReadLine();
            csvDatas.Add(line.Split(','));
        }

        var points = new List<Vector3>();
        foreach(var data in csvDatas) {
            var point = Vector3.zero;
            point.x = -float.Parse(data[0]) * unit;
            point.y = float.Parse(data[1]) * unit;
            point.z = float.Parse(data[2]) * unit;

            points.Add(point);
        }

        var pointsObject = ScriptableObject.CreateInstance<PointsObject>();
        pointsObject.Points = points.ToArray();

        ctx.AddObjectToAsset("MainAsset", pointsObject);
        ctx.SetMainObject(pointsObject);
    }
}

9日目の動画

youtu.be

10日目

仮のブリーフィングを作成

ゲーム開始から終了までの流れを確認したかったので、真っ黒な画面にテキストだけ表示する仮のブリーフィングを作成

オブジェクトの更新系をFixedUpdateからUpdateに移動

弾やパワーアップアイテムなどあらゆるオブジェクトの更新処理をなぜかFixedUpdateで統一していたのでUpdateに処理を移動

戦闘開始時の演出を追加

戦闘開始時のカメラワークと戦闘終了演出を流用した戦闘開始演出を追加。

カメラワークの制御は、テキスト再生システムのタグを利用してCinemachine Virtual CameraのON, OFF制御を行った。

f:id:kaneta1011:20210627204542p:plain
戦闘開始のカメラワーク

f:id:kaneta1011:20210627204611p:plain
戦闘開始演出

10日目の動画

youtu.be

11日目

Cinemachine + Timelineでブリーフィングを作成

Call of Dutyみたいなかっこいいブリーフィング画面を作りたかったので、ここで初めてTimelineを利用してブリーフィング演出を作りました。

背景の素材は基本的にSubstance Designerで作成し、世界地図のみ会社の同僚に素材を作ってもらいました。

f:id:kaneta1011:20210627204933p:plain
ブリーフィングのタイムライン

f:id:kaneta1011:20210627204827p:plain
ブリーフィング

11日目の動画

youtu.be

12日目

この日がGWの最終日でした

以降は作業ペースが落ちることを予想できたので、あまり挑戦的なことはやらないことにして細かい調整などを行う事にしました。

ブリーフィングのBGMを追加

ブリーフィングにピッタリのかっこいいBGMがあったのでブリーフィング画面にだけ先行してBGMを追加

dova-s.jp

戦闘ステージの地面テクスチャを作成

Substance Designerのノイズを良しなに使いまくって砂漠マテリアルを作成

f:id:kaneta1011:20210627205451p:plain
Substance Designerで砂漠っぽいテクスチャを作成

今まで使っていた仮のテクスチャと比べてループ感が軽減されて、見た目の品質が一気に向上した気がする。

f:id:kaneta1011:20210627205531p:plain
戦闘エリアの床を砂漠マテリアルに差し替え

12日目の動画

youtu.be

13日目

チュートリアル開始時のカメラワーク作成

例によってCinemachineでカメラワークを作成

f:id:kaneta1011:20210627211643p:plain
チュートリアル開始時のカメラワーク

13日目の動画

youtu.be

14日目

BGMやSEを設定

BGMやSEはゲーム利用可能なフリーの素材をネットで探して利用させて頂いた。

ゲームにあったBGM・SEを探すのは意外と大変で、音の対応だけで丸一日時間がかかってしまった。

ただし音追加の効果は絶大で、敵を打ち落とすのがとても楽しくなった!

作業の1/4は無駄にテストプレイをしていた気がする。

14日目の動画

youtu.be

15日目

スコアを追加

Call of Duty Black Opsシリーズのゾンビモードのように、敵への攻撃ヒット時と敵撃破時にスコアを加算するようにしました。

スコア獲得のアニメーションも COD:BOのゾンビモードを真似ました。

f:id:kaneta1011:20210627213839p:plain
スコア

リザルト画面を作成

スコアが記録できるようになったので、ゲームクリア後にリザルト画面を表示するようにしました。

こちらの表示アニメーションも UnityのAnimationを利用して作成しました。

f:id:kaneta1011:20210627214016p:plain
リザルト画面

15日目の動画

youtu.be

16日目

ランキング作成

二フクラのMobile Backend無料枠を利用してオンラインランキングを実装しました。

送信するデータだけ変更して、こちらのランキングチュートリアルをほとんどそのまま利用してます。

mbaas.nifcloud.com

容量の関係で日本語フォントを少数しか入れることができていなかったので送信できる名前は正規表現で英数字 + いくつかの記号のみに制限しました。

private string NameValidate(string name) {
    if (name == "") {
        return "名前を入力してください";
    }
    if (name.Length > 30) {
        return "名前は30文字以内にしてください";
    }
    var isAlphaNumericAndSymbolOnly = !Regex.IsMatch(name, @"[^a-zA-z0-9-_.!?\-()\[\]@#$%&+:;{}=/*]");
    if (!isAlphaNumericAndSymbolOnly ) {
        return "英数字のみ入力してください";
    }
    return "";
}

f:id:kaneta1011:20210627214350p:plain
ランキング送信ダイアログ

16日目の動画

youtu.be

17日目

チュートリアルステージのブラッシュアップ

トンネル内のディティール不足を感じていたので橋の落下対策も兼ねて手すりを追加することにしました。

f:id:kaneta1011:20210627215351p:plain
トンネル内の手すり

17日目の動画

youtu.be

18日目

ダメージエフェクト・瀕死エフェクト・死亡演出を作成

ダメージエフェクトはポストエフェクトのみで表現することにし、グリッチとRGBノイズを利用することにしました。

f:id:kaneta1011:20210627220245p:plain
グリッチのみ
f:id:kaneta1011:20210627220331p:plain
RGBノイズのみ
f:id:kaneta1011:20210627220416p:plain
グリッチ + RGBノイズ

今回はポストエフェクトシェーダーもShaderGraphで作成しました。

f:id:kaneta1011:20210627215721p:plain
グリッチシェーダーの実装
f:id:kaneta1011:20210627215901p:plain
RGBノイズの実装

瀕死時・被ダメージ時・死亡時は、これらポストエフェクトの強度を変更して適用することで表現しています。

以下は死亡時の例ですがポストエフェクトを強めに掛けることでゲームオーバー感を出せました。

f:id:kaneta1011:20210627215531p:plain
死亡演出

ダメージの実装に合わせて回復アイテムの追加なども行いました。

18日目の動画

youtu.be

19日目

タイトル画面作成

ほんとうはTimelineを活用してかっこいいタイトル画面を作りたかったんですが、残り時間を考えると全然間に合わなそうだったのでチュートリアルステージをそのまま利用し、カメラの位置だけ調整してタイトル画面とすることにしました。

リソース使いまわしの手抜きですがそれっぽいタイトル画面になった気がしてます。

f:id:kaneta1011:20210627221704p:plain
タイトル画面

獲得したアイテムを画面に表示

獲得したアイテムが何かわからないことが多々あるので画面上にアイテム名を表示

自分が何のアイテムを獲得したか確認したい事が何度かあったので、この機能追加で遊びやすくなった。

f:id:kaneta1011:20210627222558p:plain
獲得したアイテム名を表示

撃破スコアの高い敵を追加

何人かの友人にテストプレイをしてもらったところ、自分を含めてみんな同じような点数に収束することが判明し、急遽撃破スコアが異なる敵がスポーンするように調整しました。

これによってスコアが多少バラつくようになりました、ゲームのレベル調整難しい...

f:id:kaneta1011:20210627221915p:plain
銀、赤、金の三種類の敵を追加

ダブルスコアアイテムを追加

こちらもスコア収束対策のために、追加しました。

獲得すると一定期間獲得スコアが2倍になるアイテムで、これの追加によって、プレイヤーはHP、弾薬、ダブルスコアを管理しつつ戦うことになったので、ハイスコアを狙うための難易度が丁度いい感じに上がりました。

f:id:kaneta1011:20210627225459p:plain
ダブルスコア

低フレームレートでカメラがガクガクになる問題修正

これに関してはCinemachineの設定によるものだったようで、以下のスクショの通りCinemachineFreeLookコンポーネントのSpeed設定を「Max Speed」から「Input Value Gain」に変更することでかなりマシになりました。

f:id:kaneta1011:20210627221357p:plain
Max SpeedをInput Value Gainにすることでカメラのガクガクが改善した

19日目の動画

youtu.be

20日

戦闘中、エリア外に落ちないために壁を作成

広めに戦闘エリアを取っていたので落ちることは無いと思いますが、わざと落ちようとするユーザーのためにエリア外に行けないようにするための壁を設置しました。

f:id:kaneta1011:20210627224225p:plain
エリア外の壁

壁のシェーダーもShaderGraphで実装しています。

f:id:kaneta1011:20210627224330p:plain
エリア外壁の実装

低フレームレート時に難易度が激減する問題修正

敵の弾発射の抽選をフレーム単位で行っているため、FPS低下時に抽選回数が減ってしまい全然弾を撃ってこなくなる問題があった。

以下のようにフレームレートに応じて抽選確率を変動させて解消

var baseShotRate = 0.0025f;
var lotteryCount = Time.deltaTime * 60.0f;
var shotRate = LotteryUtility.GetProbabilityFromBaseProbilityAndCount(baseShotRate, lotteryCount);
if (Random.Range(0.0f, 1.0f) < shotRate) {
    gun.Shot(((AttackTarget.position + AttackRandomOffset) - gun.transform.position).normalized);
}

public static float GetProbabilityFromBaseProbilityAndCount(float probility, float count) {
    var oneMinusProbility = 1.0f - probility;
    var allComeOffProbility = Mathf.Pow(oneMinusProbility, count);
    return 1.0f - allComeOffProbility;
}

20日目の動画

youtu.be

21日目

ダブルスコアを重複取得可能にする

その後も友人とテストプレイを行っていましたが、結局スコアが収束してしまいました。

苦肉の策でダブルスコアを重複取得できるようにしたところ、ダブルスコアのドロップ運にかなり左右されてしまいますがスコアの頭打ちを無くすことができました。

オンラインランキングもあったのでこちらの方がランキング狙い甲斐があるだろうと言う事で重複可能で行くことにしました。

細かい修正や雑務

この日は5/31でゲームジャムの最終日だったので、ずっと放置していたバグ修正などを行いました。

具体的に行った作業は以下です。

f:id:kaneta1011:20210627230345p:plain
当時のタスクリスト

結局間に合わなかったタスクもありましたが、おおむねタスク通りに作業が進みました。

21日目の動画

youtu.be

Unityの学習を兼ねて久しぶりにゲーム制作をした

f:id:kaneta1011:20210626164241p:plain

GW期間中にUnityやHoudiniやSubstance Designerの学習を兼ねてシンプルなシューティングゲーム制作を行っていました。

今回は僕の好きな人気ゲーム(地球防衛軍アーマードコアCall of Duty)に多大に影響を受けつつ作りました。

いつかオリジナルなゲームを作れるように今後も精進していきます。

GW中にゲーム制作を始めたきっかけは、Unityでゲームを作ったことがなかったので今後のためにもやってみたかったというのもあったのですが、会社でGW+5月いっぱいの期間を利用したゲームジャムが開催されていて、それに乗っかれたというのがとても大きかったです。

4/29から制作を始めて完成したのは5/31、作業した日数にすると21日間で↓のようなゲームになりました。

お手軽にプレイしてもらいたかったのでWebGLビルドをUnity Roomで公開したところ、想像以上にたくさんの方にプレイして頂きました。さらに、ゲームニュースサイトのGameSparkにもなぜか取り上げてもらいました!びっくりしたのと同時に、昔から見ていたゲームニュースサイトに自分が作ったゲームが乗っているのがとても嬉しくて昔からの友達とかに自慢してしまいました。(ミーハー) www.gamespark.jp

今回のゲーム制作を通して、ゲームの公開から反応を身近で見ることができて、ゲーム制作楽しい!と再認識することができました!

学生時代にいくつかゲームもどきのようなものは作っていましたが、ゲームとして体裁をしっかり整えたゲームを完成させたのは初めてだったので、とても良い機会になりました。

大雑把な粒度ですが、期間中の作業ログが残っているので、それを元に後日制作記録を書こうと思います。

第二回「KLab Expert Camp」のメンターを担当させて頂きました。

f:id:kaneta1011:20200915171231j:plain
かねた
先週9/8~9/12の期間中 KLab Expert Camp(KEC)というイベントの第二回目が開催されていて、それにメンターとして参加していました。

当日の様子はハッシュタグ#KLabExpertCamp」を追ってもらえれば確認できます。 twitter.com

参加者の方々にめちゃくちゃ刺激を貰えていた5日間でしたので、終了して数日たってますが未だにロスを感じています。

皆さんとMeetで会話しながら作業するのはとても楽しかったですし、僕自身も色々と学びになることが多くてよい経験となりました。

KLab Expert Camp(KEC)とは

KECは、技術的に深いテーマに取り組んでいる学生の発掘・育成を目的とした KLab の新しいインターンの取り組みです。

第一回目当時の様子や、どういった経緯で開催されたかといった内容は以下の会社のブログに詳細に書かれているのでよかったら見てください。 dsas.blog.klab.org

色々と事情があり今回の第二回はオンラインでの開催となりました。

開催の経緯

堅苦しいことを書きましたが、シェーダー関連でインターンをしたいという話が最初に会社であって、普段Twitterでシェーダを使って何かしらを制作されている皆さんの(メンター含めた) オフ会 交流の場になればいいなと思って人事部の担当者ともう一人のメンターの @gam0022さんと一緒に色々企画させてもらいました。

最初はオフライン開催でGLSLを用いた映像制作と、Unityを用いた映像制作二つの部門に分けてそれぞれのコースで講義を行いつつ3日間で作品を作ってもらい、部門ごとに投票システムで順位を決めるというデモパーティのようなものを構想していました。

ただ3日間はさすがに期間が短すぎると思っていたのでいろいろ相談した結果、5日間での開催へと変更されました。(それでも短すぎますが....)

また残念ながら延期に延期を重ねてオンラインでの開催となってしまい、運営メンバーが一人しかおらず管理の面で懸念の声があったため、コースを統合しての開催となりました。

当初の予定とは色々と異なってしまいましたが、参加者からはよい交流の機会になったといった声も聴けたので開催させていただいてよかったです。

どういったことをしたのか

事前準備

事前準備として講義資料に利用するアニメーションのシェーダーを作ったり、ライティングの資料を作るために以下の記事でも書いたように復習を兼ねて勉強したりしていました。 nanka.hateblo.jp

講義用の資料制作自体は開催の一か月ほど前から着手し始めて、自分の筆の遅さもあって開催日前日にようやく完成しました。。。

その他いろいろな事務作業であったり、郵送であったりといった大変そうな作業はすべて人事部の方にやっていただきました。ありがとうございます...

期間中

期間中は基本的な進行や管理などは人事部の方に全てお任せさせていただいたので、僕は二度の講義と参加者からの質問対応などに集中していました。

一回目の講義

一回目の講義は「距離関数プリミティブを用いたモデリングとライティング」というタイトルで、昨年の4月ごろに作成したThe cake is a lie.というフラグメントシェーダーオンリーの作品のモデリングとライティング部分をアニメーションを交えつつ解説しました。

そのシェーダーを書いた当時の記事はこちらです。この記事を書きながら見返したんですが既に懐かしいですね... nanka.hateblo.jp

二回目の講義

二回目の講義は、「今日から始める盛り盛りポストエフェクト」というタイトルで、今回のインターン延期直後に、 @lucknknock さんと @butadiene121 さんが主催された Shader1WeekCompo に投稿させてもらった、 Delayed という作品で使用したポストエフェクトの解説を行いました。

講義するネタに困っていたときに、ちょうどShader1WeekCompoが開催されたので良い機会でした。とても感謝しています。

講義の内容としては、Delayedで実装した描画パスを一つずつ順番に解説して行き、しょぼい描画結果から徐々に綺麗な画面になっていくところをお見せしていきました。

なんかもっとかっこいいタイトルにしたかったんですが、何にも思いつかなかったのでIQ低そうなタイトルになってしまいました。

感想

参加者のレベル高すぎ

まず最初に出てくる感想は、参加者のレベル高すぎという事です...

4日間というかなり短い期間でしたが、参加者全員がちゃんと作品を完成させて5日目の成果発表でしっかりと発表されていました。

中には精神と時の部屋で作業したのか?と思うほどかなり密度の濃い作品があったりして、メンターは成果発表の前に作品を見る機会があったんですが開いた口が塞がらないというのはこういう事なのかという感じでした...

正直自分では4日間で何かしらを完成させるという事ができる気がしないです。参加者の方々は無茶ぶりに答えてくださって本当にありがとうございます。

かなりニッチなインターンでしたが人が集まってよかった

シェーダー+映像制作というかなりニッチなテーマで人が集まるかどうか、実はすごく不安だったんですが、蓋を開けてみると当日は13名も参加してくださり本当に安心しました。ありがとうございます。

他にも参加予定の方々がいたのですが、イベントそのものの延期等で残念ながら都合が合わずに参加できませんでした...

参加できなかった方々にも、せめてもと思い期間中利用する予定だったネームカード(冒頭の画像のやつです)や、おやつセット等をお送りさせてもらいました。

講義で何かしら知識を持って帰ってもらえてよかった

今まで専門学校やテックイベント等で講義をした経験は何度かあったんですが、ほとんどが初心者向けの講義でしたので、今回既にシェーダーがめっちゃできる参加者の皆様に向けて、簡単すぎる内容にならないように結構プレッシャーを感じていました。

事後のアンケートなどを見ると大半の方には講義を通して何かしらの知識を吸収して頂けたようで本当に良かったです...

講義後にも参加者の方からとても沢山の質問を頂けたので、興味を持ってもらえているんだなと安心しました。

期間中制作した作品に早速今回の講義内容を実践してみた、といった声が、成果発表中に聞けたりしたのでメンターとして参加させていただいて本当にありがたいことでした。

反省点もいくつかある

今回開催させていただいて反省点もいくつかありました。

特に

  • 提出物のレギュレーションが定まっていなかった
  • 後出しで音声の後付けを許可した

といったような提出物に関わる内容は参加者を混乱させてしまっただけではなく、成果物の品質にも影響を与えてしまったと思います。

次回こういったイベントをやる際はしっかりと固めてからやりたいです。

最後に

特段偉いわけでもないのに、期間中は終始偉そうにアドバイスとかコメントさせていただきましたが、参加者が4日間という超短期間で製作された作品はどれも素晴らしいものでした。僕が同じ期間貰えてもこれだけ素晴らしい作品は作れないと思います。

今回はメンターとインターン参加者という立場でしたが、グラフィックや映像が好きな友人としてこれからも仲よくしてもらえたらうれしいです。

参加者の皆さん5日間本当にお疲れ様でした。

参加者のブログ記事リンク

既に数人のインターン参加者が参加報告をブログに書いてくださっていたので、こちらにリンクを貼っておきます。 発見次第随時追加します。

※投稿時間順 tonoshake.hateblo.jp butadiene.hatenablog.com

マイクロファセットBRDF

f:id:kaneta1011:20200818191926p:plain f:id:kaneta1011:20200818193816p:plain

Shader - Shadertoy BETA

今までいろんなShadertoyの作品で使ってきてましたが、正直理解が浅すぎたので改めていろいろな記事を参考に再実装しました。

用語の整理等もでき始めたのでそこそこ理解が進んだ気がします。

特にこのシリーズはとても参考にさせていただきました。

qiita.com

qiita.com

BRDF,Irradiance,Radianceの定義shikihuiku.wordpress.com

2019年雑に振り返り

今年も大晦日になりました。

今まで一年を振り返るというのをやったことが無かったのですが(振り返れるものが無かったともいう)今年はいろいろあったので、RIZINを見ながら軽く振り返ります。

2018年まで

2018年は就職して3年目でした。

基本的には仕事以外でプログラムを書くようなことは無かった上に、この時は、スマートフォンゲームにハマっており、twitterは課金の報告用になっていました。 f:id:kaneta1011:20191231183413p:plain

しかし、2月頃から以前から興味があったTokyo Demo Fest 2018に向けてメガデモを製作するための技術を勉強し始めました。

これを起点に情報発信や作品の制作を行うようになって、2019年に繋がったと思います。

2019年

シェーダー芸の投稿

f:id:kaneta1011:20191231193325p:plain

2018年の12月から今年の9月までコンスタントに月一個以上はShadertoyやNEORTにシェーダー芸を投稿しました。

本当は12月までの一年間続けたかったんですが、仕事やその他のイベントなどで作る時間を捻出できなくなってしまいできませんでした。

ただ、三日坊主で有名な僕が10か月以上も継続できたことが自分でも驚きなので、ポジティブにとらえていきたいです。

また、NEORTに投稿していた作品の一つが渋谷駅に展示されました。

継続的に投稿を行っていないとこのような素晴らしい機会を得ることは絶対にできなかったと思います。

登壇等

2件やりました。

6月に「UnityエンジニアによるShader勉強会!登壇」でなぜかライブコーディングに関する発表を行い、終了後の懇親会では20分のライブコーディングの実演を行い場を盛り上げることができたと思います。

11月にWebGL総本山のdoxas氏が主宰する「GLSLスクール2019」でレイマーチングによるシェーダーアートの制作例とテクニックについて1時間の講義を行いました。

Twitterを今のように活用する前からこのスクールを知っていたので、呼んでいただいて本当に光栄でした。

記事の投稿

Unityやシェーダーに関する記事をいくつか書きました。

いろんな人に見てもらいたいものはQiita、雑なメモや個人的なものはブログに書くように使い分けていました。

いずれもたくさんの方に見て頂けて嬉しかったです。

これから

今年は自分の情報を外に出すようにしたからか、新しい出会いがたくさんありました。

また、仕事でもいい方向にいろいろと環境が変わった年でもありました。

来年からもマイペースに作りたいものを作って、やりたいことを勉強する年にしていきたいです。

2019年9月のシェーダーを紹介する

f:id:kaneta1011:20190929202951p:plain
サムネ用

はじめに

毎月やっている月毎に制作したシェーダーの紹介第8回目です。

今月はShadertoyで一つシェーダーを作ったので簡単な解説をします。

Energy Lab

インスピレーション

9月頭あたりにこのシェーダーを見て、金色のケージっぽいオブジェクトかっこいいなと思ったので今回登場させました。

www.shadertoy.com f:id:kaneta1011:20190929204742p:plain

また以前からパイプをエネルギーが伝っているようなシーンを作ってみたかったので合わせて今回のシェーダーになりました。

形状

f:id:kaneta1011:20190929224827p:plain

ケージの形状はトーラスをpmodで二重に複製しています。

vec2 cage(vec3 p) {
    p.y += sin(time) * 0.1;
    p.xy *= rot(-pi*0.5);
    p.yz *= rot(time * 0.5);
    p.yz = pmod(p.yz, 7.0);
    p.yx = pmod(p.yx, 7.0);
    return vec2(torus(p, 0.025, 0.55), MAT_CAGE);
}

ステージのモデリングには例に漏れず、Mercuryのhg_sdfを活用させていただいています。 mercury.sexy

特にパイプは fOpPipe を使用して、元のステージとboxを組み合わせて這うパイプを表現しています。

f:id:kaneta1011:20190929231717p:plain
fOpPipe前

f:id:kaneta1011:20190929231748p:plain
fOpPipe後

ライティング

f:id:kaneta1011:20190929202951p:plain

n kbデモを意識してコンパクトでそれなりの見た目になる物を用意して使っています。

Diffuseに正規化Lambert、Specularに正規化BlinPhongを使用したもので、反射率はフレネルを考慮した方が良い結果になりますが、マテリアル毎に一定としています。

vec3 light(vec3 p, vec3 n, vec3 v, vec3 lp, vec3 baseColor, float roughness, float reflectance, float metallic, vec3 radiance) {
    vec3 ref = mix(vec3(reflectance), baseColor, metallic);

    vec3 l = lp - p;
    float len = length(l);
    l /= len;
    
    vec3 h = normalize(l + v);
    
    vec3 diffuse = mix(1.0 - ref, vec3(0.0), metallic) * baseColor / pi;
    
    float m = roughnessToExponent(roughness);
    vec3 specular = ref * pow( max( 0.0, dot( n, h ) ), m ) * ( m + 9.0 ) / ( 9.0 * pi );
    return (diffuse + specular) * radiance * max(0.0, dot(l, n)) / (len*len);
}

今まで、複数回のリフレクションに対応するシェーダーを書いたことが無かったので、今回はそれに対応しました。

シェーダー内では再帰関数を利用できないので、依存関係にならないように処理を小さい粒度で関数化して、レンダリングの後方でトレースをループすることで実現しました。

vec3 shade(vec3 p, vec3 ray, vec2 mat) {
    vec3 baseColor, emission;
    float roughness, metallic, reflectance;
    
    getSurfaceParams(p, mat, baseColor, emission, roughness, reflectance, metallic);
    vec3 n = normal(p);
    
    vec3 result = evalLights(p, n, ray, baseColor, roughness, reflectance, metallic) + emission;
    vec3 f0 = vec3(1.0);
    for(int i=0; i<1; i++) {
        f0 *= mix(vec3(reflectance), baseColor, metallic);
        vec3 secondPos;
        vec2 secondMat;
        float depth;
        ray = reflect(ray, n);
        trace(p + n * 0.001, ray, 100.0, 24, secondPos, secondMat, depth);
        getSurfaceParams(p, secondMat, baseColor, emission, roughness, reflectance, metallic);
        n = normal(secondPos);
        p = secondPos;
        result += (evalLights(secondPos, n, ray, baseColor, roughness, reflectance, metallic) + emission) * f0;
    }
    
    return result;
}

ポストエフェクト

過去に紹介したものばかりなので箇条書きで書きます

さいごに

来月はGLSLスクールがあるので頑張って資料作ります!

10月は何を作るかまだ未定ですが、もし何か作ったら紹介します。

2019年8月の成果物を紹介する

f:id:kaneta1011:20190831142630p:plain
サムネ用

はじめに

毎月やっている月毎に制作したシェーダーの紹介第7回目です。

今月はShadertoyでシェーダーを書きませんでしたが、最近触り始めたUnityのさらにHDRPで色々やっていたので紹介します!

新幹線でライブコーディング

会社の出張で大阪に向かった時にライブコーディングをやりました。

以前Shadertoyでトンネルを二つ組み合わせた面白い表現を見たので、ボリュームライトと組み合わせてライブコーディングでやってみました。

Unity HDRP

8月上旬から中旬はずっとこれをやっていました。

凄い人が作ったであろうHDRPを実際に触って改造してみることで色々な知見を得られるのではないかという作戦で進めました。

実際に今まで知らなかった手法やテクニックがたくさん使われており大変勉強になっております!

HDRPのキャッチアップ

まず手始めにHDRPを改造できる環境を整えました。

以下にその時のメモが残っています。

nanka.hateblo.jp

また改造の練習にHDRPにレンダリングパスを追加してアウトラインを描画するということもやりました。

nanka.hateblo.jp

レイマーチング

HDRPでレイマーチングをするというのが今月の目標でした。

HDRPの改造方法も大体わかってきたのでこの辺りから詩作を始めました。

レンダリングパスを追加できるようにしておく等入念に準備していましたが、意外とHDRP側の修正は少なく済みました。

この辺りからはHDRPの素敵なライティングでスクショを取るのが楽しすぎて、作業よりもスクショを取ってる時間の方が長かったです。

そして完成したものがこちらです。

この時に得た知見等はこちらの記事で解説しています。(まだ途中だけど...)

nanka.hateblo.jp

さいごに

今月はやったことをほとんど別記事で書いてしまったので内容が薄くなってしまいました...

もうしばらくUnity + HDRPを触っていろいろやりたいなと考えています。

また、10月に@h_doxas氏主催のGLSLスクールでプラスワン講義を担当することになったので、来月はそちらの資料作りに時間を取りたいとも考えています!

webgl.souhonzan.org

来月ももし何か作ったら紹介します。