猫茶の研究日誌

ゲーム開発などの技術や、そのほか趣味などの雑記。

【Unity/InputSystem】新・操作中のコントローラーの種類を識別する

はじめに

Unityを使ったPCゲームで、上動画のように、

  • PS5コントローラなら「×で決定」
  • Xboxコントローラなら「Aで決定」
  • SwitchのProコンなら「Bで決定」
  • マウス&キーボードなら「左クリックで決定」

と、自動で表示を切り替えたいとき、ありますよね?
(余談ですが、私が学生時代で制作したゲームたちには、ほぼ100%この手の仕組みを入れてました。こだわり。)

そこで、「今使っている入力デバイスを識別できるようにしよう!」という内容の記事です。

本記事は、以前に書いた記事のリメイクです。
2年近く前に書いた「Unityでコントローラーの種別を識別する(InputSystem)」の内容から、いろいろ追加・書き直しをした内容です。
qiita.com 対応デバイスの追加、実装のアプローチの変更、デバイス変更時のイベント処理など、変更点盛り沢山。

動作確認環境

  • Unity 2022.3.18f1
  • Windows 11 22H2

サンプルソース

とりあえず早速、サンプルソース貼ります。

InputDeviceManager

今回の記事の内容のすべてのソースです。

シングルトンクラスとして実装してみました。
シングルトンなので、シーンに空のゲームオブジェクトを1つ作って本コンポーネントをセットしておく必要があります。

using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;

public class InputDeviceManager : MonoBehaviour
{
    // シングルトン
    public static InputDeviceManager Instance { get; private set; }

    /// <summary>
    /// 入力デバイスの種別
    /// </summary>
    public enum InputDeviceType
    {
        Keyboard,   // キーボード・マウス
        Xbox,       // Xboxコントローラー
        DualShock4, // DualShock4(PS4)
        DualSense,  // DualSense(PS5)
        Switch,     // SwitchのProコントローラー
    }

    // 直近に操作された入力デバイスタイプ
    public InputDeviceType CurrentDeviceType { get; private set; } = InputDeviceType.Keyboard;

    // 各デバイスのすべてのキーを1つにバインドしたInputAction(キー種別検知用)
    private InputAction keyboardAnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<Keyboard>/AnyKey", interactions: "Press");
    private InputAction mouseAnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<Mouse>/*", interactions: "Press");
    private InputAction xInputAnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<XInputController>/*", interactions: "Press");
    private InputAction dualShock4AnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<DualShockGamepad>/*", interactions: "Press");
    private InputAction detectDualSenseAnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<DualSenseGamepadHID>/*", interactions: "Press");
    private InputAction switchProControllerAnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<SwitchProControllerHID>/*", interactions: "Press");

    // 入力デバイスタイプ変更イベント
    public UnityEvent OnChangeDeviceType { get; private set; } = new();

    private void Awake()
    {
        // シングルトン
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }

        // キー検知用アクションの有効化
        keyboardAnyKey.Enable();
        mouseAnyKey.Enable();
        xInputAnyKey.Enable();
        dualShock4AnyKey.Enable();
        detectDualSenseAnyKey.Enable();
        switchProControllerAnyKey.Enable();
    }

    
    private void Start()
    {
        // 初回のみ、必ず入力デバイスの種別検知を行ってコールバック発火
        StartCoroutine(InitializeDetection());
    }

    private void Update()
    {
        // 検知の更新処理
        UpdateDeviceTypesDetection();
    }

    /// <summary>
    /// 入力デバイスの種別検知を初期化する
    /// </summary>
    /// <returns></returns>
    IEnumerator InitializeDetection()
    {
        // 入力デバイスの種別検知を更新
        UpdateDeviceTypesDetection();
        // 1フレーム待機
        yield return null;
        // イベント強制発火
        OnChangeDeviceType.Invoke();
    }

    
    /// <summary>
    /// 入力デバイスの種別検知を更新する
    /// </summary>
    public void UpdateDeviceTypesDetection()
    {
        var beforeDeviceType = CurrentDeviceType;

        if (xInputAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.Xbox;
        }

        // DualSense(PS5)は、DualShock4(PS4)としても認識される。
        // つまり、DualSenseを操作しているときは、DualSchock4とDualSenseの両方が検知される。
        // DualSenseとDualShockの両方から同時に入力検知した場合は、DualSenseとして扱うようにする。
        if (dualShock4AnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.DualShock4;
        }
        if (detectDualSenseAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.DualSense;
        }

        if (switchProControllerAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.Switch;
        }

        if (keyboardAnyKey.triggered || mouseAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.Keyboard;
        }

        // 操作デバイスが切り替わったとき、イベント発火
        if (beforeDeviceType != CurrentDeviceType)
        {
            OnChangeDeviceType.Invoke();
        }
    }
}

使い方説明のサンプルソース

下画像のように、操作中のデバイスが変わったときに、そのデバイスの種別をコンソールに出力するサンプルです。

using UnityEngine;

public class InputDeviceDetectionExample : MonoBehaviour
{
    private void Start()
    {
        // イベントハンドラの登録
        InputDeviceManager.Instance.OnChangeDeviceType.AddListener(OnChangeDeviceTypeHandler);
    }

    private void OnDestroy()
    {
        // イベントハンドラの解除
        InputDeviceManager.Instance.OnChangeDeviceType.RemoveListener(OnChangeDeviceTypeHandler);
    }

    private void OnChangeDeviceTypeHandler()
    {
        // 入力デバイスの種別が変更されたときの処理
        Debug.Log("入力デバイスの種別が変更されました。\n現在の入力デバイスの種別:" + InputDeviceManager.Instance.CurrentDeviceType);
    }
}

解説

ざっくりとだけ解説しておきます。

「いま使われているか?」は、直前のフレームでいずれかのボタンが押されたかで判定します。
このために、「毎フレーム、それぞれのデバイスについて、すべてのボタンの状態を確認する」 ということをやっていきます。

"いずれかのキー"が入力されたかの監視

すべてのキーを1つの入力と捉えるInputActionを作成

バイス種ごとに、すべてのキーを1つの入力と捉えるInputActionを作成します。
これにより、各デバイス種ごとに「いずれかのキーが押されたか?」を確認できるようになります。

InputActionのコンストラクタでbinding"<デバイス名>/*"を指定することで、そのデバイスのすべての入力を対象にしたアクションを作成できます。

...
    // 各デバイスのすべてのキーを1つにバインドしたInputAction(キー種別検知用)
    private InputAction mouseAnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<Mouse>/*", interactions: "Press");
...

キーボードには「AnyKey」がもとからあるのでそれを使います。

   private InputAction keyboardAnyKey = new InputAction(type: InputActionType.PassThrough, binding: "<Keyboard>/AnyKey", interactions: "Press");
入力デバイス名の調べ方
InputActionのコンストラクタで指定するデバイス名は、以下の手順で調べることができます。
下動画と併せてご参考ください…👀
  1. なんでもいいのでInputActionの設定を開く。
  2. 任意のBindで、確認したい対象のデバイスのボタンを登録する。
    (下動画ではListenボタンを使って登録してます)
  3. 「T」ボタンを押して、登録されたPathを確認する。
    (冒頭の「<>」で囲まれた部分がデバイス名です。「<DualSenseGamepadHID>/dpad/down」なら、コンストラクタに登録するPathは「<DualSenseGamepadHID>/*」です。)

作成したInputActionの有効化

Awake関数内で、各InputActionを有効化しておくことをお忘れなく。

        // キー検知用アクションの有効化
        keyboardAnyKey.Enable();
        mouseAnyKey.Enable();
        xInputAnyKey.Enable();
        dualShock4AnyKey.Enable();
        detectDualSenseAnyKey.Enable();
        switchProControllerAnyKey.Enable();

入力されたか判定

あとは、各デバイスについて、毎フレームtriggeredを確認するだけです。
trueになっていれば、何かしら操作がされたということです。

if (xInputAnyKey.triggered)
{
    // XInputデバイスに、"いずれかのキー"の入力があった!
}

毎フレーム操作されたデバイスを確認する

さきほどの方法で、毎フレームすべての入力デバイスについて確認していくだけです。

...
        if (xInputAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.Xbox;
        }

        if (switchProControllerAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.Switch;
        }
...
        if (keyboardAnyKey.triggered || mouseAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.Keyboard;
        }
...

DualSenseコントローラーについて

…が、DualSense(PS5コントローラー)について、注意点があります。

DualSenseは、Unity上ではDualShock4(PS4コントローラー)としても認識されます

つまり、DualSenseで〇ボタンを押したら、同時にDualShock4でも〇ボタンが押されたと認識されます。
そのため、必ずDualShock4の判定を終えてから、DualSenseの判定を行う必要があります。 (DualSenseが入力中は、DualShock4が入力されていても無視する)

...
        // DualSense(PS5)は、DualShock4(PS4)としても認識される。
        // つまり、DualSenseを操作しているときは、DualSchock4とDualSenseの両方が検知される。
        // DualSenseとDualShockの両方から同時に入力検知した場合は、DualSenseとして扱うようにする。
        if (dualShock4AnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.DualShock4;
        }
        if (detectDualSenseAnyKey.triggered)
        {
            CurrentDeviceType = InputDeviceType.DualSense;
        }
...

バイスが切り替わった瞬間の通知

毎フレームの各デバイスについての入力されたか判定の際に、最後のフレームから変わったかを確認しておきます。

この際、変わったときのイベント等を用意しておくと便利です

 // 入力デバイスタイプ変更イベント
public UnityEvent OnChangeDeviceType { get; private set; } = new();
...
        var beforeDeviceType = CurrentDeviceType;
...(各デバイスについての入力されたか判定)
        // 操作デバイスが切り替わったとき、イベント発火
        if (beforeDeviceType != CurrentDeviceType)
        {
            OnChangeDeviceType.Invoke();
        }
...

初期化処理的なもの

初回フレームで、「操作中のデバイスの確認」、「強制的に変更時イベントを発火」をしておく必要があります。

これをしておかないと、デバイスの切り替え検知が正しく動かないことがあるので…

   private void Start()
    {
        // 初回のみ、必ず入力デバイスの種別検知を行ってコールバック発火
        StartCoroutine(InitializeDetection());
    }

    /// <summary>
    /// 入力デバイスの種別検知を初期化する
    /// </summary>
    /// <returns></returns>
    IEnumerator InitializeDetection()
    {
        // 入力デバイスの種別検知を更新
        UpdateDeviceTypesDetection();
        // 1フレーム待機
        yield return null;
        // イベント強制発火
        OnChangeDeviceType.Invoke();
    }

使い方

シングルトンクラスなので、シーンに空のゲームオブジェクト作ってInputDeviceManagerコンポーネント付けてあげれば、シーンのどこからでも使える。

いま使っているデバイス種を知りたければ、以下で取ってこれます。

InputDeviceManager.Instance.CurrentDeviceType

変更した際のイベントを登録しておけば、キーガイドのアイコンを切り替えたりするときに便利。

    private void Start()
    {
        // イベントハンドラの登録
        InputDeviceManager.Instance.OnChangeDeviceType.AddListener(OnChangeDeviceTypeHandler);
    }

おわりに

以前の記事では、すべてのキーに対してfor文で認識してましたが、今回はすべての入力をまとめてバインドしたInputActionを作成するというアプローチに変えてみました。

マウス移動やアナログパッドのような入力も取ってこれる上に、今後新たなコントローラに対応したくなった際にも、デバイス名を確認すれば理論上いくらでも可能だったりと、メリットの多い方式だと思います。
ちょっと実装長くなっちゃうけど。

とりあえず、いつか書き直したい記事ナンバーワンを書き直せて満足です。
(こんどは実際にキーアイコンを切り替えるところの解説書きたいなぁ…書いてる時間あるかなぁ…)

…あと、本記事冒頭に出てきたドタバタ対戦ゲーム「Prank Heart」は絶賛販売中!
特にフレンドと遊ぶと盛り上がると評判なので、ぜひ遊んでみてね。(宣伝)

store.steampowered.com

【Unity】ProBuilderの挙動が何故かおかしい

はじめに・症状

まずはこのgifを見てください。

ProBuilderでエッジループを追加(Insert Edge Loop)しようとしただけなのに…

怒涛のエラー

NullReferenceException: Object reference not set to an instance of an object
UnityEngine.ProBuilder.ProBuilderMesh.get_edgeCount ()
GUI Error: You are pushing more GUIClips than you are popping. Make sure they are balanced.
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

面や辺が欠ける…

仕舞いには、Undo/Redoもめっちゃぶっ壊れてます。

エッジループに限らず、とにかくProBuilderの様子がおかしい。
とても使えるとは言えない。

試したこと

以下の2つでは、問題なく動作することを確認しました。

  • 違うPCで問題発生しているプロジェクトを開く
  • 新規プロジェクトを作成して試してみる

ということは、プロジェクトのLibraryフォルダ以下全削除で直るのでは…?と思い試したましたが、全く直らず。

なんやこれ。

解決法

Localization Scene ControlsのTrack Changesのチェックを外す。
それだけです。

考察

考察ってほどのものでもないですが…

Localization Scene ControlsのTrack Changesにチェックが入っている状態で選択したゲームオブジェクトに変更があると、
自動で「GameObjectLocalizer」コンポーネントが勝手に付きます。

docs.unity3d.com

明らかにこいつが狂わせています。
(というか当たり前だけどこれのせいでエディタ全体の挙動がおかしかった)

これでとてつもなく長い時間が吸われました。
ProBuilderの不調の原因が、Localizationで不要な設定をしたことであるなんて想像しなかったから…
英語で検索掛けても、該当する情報が見当たらず、本当に絶望してました…

Localization Scene ControlsのTrack Changesには間違えてチェックを入れないように、以後気を付けます。
以上。

学生が自作ゲームエンジンで就活を終えたので振り返る

目次

はじめに

以前こんな記事を書きました。

nekocha.hatenablog.com

今回はその続編です。

あれ以来、某社に内定して就活を終えることができました。
しばらく経ったので、振り返りってみる記事です。

良かったこと、後悔してること、いろいろ書きます。 前回の記事と重複する部分もちょっとあります。

本記事のコンセプト

本題に入る前に… この手の話、可燃性が高いので説明しておきます。

この記事は、「学生向けの」、前例としての記録記事です。


「もし今後、"ゲームエンジン作りたいです!"と、自作ゲームエンジンをメインに携えて就活をするレアな人が出てきたときに、前例として少しでも参考になれば…」
という思いで書いています。
(もちろん、書ける範囲のみになります…)

私自身、学内で前例が(多分)無くて手探りでやってたので…



「学生向けの記録」です。
燃料になることは望んでいません…💦
よろしくお願いします。


軽く自己紹介

ゲーム系専門学校に通う4年生です。

小学生のころから、プログラミングとかはよく知らないけど、技術が好きだった人です。
特にゲームの技術に興味があって、それを学ぶために専門学校に入学ました。
高校は普通科で、ゲーム開発は専門入ってから学んでます。

専門時代ではゲームも作ってきましたが、2年生になったあたりから自作ゲームエンジンを作り始めました。

ゲームエンジン開発を通して、ゲーム開発をより良くしたいなぁという思いがあります。

だいたいそんな感じです。以上。

就活準備

資格

一応触れておきます。

1年生の7月に、2つの資格を取得しています。

というのも、入学当初(2020年)はコロナ禍入ったばかりでした。
入学したものの、授業開始は5月中旬、しかもオンライン授業。
社会的にも今後が分からなくて不安だったので、とりあえず一気にまとめて取っちゃいました。

  • C言語プログラミング能力認定 2級
  • ITパスポート

基本情報も受けようと勉強はしてましたが、あまり意味なさそうと判断して結局受けてません。

これらの資格が本当に必要だったかはさておき、基本情報レベルまで勉強した知識は間違いなく役立っています。

自己分析

前述の自己紹介のように、ゲームエンジン開発等でゲーム開発をより良くしたい」 というやりたいことがハッキリとしていました。

自分の中での考え方としても、他人に自分の考えを伝える際にも、これを軸にしました。

あとは、面接やESでは、長所や短所などをエピソードと紐づけて喋っていく必要があるので、自己分析しながらWord(Googleドキュメント)にまとめていきました。
これをもとに、ESを書いたり、面接対策をしていました。

下図は、実際の自己分析ノートの一部のスクショです。
先にアピールになりそうなエピソードを列挙して、それを分類するといった流れで作成しました。

企業調べ

やりたいことが若干特殊ということもあり、それなりに時間掛けました。
2年生の秋ごろから考え始めてました。

というのももちろん重要ですが、

  • もし自分がここに入ったら、どのように働いて、成長し、貢献するだろうか?(キャリアプラン

という点も着目して考えながら動きました。

こう考えていると、大手で既にすごい内製エンジンを持っているような企業も当然魅力的ではありますが、
少数精鋭ですごく頑張っている、今現在新たに内製エンジン開発を開発しているなどのケースもぜひ受けたいと考えるようになりました。

だいたいそんな考え方もとに、次に応募する企業を考えていきました。

インターン

結果的に、インターンは参加しませんでした
というよりも、「参加できなかった」のです… (ほかにもインターン申し込みたいところが複数社あったのですがどうしても日程や色んな都合が合わず、断念…)

というのも、1社だけ申し込みはしましたが、書類選考落ちでした。
正直、ちょっと凹みました。

ちなみに、内定先です。
インターン落ちても本選考でなんとかなりました。

話を戻しますと…
このときの反省点は、提出したポートフォリオ資料の分かりづらさでした。
(実際落ちたのがポートフォリオが原因かどうかは不明ですが)

システム方面は特に、アピールしないと何も伝わらない分野です。
なので、効果的にアピールしないと意味がないことを改めて思い知りました。

そして、インターンではありませんが、座談会等のようなイベントにも参加してました。
とても刺激を受けましたし、
特に初対面のプロのエンジニアと話す経験は、面接での不安の低減にもつながりました。

適性検査

鬼門でした。

私は専門学生です。
英語や数学、物理は、普段の開発でも使いこそはしますが、
一般教養的に体系的に学んだり試験を受けたりというのは、長らくやってません。
(私の場合、高校卒業からすこし期間あけて専門に入学してるので、もう5年以上とか?)

なので、丸々1か月かけて対策をしました。(開発や面接対策と並行ですが)

SPI・CAB/GAB

正直すごく苦手でした…特にCAB/GAB。
図形の規則性のやつが全く分からん。苦手すぎる。

とはいえ、導入してる企業が多いので頑張って対策しました。

対策はシンプルで、とにかく模擬問題を解きまくりました。
まずは慣れようと。
マイナビの全国一斉Web模擬テストを受けてみたり、 模擬試験受けれるサイトを使ったりしてました。

こことか。

career-world.net

しかし、うまくいった感触は最後までありませんでした…

ところでこの試験どこまでまともに評価されてるの?

コーディング試験

これもちゃんとした対策が必要でした。

ゲームエンジンを内製している企業は、
多分、採用プロセスにコーディング試験を取り入れていることが多い…ように見えます。知らんけど。どのくらい重視されてるかも知らんけど。

というわけで、ここも対策しました。

たとえばこの辺やってました。

  • Paizaラーニングのレベルアップ問題集で、標準入出力をおさらいする。
    クラウド上でテストが走って判定される出題方式についても、これで練習できる。
  • 算術ライブラリに頼らない数学計算を勉強、実装する練習をしておく
  • 学校の先生や、(卒業年度の違いで)先に就活をしていた同級生から、業界の出題傾向を聞く

だいたいこれらでギリギリ何とかなりましたが、
正直、気楽に練習できる場所が少ないなぁ という印象でした。
Paizaスキルチェックにせよ、AtCoderにせよ、練習用に解いてみるにしてはお手軽感が足りない……

ついでに作ったコーディング試験等練習用ソフトの宣伝

というわけで少しだけ宣伝です。
最近、オリジナルの模擬問題を作成して解くことができるソフトを開発しました

今後の誰かのために。

コーディング試験や競技プログラミングなどの練習に使うことを想定しています。

周囲の人や先輩、先生などに作問してもらった問題をシェアしてもらって、
気楽に練習してもらえたらな、という思いで開発しました。

実際に、学内向けに私や先生などが作問した問題を解く勉強会もやりました。
(問題に身内ネタとかも入れつつ、楽しく学べる感じでやりました)

GitHubで公開してるので、もしよろしければご活用ください!!

github.com

宣伝は以上!

就活で見せる作品とポートフォリオ

前述のインターン書類選考落ちの経験から、ちゃんと効果的にアピールしないと意味が無いと思ったので、

  • 効果的にアピールできる作品作り
  • 効果的にアピールできるポートフォリオサイト作成

を頑張りました。

「使えるものは全部使う(アピールにする)」という精神でやってました。
少し貪欲すぎるくらいの姿勢で挑んでました。

(私が緒事情で高校卒業から専門入学までの間に空白の期間がある、ということをそれなりに気にしていたという理由もあります)

以前作っていたエンジン

2年生のときに開発していたエンジンです。

はじめて開発したゲームエンジンです。
ゲーム開発用にフレームワークやエディタを作っていたら楽しくなってきて、
先生からの後押しもあり、エンジン開発にシフトしたときのものです。

このときは、最低限の機能は備えつつ、自分のやりたいことを探っていました。

www.youtube.com

エンジン

一番アピールする作品としてました。
3年生の7,8月くらいから開発始めました。
なので、開発期間は数か月程度って感じです。

どんなものを作ったのかは、長々と書くよりも、学内発表会でのプレゼン動画見てもらうのが一番早いと思います。

「ゲーム開発をより良く・便利にしたい」という考えを自分の中でも認識しつつありました。
これを、達成・効果的にアピールすることを最大の目標にして、設計から丁寧に考えていました。

  • C#スクリプト対応
  • テストも書いてる
  • CI組んでる
  • マルチプラットフォームを想定
    • 算術ライブラリも自作
    • レンダラにはグラフィックスAPIとの中間層となるRHI(Render Hardware Interface)を実装
  • エディタはMVVMアーキテクチャ
  • Application(Runtime)とEditorを完全分離して、プロセス間通信する設計

youtu.be

youtu.be

自分のやりたいことを効果的にアピールするという点では、ある程度要件は満たしていたと思います。

一方、見た目やほかの機能面など、最低限欲しいのに揃っていない部分が多いです(-_-;)
まだUnLitな状態ですし、当たり判定のシステムもまだたったりします…

描画系の技術とかも、CGWORLDをよく購入するほど好きなのですが、手が回りませんでした…

あと、サンプルシーンも、ドラム缶を等速直線運動させて、当たったら爆発、という最低限過ぎるものになっています。
もっとまともなゲームらしいデモを作っておくべきだった、
そして、それが動作するだけの基本機能を実装しておくべきだった
と思っています。

この原因は、

  • アピールすることに集中しすぎていた
  • このエンジンを使ったゲームを作るのは、就活終わってからにしようと考えていた

の2つにあると思います。

そのため、「就活のための機能開発になってしまう(機能実装の優先順位などがおかしい)」ということが起きてしまい、これはエンジン開発そのものにも悪影響を及ぼしていました。

つまり、就活としては結果的に悪くはなかったと思いますが、エンジン開発としてはミスをしたと思っています。

なお、エンジン開発は諦めていません。
ちゃんと就活から解放&目的を再認識したうえで、リスタートのための準備を進めています。
これは、近日また別の記事で書くつもりです。

ゲーム

アピールの主軸ではありませんが、専門入学してから大小含めて7つ以上作ってきました。
その中から、大きめ&一般公開している2作品程度をポートフォリオに書きました。

すべてプログラマ/エンジニアだけで作ってます。うちの学校にもプランナーやアーティストが欲しい…

ゲームエンジンを作る以前に、「ゲームを作ることを知る」ことは必須で重要なことであると考えているので、作ったゲームもちゃんとアピールしました。

1年生:2Dゲーム(DirectX 11ネイティブ)

マップが自動生成されたり、武器を自由にカスタムできる、ローグライトアクションゲーム。
自動生成のマップでもゲームバランスを保てるように調整した、マップ生成アルゴリズムなどがアピールポイント。

youtu.be

3年生:3Dゲーム(Unity、WindowsXboxでリリース)

人力車の運転手(車夫)となり、派手な運転でお客さんを楽しませて、がっぽりとお金を稼ぐゲーム。
和風&物理演算系ゲーム要素のある〇レイジータ〇シー。

2か月の短期間開発、広大なフィールド(最適化、製作支援ツールの開発)、Xbox版リリースなどがアピールポイント。
youtu.be

ポートフォリオサイト

頑張ってよかったこと圧倒的No.1です。

先述したように、インターン書類落ちでちゃんと効果的にアピールしないと意味が無いと思い知ったので、頑張って作りました。

ちゃんと書いたら、ちゃんと読んでくれます。
面接でも、「ポートフォリオサイト読んだのですが…」とポートフォリオサイトの内容にに触れていただけることが多かったです。

実際のポートフォリオサイトも貼っておきます。
今読んでみると、ちょっと文章読みづらい…

mewmew-tea.github.io

また、ここで変に印象悪くするのも避けたかったので、全体的な見やすさも配慮しました。
特にカラーユニバーサルデザインには配慮して、誰にとっても見やすい配色なるよう努めています。

Markdownで書きたい&ある程度オリジナリティがあるWebページを作りたかったので、
MarkdownファイルをMkDocsでHTML化してます。
それをGitHub Pagesのサーバー上にデプロイしてます。

Markdownからの変換から、GitHub Pages上にデプロイするまでのワークフローは、すべてGitHub Actionsで自動実行してます。
自作エンジン開発でCI/CD方面にも力入れてたので、この辺もアピールポイントにつなげていました。

ちなみに、Google Analysis埋め込んでアクセス解析もしてました。
見てほしいページがあまり見られてなかったら、より目立つように構成を改善したりもしました。
もちろん効果は不明。半分自己満足。

履歴書やエントリーシート(ES)の自由記入欄

ESと言っても、アンサンブルなスクエアのことではないです。

ジョークは置いておいて、
エントリーシートの自由記入欄などについての話です。

例えばこんなことを書いてました。

  • 自分の長所:技術者としての興味や知識の広さと深さ
  • その根拠:学内での技術交流/共有、Twitter、ブログやGitHubなどでの活動
  • ゲームエンジン開発がやりたい:ゲームエンジン開発を通して、長所を活かしてゲーム開発に貢献したい!

なるべく作品やアピールしている注力した点と関連/紐づけできるような、長所や大事にしていることを書くようにしてました。

あとは、各社の特色に合わせて、「自分がどのようにマッチしているか」「自分がどのように活躍できるか」といったことを書いてました。

面接

ESに書いた内容を主軸に、先述の自己分析ノートの内容も交えつつ、あとはアドリブで喋る感じでやってました。

面接練習

3年生の10月、11月あたりから対策を始めていたと思います。

学校の先生方や、学校の同期で1年早く卒業する友人たちに、複数回面接練習をお願いしました。

(あと、学校のキャリアセンターによる全学生強制参加の面接練習もありました)

自己分析ノートの中身をアドリブで引き出せるように、練習していきました。
就活用の丁寧な言葉遣いなども、面接練習の中で慣れさせました。

気を付けていたこと

とにかくハキハキと、楽しそうに話す!

ほとんどこれです。喋り方です。
(あと、よく言われる「福利の法則」ってやつも)
エンジン全然関係ないけど。

よく「抑揚をつける」「声のトーンを上げる」「ゆっくりと話す」と良いと言いますが、同じことのつもりです。

いずれも具体的なワードですが、個人的には意識するのが難しかったので、
「ハキハキと、楽しそうに」という抽象的なメンタルの持ち方として意識してました。

私は接客のバイトもやっていて、その経験から喋り方の重要さをよく知っています。
喋り方1つで、自分の言葉の伝わり方が劇的に変わります。

なので、オンラインでの面接中はずっと目の前に

ハキハキと、楽しそうに喋る!

と太字のペンでデカデカと書いた紙を貼ってました。
こういうのは意識しないと、つい忘れちゃいますし。
(なお、受け答えのカンペなどは作っていません。あれ逆に思考のノイズになりそうじゃね?)

正直、活舌は悪いし、面接も得意なほうではないと思っていたので、印象良くできるところは良くする努力をしました。

結果として、喋り方はある程度評価されていたっぽいので成功だと思ってます。

余談ですが、先日このことを学校の後輩に話したら、オンライン発表会の日に、下図のような絵が描かれた紙をPCに貼っている写真が送られてきました。

^  ^
 v

自分自身とって理解しやすい形にすることはやはり重要ですね。

よく聞かれたこと

大抵は「ゲームエンジン開発に携わりたい」という気持ちにや理由について深堀されました。

「なぜゲームエンジンを?」
「何がキッカケで始めた?」
ゲームエンジンの中でもどんな箇所を担当してみたい?」

とか。

リアクション

面接まで進んだときは、珍しがられたり、エンジン開発をやりたい理由について興味を持っていただけることが多かったです。

また、書類選考通過率も高かったです。
自分を好意的に捉えてくれそうなところ中心に受けたのもありますが。

そのほか

個人技術ブログ

このブログなどで、挑戦したことを積極的に技術記事として共有しました。
もちろん、ポートフォリオサイトにリンクを貼ってます。

あと、自作エンジンの目玉機能(?)としていたC#スクリプト機能で使っている「mono」についても、本ブログで解説記事を書きました。
単純に記事書きたいという気持ちと、ちゃんと理解してますよというアピールの両方が目的です。

nekocha.hatenablog.com

ちなみに、冬の間は無駄に雪を降らせたりもしてたりします。
画像は、3年生(2022年)の12月初旬ごろの本ブログ。
当時の降雪量激しすぎてワロタ(のちにナーフしました)

学校

とにかく利用しまくりました。
特に先生方。

就活の全般的な方針の相談(2年生のころから)、作品、エントリーシートや履歴書、面接練習などなど…
頻繁に、沢山相談に乗っていただきました。

夏休みなどにも自習教室(通称ラボ)を開放していただけたので、ほぼ毎日行って作業しては沢山相談してました。

もし先生方に相談してなかったら、明らかにおかしな方面に突っ走ったりして業界内定貰えなかった可能性も充分にあったと思います。
お金払っているのだから、ちゃんと利用して大正解だったと思うと同時に、先生方には心から感謝しています。

また、友人たちとも沢山情報共有をしてました。
専門学生という立場をフル活用できたと思います。

0時以降作業しないルール

前回の記事で触れた、「0時以降作業しない」マイルールですが、ちゃんと守り通せました。
面接でも話しましたが、それなりにウケ良かったです。(多分)

SNS

おもにTwitter(現𝕏)です。
就活終わったのでぶっちゃけると、運用はけっこう気を遣ってました。
ポートフォリオサイトにもリンク貼ってましたし。

ツイート内容自体は、普通に開発の進捗から趣味まで雑多に書いてました。

最低限のこととして、下手なことは絶対にツイートしないようにしました。
普段から心掛けてるけど、就活中は特に。

あとは、特にメディア欄も、技術方面の割合が半分以上になるようにも注意してます。
中でも、Webブラウザでプロフィール開いた時に見える直近6枚分。
画像のような感じで、技術のことがここに必ず映るようにしてます。
ゴ〇ラとかブル〇カの超〇磁砲コラボとかのツイートもありますが、そういったものでここが埋まらないようにしてます。

私自身、初めて見るTwitterアカウントは、どんな人か把握するために真っ先にメディア欄を見に行くので、第一印象の材料の1つだと思ってるためです。

おわりに

成功した部分も沢山ある一方で、後悔している部分も数多くありました。

特に、自作エンジン開発としては(ありがちな?)ミスをやってしまったように思います。
(この反省は今後に活かします)
まぁ、でも就活としては(入社してみないと分かりませんが)成功だったと思ってます。
そこは本当に満足しています。

そして、ミスしてる部分は他のことでカバーできていたと思います。
開発したゲームだったり、学内での活動(受賞歴、200人以上の技術共有Discordサーバーの運営等)、技術記事、ポートフォリオサイト、面接対策、今回はあまり触れませんでしたがGitHubでの活動とか。
持っている武器の弾数と種類が、とにかく多かったからカバーできたのだと思います。
通っている専門学校には2,3年制学科もあったのですが、4年制を選択&頑張り続けた分、沢山揃っていました。ラッキー。
でもやっぱり、自作エンジン開発&デモをちゃんと作れてたら、こんなに武器多くなくても余裕だったのでは…?とも同時に思ってます。

あとは、専門学生という立場も利用しながら、人を沢山頼りにしました。
皆様には感謝しかないです。

(言える範囲なので、知りたい部分に限って情報書けていないかもしれませんが)
今回の成功や失敗が、今後の誰かの参考になれば幸いです。

追伸:コメントでご質問頂いた場合でも、言えない範囲のことも多いのでお答えできないことがあります。ご理解お願いしますm(_ _)m

SourceTreeが起動しなくなったので直した「Unable to load MEF components」

現象

記事名のとおり、SourceTreeが起動しなくなってしまいました。
Twitterで周囲でSourceTreeが起動しないという声をちらほら見かけたけど、同じ現象?

具体的には、 SourceTreeを起動ようとすると、スプラッシュ画面が表示だけしたのちに、
何かエラー画面を出すということもなく落ちるようになりました。

多分、2023年6月のWindows Updateを適用したのがキッカケ。
KB5027119KB5027231

ログファイル( %LOCALAPPDATA%\Atlassian\SourceTree\sourcetree.log )を確認してみると、

「Unable to load MEF components」

とか言ってる。

解決法

ここに書いてました。

community.atlassian.com

以下の2つのファイルを削除すればOK。

  • %LOCALAPPDATA%\Atlassian\SourceTree.exe_<UID>\3.4.9.0\Assemblies.cache
  • %LOCALAPPDATA%\Atlassian\SourceTree.exe_<UID>\3.4.9.0\Composition.cache

<UID>はユニークなID。適宜読み替えてください。
3.4.9.0はSourceTreeのバージョン番号。インストールしてるバージョンによって違います。適宜読み替えてください。

見た感じ、WindowsUpdateで.NETの何かしらに変更が入って、SourceTreeのキャッシュとの整合性が取れなくなった・・・
ということなのかなぁと思ってます。

HLSL→WGSLへの変換をやってみる(SPIR-V経由)

目次

はじめに

DirectX向けにHLSL言語で書かれたシェーダプログラムを、WebGPUで使えるWGSLへと変換してみる記事です。
いわゆるトランスパイルというやつです。

実験してみたものの、すぐに忘れそうなので備忘録を兼ねて記事にしておきます。

SPIR-VとTintを使います。(詳細は後述)

余談ですが、WebGPUについて、C++でブラウザ向けに使う入門記事を先日書いたので、試してみたい方はぜひご参照ください。

zenn.dev

また、今回はWindows環境で解説します。
そのほかの環境は以下の通りです。

DirectX Shader Compiler(dxcompiler.dll: 1.7 - 1.7.2212.40 (e043f4a12); dxil.dll: 1.7(101.7.2212.36))
Tint (mainブランチ、 コミット 4ed79e689a0632837d3d956038ef7d78206dc22d(2023年4月上旬ごろ))
ninja 1.11.1
CMake 3.24.0

変換の流れ

HLSL→WGSLの直接変換する手段は、私が知る限り存在しません。
しかし、世の中には「 SPIR-V 」という便利な中間言語があります。

SPIR-Vは、シェーディング言語に特化した中間言語で、多くの環境に向けて使うことができます。
HLSLやWGSLも、SPIR-V形式での入出力に対応しています。
こちらのSPIR-Vを中継して、変換していきます。

つまり、 HLSL→SPIR-V→WGSL という流れでの変換になります。

HLSL→SPIR-V

DirectX Shader Compiler(DXC)を使います。
Microsoftが提供するオープンソースのHLSLコンパイラです。
ShaderModel 6.0以降にも対応しています。
SPIR-Vへ変換する機能もあるため、こちらを使います。

ちなみに、HLSL→SPIR-Vの変換ができるソフトウェアは、
UnityTechnologiesの HLSLccHLSLCrossCompiler のFork)や、
GoogleShaderc などもあります。

SPIR-V→WGSL

Tint を使います。
Tintは、Googleによって、Chromium向けのWebGPUの実装「Dawn」とともに開発されている、シェーダーコンパイラです。

スタンドアロンで、exe形式として使ったり、ライブラリとして組み込むなどの使いかたもできます。

環境構築

DirectX Shader Compiler(DXC)

GitHubにビルドされた状態でReleaseされてるので dxc_xxxxxxxx.zip をDLしてきます。

https://github.com/microsoft/DirectXShaderCompiler/releases

DLできたら、適当なところに解凍します。

あとは、環境に合ったdxc.exeのある場所にPATHを通してください。
(比較的基礎的な部分なので、PATHの通し方は本記事では触れません)

私はx64環境なので、[DXCを解凍したディレクトリ]\bin\x64 にPATHを通しました。

任意のディレクトリで、

dxc --version

を実行して正常に動作すればOKです。

Tint

Tintはバイナリ形式では配布されていないので、自力でビルドする必要があります。

depot-toolsのインストール

Chromiumの開発で必要なツール群です。
公式のドキュメント(https://dawn.googlesource.com/tint/+/refs/heads/main#building)いわく、Chromiumの依存関係管理まわりが必要だそうなので、入れておきます。

公式のセットアップページ(http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up
にアクセスして、

WINDOWS
Download the depot_tools bundle and extract it somewhere.

と書いているあたりのリンクから、Windows用のzipファイル(depot_tools.zip)をDLしてきてください。

DLできたら、解凍、適当な場所に配置します。
こちらも同様にPATHを通しておきます。

Tintのビルド

copy standalone.gclient .gclient

gclient sync

# 出力先ディレクトリを作る
md out\Debug\
cd out\Debug\

cmakeとninjaを使ってビルドをします。
SPIR-Vを読み込みできるように、 TINT_BUILD_SPV_READER オプションを指定しておきます。

(ninjaをインストールしていない場合は、ninjaをダウンロードしてきて、PATHを通してください。
https://github.com/ninja-build/ninja/releases

cmake -GNinja ../.. TINT_BUILD_SPV_READER
# ビルド
ninja

ついでなので、ninjaを使わずに、CMakeとmakeでビルドする方法も書いておきます。

cmake ../.. TINT_BUILD_SPV_READER
# ビルド
make

Tintのインストール

ビルドに成功すると、
out/Debugにtint.exeファイルなどが沢山できているので、
これらを適当なディレクトリに配置し、これにもPATHを通しておきます。

任意のディレクトリで、

tint --help

を実行して正常に動作すればOKです。

変換をする

変換前のHLSL形式のShaderはこちらです。
ごくシンプルな、頂点カラーそのままを出力するFragmentShader(PixelShader)です。

struct VSOut {
        float4 Pos : SV_Position;
        float3 Color : TEXCOORD0;
};

float4 main(VSOut In) : SV_Target0
{
    return float4(In.Color, 1.0f);;
}

HLSL→SPIR-V(DXCを使用)

dxc.exe を使います。

dxc -spirv -T ps_6_0 -E main FragmentTest.hlsl -Fo FragmentTest.spv

指定しているオプションについては、下表のとおりです。
dxcのオプションの詳細は、公式ドキュメントdxc --help コマンドから確認してください。

オプション 説明
-spirv SPIR-V形式に出力
-T ps_6_0 ターゲット。
シェーダーステージ(vs/ps/csなど)や、
シェーダーモデル(6_0など)を指定。
ここでは、PixelShaderとShaderModel 6.0を指定しています。
-E main エントリポイントの関数名を指定します。
FragmentTest.hlsl コンパイル元のhlslファイル。
-Fo FragmentTest.spv 出力先のSPIR-V形式のファイル。

成功すれば、FragmentTest.spvが出力されます。

SPIR-V→WGSL(Tintを使用)

tint.exeを使います。

tint FragmentTest.spv -o FragmentTest.wgsl

無事、WGSL形式のシェーダーが出力されました。

var<private> in_var_TEXCOORD0 : vec3<f32>;

var<private> out_var_SV_Target0 : vec4<f32>;

fn main_1() {
  let x_13 : vec3<f32> = in_var_TEXCOORD0;
  out_var_SV_Target0 = vec4<f32>(x_13.x, x_13.y, x_13.z, 1.0f);
  return;
}

struct main_out {
  @location(0)
  out_var_SV_Target0_1 : vec4<f32>,
}

@fragment
fn main(@location(0) in_var_TEXCOORD0_param : vec3<f32>) -> main_out {
  in_var_TEXCOORD0 = in_var_TEXCOORD0_param;
  main_1();
  return main_out(out_var_SV_Target0);
}

おわりに

HLSL→SPIR-V→WGSLの変換をやりました。

エンジン開発などでのクロスプラットフォーム対応や、HLSLのincludeなどのプリプロセッサ機能を使いたい場合などに活きそうな技術です。

あと、ゲームコンソールでもSDKとかにトランスパイル出来る仕組みってあるのかなぁってふと疑問を抱いたりしてます。NDAに触れちゃうであろう事項なので、いまのただの学生という身分では知りようがないのですが。

これからも、WebGPUなど色々と調査・研究していきます。

参考にしたもの

HLSL を SPIR-V 経由で GLSL に変換してみた - Qiita

SPIR‐V CodeGen · microsoft/DirectXShaderCompiler Wiki · GitHub

refs/heads/main - tint - Git at Google

http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up

学生が自作ゲームエンジンで就活している話

この記事は、GameEngineDev Advent Calendar 2022の25日目の記事です。

目次

はじめに

まずはじめに。
雑記です。ゆる~く読んでください。
私も、あえてゆる~く書いてます。
(なので構成も少しgdgdです)
こんな記事を最終日にして良かったのだろうか…

この記事では、
専門学生の私が「ゲームエンジンそのもの」を就活作品として開発する中でのいろいろを書きます。

「やってよかったこと」
「苦労していること」
を書き連ねます。

技術のことは少なめです。
(技術方面のことは、私の昨日24日目のAdCの記事でいっぱい書いたし…)

あと、面接や選考については、書けそうだったら、就活終わってから改めて別途書こうと思います。

自己紹介

簡単に自己紹介しておきます。
ゲームプログラミング系の専門学生です。高校は普通科

4年卒学科の3年生です。12月なので、説明会やエントリーなど就活始まったところ。
幅広い技術に触れられるゲームエンジン開発に魅力に感じていて、実際にゲームエンジンを自作してます。
ゲーム業界就職してエンジンとかミドルウェアとかやりたいと思っています。

はじめて作ったエンジン

2年生のとき、当初は特定のゲームを開発するためのゲームフレームワークのつもりで開発していたものを、
ゲームエンジンとして本格的に開発し始めました。

物理エンジンPhysXを組み込んでいたり、Undo/Redo機能があったりと、機能はわりと充実させました。

youtu.be

いま作っているエンジンについて

経験を積んで出来ることが増えて、エンジン設計そのものを大きく変えたいと思い、別のエンジンをイチから開発することにしました。

C#スクリプトに対応したゲームエンジンを作っています。
その名も、「Miyadaiku Engine 1.0」です。
実はオープンソースです。

github.com

マルチプラットフォーム対応と、ゲーム開発のイテレーション向上を目的としたエンジンです。
まだ開発の初期段階です。

  • ApplicationとEditorの完全分離(別exeファイル)
  • ApplicationとEditor間のプロセス間通信
    (TCP/IPで通信。ゲーム機で動くランタイムでも、PCのエディタから編集できるという設計)
  • C#スクリプト対応
  • エディタはC#WPFMVVMパターンで開発
  • CIも構築(GitHub Actions)

などの特徴があります。

ポートフォリオサイトにたくさん説明を書いてます。
ここで説明すると長くなるので、こちらを参照いただければと思います。

mewmew-tea.github.io

やってよかったこと・意識していること

ゲームエンジンソースコードや実装を研究

かなりの時間を掛けてます。

Godot Engine、Unreal Engine、DiligentEngine等々… 世の中には、ソースコードが公開されているゲームエンジンが大量にあります。
それらのソースコードを、通学時間含めて、ひらすら読み漁っています。

設計や実装のアイデアを頭に蓄積しています。

例えば、私のエンジンでは、グラフィックスAPIを薄くラッピングしてマルチプラットフォーム対応しやすくする、RHI(Rendering Hardware Interface)という概念がありますが、これはUnreal Engineなどのソースコードから着想を得て実装しました。

私のエンジンもオープンソースなので、いつか参考にされるようになりたいなぁ・・・

既存のゲームエンジンを使ってみる

ゲームエンジンを知らずして、ゲームエンジンは開発できません。
Unreal EngineやUnityを実際に使ってみたり、作品を作ったりしています。

これをすることで、はじめて先述のソースコードや実装の研究ができます。

ゲームを作る

当然ですが、ゲーム作りを経験しないとゲームエンジン開発できないです。
ゲームはこれまでに(大小ありますが)6つ以上は作ってきました。

ゲームを開発する際は、一般公開を目標にしています。
公開のためには、完成させる必要があって、
この「完成させる」という経験が、特に力になると思っています。

ちなみに、いま開発中のエンジンでもゲームを作ります。

ゲーム機向けにゲームを開発する

Windows PCだけで開発を完結させることもできますが、ゲーム機向けにゲームを開発するという経験は重要です。
ゲームエンジンに必要なことを別の視線で知ることができます。

Xboxなら、3000円程度払って開発者プログラムに登録すれば、一般に小売店で売られている本体でも開発機にできます。

私は、個人的にXbox Series Sを買って、毎日学校に持って行ってゲーム開発してました。
Xbox Series Sはティッシュケース程度のサイズなので、電車通学だけど何とかなりました) 無事、PCとXboxでゲームを公開できました ( がっぽり!爆走人力車

楽しんで開発する

ゲームも、ゲームエンジンも、それ以外も楽しんで開発するのが一番良い成果が出ます。
なので、楽しんで開発するように&できるように意識しています。

お遊びの開発

学校で出席登録サイトで毎日出席登録する必要があるのですが、これがとても忘れがちでした。
忘れないように、リマインダーするDiscord Botを作ったことがあります。
面白おかしいメッセージ(全4種 ランダム)とともに。
(GASとDiscordのWebhookを使ってます。このためだけに勉強しましたw)

この一見おふざけな開発の技術は、自作エンジンのCIの成否通知の技術に応用することができてます。

幅広くアンテナを張る

もはやライフワークという感じで、技術に関する情報を収集しています。
普段はTwitterで。
面白いと思った技術は、即ブックマークにぶち込んで、あとで技術の詳細を調べる。

あとは、CEDECなどのカンファレンスの過去資料も沢山見てます。

わからない技術が出てきたら、すぐにググるクセをつけたりもしてます。
調べてもよく分からなくても、名前だけは頭の片隅に置いています。

ゲームエンジン開発は、幅広い知識を要求されるので、このクセはとても役に立っています。
実際、ネットワークを使ったプロセス間通信機能やCIなどは、普段からの情報収集が無ければ実現できなかったと思います。

交流と露出

これは凄く重要です。
コミュニケーションスキルが身に付きますし、自分自身を多くの人に知ってもらうことで色んな良いことが訪れます。

学内での交流

先輩後輩同級生問わず、頑張ってる人達と沢山仲良くしてます。

学内の技術共有Discordサーバー運営もしてます。200人以上居ます。
勉強会もやってたりします。

先生方には沢山相談に乗っていただいています。
高い学費を奨学金借りてまで払ってるのだから、使えるものは使っていく!

とにかく、モチベ高い人との交流を大事にしてます。

Twitter

進捗ツイートとか疑問に感じたことをツイートしたりしてます。

また、他校の学生やプロのエンジニアもフォローすることも大切にしています。
学内の知り合いだけでは井の中の蛙ですし。
はじめは勇気が要りましたが、今は軽い気持ちでフォローしに行ってます。

頑張ってる学生の刺激や、学校では知り得ない知識を得ることができます。
あと、進捗ツイートをいいねやRTして貰えると嬉しくてモチベ上がります。

そして、何よりもゲームエンジン開発者仲間」を見つけられます。
学内では仲間が居なくて孤独だったので、とても嬉しいことです。

タダでこれらが得られるなんて!Twitter最高!!

アウトプット

色々やってます。

  • 先述の学内の技術共有Discordサーバーで勉強会を開く
  • Qiitaや個人ブログで技術系記事書く
  • 友人や先生などと技術談義
  • GitHubでPR投げる
  • 自分のソースをGitHubのpublicなリポジトリに置く

アウトプットすることで理解が深まりますし、自信もつきます。
就活のときも、アウトプットしてることはアピールポイントになります。

英語でコメントやコミットメッセージを書く

これは開発途中からやってます。

とにかく英語に慣れることができます。
そして、ソースコード命名が良くなります。

良いことづくめです。

文法が少し怪しいことは気にしすぎず、ググったりしながらガリガリ書いています。

深夜0時以降のコミットを原則禁止

マイルールです。

ちゃんと睡眠時間確保するためにやってます。
徹夜作業しても良いことありません。

徹夜作業をアテにしないことで、作業効率を安定させています。

苦労していること・悩んでいること

頑張った部分を伝えるのが難しい

「シェーダー頑張りました!」とか「ゲームAI工夫しました!」とか「おもろいゲーム作りました!」なら、すぐに伝わります。
あと、多機能で便利なエディタも伝わりやすいです。

ですが、私が今回特に頑張ったのは、もっと"地味"な部分です。
CIとか、C#スクリプトとか、プロセス間通信とか。
これらを上手に伝えるのはとても難しいです。

上手に就活するためには、地味じゃない機能も入れていく必要があるかも?

ひとまず、ポートフォリオに分かりやすく図説してみたりするなど工夫をしています。

エディタの機能の実装が少ない

これはただの私のやらかしに近いですが、現状ではエディタの機能の実装が少ないです。
就活作品として企業に提出した際、企業の方が

  1. まずは起動
  2. エディタの機能の実装が少ないなぁ
  3. もうええわ(以降は一切見てもらえない。添付したポートフォリオ完全無視)

という流れになって、選考落ちする可能性がそこそこあります。
今になって危機感を覚えていて、何とか改善しようとしているところです。

"就活のための開発"になってしまうことがある

就活作品製作あるあるだと思うのですが、開発の優先順位が、期限とか就活のためのアピールポイントを考慮して決まっちゃうので、思うように開発できません。
少しつらい。

経験が足りない

まだまだ経験が浅いがゆえに、実装や勉強に時間が掛かってしまうことがあります。
もっと経験が欲しい……下積みを続けていきます。

おわりに

自作ゲームエンジンを就活作品にするのは大変です。
今後も課題は解決しつつ、就活頑張ります。

本当にただの雑記になっちゃったけど、 就活終わったら振り返り記事とかを書いてみたいなぁ。

追記:就活終わったので書きました。

nekocha.hatenablog.com

おわり。

MonoでC#スクリプトをC++から実行入門

この記事は、GameEngineDev Advent Calendar 2022の24日目の記事です。

目次

はじめに

開発中の自作ゲームエンジンでは、Monoを使ってC#スクリプトに対応させています。
ところが、英語記事ですらとても情報が少ない...まずは私が入門記事書いちゃいます! あと、この記事はあくまでも入門で分かりやすさ優先です。きれいな実装とかでは全くないのでご了承くださいまし…

記事の内容をざっくりと説明すると、UnityのC#スクリプトと同じことをやります。
C#C++を埋め込む(P/Invoke)のではなく、C++C#を埋め込み」をします。
この手法の利点は、C++側から、C#の型情報やインスタンス生成とかVMなどを弄れることです。

UnityはもともとMonoを使っていたので、この記事を読むと、少しだけUnityの気持ちが分かるようになる…かも…?知らんけど。

サンプルソース

今回の内容のサンプルソースGitHubに置いておきました。

よろしければ、ご参考までにどうぞ。

github.com

Monoとは

Monoは、クロスプラットフォームで動作する、.NET Framework互換のフレームワークです。オープンソースです。

github.com

あと、PS4のようなゲームコンソールにも対応しているので、ゲームに向いているとも言えるかも?
実際、UnityはIL2CPP以前はMono(をForkして改造したやつ)を使ってC#スクリプトを走らせてました。

ちなみに、.NET 「Framework」互換なので、C#のバージョン7.0の一部機能までしかカバーしません。
本記事執筆地点(2022年12月)で、最新の.NET 7でC#11をサポートしてるので、正直少し古さはあります。
(私の知る限りでは、どうにかしたければVMやIL2CPPを自作するしかなさそう?)

C#スクリプトを実行する流れ

今回やる内容は、だいたい下の図のようになってます。

C#スクリプトを事前にIL(中間言語)に変換して、dll形式にします。
実行時、dllの中にあるILを、
C++に埋め込まれたMonoのVM仮想マシン)上で動かします。

JITコンパイルについて

これは、JIT(Just In Time)コンパイルという手法です。
JITコンパイルについては、この記事が分かりやすいです。
いま出てきたILやVMについても理解できると思います。

qiita.com

あるいは、「JVM」とかでググるとこのあたりの概念が説明されてます。
Java言語ですが、同じことです。

(記事を書く時間が足りず、JITコンパイルの説明雑くなっちゃいました…スミマセン)

具体的な流れ

こんな感じです。
こうやって並べてみると、案外シンプルです。

  1. C#スクリプトアセンブリをビルドしておく
    【ここからC++側】
  2. Monoの初期化
  3. アセンブリの読み込み
  4. クラス型情報の取得
  5. クラスのインスタンス生成
  6. 関数の情報の取得
  7. 関数の呼び出し

環境

念のため書いときます

Windows 11 Pro 21H2  
Mono 6.12.0  
Visual Studio Community 2022 17.2.6  

1.Monoをインストール(環境構築)

Monoの開発環境の構築は、今回は(多分)一番お手軽な方法で行きます。

Mono公式サイトから、「64bit」版のインストーラーをダウンロードしてください。

Download - Stable | Mono

次に、DLしたインストーラーを起動し、画面の指示に従ってインストールをしてください。
インストール先の場所は、あとで使うので覚えておいてください。
インストーラーの操作は今回は割愛します)

2.C#スクリプトアセンブリ作成

アセンブリとは、C#スクリプトをIL(中間言語)にビルドした状態のことです。
今回はdllファイルの形式です。

今回は、VisualStudioでビルドをします。

ソリューションとプロジェクトの作成

VisualStudioでソリューションを作成します。
プロジェクトテンプレートは、「クラスライブラリ(.NET Framework)」です。
.NET Framework」と書いてるほうにしてください。(Monoが.NET Framwork互換なので、合わせます)

プロジェクト名は「CSScript」とでもしておきます。
今回は「ソリューションとプロジェクトを同じディレクトリに配置する」を有効にしておきます。

クラスの関数を実装

プロジェクト作ったときに自動で「Class1.cs」生成されていると思うので、今回はそれ使っちゃいます。

こんな感じの関数を実装します。
Monoを使ってC++側から呼び出したり、C#の関数を呼び出したりする関数です。
(Multiply関数に書いてる属性については、あとで解説します)

using System;
using System.Runtime.CompilerServices;

namespace CSScript
{
    public class Class1
    {
        private void PrintMessage()
        {
            Console.WriteLine("Hello, Mono!!!");
        }

        // C++の関数(内部呼び出し)
        [MethodImpl(MethodImplOptions.InternalCall)]
        private extern static int Multiply(int a, int b);

        // C++の関数を呼び出す版
        private void PrintMessage2()
        {
            // C++の関数を内部呼び出し
            Console.WriteLine("2 * 3 = " + Multiply(2, 3));
        }
    }
}

ビルド

ビルドします。
正常にビルドができていればOKです。

3.C++側とMonoのAPI等準備

ソリューションとプロジェクトの作成

C++側のソリューションとプロジェクトです。 プロジェクトテンプレートは、「コンソール アプリ」です。

名前は「NativeApplication」にしておきます。 こちらも、今回は「ソリューションとプロジェクトを同じディレクトリに配置する」を有効にしておきます。

Monoのライブラリ導入

1でインストールした場所にあるやつを使います。
デフォルト設定でインストールしたなら、C:/Program Files/Mono/ にあるかと思います。

※この節では、先ほど作成したNativeApplication.slnとかNativeApplication.cppがあるディレクトリを「プロジェクトディレクトリ」と呼びます。

include用ヘッダファイル

{Monoインストール先}/include/mono-2.0/mono フォルダごと
プロジェクトディレクトリにコピーしてください。

VisualStudioのプロジェクト設定で、追加のインクルードディレクトリに「./」を設定しておきます。

libファイル

{Monoインストール先}/Lib/mono-2.0-sgen.libを、
プロジェクトディレクトリにコピーしてください。

dllファイル

{Monoインストール先}/bin/mono-2.0-sgen.dllを、
プロジェクトディレクトリにコピーしてください。

C#のライブラリ群

プロジェクトディレクトリ内に、MonoAssembly/bin/mono フォルダを作成してください。
{Monoインストール先}/Lib/mono/4.5/ フォルダごと
プロジェクトディレクトリ/MonoAssembly/bin/mono/ 内にコピーしてください。

設定ファイル

プロジェクトディレクトリ内に、MonoAssembly/etc フォルダを作成してください。
{Monoインストール先}/etc/mono フォルダごと
プロジェクトディレクトリ/MonoAssembly/etc 内にコピーしてください。

ビルドしたC#スクリプト

1でビルドしておいたやつです。
(MonoのAPIとかではありませんが、一緒にやっちゃいます。)
CSScript.dllを、
プロジェクトディレクトリにコピーしてください。
CSScript.dllは、{CSScriptのディレクトリ}/bin/Debug(またはRelease)にあるかと思います。

4.C++からC#スクリプトを呼び出す

Monoのインクルードとリンク

面倒なので、先にすべてIncludeとかリンクを書いてしまいます。 NativeApplication.cpp

// Mono
#pragma comment (lib, "mono-2.0-sgen.lib")
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/object.h>
#include <mono/metadata/appdomain.h>
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/exception.h>

Monoの初期化と終了

初期化&終了処理です。
MonoDomainは、Monoの仮想マシン上でのアプリケーションの処理の単位です。
OSにおけるプロセスとほぼ同じものです。

int main()
{
    // Monoのアセンブリと設定ファイルのディレクトリをセットする
    mono_set_dirs("./MonoAssembly/bin/", "./MonoAssembly/etc/");
    
    // ドメイン(OSにおけるプロセスのようなもの)
    MonoDomain* domain = nullptr;
    // Monoの初期化
    domain = mono_jit_init("CSScriptTest");
    if (!domain)
    {
        printf("Monoの初期化に失敗\n");
        return 1;
    }

    // Monoの終了処理
    mono_jit_cleanup(domain);

    return 0;
}

スクリプトアセンブリのロード

C#スクリプトアセンブリを読み込みます。
アセンブリとは、中間言語(IL)の状態に変換したC#のことです。
つまり、さっきビルドしたdllファイルです。

つづいて、アセンブリのImageを取得します。
Imageには、C#コードの情報が格納されています。

// スクリプトのアセンブリ(中間言語の状態に変換したC#)
MonoAssembly* assembly = nullptr;
// スクリプトのアセンブリ(DLL)をロード
assembly = mono_domain_assembly_open(domain, ".\\CSScript.dll");
if (!assembly)
{
    printf("スクリプトのアセンブリのロードに失敗\n");
    mono_jit_cleanup(domain);
    return 1;
}
// アセンブリのイメージ(アセンブリ内のコード情報を実際に保持しているもの)
MonoImage* assemblyImage = nullptr;
assemblyImage = mono_assembly_get_image(assembly);
if (!assemblyImage)
{
    printf("スクリプトのアセンブリイメージの取得に失敗\n");
    mono_jit_cleanup(domain);
    return 1;
}

クラスの読み込み

先述のように、アセンブリのImageにはC#コードの情報が格納されているので、
そこからクラスの型情報を貰ってきます。

// クラスの型
MonoClass* mainClass = nullptr;
mainClass = mono_class_from_name(assemblyImage, "CSScript", "Class1");
if (!mainClass)
{
    printf("クラスの型取得に失敗\n");
    mono_jit_cleanup(domain);
    return 1;
}

クラスのインスタンス

クラスの情報をもとに、インスタンス化します。

// クラスのインスタンスを作成
MonoObject* classInstance = nullptr;
classInstance = mono_object_new(domain, mainClass);
if (!classInstance)
{
    printf("クラスのインスタンス生成に失敗\n");
    mono_jit_cleanup(domain);
    return 1;
}

クラスの関数読み込み

クラスの型情報から、関数の情報を取得します。

検索するためには、まず検索条件情報を作ってあげる必要があります。 MonoMethodDesc(定義情報)を作成します。
mono_method_desc_new関数の第一引数の文字列は、「名前空間名.クラス名::関数名」を書きます。

次に、作成したMonoMethodDescをもとに、mono_method_desc_search_in_classで検索します。

// 関数情報定義
MonoMethodDesc* methodDesc = nullptr;
methodDesc = mono_method_desc_new("CSScript.Class1::PrintMessage()", true);
if (!methodDesc)
{
    printf("関数情報の定義作成に失敗\n");
    mono_jit_cleanup(domain);
    return 1;
}

// スクリプトの関数
MonoMethod* method = nullptr;
// 関数情報定義をもとに、クラス内の関数を検索
method = mono_method_desc_search_in_class(methodDesc, mainClass);
if (!method)
{
    printf("関数取得に失敗\n");
    mono_jit_cleanup(domain);
    return 1;
}

クラスの関数呼び出し

mono_runtime_invoke関数で、関数を呼び出します。
ちなみに、staticな関数なら、第二引数で渡すクラスのインスタンスはnullptrでもOKです。

関数実行時の例外は、第四引数で受け取れます。
例外は文字列で渡されますが、変換してあげる必要があります。

// 関数実行時の例外情報
MonoObject* excObject = nullptr;
// 関数を呼び出し
mono_runtime_invoke(method, classInstance, nullptr, &excObject);
if (excObject)
{
    MonoString* excString = mono_object_to_string(excObject, nullptr);
    const char* excCString = mono_string_to_utf8(excString);
    printf("関数実行時例外%s\n", excCString);
    mono_jit_cleanup(domain);
    return 1;
}

実行結果を確認すると、C#で定義した内容が出力されていることが確認できます。

実行結果
Hello, Mono!!!

5.C++の関数をC#から呼び出す(内部呼び出し)

C++から呼び出した、C#の関数の中で、C++の関数を使う」
ということもやってみます。

これで、例えばゲームエンジンで、「シーン情報をC++で管理していて、C#スクリプトからもシーン情報取得したい」というニーズにも対応できるようになります。

「内部呼び出し(Internal Call)」という方式です。
ちなみに、公式ドキュメンテーションによると、
内部呼び出しは、MonoでC(C++)コードを呼び出す手段としては、最もオーバーヘッドが少ないです。

今回は掛け算をするC++の関数を呼び出してみます。

C++の関数(呼ばれる側)

なんの変哲もない、普通の掛け算です。

// C#側から呼び出される関数
int32_t Multiply(int32_t a, int32_t b)
{
    return a * b;
}

C#の関数(呼び出し側)

P/Invokeと大体同じで、externなstatic関数に属性を指定してあげます。
この属性を使うには、System.Runtime.CompilerServices をusingしてあげる必要があるので注意。

// C++の関数(内部呼び出し)
[MethodImpl(MethodImplOptions.InternalCall)]
private extern static int Multiply(int a, int b);

これで、C#側からC++の関数を呼べるようになります。

private void PrintMessage2()
{
    // C++の関数を内部呼び出し
    Console.WriteLine("2 * 3 = " + Multiply(2, 3));
}

C++の関数を、内部呼び出しの対象として登録する

最後に、C++の関数をmono_add_internal_call で登録してあげます。
第一引数は、登録先のC#の関数の「名前空間.クラス名::関数名」形式の文字列です。
第二引数は、登録するC++の関数ポインタです。

// C++の関数を、内部呼び出し対象として登録
mono_add_internal_call("CSScript.Class1::Multiply", &Multiply);

あとは、さきほどと同様に、C++の関数を呼び出すC#の関数(PrintMessage2)を呼び出してみましょう。
呼び出しの実装はさきほどと何も変わらないので、割愛します。

PrintMessage2関数を実行すると、正しくC++の関数が呼び出されたことが確認できます。

実行結果
2 * 3 = 6

おわりに

ね、簡単でしょ? …と言いたいところですが、これは入門編。
ここからが本番です。

今回は関数の呼び出しだけやりました。
他にも、フィールドやプロパティ、カスタム属性、GCなど、出来ることは大量にあります。
(ホットリロードも一応できますが、かなりクセがありました。分かるわけないやんあんな仕様

私自身、研究しながら使っています。
何かしら、小ネタとか含めてこのブログで発信出来たらなぁ…やりたいなぁって感じです。

そして、Monoはやはりネット上の情報はとても少ないです。
私がよく参考にしているところを紹介しておきます。

なお、お気づきかもしれませんが、MonoのAPIC言語による実装です。
なので、C++オブジェクト指向な形で良い感じにラッピングしてあげる必要があるでしょう。
…良い感じに…難しい…

参考にしたもの

Mono Documentation
Monoの埋め込みのサンプルソース