imog

主にUnityとかの事を書いています

Unity LoggingでSinkを自作する

概要

2/29日にUnityプログラミング・バイブル R6号が出ます。 この本でUnity Loggingの使い方について執筆させていただきました。ぜひみんな買ってね。

Unityプログラミング・バイブル R6号

この本ではUnity Loggingについて執筆していましたが、ページ数の都合上いくつか削除した項目が存在します。 今回はその中で、自作Sinkの作り方についてちょっと記載していきます。

docs.unity3d.com

自分でSinkを作りたいというときに参考になれば幸いです。

ちなみにBurstの説明はそんなにないです。

Sinkを自作する

Sinkは用意された出力先だけでも強力ですが自作することもできます。ただし、自作する場合は以下の知識が必要です。

  • unsafeコードの理解
  • Burstの理解

本章では上記の詳細な解説は省略します。

Logの個数をカウントするSinkを自作してみる

今回は、簡単な例として「Logの個数をカウントするSink」を実装してみましょう。最終的な呼び出しは以下のようになります。

using Unity.Logging;
using Unity.Logging.Sinks;
using UnityEngine;

namespace LoggingSample.Sinks
{
    public class CustomSinkSample : MonoBehaviour
    {
        private void Start()
        {
            var config = new LoggerConfig()
                .SyncMode.FullSync()           // 非同期だとすぐに出力されないので同期
                .WriteTo.UnityEditorConsole()  // 動作確認のためにUnityEditorConsoleにも出力
                .WriteTo.CountLogger();
            Log.Logger = new Unity.Logging.Logger(config);
            Log.Info("foo");
            Log.Info("bar");
            Log.Info(CountSinkSystem.LogCounter); // => 2
        }
    }
}

SinkSystemBaseとSinkConfiguration

自作Sinkを作る場合、以下の3つの実装が必要です。

  • SinkConfigurationを継承したクラスの実装
  • SinkSystemBaseを継承したクラスの実装
  • 呼び出すための拡張メソッドの実装

Sinkは実際にログを受け取ったときの処理を行うSinkSystemBaseと、SinkSystemBaseの設定及びインスタンスの生成を行うSinkConfigurationの2つで構成されています。今回はCountSinkSystemという名前で実装してみましょう。

まずはConfigurationクラスの作成です。

using Unity.Collections;
using Unity.Logging;
using Unity.Logging.Sinks;

namespace LoggingSample.Sinks
{
    public class Configure : SinkConfiguration
    {
        public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null)
            : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride)
        {
        }

        public override SinkSystemBase CreateSinkInstance(Logger logger)
        {
            // CountSinkSystemのインスタンスを作成する
            return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this);
        }
    }
}

ConfigureでやるべきことはSinkSystemインスタンスの生成です。これは CreateSinkInstanceをオーバーライドする必要があります。インスタンスの生成は直接行うのではなく、baseに実装されている CreateAndInitializeSinkInstance<T>を呼び出します。今回はここに自作SinkであるCountSinkSystemを指定します。

次にCountSinkSystem本体を実装します。コンパイルが通る最低限の状態にすると以下のとおりです。

using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Logging;
using Unity.Logging.Sinks;

namespace LoggingSample.Sinks
{
    // SinkSystemBaseを継承する場合BurstCompile対応が必須になる
    [BurstCompile]
    public class CountSinkSystem : SinkSystemBase
    {
        public static int LogCounter;

        public class Configure : SinkConfiguration
        {
            public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null)
                : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride)
            {
            }

            public override SinkSystemBase CreateSinkInstance(Logger logger)
            {
                // CountSinkSystemのインスタンスを作成する
                return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this);
            }
        }

        public override LogController.SinkStruct ToSinkStruct()
        {
            var s = base.ToSinkStruct();
            // ログ取得時のデリゲートを設定する
            s.OnLogMessageEmit = new OnLogMessageEmitDelegate(OnLogMessageEmitFunc);
            return s;
        }

        public override void Initialize(Logger logger, SinkConfiguration systemConfig)
        {
            base.Initialize(logger, systemConfig);
        }

        /// <summary>
        /// ログメッセージが発行されたときに呼ばれるコールバック
        /// </summary>
        [BurstCompile]
        [AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))]
        internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator)
        {
        }
    }
}

先程定義したConfigureCountSinkSystemのインナークラスにしています。これは、File SinkやJson Sinkなどデフォルトで用意されているSinkの実装方法に合わせているためです。

SinkSystemの実装にあたりやらなければならないことは以下の3つです。

  • クラスをBurstCompileに対応させる
  • ToSinkStructでログ取得のメッセージを受け取れるようにする
  • ログメッセージを受け取った際のデリゲートを実装する

大前提としてSinkはBurstCompilerによる最適化が行われているため、自作する場合もBurstCompile対応が必要です。そのためCountSinkSystemに[BurstCompile]属性を付与しています。

ToSinkStruct()はLogger側で扱うSinkに関する構造体SinkStructを返すメソッドです。ここで生成する構造体にはログを流すためのデリゲートOnLogMessageEmitが用意されており、ここに任意のメソッドを渡すことで実際のSink側の処理を実現できます。今回だと「CountSinkSystem.LogCounterを加算する」というのが詳細なロジックになります。

OnLogMessageEmitFuncはログ情報を受け取って実際の処理を行うデリゲートです。

[BurstCompile]
[AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))]
internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator)
{
}

ここで加算処理を行えると良いのですが、BurstCompileを行う場合readonlyではないstatic変数にアクセスができないためLogCounterはこのメソッド内では直接更新ができません。

つまり、Burstの世界だけだとできることが限られてしまうので、このメソッドから先はManaged C#の世界と繋いで処理を委譲させることになります。Unity Loggingパッケージは内部で Burst2ManagedCallというクラスを定義しており、BurstとManaged C#をFunctionPointerで繋いでいます。このクラスはinternalで外部からアクセスできないので、実装を参考にして定義します。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Unity.Burst;

namespace LoggingSample.Sinks
{
    /// <summary>
    /// BurstからManagedコードを呼び出すためのデリゲートを管理するクラス
    /// </summary>
    internal static class Burst2ManagedCall<T, Key>
    {
        private static T _delegate;
        private static readonly SharedStatic<FunctionPointer<T>> _sharedStatic = SharedStatic<FunctionPointer<T>>.GetOrCreate<FunctionPointer<T>, Key>(16);
        public static bool IsCreated => _sharedStatic.Data.IsCreated;

        public static void Init(T @delegate)
        {
            CheckIsNotCreated();
            _delegate = @delegate;
            _sharedStatic.Data = new FunctionPointer<T>(Marshal.GetFunctionPointerForDelegate(_delegate));
        }

        public static ref FunctionPointer<T> Ptr()
        {
            CheckIsCreated();
            return ref _sharedStatic.Data;
        }

        [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")]
        private static void CheckIsCreated()
        {
            if (IsCreated == false)
                throw new InvalidOperationException("Burst2ManagedCall was NOT created!");
        }

        [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")]
        private static void CheckIsNotCreated()
        {
            if (IsCreated)
                throw new InvalidOperationException("Burst2ManagedCall was already created!");
        }
    }
}

次に、カウントを加算するだけのクラス CountLogWrapperを定義します。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Logging;
using Unity.Logging.Sinks;

namespace LoggingSample.Sinks
{
    /// <summary>
    /// ログが出力された回数をカウントするクラス
    /// </summary>
    internal static class CountLogWrapper
    {
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate void WriteDelegate();

        private struct CountLogWrapperKey
        {
        }

        private static bool _initialized;

        internal static void Initialize()
        {
            if (_initialized) return;
            _initialized = true;
            // デリゲートを登録する
            Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Init(WriteFunc);
        }

        [AOT.MonoPInvokeCallback(typeof(WriteDelegate))]
        private static void WriteFunc()
        {
            // カウント処理
            CountSinkSystem.LogCounter++;
        }

        public static void Write()
        {
            var ptr = Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Ptr();
#if LOGGING_USE_UNMANAGED_DELEGATES
            ((delegate * unmanaged[Cdecl] <LogLevel, byte*, int, void>)ptr.Value)(level, data, length);
#else
            ptr.Invoke();
#endif
        }
    }
}

WriteFuncが実際の加算処理となっており、これをBurst2ManagedCallに登録してWriteメソッド内でFunctionPointerとして取得、及び呼び出すことでBurst上でも呼び出せるようにしています。

あとは外部からCountLogWrapper.Initializeを呼び出して登録をしなければならないので、CountSinkSystem側で登録します。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Logging;
using Unity.Logging.Sinks;

namespace LoggingSample.Sinks
{
    // SinkSystemBaseを継承する場合BurstCompile対応が必須になる
    [BurstCompile]
    public class CountSinkSystem : SinkSystemBase
    {
        public static int LogCounter;

        public class Configure : SinkConfiguration
        {
            public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null)
                : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride)
            {
            }

            public override SinkSystemBase CreateSinkInstance(Logger logger)
            {
                // CountSinkSystemのインスタンスを作成する
                return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this);
            }
        }

        public override LogController.SinkStruct ToSinkStruct()
        {
            var s = base.ToSinkStruct();
            // ログ取得時のデリゲートを設定する
            s.OnLogMessageEmit = new OnLogMessageEmitDelegate(OnLogMessageEmitFunc);
            return s;
        }

        public override void Initialize(Logger logger, SinkConfiguration systemConfig)
        {
            // インスタンスの初期化時にログのカウント回数も初期化する
            CountLogWrapper.Initialize();
            base.Initialize(logger, systemConfig);
        }

        /// <summary>
        /// ログメッセージが発行されたときに呼ばれるコールバック
        /// </summary>
        [BurstCompile]
        [AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))]
        internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator)
        {
            try
            {
                // BurstCompileに対応させる場合readonlyではないstatic変数にアクセスできないため、
                // CountLogWrapperを経由してカウントする
                CountLogWrapper.Write();
            }
            finally
            {
                messageBuffer.Length = 0;
            }
        }
    }
}

OnLogMessageEmitFuncではWriteを呼び出すだけですが、これでログが流れるたびにカウントされるようになりました。

あとは、このSinkを有効にできるような拡張メソッドCountLoggerを定義します。

using Unity.Logging;
using Unity.Logging.Sinks;

namespace LoggingSample.Sinks
{
    // WriteTo.CountLogger()のように呼び出せるようにするための拡張メソッド
    public static class CountLoggerExtensions
    {
        public static LoggerConfig CountLogger(this LoggerWriterConfig writeTo, LogLevel? minLevel = null)
        {
            return writeTo.AddSinkConfig(new CountSinkSystem.Configure(writeTo, default, false, minLevel));
        }
    }
}

AddSinkConfigはSink設定を追加するメソッドです。ここでConfigureを生成することで新たにSinkを追加できます。 ちなみにSinkの拡張は、対応するExtensionsクラスを作って拡張メソッドを追加していくのが慣例となっています。

最終的なコードは以下の通りです。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Logging;
using Unity.Logging.Sinks;


namespace LoggingSample.Sinks
{
    public static class CountLoggerExtensions
    {
        // WriteTo.CountLogger()のように呼び出せるようにするための拡張メソッド
        public static LoggerConfig CountLogger(this LoggerWriterConfig writeTo, LogLevel? minLevel = null)
        {
            return writeTo.AddSinkConfig(new CountSinkSystem.Configure(writeTo, default, false, minLevel));
        }
    }

    // SinkSystemBaseを継承する場合BurstCompile対応が必須になる
    [BurstCompile]
    public class CountSinkSystem : SinkSystemBase
    {
        public static int LogCounter;

        public class Configure : SinkConfiguration
        {
            public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null)
                : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride)
            {
            }

            public override SinkSystemBase CreateSinkInstance(Logger logger)
            {
                // CountSinkSystemのインスタンスを作成する
                return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this);
            }
        }

        public override LogController.SinkStruct ToSinkStruct()
        {
            var s = base.ToSinkStruct();
            // ログ取得時のデリゲートを設定する
            s.OnLogMessageEmit = new OnLogMessageEmitDelegate(OnLogMessageEmitFunc);
            return s;
        }

        public override void Initialize(Logger logger, SinkConfiguration systemConfig)
        {
            // インスタンスの初期化時にログのカウント回数も初期化する
            CountLogWrapper.Initialize();
            base.Initialize(logger, systemConfig);
        }

        /// <summary>
        /// ログメッセージが発行されたときに呼ばれるコールバック
        /// </summary>
        [BurstCompile]
        [AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))]
        internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator)
        {
            try
            {
                // BurstCompileに対応させる場合readonlyではないstatic変数にアクセスできないため、
                // CountLogWrapperを経由してカウントする
                CountLogWrapper.Write();
            }
            finally
            {
                messageBuffer.Length = 0;
            }
        }
    }

    /// <summary>
    /// ログが出力された回数をカウントするクラス
    /// </summary>
    internal static class CountLogWrapper
    {
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        internal delegate void WriteDelegate();

        private struct CountLogWrapperKey
        {
        }

        private static bool _initialized;

        internal static void Initialize()
        {
            if (_initialized) return;
            _initialized = true;
            // デリゲートを登録する
            Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Init(WriteFunc);
        }

        [AOT.MonoPInvokeCallback(typeof(WriteDelegate))]
        private static void WriteFunc()
        {
            // カウント処理
            CountSinkSystem.LogCounter++;
        }

        public static void Write()
        {
            var ptr = Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Ptr();
#if LOGGING_USE_UNMANAGED_DELEGATES
            ((delegate * unmanaged[Cdecl] <LogLevel, byte*, int, void>)ptr.Value)(level, data, length);
#else
            ptr.Invoke();
#endif
        }
    }

    /// <summary>
    /// BurstからManagedコードを呼び出すためのデリゲートを管理するクラス
    /// </summary>
    internal static class Burst2ManagedCall<T, Key>
    {
        private static T _delegate;
        private static readonly SharedStatic<FunctionPointer<T>> _sharedStatic = SharedStatic<FunctionPointer<T>>.GetOrCreate<FunctionPointer<T>, Key>(16);
        public static bool IsCreated => _sharedStatic.Data.IsCreated;

        public static void Init(T @delegate)
        {
            CheckIsNotCreated();
            _delegate = @delegate;
            _sharedStatic.Data = new FunctionPointer<T>(Marshal.GetFunctionPointerForDelegate(_delegate));
        }

        public static ref FunctionPointer<T> Ptr()
        {
            CheckIsCreated();
            return ref _sharedStatic.Data;
        }

        [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")]
        private static void CheckIsCreated()
        {
            if (IsCreated == false)
                throw new InvalidOperationException("Burst2ManagedCall was NOT created!");
        }

        [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")]
        private static void CheckIsNotCreated()
        {
            if (IsCreated)
                throw new InvalidOperationException("Burst2ManagedCall was already created!");
        }
    }
}

2023年ふりかえり

今年もお疲れ様でした。実家で酒を飲みながら書いています。

 

 転職

1月から正式にミラティブに入社しました。Unityエンジニアとしてゴリゴリゲーム作らせていただいております

 

note.com

 

ベンチャーに入社するのは初めてだったので温度感の違いとかは色々ありましたが楽しめております。

個人的にはスピード感がとても好きで、最小限のコストで如何に価値を出すかみたいな取捨選択をしながら開発してます。その結果、1年間で2本ゲームをリリースできたのでかなり楽しかったです。スマホゲーム界隈だと数年に一本リリースできるとよいという速度だったのでいい変化かなぁお思ったりしてます。

 

個人的な話ですが、今の開発はユーザーとの距離が近いのが最高だと思ってます。元々ゲーム出したらエゴサする性格だったので、リリースした直後にユーザーの配信を見に行ってリアルタイムで楽しいつまらんなど遊んでる声を聞けるというのは制作者冥利に尽きます。大体リリース直後からずっと見てます。次はお前の配信だ。

もちろんとりあえず既存に倣って実装すればいいという楽な開発ではないんですが、それはそれで新しいことを試せる機会ではあるので好き放題やらせていただいております。Photon Fusionでリアルタイム通信やったりとかデバッガツール刷新したりとか。付いてきてくれているチームに感謝。

 

引越し

会社がフルリモートなので2月から東京→福岡に引っ越しました。

元々福岡出身なので6年ぶりのUターンというのが正しい。

 

家賃下がって広い部屋に住めたおかげで文化的な生活を遅れております。今までほとんどやらなかった自炊も始めており、ペペロンチーノも作れるようになりました。

やはり住むには最高の土地。いいぞ福岡。福岡はみんなを待ってるぞ。

 

ついでに博多でボイトレ再開しました。Queen歌ってます。

 

個人開発

引き続き開発お手伝いしております。

store.steampowered.com

 

仕事が忙しくてそんなにやれなかったので申し訳無さ。来年頑張ります。

 

あと趣味で東方キャラの首から下を当てるゲーム作りました。

 

 

GodotEngineに慣れるぞという目的兼みんな首の下も知ってくれという情熱でやり切った。

 

あと色々やってるんですがそのへんは来年話せそう。

今年はUnityのRuntimeFee関連もあってGodotが話題になったのが印象的でした。元々触ってたけどキチッとリリースまでやった作品がなかったので今年は一歩進んだなぁという感じ。来年はもうちょっと小さいやつ出していきたい。

 

所感

今年は技術研鑽!というよりは何を作るか?何で作るか?みたいなところに頭を悩まされた一年でした。特定技術のプロフェッショナルです!と言える自信はないが、こういう考えでこれを作りました!くらいまでは自信持って言えるようになったかもしれない。

 

まあ実現するには技術が必要なので両輪揃えていきたいですなあ。

今年もお疲れ様でした。

 

 

GodotEngineで東方Projectキャラの首から下を当てるゲームを作った話

先月こんなゲームを作りました。

godotplayer.com

神子の彼女枠競争率高そう

首から上が表示されるので、下半身を4択から選んで正しいキャラを作るゲームです。失敗してもなんらかのキャラは生成されます。だいたいキメラです。

一ヶ月経ってしまったけど、せっかく作ったので裏側の話を諸々書いておきます。

きっかけ

元々GodotEngineで作成したゲームをアップロードできるGodotPlayerというサイトがありまして、そこでGodot1Weekという一週間でゲームを作るイベントが始まったので、普段はUnityをメインに使っているけどGodotEngineの勉強がてらなんか作るかなーと考えていました。

godotplayer.com

手元に東方キャラの素材があったのでそれを使ってプロトタイプの開発をDiscordで配信していたら、東方にそこまで詳しくない友人が霊夢魔理沙の二人を見て「ゆっくり組だ」と言いました。

東方にそこまで詳しくない友人が霊夢魔理沙の二人を見て「ゆっくり組だ」と言いました。

東方にそこまで詳しくない友人が霊夢魔理沙の二人を見て「ゆっくり組だ」と言いました。

その瞬間に首から下も叩き込んだろうという気持ちが沸き上がって2日で作りました。

ゲーム内容の話

ただの組み合わせゲームだけだと正解を当てるゲームになっちゃうので、せっかくならキメラを作れると面白いなあと思い、名前やスペカも分割することにしました。以下の5つの名称が首から上と首から下で分かれており、合成されます。

  • 名前
  • 二つ名
  • 能力名
  • スペルカード
  • BGM

これをやるためにはそもそも全キャラの上記データを用意する必要があるんですが、とてつもなく多い。気づいたらwin版と書籍合わせて132体もいるので打ち込むだけでもだいぶ大変です。

自分がやるとしても開発の時間をごっそり持っていかれてしまうので、信頼できる友人にデータの打ち込みをお願いしました。全作品遊んでて概ね把握している人だったので勝手に進めてくれたし、スペカはこっちのほうが切りやすいとか考えていい感じのものを選んでくれました。圧倒的感謝。

その結果生まれたのが以下のマスターデータです。今後なにかに使えるかもしれない。

docs.google.com

ちなみに立ち絵はdairiさんのものをお借りしています。

www.pixiv.net

開発当時は宮出口瑞霊の立ち絵がなかったので出さない予定だったんですが、なんと開発終盤で立ち絵が上がってきたので緊急で追加しました。これは運命です。

ちなみに旧作キャラも追加できたんですが、能力も二つ名もスペカもほぼないので組み合わせたときに上手いことキメラが誕生しなくなるので今回は見送りました。機会があればまた。

結果的には正解率を競うよりはおもしろキメラを作る楽しみのほうが勝ったけど、楽しいのでヨシ!

ちなみにタイトル画面のシルエットは実は毎回変わります。

開発の話

GodotEngineはちょこちょこ触りつつもちゃんとリリースまで出したものがないので、これを機にちゃんとサイクルが回ってるやつを作りました。

マスターデータ管理

マスターデータは上記のスプレッドシートからcsvに変換して、プロジェクト内に取り込んでいます。

csvの管理は godot-csv-data-importerを使いました。

github.com

これなら loadした時点でシンプルな配列になっているので便利。

ネタ管理

企画の内容とか実装のメモとか、文章を書くところをGodot内に集約しようと思い、godot-idea-boardを入れてみました。

github.com

雑なメモ

本当にメモ置き場とか図を書くためだけに使ったけど、GodotEngingeからアプリケーションを切り替えることなく書けるのは結構おもしろ体験でした。notionに書いてもいいんだけど、コンテキストスイッチが0になった感じがする。

首の分割

首から上と下がどのように管理されているのかというと、2つのシーンファイルが生成されています。

  • head_xxx.tscn
  • body_xxx.tscn

また、合体時には首がキレイに繋がってほしいのでheadとbodyはそれぞれ原点位置がおなじになるように調整しています。 1つのキャラクターを分割する手順は以下のとおりです。

  1. 手動で原点位置の調整
  2. 首のシーンを作成し、原点とUVを設定する
  3. 胴体のシーンを作成し、原点とUVを設定する

こんな手順を132個やらないといけないわけですが、これを愚直に全キャラ行っていると時間がかかりすぎて心が折れます。とはいえ画像ごとに首の位置はぜんぜん違うので、単純にUVの位置を基準に一律切り出してしまうと肩に顔が生えたり顔が2つになったりと見た目として大変よろしくありません。

ということで、この辺の処理を楽にできるようにプラグインを書きました。

首を切る

1枚絵のoffsetを設定して「首を分割する」ボタンを押すとhead_xxxとbody_xxxの2つのシーンファイルを生成するプラグインです。それぞれoffsetを基準に画像のUVと原点位置が調整されています。ちなみに名前は headhunting.gdです。

これのおかげで首の位置を調整するだけで済んだのでかなり時間短縮できました。ちなみにそれでも全キャラ30分はかかりました。

GodotEngineのエディタ拡張は初めてですが、エディタ用のコードみたいなのを覚える必要gなくGodotEngineのゲームコードと同じクラスが使えるので敷居はかなり低く覚えやすいです。例えばボタンひとつ置くにしてもエディタ用ではなくControlのButtonを生成すればいいだけです。

おわりに

やはりサクッと書くにはGodotEngineは使いやすくて楽しいなということを再度理解したのでもうちょっと扱えるようになっていきたい。

あとみんな遊んで欲しい。首から上しか知らない人もこれを機に首から下のことを知ってほしい。

上手に食べれてなくてかわいい

AndroidでWireguardのVPN接続ができない時の対処法

事象

Pixel7(Android 13)でWireguardを使ってVPN接続を行なっているんだけど、社内に接続できない事象に遭遇した。 ログを見てもらったところ普通に自分のグローバルIPでアクセスしており、WireguardがONになっているがトンネル通ってないよということがわかった。

端末側でも、オプションから「VPN以外の接続のブロック」にチェックを入れたところ全てのページが見れなくなったので、これはトンネル通ってないなと確信した。

対応

とりあえず一回アンインストールしてみるかーということで削除したが、特に状況は変わらなかった。 なので次はインストールのプロファイルを変えるようにした。

現状端末は個人プロファイルと仕事用プロファイルに分かれており、Wireguardは個人プロファイル、社内にアクセスするアプリは仕事用プロファイルに所属していた。

https://support.google.com/work/android/answer/6191949?hl=ja

仕事用でしかVPN使わないのだからWireguardも仕事用プロファイルにインストールしたら、無事にVPN接続できるようになった。 結論としては、VPNはプロファイルごとに適用されるので、実際に接続しにいくアプリと同じプロファイルにまとめましょうという感じでした。

余談

それはそれとしてAndroid13で動かないという不具合報告は結構あるっぽい

https://www.reddit.com/r/GooglePixel/comments/wpcy0n/psa_dont_upgrade_to_android_13_if_you

Bakinのバトルスクリプトのプロジェクトを開くとエラーが発生するときの対応

2023/03/11 追記

この記事の現象は1.2.0.2で解消されました。 store.steampowered.com

1分で終わるけど多分毎回忘れるので備忘録。

結論

本体の lib 以下にdllが揃ってるのでそれを使いましょう

Bakin 1.2でバトルスクリプトの拡張ができるようになりました。

store.steampowered.com

拡張機能」から「バトル関連ソースをゲームファイルにコピーする」を選択すると、csprojファイルとサンプルコード一式が出力されます。

ただ、そのままだとDLLが足りないので怒涛のコンパイルエラーが発生します。

Projects以下を見ると、Common SharpKmyCore Yukar Engine が参照できていないことがわかります。

これらを全部参照してあげたらとりあえずコンパイルエラーは解消できます。

DLLは全部BAKIN本体のプロジェクト以下にあるのでそれを使いましょう。

  • bakinengine.dll
  • common.dll
  • SharpKmyCore.dll

まだアーリーアクセスなので、細かいところは踏み抜いていきましょう

2022年ふりかえり

今年はあんまり表立った活動はしていないなあというアレ。

ゲーム開発

touhou-gamejam.web.app

毎年参加してるゲームジャムで、「Five Elements of wisdom」というパズルゲームを作った。

www.youtube.com

5種類の弾幕の特性を組み合わせて妖精を全滅させるゲームで、いかに最低限の手数で全滅させられるかを競うゲーム。理論上はすべて1手で倒すことができる(頭は使うけど) このパズル部分のシステムをメインで担当しました。

チームメンバーがかなりグローバルで、日本語・英語・ロシア語が飛び交う中で僕は翻訳にかけながらコミュニケーション取ってました。ちゃんと英語は読み書きできたほうがいいぞという自戒。

もう一個は、幻想戦略録というゲームのお手伝いをしております。がんばりますうへぇ。

store.steampowered.com

OSSとか

今年はVContainerとUniTaskのコードを見る機会に恵まれたなという感じでした。

github.com

github.com

だいたい業務や趣味で使ってるときに不具合っぽいのを見つけて対応->PRみたいな流れをやってました。そんなに数をこなしたわけではないのでゴリゴリやってましたとはいえないけど、少なくとも何かあったときにコード読んでここじゃないか?と当たりをつけるくらいはできるようになったのでそれはよかったかもしれない。 両ライブラリにはめちゃめちゃお世話になっております。

転職とか

ご縁があってMIXIを退職してミラティブに転職しました。業務的には引き続きUnityを使った開発がメインです。新卒から数えて3社目ですが、同じ会社というものは殆どなくてそれぞれ違う空気感、スピード感を出してます。 まだ入ったばかりなのでアウトプットはないですが、来年から頑張っていきたいところ。

また、ほぼフルリモートになるので東京を離れて故郷の福岡にお引越し活動中です。

ゲーム

記憶に残ってるやつだけ・・

アルセウスに始まりSVに終わるというポケモンな1年だったかもしれない。 普段スマホゲームはそこまで続かないんだけど、ダダサバイバーだけはかなりハマりました。このゲームがなんで面白いんだろうみたいなのを同僚と結構話してました。ただ、ヴァンパイアサバイバー系は増えまくって食傷気味ではある・・

ちなみに2022年前期はマスターデュエルに時間を根こそぎ奪われていったので封印しました。

今年はこれいいよ!と言われた本をその場で購入するムーブを繰り返してました。

所感

2022年は面白いゲームって何なんだろうなみたいな気持ちになることが多く、その反動もあっていろんなゲームを遊ぶことが増えました。なんで繰り返し遊びたくなるんだろうとか、なぜこの演出が好きなんだろかとか、なんで苦行をしてまでマラソンをするのかみたいな、ハマっている理由を自分の中で言語化する機会が増えた気がします。わりかしレトロゲー好きの自分にとってはエルデンリングをプレイしたことだけでも大きな一歩かもしれない。ゲームを作るプログラマーである以上、現代のゲームのシステムで会話できないのは良くないなと思ったのでした。

とは言え、本業のリリースでいっぱいいっぱいでかつゲームやってたので、正直外部活動に目を向けられてなかったなあというのはあった。それは来年の目標ということで・・

今年もお疲れ様でした。

Zennでアドカレ書いた

今年のアドカレです。

zenn.dev

初めてZennを使ってみたけど、技術記事書くならはてなよりだいぶ楽だな・・と思ってしまった。

とはいえあっちは雑記を書く感じじゃないの使い分けになるんだろうか。それはそれでめんどう