【C#】木構造の中のフィールドに親やルートの参照を注入する

XMLのデシリアライズ等で得られた木構造を解析する際、あるノードから親やルートを見たくなることありませんか…?

例えばこういう木構造があったとして、 あるNodeが別のNodeへの参照を持ってますが直シリアライズではなくidを持ってる形とか考えられます。

class Node{
    public string id;
    public string[] dependencies;
}
class Root{
    public Node[] nodes;
}

Nodeにこういうメソッドかプロパティを生やしたいですね

public IEnumerable<Node> Dependencies => dependencies.Select(dep => root.nodes.FirstOrDefault(n => n.id == dep));

ここで突然お出ししたrootというのがルートノードです。 こういうルートとかの参照を雑に注入するヘルパーを用意しました。

できたもの

↓コード
木構造にルートや親の参照を注入する。デシリアライズしたオブジェクトとかに使う

属性としてInjectRootInjectParentをつけた上で

class Node{
    public string id;
    public string[] dependencies;
    [InjectRoot]
    public Root root;
}

拡張メソッドのInjectRootやらInjectParentを呼ぶと諸々注入して返します.

var result = serializer.Deserialize(xmlText).InjectRoot().InjectParent();`

ルートでInjectParent指定したり,注入先の型が違う場合は例外出るのでご注意!

【Unity】PlayerLoopのタイミングをシリアライズしてInspectorから指定

PlayerLoop便利です。昔はイベント拾うためだけのMonoBehaviourを生成したりしてましたが、もう要りません。 tsubakit1.hateblo.jp

UniTaskでもめちゃ使ってるようです。多分YieldやDelayの終了通知だと思う
neue cc - C#のasync/await再考, タイムアウト処理のベストプラクティス, UniTask v2.2.0

これ、コードからだけじゃなくInspectorからも指定したくないですか…?

できたもの

コード

PlayerLoop挿入くん。シリアライズしてEditorからタイミング指定できる。

使い方

SerializablePlayerLoopをどっかにシリアライズして持っておくと…

[SerializeField]
SerializablePlayerLoop updateMethod = new SerializablePlayerLoop();

こんな感じでドロップダウンから選べるようになります。

スクリプトでは下記のようにイベントをaddしておけば指定したタイミングで呼んでくれます。

updateMethod.OnPlayerLoop += UpdateMethod;

やってること

上記で貼ったコードですが、3つのクラスがあります

PlayerLoopInjector

PlayerLoopを差し込んだり、差し込んだのを外したりします。 タイミングの指定にはUnityEngine.PlayerLoop以下の型を指定します。 例えば↓とか
PlayerLoop.Update - Unity スクリプトリファレンス

PlayerLoopは一度挿入するとドメインリロードまで刺さりっぱなしになるのでPlayMode終了時に初期状態に戻す処理を入れてます。

SerializablePlayerLoop

シリアライズ対象とする型です。 この中のtimingフィールドがSerializeReferenceになってますが、この属性を持ってるとUnityが型情報もシリアライズしてくれます。 その型情報を使って上記のPlayerLoopInjectorに処理を差し込むようになってます。 型情報は見てますが中の値は使ってないです。

SerializablePlayerLoopDrawer

SerializablePlayerLoopをInspectorで表示する際の処理を書いてます。 具体的には、タイミング指定に使える型を収集してきてドロップダウンとして表示します。 UnityEditor.EditorGUI.Popupはオプション文字列に/を入れとくとグルーピングしてくれて便利です。

「タイミング指定に使える型」の候補は PlayerLoop.GetDefaultPlayerLoop() で取ってきてます。

おしまい

そういうわけで処理の実行タイミングをInspectorから指定する仕組みができました。 もともとは次のリポジトリ弾幕の更新タイミングをプロジェクト設定として指定するために用意したものです。 UpdateにしたかったりFixedUpdateにしたかったりプロジェクトごとに違うので。

github.com

自作ゲーム内でずっと修正を重ねながら使ってるものですが、この弾幕ももうちょっと整備したいですね…

【Unity】クソ雑に使えるオブジェクトプール

Unityでエフェクトやらを出すときってオブジェクトをプーリングすると思います。 Unity向けのオブジェクトプールの実装はいろいろありますが、雑に拾って雑に返せるものを用意して使ってます。

コード

クソ雑に使えるオブジェクトプール

つかいかた

プレハブを指定してインスタンスを取得します。 まだプールになければ中で実際にInstantiateされます。

var instance = GameObjectPool.Instantiate(original, position, rotation);

返すときはそのインスタンスを指定するだけです。

GameObjectPool.Destroy(instance);

プール自体の管理はいまのところ全削除だけ用意してます。 これはシーンのアンロード時にも勝手に呼ばれます。

GameObjectPool.ClearAll();

クソ雑ポイント

その1 : あらゆるGameObjectを突っ込める

既存のオブジェクトプール実装としてUniRxのObjectPool等がありますが、 これは対象のGameObjectが特定のコンポーネントを持っていることを前提としています。

専用のコンポーネントを持たない単純なエフェクトをプーリングしたい場合もあるのでそういう制約はナシにしました。

その2 : プールは世界に1つだけ

本当はその1つの中で複数持ってるんですが…使う側でそれを意識する必要ないよねという考えです。 また引き合いに出すと、UniRxのObjectPoolは管理したいオブジェクトの種類ごとにプールのインスタンスを持ってそれを制御する必要があります。

今回の実装では全てstaticなメソッドで制御する形にしてます。 プレハブを指定してインスタンスを取得。返すときはどのプールに返すべきかを知っている必要はありません。

例えば、こういうコンポーネントをエフェクトにくっつけておくと…

using UnityEngine;
using Cysharp.Threading.Tasks;
public class ReturnPoolInTime : MonoBehaviour
{
    [SerializeField]
    int ms;
    private async void Awake()
    {
        await UniTask.Delay(ms);
        GameObjectPool.Destroy(gameObject);
    }
}

指定したtimeだけ経過した後にプールに返す処理をエフェクトの種類によらず使い回すことができます。 この場合、生成する側はGameObjectPool.InstantiateしてほっとけばOKです。

おしまい

実は、これはプーリングなしで作っちゃってたゲームに後から導入するために作ったものです。 エフェクトのInstantiateやDestroyの前にGameObjectPool.だけくっつけとけばなんとなく動いてくれます。

同じくらいクソ雑にやりたければどうぞ。 正直、ちゃんと目的毎に別のプール作って管理できるならそのほうがいいかも…

【Unity】.blendのインポート挙動を上書きするスクリプト

Unityは.blendのインポートに対応しています。 その挙動がUnityの仕様変更によって大破壊されてます。 キレちゃった。

何が問題なのか

.blendをインポートすると従来は各Actionを別々のAnimationとしてインポートできてました。

好きな数だけAnimationClipを作ってそのソースActionをドロップダウンから選べる

ところが、ある時から.blendの保存時に選択していたAction1つだけしかインポートしてくれなくなりました。

ドロップダウンがない!

Animationの数だけFBXのエクスポートをしなきゃいけないんですか…?

なぜこんなことに…

下記のissueに対する修正で挙動が変わっちゃいました。これをパッチリリースで出せる心粋。

issuetracker.unity3d.com

皆さんお困りのようでフォーラムやらissurtrackerにも情報がありました。

Blender 2.8 animations not importing in Beta 2019.3.0b4 - Unity Forum Unity Issue Tracker - Using multiple animation clips in Blender not all Animation clips are imported using a .blend file

Won't Fix じゃあないんだよ

どうやってなおすの

この不具合は、上記フォーラムにある通りUnity-BlenderToFBX.pyの一部を書き換えると回避できます。 こいつらがFalseのとこをTrueにするとよい。

bake_anim_use_nla_strips=True
bake_anim_use_all_actions=True

これをすべての開発環境で、全てのUnityバージョンで修正するの面倒だよね…? ということでタイトルの「.blendのインポート挙動を上書きするスクリプト」を用意したわけです。

.blend のインポート時のFBXエクスポート処理を上書き太郎

適当なEditorフォルダに突っ込んであげてください。その環境のUnity-BlenderToFBX.pyを書き換えます。 書き換える内容はハードコードされちゃってますが必要に応じていじってください。

「同じ環境の別プロジェクトではデフォルトのインポート挙動にしたい」みたいな場合は#if falseのとこをtrueにすればEditor終了時に戻してくれるはず。

おしまい

.blendのインポート…全部の環境にBlenderが入ってる必要があるので、ある程度の規模になるととても使えない機能だろうとは思います。 しかし個人で作る分には便利で使いまくってるので、もっと気の利いたメンテをしてほしかったです。

【Unity】数式からメッシュアセット生成くん

エフェクト作るときに単純なメッシュ形状を作りたくなることあると思います。 例えば円錐台とか…

こういうシンプルなメッシュ達のためにfbxやその元ファイルを管理するのが面倒…というお気持ちがあります。 そこで、今回はC#の式を評価して簡単なメッシュを生成するインポータを作りました。

↓の仲間みたいなやつですね

friendsea.hateblo.jp


Quaternion.Euler(0,-u*360, 0)*(Vector3.forward*(1 + v*0.5f) + Vector3.up*v)

こんな式を…

こういうメッシュとしてインポートします。

できたもの

コード

C#の式からメッシュ作るくん

つかいかた

上記のスクリプトは適当なEditorフォルダに入れてください。 あと、Code Analysisパッケージが必要なのでパッケージマネージャから入れてください。

Create Asset MenuからSuface Mesh Generatorを作ると…

なんかうねうねのメッシュとしてインポートされます

デフォルトのファイルの中身をそういう数式にしてるからです。 アセットのファイルはmeshgeneratorという拡張子のテキストです。 この中身にいい感じのC#式を書いて曲面形状を定義してください。

Inspectorのインポート設定では曲面→メッシュ化の分割数を指定できます。

記述する式について

書く式は次のようなものです

  • Vector3として評価される
  • ふたつのfloat変数uvを使える
  • UnityEngine名前空間がusingされてる

つまり…

using UnityEngine;
Vector3 Position(float u, float v){
    return (ここに入る中身を書くってことです);
}

uvは[0, 1]の曲面座標系です。 例えばファイルの中身がnew Vector3(u,v,0)の場合ただの平面ができますね。

もうちょっと凝った例だと、こういうタツマキ系のエフェクトのメッシュとかもできます。

Quaternion.Euler(0,-u*360*1, 0)*Vector3.forward*(v/2f+1+u-Mathf.Abs(0.5f-v)) + Vector3.up*(v+u*3)

おしまい

これも前回同様Unity上でしかメッシュとして取り扱えない上に、現状式の記述に何もエディタ補助が効かなくて辛いです。 個人開発だとめちゃ重宝してるんだけどね…

【Unity】ShaderGraph焼き込みくん

ShaderGraph便利ですよね。プロシージャルテクスチャみたいなこともできちゃいます。

「みたいな」と言ってるのはこれがテクスチャではなくGPUで生成されているからです。 ある程度複雑な処理だとテクスチャにしてサンプリングしたほうがマシな場合もあるでしょう。 また、テクスチャだけ差し替えて共通のエフェクトシェーダを使うことも多いかと思います。 ということでシェーダーの描画結果からTexture2Dを作る拡張を用意しました。

出来たもの

コード

Shader焼き込んでTexture2D作るくん

つかいかた

上記のcsスクリプトは適当なEditorフォルダに入れときます。 CreateAssetMenuに"ShaderBaker"くんがいると思うので作ってあげましょう。

生成したアセットはRGBA=0000のまっさらなテクスチャになってます。シェーダーがないからですね。 シェーダーを指定して適当な解像度やその他設定をいじってApplyするといい感じのテクスチャになります。

これはpngのインポートなどと同じTexture2Dのアセットになってるので同じ用途で使い放題です。やったね。

使用例

こんなかんじのShaderGraphを焼き込んで…

こういうテクスチャができる。

NovaShaderと一緒に使って斬撃が出来た!

似たようなアイデア

まず、ノードベースでプロシージャルテクスチャを作るEditor拡張は存在します。 めちゃくちゃよく出来てます。

https://github.com/alelievr/Mixture

というかShaderGraphと同等の機能やEditorの再実装に近いですね… あと、ノードベースになっているのにアセット内にバイト列が保存されてます。 そういうのはバージョン管理に入ってこなくていいです。

もう一つのアイデアとしてあるのが、今回もやっているシェーダーの焼き込みですが、これも既にあります。

https://www.ronja-tutorials.com/post/030-baking-shaders/

ただし、これは必要なタイミングでテクスチャの再生成を行って適当な場所に出力する必要があります。 本当はシェーダー内容を更新した時点で勝手に再インポートされてほしいので今回は別の実装を作りました。

実装

コード見てもらえばそれだけですが、ScriptedImporterを使ってTexture2Dを生成してます。 ScriptedImporterは独自形式ファイル向けのImporterを作るための機能ですが、 今回は独自拡張子".shaderbaker"だけついてる「無」をインポートして、.metaに刻まれてるインスペクタ設定のみ読んで生成してます。

アセットの生成自体は指定されたシェーダでRenderTextureにBlitして、それをコピーしたTexture2Dを作る実装です。

便利ノード

シェーダ向けだと無いけど、テクスチャ生成になるとちょっと欲しい…みたいなノードがいくつかある。 用意しとくと後で楽できるかもです。

おしまい

絵心がないのであらゆるテクスチャを似たような手段で生成してきました。 Unity内で完結させるのは楽ですが、Unity内でしかテクスチャとして使えないデメリットがあります。 キャラモデル向けテクスチャなどは素直に外で作ってインポートしたほうが良いです。

ゲームのデータが毎日更新されるようにした話.GitHub Actionsでdotnet-scriptを走らせる話.

このまえ変なゲームを作りました. friendsea.hateblo.jp

ゲームジャム版は9/25で一旦頒布終了なのですが,10/15からの博麗電脳試遊会向けに「集計データを毎日更新するバージョン」を作りました. この「集計データを毎日更新する」っていうのををGitHub Actions上で走るようになってます.

方法

ゲームルールとしてはpixivのレスポンス内容を参照してるだけなので各クライアントが直に取りに行くこともできますが,pixiv側要因でうまく取れなくなった場合に開発者(ぼく)が何もできなくなっちゃいます. そこで,必要な情報だけまとめたCSVを用意してそれを見に行くようにしてます.

このCSVにpixivから取ってきた情報を書き込む処理を毎日走らせればよい. こうすればpixiv記事の内容が変わってうまくパースできなくなった場合もゲームクライアントはそのままでCSV更新処理のみで対応可能です.

締め切り時点で未完

博麗電脳試遊会の締切は10/2だったのですが,その時点ではツイートの通り「起動時に取得」のとこしかできてません. 当日夜まで何もしてなかったのですが,情報ソースを内部からCSVに変更する対応だけやって提出してます. 締切後になんとかして「CSVを定期更新する仕組み」ができれば良しと考えてますがめちゃくちゃ見切り発車ですね.

このため,定期更新の仕組みの都合でどこにCSVをホストするか未知の状態でした.そこで,ゲームに取得先を直に持たせないようにしてます. まずは設定ファイル(これはjson)を取りに行って,そこに書いてあるURIからCSVを取得するようにしてます.CSV更新の仕組みができたらそれに合わせて設定ファイルを一度だけ手動更新して完成という魂胆ですね.

GitHub Actionsとdotnet script

定期更新の集計スクリプトGitHub Actionsで走らせてます.今回はじめて触ったけど雑に何でもできて便利. 集計スクリプトdotnet-scriptで書いてたので「コレを動かせる環境あるのか...?」というのが心配でしたが下記のようにすれば動きます.

    steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 6.0.x
    - name: Install detnet-script
      run: dotnet tool install -g dotnet-script
    - name: UpdateData
      run: dotnet script CollectData.csx tags.csv.tags

actions/setup-dotnetGitHubの公式アクションでdotnet環境を用意してくれます流石にdotnet script関連のアクションはなかったですが, 普通に環境作る場合と同様にdotnet tool install -g dotnet-scriptを叩いてやればよいだけでした. その後CollectData.csxで集計してます.

おしまい

集計処理はかなり非効率な書き方してて10分ほどかかるのですが,GitHub(Microsoft?)が富豪なので無課金で月2000分使っていいそうです. このゲームのデータ更新に毎日は過剰だろとも思うけどタダなのでいくらやってもいいですね. GitHub Actions便利なのでまた使う機会あるかも.