YAMADA TAISHI’s diary

ゲームについてとか私の日記とか。このブログのあらゆるコードは好きにどうぞ。利用規約があるものは記事内のGitHubのRepositoryのリンクで貼られていると思うので、そちらを参照ください。

【備忘録】ゲーム制作者向け(Pythonで)RPAツールを作ってみる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
私はゲーム業界にいるわけですが、面倒くさい作業は結構な割合で企画職がやってくれます。
私が前にシステムエンジニアをしていた時は結構な割合で自動化ツールが豊富にありました。
しかし、現在はソコまで自動化ツールは見当たりません。企画職である方々には何が自動化出来るのか判別出来ないのと、
プログラマーがソコまで気が回っていないからだと思いました。
なので、そういったツールを民主化させる第一歩としてRPAツールを作る方法を共有できれば良いなと思い今回作ってみることにしました。

目次


既存RPAもどきツールについて


ゲーム業界にも自動化ツールはありますが、ほとんどがレガシーな技術、エクセルVBAがせいぜいです。
今回はあえて処理速度の観点からゲームプログラマーが絶対触らないであろうPythonを触ってツールを作っていこうと思います。

環境


Windows10
Python3.9
PyCharm2021.3系(VSCodeとかでも良いと思います)

利用プラグイン
PyAutoGui
Pillow
opencv-python

参考記事


qiita.com

qiita.com

qiita.com

learnedmark.com

qiita.com

今回自動化する流れ


音声テキストを生成するツールVoicePeakというのがあるが、csv,tsv,改行区切りなどのインプット形式でインプット可能だが、
どのキャラでインポートするかまでは指定できない。

一括で初回ぐらいはやってしまいたいが、テキスト形式では無理がある。
なので、そこの指定を自動化出来るツールを作ろうと思い今回対応。

大まかとなる流れはこうだ。

Spreadsheetでセリフを管理

GASによりJson形式でセリフを吐き出し

Pythonを使いVoicepeakに取り込み

Spreadsheetでセリフを管理


Spreadsheetでは以下のような構成にしました。

GASによりJson形式でセリフを吐き出し


GASは以下のような内容にして、
ボタンを押したらJson形式でファイルがダウンロードされるようにしました。

GAScript

function getJsonData(sheetName) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  
  var maxRow = sheet.getLastRow();
  var maxColumn = sheet.getLastColumn();
  
  var keys = [];
  var data = [];

  for (var x = 1; x <= maxColumn; x++) {
    keys.push(sheet.getRange(1, x).getValue());
  }

  //実際のデータが2行目からなので【y = 2】から開始
  for (var y = 2; y <= maxRow; y++) {
    var json = {};
    if(sheet.getRange(y, 1).getValue() === false){
      continue;
    }
    for (var x = 2; x <= maxColumn; x++) {
      if(x === 2){
        const sheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート2");
        json[keys[x-1]] = vlookup(sheet.getRange(y, x).getValue(),sheet2,2);
      }else{
        json[keys[x-1]] = sheet.getRange(y, x).getValue();
      }
    }
    //データ格納
    data.push(json);
  }
  Logger.log(data);
  return JSON.stringify(data, null, '\t');
}

function getJson(){
  return getJsonData('シート1');
}

function main() {
  
  var dl_html = HtmlService.createTemplateFromFile("dl_dialog").evaluate();
  SpreadsheetApp.getUi().showModalDialog(dl_html, "JSONファイルをダウンロード");
}

function vlookup(value,sheet,column) {
  let returnValue = "1";
  for (var i = 2; i <= sheet.getLastRow(); i++) {
    if(value == sheet.getRange(i,1).getValue()){
      returnValue = sheet.getRange(i,column).getValue();
      break;
    }
  }    
  return returnValue;
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script type='text/javascript'>
      function downloadJson(elm) {
        var content = <?= getJson(); ?>;
        var blob = new Blob([content], { "type": "application/json" });
        document.getElementById("download").href = window.URL.createObjectURL(blob);
      }
  </script>
  </head>
  <body>
    <!-- JSONダウンロードボタン:json名は適宜設定してください-->
    <a id="download" href="#" download="VoicePeak.json" onclick="downloadJson()">ダウンロード</a>
  </body>
</html>

Pythonを使いVoicepeakに取り込み


自動化の仕組み


今回はPyAutoGuiを利用しますので、要素の検査とかは必要ありません。
要素の検査をして色々するツールの方が動作の確実性が高いですが、実装の敷居が若干上がます。
後、単純に私がPython使いたい。

もしPyAutoGui以外を使ってWindowsで要素の精査を行うならコチラ↓の利用を推奨。
https://accessibilityinsights.io/

仕組みとしては簡単で、画像認識や座標指定などでクリック位置を特定してPython経由でマウスやキーボードを操作してあげる感じです。
そのためクラウド上の挙動は出来ません。

PyAutoGuiインストール


pipコマンドを打ってインストールします。

大分前のことで忘れたけど↓あたり?

pip install pyautogui
pip install opencv-python
pip install Pillow

pythonのコードを書く


main.py

import json
import os
import tkinter.filedialog
import tkinter.messagebox
import pyautogui
import time
import pygetwindow
import keyboard
import pyperclip


# Jsonを読み込むところ
def open_json(json_path):
    json_file = open(json_path, 'r', encoding="utf-8")
    json_dict = json.load(json_file)  # 辞書型変数にデータをつっこむ
    json_file.close()
    print('json_dict:{}'.format(type(json_dict)))
    return json_dict


# 指定の画像が表示されるまで待つ
def wait_picture(f, time_out):
    print(f)
    ret = None
    while ret is None:
        ret = pyautogui.locateOnScreen(f, grayscale=True, confidence=.7)
        print(ret)
        if ret is not None:
            return ret
        time.sleep(0.1)
        time_out -= 1
        print(time_out)
        if time_out < 0:
            return None


# プロジェクトを作り直す
def new_project():
    while True:
        time_zero_item = wait_picture("image/0.png", 1)
        if time_zero_item is not None:
            x, y = pyautogui.center(time_zero_item)
            pyautogui.click(x, y - 15)  # 一旦アイテムの上にカーソル持っていく
            time.sleep(0.05)
            keyboard.press_and_release('ctrl+n')
            item = wait_picture("image/No.png", 1)
            pyautogui.click(item)
            time.sleep(0.05)
        break  # なければ終わる


def window_resize():
    voicepeak.maximize()  # 最大化して
    time.sleep(0.1)
    voicepeak.restore()  # もとに戻す
    time.sleep(0.1)


# Press the green button in the gutter to run the script.
if __name__ == '__main__':
    exe_name = 'VoicePeak Json Importer'
    root = tkinter.Tk()
    root.withdraw()
    file_type = [("", "*.json")]
    directory = os.path.abspath(os.path.dirname(__file__))
    tkinter.messagebox.showinfo(exe_name, '処理ファイルを選択してください')
    file_path_str = tkinter.filedialog.askopenfilename(filetypes=file_type, initialdir=directory)
    print('読み込みファイル:', file_path_str)

    if file_path_str == "":
        exit()

    title = 'VOICEPEAK'
    voicepeak = [g for g in pygetwindow.getWindowsWithTitle(title) if g.title == title][0]
    voicepeak.activate()
    time.sleep(1)
    window_resize()
    new_project()
    window_resize()
    voicepeak.activate()

    mainItem = wait_picture("image/addword.png", 10)
    pyautogui.click(mainItem)  # 「クリックしてセリフを入力してください」をクリック

    json_data = open_json(file_path_str)
    for row in json_data:
        print('row:{}'.format(row))
        select_voice = row['VoiceData']
        if bool(select_voice != "1"):
            pulldown_item = wait_picture("image/PulldownItem.png", 1)
            pyautogui.click(pulldown_item)
            x, y = pyautogui.center(pulldown_item)
            time.sleep(0.1)
            y = (25 * (select_voice - 1)) + y + 30
            pyautogui.moveTo(x-10, y)   # 何故か一度動かしてからじゃないとクリック反応しない……
            pyautogui.click(x-10, y)

        word = wait_picture("image/addword.png", 10)
        pyautogui.click(word)

        voice_text = row['VoiceText']
        pyperclip.copy(voice_text)
        keyboard.press_and_release('ctrl+v')
        time.sleep(0.05)
        keyboard.press('enter')
        pyautogui.scroll(-500)

使った画像たち




(超久しぶりにPython触ったけど、命名規則特殊だね)

後はビルドしてexeファイルを実行できるようにする。

これで実行できるようになった

実際の実行の様子


まとめ


かなり前に書いていたのですが、Scriptなど公開してなかったので公開してみました。
アップデートがあってVoicepeakは設定周り保存できるようになったりした気がしますが、同じようにGUIツールの自動化ツールは作れるので是非試してみてください。
皆様の助けになれば幸いです。

BlenderとUnityの3Dモデルの状態を同期してくれるMeshSyncを触ってみる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
今回はMeshSyncツールの備忘録です。
短い記事になってしまったので、ずっと公開していないでアーカイブしていたのですが、勿体ないかなぁと思い公開しました。

目次


MeshSync?


MeshSyncとはUnity公式が作っているMayaやBlenderなどDCCツールとUnityを連携させる機能です!

これを使うことによって毎回モデルをインポートしなおす必要がなくUnity上で動作確認が出来ます。
VRChatterも嬉しいね!

しかも、日本語の説明もある。

docs.unity3d.com

環境


Blender3.0.0
URP環境
Unity側のプラグインは0.12.6-previewを利用
(0.12.0ではなくて0.12.6を使う理由としてはURP対応してるため)
github.com
docs.unity3d.com

Blender側は0.12.0-preview
github.com
(0.16.2でも挙動確認済み)

Unityバージョンは2021.2.17f
(2021.3.15でも確認済み)
Windows 10
(Windows 11でも確認済み)

Unity側のインストール


Window>Package Manager

  • ボタンをクリック、Add package from git URL…

com.unity.meshsync@0.12.6-preview と記入(com.unity.meshsyncと入力すれば最新版取得できる)

インストールされたか確認

ダウンロードされたら更新マークをクリックして、
InProjectで絞り込んで、検索ウィンドウにMeshなどと入れ
候補に表示されていたらインストールされている。

Blender側のインストール


Unity側でインストールしたら、Unity側のプラグインと一緒にUnityのプロジェクトフォルダにBlenderプラグインもダウンロードされています。(多分)

Unityのプロジェクトフォルダ\Library\PackageCache\com.unity.meshsync.dcc-plugins@0.12.0-preview\Editor\Plugins~\UnityMeshSync_Blender_Windows.zip

(バージョンはインストールしたものを使ってください)
(最近Pythonファイルに変わったっぽいけど手順は一緒のはず……知らんけど)

無理だったら、別途コチラからzipインストール。
github.com

UnityMeshSync_Blender_Windows.zipを解凍しておきます。

アドオンインストールをしていきます。
編集→プリファレンス

アドオン、インストール

そして先程解凍した、UnityMeshSync_Blender_Windowsフォルダ配下の該当Blenderバージョンのzipを選択。 今回はBlendr3.0がないので、一旦前バージョンのもの、blender-3.0.1を選択しておきます。
(ちゃんと動かなかったらBlender側のバージョンを上げたほうが良いかも?ちなみに2.93.7は動きませんでした)

チェックを入れて有効化

使い方

UnityのGameObjectメニューから、MeshSync> Create Serverを選択して、サーバーオブジェクトを作成。
(なかったらUnity開き直す、ダウンロードバージョンの見直しなどを行ってください)

MeshSyncServerがつくられたらシーンを実行。

同じPC上でBlenderを開き、同期したいメッシュを持つプロジェクトを開きます。

[アクティブツールとワークスペースの設定]タブの[MeshSync]パネルにある[AutoSync]ボタンをクリック

シーン上にメッシュが生成されれば成功です。

↓日本語で各パラメータの説明を見たい人はコチラ

docs.unity3d.com


まとめ

Unity上でのアニメーションをさせる場合Unityのアセットでアニメーションさせるツールも有るが、やはりBlenderなどのDCCツールには使い勝手が及ばない部分も非常に多い。
かといって、Unity上でちゃんと上手く動くかインポート作業を何度もするのも問題です。
そういったときにこのMeshSyncはを作るときに便利そうだなーと思いました。マル

【アドカレ】Timelineの拡張をわかりやすく解説してみる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
本記事はUnityゲーム開発者ギルドアドベントカレンダー2の22日目の記事です。

目次


UnityのTimeLineの拡張について


UnityのTimeLineの拡張をしたので分かりやすく説明をするために本記事を作成しました。

テキストを読み終えてないと TimeLineが停止するエディタ拡張(自作)を
参考にTime Line拡張の書き方の紹介をします。

TimeLineの拡張方法


タイムラインの拡張方法は主に2つあります。

1.Control Track
2.カスタムPlayable Track
です。

Control TrackはインターフェースをMonobehaviorに実装する方法なのですが、
拡張できるメソッドが再生開始、停止時、タイム設定しかありません。

今回はカスタムPlayable Trackについてのご紹介です。

そもそもTimeLineの構造


TimeLineの拡張は主に3つの要素で構成されます。
Track、Mixer、Clipです。

ClipとMixerと聞いて「あれUnityの他の機能でも同じネーミング使ってるところがあるぞ?」と気づかれた方さすがです。

実はUnityのAnimationもAudioも同じような形で保持されています。
Animation Mixer,Animation Clip
Audio Mixer, Audio Clip

実はどちらもUnityのPlayableという機能で実装されています。

Unityのさまざまな時間の関係する拡張機能はPlayableという形で作られています。
Userでも触れるように作ってあるため、ものすごく頑張ればTimeLineのような機能をユーザー自ら作り上げることも可能です。


TimeLineのアセットの中身


厳密なTimelineクラス設計までは見に行ってませんが、大体このような感じで考えれば想像がつきやすいと思います。

組み合わせとしては4つのクラスでTimeLineは構成されています。
(厳密にはGroupTrackというのがあったりするのですが割愛)

1.Trackの構造を表すPlayableAsset
2.Trackの再生方法を表すPlayableBehaviour
3.Clipの構造を表すPlayableAsset
4.Clipの再生方法を表すPlayableBehaviour
です。

今回は仮にそれぞれ
1.PlayableTrack
2.PlayableMixer
3.PlayableClipAsset
4.PlayableClipBehaviour
と呼ぶことにします。

PlayableTrack


まずPlayableTrackScriptableObjectを元に作られています。
[ScriptableObject→PlayableAsset→Track Asset→(そしてユーザーがoverrideして作る)PlayableTrack]

そこに Playable という struct を持ちます。
これが Mixer です。 MixerPlayableBehaviour を元にして作られます。

(mixerタイポってる……)

PlayableTrack 自体はTrackを表すのでSerializeFieldに変数を実装するとインスペクターに設定項目が現れます。

ちなみにBaindingsの設定もPlayableTrackアトリビュートを設定することでアタッチ出来るものを設定できます。

PlayableMixer


PlayableTrack にてCreateTrackMixer という関数をオーバーライドしてScriptPlayableCreateの処理を行いMixerを作ります。

ScriptPlayableCreateの処理にてMixer の元になるクラスを渡しています。
PlayableMixerPlayableBehaviourを継承…オーバーロードで作ります。

PlayableMixerはインスペクターから設定できる箇所がないので、
Mixerで色々参照したい時は基本的にCreateTrackMixerをする際にTrackから値を貰ってきます。

PlayableMixermixingを行うクラスです。
Clip同士のMixを行います。
また、Clipのクラスではクリップ内の再生制御ができないのに対し、Mixではクリップ外の制御ができます。

(↓こんな感じで表示される)

PlayableClipAsset・PlayableClipBehaviour


PlayableClipAssetPlayable Track同様Playable Assetにて構成されています。
PlayableClipTrack Asset(Playable Track)に配列で保持されます。

PlayableClipBehaviourPlayableMixerとほぼ同様です。
ScriptPlayableCreateの処理にてClip の元になるクラスを渡しています。
PlayableBehaviourを継承…オーバーロードで作られます。

Clipではクリップ内の再生制御しかできないので、
基本的には処理内容はPlayableMixerに記述しPlayableClipAssetPlayableClipBehaviourでは動作に最低限必要な内容を記載します。

PlayableClipAssetにはインスペクターの内容が記述可能です。


実際にスクリプトを書いてみる


実際に私が作ったスクリプトを簡単に解説しようと思います。

PlayableTrack


Trackの構造を表すPlayableAsset

Script


NobelPlayableTrack.cs

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace SimpleNovel.TimeLine
{
    [TrackColor(0.05f,1.00f,0.71f)]
    [TrackClipType(typeof(NobelPlayableClip) )]
    [TrackBindingType(typeof(GameObject))]
    public class NobelPlayableTrack:TrackAsset
    {
        public GameObject novelPrefab;
        public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
        {
            var mixer = ScriptPlayable<NobelPlayableMixer>.Create(graph,inputCount);
            mixer.GetBehaviour().NovelPrefab = novelPrefab;
            return mixer;
        }
    }
}

Sctiptについての解説


ここでは先ほども解説した通りTrackの構造定義とPlayableMixerの生成を行います。
CreateTrackMixer にて直接クリップを取得してくることも可能です。

今回はミキサーにてテキスト表示のアセット基本となるPrefabを前もってPlayable へ渡しています。
PlayableClipAssetPlayableTrack の紐付けはアトリビュートに書くことで紐付けができます。

PlayableMixer


Trackの再生方法を表すPlayableBehaviour

Script


NobelPlayableMixer.cs

using Cysharp.Threading.Tasks;
using SimpleNovel.NovelEvent;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;

namespace SimpleNovel.TimeLine
{
    public class NobelPlayableMixer:PlayableBehaviour
    {
        public GameObject NovelPrefab;
        private GameObject _novelParent;
        private GameObject _tempObject;
        private NovelEventMediator _mediator;
        private int _activeIndex = -1;

        public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            _novelParent = (GameObject)playerData;
            if (_activeIndex >= 0)
            {
                var inputPlayable = playable.GetInput(_activeIndex);
                if (inputPlayable.GetPlayState() == PlayState.Paused && inputPlayable.GetTime() > 0f)
                {
                    //何もしない
                }
            }

            var presented = false;
            var inputCount = playable.GetInputCount();
            for (var i = 0; i < inputCount; i++)
            {
                var inputWeight = playable.GetInputWeight(i);
                if (!(inputWeight > 0f)) continue;
                if (_activeIndex != i)
                {
                    _activeIndex = i;
                    var inputPlayable = (ScriptPlayable<NobelDataBehaviour>)playable.GetInput(i);
                    NobelDataBehaviour input = inputPlayable.GetBehaviour ();
                    BeginClip(_activeIndex,input);
                }
                presented = true;
                break;
            }

            if (presented || _activeIndex < 0) return;
            // クリップのないフレーム
            EndClip(_activeIndex,playable);
            _activeIndex = -1;
        }


        private void BeginClip(int clipNum,NobelDataBehaviour behaviour)
        {
            
#if UNITY_EDITOR  
            if (!EditorApplication.isPlaying)
            {
                return;
            }
#endif
            if (NovelPrefab == null) {
                return;
            }
            _tempObject = Object.Instantiate(NovelPrefab, _novelParent.transform);   //ノベルを作成
        
            //ノベルの値設定スクリプトに値を入れていく
            _mediator = _tempObject.GetComponent<NovelEventMediator>();
            _mediator.luaScript = behaviour.luaScript;
        }

        private async void EndClip(int clipNum, Playable playable)
        {
#if UNITY_EDITOR  
            if (!EditorApplication.isPlaying)
            {
                return;
            }
#endif
            playable.GetGraph().GetRootPlayable(0).SetSpeed(0); //TimeLineの再生を止める
            await UniTask.WaitUntil(() => _mediator != null && _mediator.GetPlayed());      //テキスト読んでる途中?
            
            if (_tempObject != null){
                Object.Destroy(_tempObject);    //ヒエラルキーに残りっぱなしは嫌なので消しとく
            }
            playable.GetGraph().GetRootPlayable(0).SetSpeed(1); //TimeLineの再生を再開
        }
        
        
        public override void OnGraphStop(Playable playable)
        {
            if (_tempObject != null){
                Object.Destroy(_tempObject);    //ヒエラルキーに残りっぱなしは嫌なので消しとく
            }
        }
    }
}

Sctiptについての解説


ここではTrackの再生制御、Mixを行います。
各Clipの初回フレームにてInput情報を取得しClipに渡しています。
ちなみにluaSctiptはテキストのスクリプトです。

そして、そのままInstantiateを行いテキスト再生を開始します。
Clipの最終フレームにてテキスト送りが終わっていなかった場合にはTimeLineのスピードを0にしUniTaskを使って処理待ちしています。
テキスト送りが終わり次第TimeLineのスピードを元に戻します。

注意点としてはTimeLineの普通の再生でもInstantiate が行われてしまうのでクローンobjectばかりになることです。
そうならないように通常再生でなければ処理を中止させるように条件付きコンパイルを追加しています。
また、再生途中でエディタ停止しても残り続けるのでClipから出る時とOnGraphStop にてゲームオブジェクトをDestroyしています。

PlayableClipAsset・PlayableClipBehaviour


Clipの構造を表すPlayableAsset(PlayableClipAsset)
Clipの再生方法を表すPlayableBehaviour(PlayableClipBehaviour)

Script


NobelPlayableClip.cs

using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace SimpleNovel.TimeLine
{
    [Serializable]
    public class NobelDataBehaviour : PlayableBehaviour
    {
        [SerializeField]
        public TextAsset luaScript;
        
        public override void OnBehaviourPlay(Playable playable, FrameData info)
        {
            //うまく動かないことがあるのでミキサーで処理を行う
        }
    }
    public class NobelPlayableClip:PlayableAsset,ITimelineClipAsset
    {
        [SerializeField]
        private NobelDataBehaviour template;
        public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
        {
            return ScriptPlayable<NobelDataBehaviour>.Create(graph, template);
        }

        public ClipCaps clipCaps => ClipCaps.None;
    }
}

Sctiptについての解説


ここではClip内の再生制御(NobelDataBehaviour)とClipのデータ構造定義(NobelPlayableClip)です。

テキスト制御のluaSctiptを持っているだけで特に何もしていません。
強いていうなら、ClipのMixタイプの設定としてClipCaps.Noneを設定しているくらいです。

スクリプトについて総括


細かいことを言えばMarkerなどもあり他にも色々なことが出来るのですが、基本としてはこれで十分なはずです。

一見難しそうに思えるかもしれませんが、ルールに紐づいて
1.Trackの構造を表すPlayableAsset
2.Trackの再生方法を表すPlayableBehaviour
3.Clipの構造を表すPlayableAsset
4.Clipの再生方法を表すPlayableBehaviour
を作ればいいだけです。

気を付けるポイントとしてはクラス名が一部スクリプト名と合っていないとうまく挙動しないことでしょうか。
まぁ難しいことではないので、ぜひ皆さんもいろんな拡張を作ったら共有していただきたいです!
皆さんもレッツ拡張!!

まとめ


という感じでTimeLineの制御について書かせていただきました。
アドカレとしては大遅刻ですが、悪くない記事になったのではないかと思います。
最近はUnity公式からanime toolboxも公開されますますTime Lineの拡張を求められることが多くなってくると思います。
この記事が皆さんのお力になればと思います。

参考記事


www.youtube.com

UniRxはもう古い?使い勝手の良さそうなMessagePipeを勉強してみた

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
イベントが使えるUnityのライブラリが使いたくて色々調べていました。
Unityには標準でもイベントが扱えるものがありますが、寿命管理や使い勝手が少々微妙です。

他にはUniRxもありますが、Linq前提なのと使いこなせる方が意外と現場に少ない&グローバルなスコープには向いてないという現実です。
(一応UniRxにもMessageBrokerがあるんですけどね……)
そこで私はMessagePipeを勉強することにしました。

今回はその時の知見を共有したいと思い記事化しました。

目次


なぜ?イベントが使いたいのか?


そもそもなぜイベントが使いたいのかという話をさせていただきます。
それはゲーム自体が疎結合に作るのが難しいというのがあります。
結合性を下げるためにMessageを使えば良いのではないだろうかと思ったからです。

MessageやEventは昔からありつつも実装が見直されてきている感じがします。
そして現在主流になりつつあるあるのを感じます。
最近ではEvent中心の設計をイベント駆動型プログラミングと読んだりします。
UEなんかでも用いられます。

一般的に、グラフィカルユーザインタフェース (GUI) を使用するオペレーティングシステムアプリケーションソフトウェアでは、イベント駆動型プログラミングを利用している。マウス操作やキーボード操作といったユーザーからの入力や、システム状態の変化・変更といった各イベントに対する処理を統一的に記述することができます。(Wiki引用)

しかしながら安直なイベントはデメリットが有る


イベントは疎結合に利用できますが、どこから呼ばれているか分かりづらくしてしまうというデメリットもあります。
UniRxが基本クローズドなイベントに適した形になっているのもこれが理由だと思います。(しらんけど)

Unityの公式にも引用されている、ScriptableObjectを使った不思議なイベントシステムもありますが、特にコチラ↓はどこから呼ばれているか分からなくしている代表的な例だと思います。

unity.com

UnityAtomsというライブラリが上記のScriptableObjectを改良したものだったのですが、
私が使ってみたところ

スタックトレースをさせるために内部でC#のStackTraceの処理を呼び出していて(実機実行時は大丈夫だが)デバッグ時にイベントが走るだけで数msかかってしまう(場合によっては10msかかってしまう)
・使用するイベントが多くなるたびにエディタ上での参照が多くなって管理がし辛くなる(コーディングが苦手な人には向いてるかも?)
・ぶっちゃけ、UnityEventの上に載ってるので、そこまで早くなく、UniRxのMessageBrokerの方が良さそう

という印象でした。

MessagePipeは良いのか?


github.com

ある程度習熟度がある方なら良いと個人的には思います。
MessagePipeが解説されている記事が少ないですが、ソレには理由があると考えます。

ライブラリ前提で組まれている

MessagePipeはDIライブラリ前提で組まれています。
DIについては割愛しますが、MessagePipeがDIライブラリを使うのには理由があります。
通常パブリックにイベントを使う場合、どこから呼ばれているかわからないというデメリットが生じます。
そこで、DIを使って依存性が注入されるタイミングや寿命を管理してしまうことで期間的にも制限が加わるので多少はスコープが大きくなりすぎてしまうデメリットを軽減できます。

しかし、DI自体を理解しないまま実装するとスコープも大きくなってしまう可能性もあり、
更にインスタンスは増大する可能性もあります。
そうなってしまうと正直良いことがありません。

さらにUniTaskというライブラリも導入が必須です。
正直導入ハードルが高く、解説をするほどユーザー数は少ないのだと思います。

また、DIを高度に利用できているようなユーザーは説明を受けなくても理解できてしまう。
しかもDIライブラリ、Zenjectなどにもイベント機能が入っており、そちらの利用でも十分だったりします。

なので、MessagePipeが解説されている記事が少ないんだと思います。
とはいえ、現状Unityでイベント駆動をするならこのライブラリが最強だと言わざるを得ないでしょう。

(Unity公式のイベントもっと頑張れよ!!!!!小声)

さっそくインストールしてみる


使用ツール


Unity2021.3.1f1
MessagePipe:Ver1.7.4
VContainer: v1.11.1
UniTask:Ver2.3.1

次に基本の実装をしていこうと思います


それぞれライブラリのインストールが済んだら、基本の形になるサンプルを実装してみようと思います。
ちなみに今回は一緒につかうライブラリのVContainerやUniTaskの解説は行いません。

MessagePipeで使える種類


メッセージにも様々なものがあります。
細かいオプションはありますが主に3つ。
どんなものがあるのか一つずつ簡単に説明していきます。

1.送り手と受け取り手(Publish/Subscribe)


送る方と受け取る方を定義して、間をつなぐのをDIが行います。

お互いがつながったところで、
送る方が条件を満たしたらDIされたオブジェクトへPublishします。
(Event発行はライブラリによって呼び方様々ですよね。SendだったりPublishだったりBroadcastだったり)

受け取る方はDIされたオブジェクトから送り手からの通知を読んで、反応をします。
終わり。

この形をPub/Subと呼びます。

で、DIがお互いをつなぐ条件は何かというとですが、Publishする型です。
しかし、コレでは頻繁にPublishされた際にどれがどれに対応しているか不明です。
そこでキー付きにできます。Guidをつけることで簡単に見分けられる!

2.イベントファクトリー


Pus/Subで通知が出来るってのはわかる……。
スゲーよくわかる送信と受信だからな……。
だが、Publishの条件が『型』っていうのはどういう事だぁ~~~~っ!?
いくらクラスがつくれるからってよ、intとか同じ型で別のメッセージやり取りしたい、型ごとにグループ化されていないことって沢山あるじゃねーか!!
それごとにクラスやキーを作れるかつーのよ――――ッ!
ナメやがって!!超イラつくぜェ~~~ッ!!チクショ――ッ!!
ってギアッチョ並にブチ切れた人が居るかもしれない。

安心して欲しい。
イベントファクトリーというものがある。

通常のPub/Subでは、DIにより、型でワイヤリングされていますが、
EventFactoryからイベントの個別のインスタンスを作り出すことが出来る。

言うなればC#のEventと似た機能らしい。
じゃあ、C#のEventでいいじゃんと思うかもしれないが、C#のEventより多機能。
一つ気をつけないといけないことは必ずDisposeしなければならないということ。
無視するとリークする。

コードの書き手によるので、うっかり破棄が漏れることもある。
その辺はアナライザがあるのでソチラを活用してほしいとのこと。

3.リクエスト/レスポンス/オール


個人的に一番オススメしたい機能。

オブザーバー デザイン パターンだけでなくメディエーターパターンで作りたいなら通知に対する返信が欲しくなる。
そこで利用するとよいのがリクエスト/レスポンス/オールだ!

Mediator(仲介者)を介してメッセージのやり取りを行う機能。
これであればいろんなところから参照したい場合にMediatorを起点(エントリーポイント)に考えやすい。
特にリクエスト/レスポンスは1対1なので、追いやすい。(レスポンスは早いものがち?)

2のイベントファクトリーでもエントリーポイントは作成できたが、投げっぱなしで、
現在どこで何の処理が行われているか見失いがちになる気がする。
そこで処理に対する結果を工夫して返却することで安心して通知処理出来るということだ。
(なんというか、イベントファクトリーとかがUDPだとするとリクエスト/レスポンス/オールはTCP的に組めるよね)

オールは1対多の通知を行う。
敵キャラに対して全滅処理を行うなどの実装が楽そうだ。

実際に書いてみる


(ネームスペースの有る無しは適当なので見逃してください)

1.送り手と受け取り手(Publish/Subscribe)(キーなし)


まずはスタンダードなPublish/Subscribe。
ぶっちゃけコレしか使わないならMessagePipe使うメリットはあまり感じない。

スクリプトアタッチ情報

コード

Test1Publish .cs

using MessagePipe;
using UnityEngine;
using VContainer;

public class Test1Publish : MonoBehaviour
{
    [Inject] private IPublisher<int> _publisher;
    private int _num;

    void Start()
    {
        _num = 0;
    }

    void Update()
    {
        if (!Input.GetKeyDown(KeyCode.Z))
        {
            return;
        }
        _num++;
        _publisher.Publish(_num);  //Zを押すとパブリッシュされる
    }
}

Test1Subscribe.cs

using System;
using MessagePipe;
using UnityEngine;
using VContainer;

public class Test1Subscribe : MonoBehaviour
{
    [Inject] private ISubscriber<int> _subscriber;
    private IDisposable _disposable;


    private void Start()
    {
        var d = DisposableBag.CreateBuilder();
        _subscriber.Subscribe(num =>
        {
            Debug.Log(num + "Subscribe");  //Subscribeに成功するとDebugLogに表示
        }).AddTo(d);
        _disposable = d.Build();
    }
    
    void OnDestroy()
    {
        // 破棄されるタイミングでOnAttackイベントの購読をやめる
        _disposable.Dispose();
    }
}

Test1LifetimeScope.cs

using MessagePipe;
using VContainer;
using VContainer.Unity;

namespace Scenes.Test1
{
    public class Test1LifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            var options = builder.RegisterMessagePipe(/* configure option */);
            builder.RegisterMessageBroker<int>(options);
        }
    }
}

結果

2.イベントファクトリー


多分モノビヘイビアに書いても使えるんだろうけどピュアスクリプト側に持たせてみた。
Unity側の依存を減らせるのは嬉しい。
正直これだけだとPublish/Subscribeと変わらなないように見えるが、
Test2Publish で生成されている eventFactory に紐づいて定義されているので、
Test3Publish などを作った場合も別々に通知される。
+=, -= で購読を管理出来るようですが、やり方が微妙にわからなかったので誰か教えて。

スクリプトアタッチ情報

コード

Test2Publish.cs

using System;
using MessagePipe;
using UnityEngine;

public class Test2Publish : VContainer.Unity.IStartable,IDisposable,VContainer.Unity.ITickable
{
    IDisposablePublisher<int> tickPublisher;

    public ISubscriber<int> OnTick { get; }

    public Test2Publish(EventFactory eventFactory)
    {
        (tickPublisher, OnTick) = eventFactory.CreateEvent<int>();
    }

    int count;
    public void Dispose()
    {
        // You can unsubscribe all from Publisher.
        tickPublisher.Dispose();
    }

    public void Start()
    {
        OnTick.Subscribe(x =>
        {
            Debug.Log(x + "Subscribe");
        });
    }

    public void Tick()
    {
        tickPublisher.Publish(count++);
    }
}

Test2LifetimeScope.cs

using MessagePipe;
using VContainer;
using VContainer.Unity;

namespace Scenes.Test2
{
    public class Test2LifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            var options = builder.RegisterMessagePipe();
            builder.RegisterMessageBroker<int>(options);
            builder.RegisterEntryPoint<Test2Publish>(Lifetime.Singleton);
        }
    }
}

結果

3.リクエスト/レスポンス/オール

さっき(の2で)はピュアスクリプトにもたせたので今回はモノビヘイビアにもたせてみる。


スクリプトアタッチ情報

コード

Test3Presenter.cs

using System;
using MessagePipe;
using UnityEngine;

public class Test3Presenter : VContainer.Unity.IStartable
{
    IRequestHandler<int, bool> requestHandler;

    public Test3Presenter(IRequestHandler<int, bool> eventFactory)
    {
        this.requestHandler = eventFactory;
    }

    public void Start()
    {
        var pong = this.requestHandler.Invoke(1);
        Debug.Log("PONG" + pong);
    }

}

Test3RequestHandler.cs

using MessagePipe;
using UnityEngine;

namespace Scenes.Test3
{
    public class Test3RequestHandler: MonoBehaviour,IRequestHandler<int, bool>
    {
        public bool Invoke(int request)
        {
            Debug.Log("PONG" + request);
            return true;
        }
    }
}

Test3LifetimeScope.cs

using MessagePipe;
using VContainer;
using VContainer.Unity;

namespace Scenes.Test3
{
    public class Test3LifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            var options = builder.RegisterMessagePipe();
            builder.RegisterRequestHandler<int,bool,Test3RequestHandler>(options); //モノビヘイビアオブジェクトNewするとWarningで怒られちゃうけど、いいんかな?
            builder.RegisterEntryPoint<Test3Presenter>(Lifetime.Scoped);
        }
    }
}

結果

モノビヘイビアを持つクラスをnewするなと怒られてしまったが、成功。
(Warning回避する方法ってあったりするのだろうか……?)

今回説明を割愛したもの


簡単に一言で説明します。

ISingleton***、IScoped***


寿命やスコープが管理できる機能。
あると便利だよね

Buffered


最後にPublishしたものをSubscribeで取得できる仕組み。
通常のメッセージとは違う使い方でRx的な使い方だったので割愛した。

Subscribe Extensions


メッセージにフィルター機能をつけて、Publishを避けたり
メッセージの変換処理を作ったり出来る。
上級者向け機能だったので解説は割愛。

Filter


メッセージの呼び出しにフックしてログを出したり出来るらしい。
よくしらんけど。

Managing Subscription and Diagnostics


デバッグ用機能。
呼び出された回数などを監視できる。

IDistributedPubSub / MessagePipe.Redis,InterprocessPubSub、IRemoteAsyncRequest / MessagePipe.Interprocess


正直よーわからんかった。
ちゃんと読めば分かると思うがネットワークに紐づく機能っぽかったので割愛。

MessagePipeOptions


MessagePipe 自体の動作の構成オプション設定。
メッセージのライフタイムの設定とかが出来たり、DI側の色々設定。
ISingleton***、IScoped***とも紐づいてくるし様々だったので割愛。

使い込むなら公式読んだほうが良いかも。

まとめ

とこんな感じで様々な形でイベントを通知出来る仕組みが存在する。
グローバルも存在するが、正直グローバルで使うのはコントローラーのインスタンスをInjectするぐらいだろうので説明は割愛した。(正直グローバルはあまり使わないほうが良いとは思うし)
個人的にはEventFactoryで作ったインスタンスをDIでInjectを出来るようになって欲しい。(やり方あんのかな?よく分からん)
またリクエスト/レスポンス/オールのEventFactoryも欲しいと思った。
まぁソレが出来てしまうと一見どこにどのインスタンスが紐づいているのかコード上からソースを追うのが難しくなってしまう側面もあり、
実際に実装されても特定のパターンでしか使わないと思うので、単純に私のワガママに過ぎないのかも知れない。

まだまだ拡張できそうな実装だが、シンプルに実装する分には分かりやすく、
レイアリングには適したライブラリだと思います。
メッセージングライブラリで個人的に一番しっくり来たライブラリです。

みなさんが使い始め、利用法が色々広がれば、きっとUnityでの利用改善も進んで行くと思うのでぜひ皆さん使ってみてください。
以上、やまだたいしでした。

UnityでRoslyn触ってみた

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
先日GitLabをつかってCIを走らせるようにしたのですが、さらに便利にしたいと思い
Roslynを触ってみることにしました。
本記事はその内容の備忘録です。

目次


そもそもRoslynとは?


NETコンパイラープラットフォーム(.NET Compiler Platform)のことです。
コード名および通称は「Roslyn(ロズリン)」。(以下、Roslynと表記)
簡単にいうとRoslynはIDEコンパイル時の情報を提供してくれるAPIみたいなものらしいです。
もらえる情報は構文解析・意味解析を行った情報っぽい?
ツールとエンド ユーザーが豊富な情報コンパイラを共有するためにある様子。

具体的にそれを通じて何が出来るようになるのかというと "アナライザー" と "コード修正機能" を構築できるようです。
後、ソース生成も。

UnityでRoslynを使うメリット


Unityを開いた時などにコードの違反を見つけたりが出来ます。
例えば、「UniRxを使ったときは必ずSubscribeにAddToをつける!」というプロジェクトルールを作りたい場合などに適しています。

初心者にわかりやすく、追加する意義を解説をします。

リーダブルコードを読んだことありますでしょうか?
いってしまえば読みやすく良いコード書こうぜ!って話なのですが。
なぜ可読性を良くする必要があるのかというと可読性の悪いコードはソースコード品質が悪くなりがちだからです。

品質の悪いコードはコードスメル(コードの臭い)と言われ嫌われます。
ヤツ(コードスメル)は駆逐しなきゃぁいけません。

とはいえ、ゲーム業界は好き勝手につくりたい人が多く、
コードスタイルの統一を極度に嫌う人が居ます。

また、コードレビューでさえ嫌う人がいます。
「コードの指摘」は「コードを作成した本人の否定」ではないにも関わらず、
混同し「意地悪されている」と勘違いしてしまう人が一定数いるようです。

特に「コードフォーマットの指摘の重要性を理解していない人」は
「コードフォーマットの指摘をする人」を「プレゼン資料の荒探してる人」と同じように見てしまう。

「コード指摘する側」は「キレイなコードを書いて欲しいと思うのに書いてくれない」と思い
「コードを指摘される側」は「好きにコード書きたいのにちょっとしたことで指摘されコードを好きに書かせてくれない」と
思うことになります。
そのようになってしまうと、対人的な衝突を生み、互いが辛い思いする羽目になってしまいます。

そのため、コード規約は大事だと理解していながら
コード指摘によって対人的な衝突を生んでしまうことを忌避し
コードスタイルが悪くても放置されてしまうことが多々あります。

これではコード品質が落ちてしまいます。ジレンマ。

であれば、どうすれば良いのか。
コードフォーマットの指摘は機械(CI)にやって貰えば良いんです。

また、晒されるようにSlackで通知が飛ぶようになれば、必然的に皆、治すようになると思います。

そういったことをするために、このRoslynは有効なのです。

もちろんUnity上だけでなくIDE上でも利用可能ですがUnityのビルド機能を使ってCIで通知を出すならRoslynを使うのが便利です。
(Unity上でつかうならUnity 2020.2以上)

Roslynの基礎的な使い方はコチラが参考になるかも?
swet.dena.com

コード指摘について熱く語っておきながら、残念。今回はSource Generatorを試します。
(コード指摘させるのは本記事のまとめにも軽く触れていますが、他の方が記事を作ってらっしゃったりするのでソチラを読んだほうが良いです)

使い込めばゲーム中に動的にコード生成が出来るのでDIのような挙動が作ろうと思えば作れるはずです。
かなりFrameworkライクな機能だと思いました。

そもそも今回触るSource Generatorが
アップルさんが「動的コード生成駄目」って言うんもんだからRoslynチームが開発したものだそう
なかなかどうしていい感じ。

実際に入れてみる


環境:
Unity2021.3.11f1
Rider:2022.2.3

今回はUnity公式で紹介されているCodeAnalysisを使ってみようと思います。

docs.unity3d.com

今回はJetBrains商品のRiderで入れてみます。
Riderの新規作成(New Solution)でプロジェクトを作成します。

ヘッダメニューのTools>NuGet >Hide Nuget Tool Windowを選択(フッタにあるならソレ選択でもOK)

CodeAnalysis.CSharpを検索します。
Unity2021.3で使えるアナライザーのVersionは3.8のようですので、3.8を選択します。
そして+を選択します。

Unity公式に載っている、ExampleSourceGenerator.csを作成。

そしてリリースを選びBuildを行います。

次にUnityでCodeAnalysisを使えるようにするためにdllをUnityに追加します。

JetBrainsで追加した場合は以下にdllファイルが出来ていると思うので、ソチラを Unity プロジェクトの Assets フォルダー内にコピーします。

dllが出来る場所:(Project名)\bin\Release\netstandard2.0

コピーしたらUnity上でコピーしたdllファイルを選択しInspectorにて各プラットフォームのチェックを外します。
そしてApply

AssetLabelsをクリックして、ラベルマークよりRoslynAnalyzerタグを追加。

↑でエンター

Unity公式に載っている、HelloFromSourceGenerator.csをAsset配下に作成。

で、適当なGameObjectにスクリプトを貼り付けて再生。

できた

他にもライブラリがたくさん


今回使ったソースジェネレーターだけでもいくつかライブラリがあります。
(全部Unityで使えるとは限らないけれど)
↓ライブラリまとめ
github.com

Roslynベースでも色々あります。
↓ライブラリまとめ
github.com

まとめ


今回はRoslynについては以上にします。
本当はRoslynとUnityでStyleCopを触るところまでやったのですが、
Unityは現在EditorConfigやAdditionalFilesをいい感じに処理してくれず思った通りな挙動をしなかったので割愛です。
(デフォルト設定で動かすぐらいなら難なくできます)

厳密に言うとAdditionalFilesは コチラ などで導入すればある程度は処理されるのですが、
「StyleCopで特定ディレクトリのファイルのみ検査させるように」&「AdditionalFilesの設定を適切に読み込む」という2つの条件を満たそうとすると上手く動かない。
特定のディレクトリだけで動かす方法は現在 こちら で紹介しているasmdefの方法がありますが、
前述の拡張の挙動との兼ね合いを考えると無理なのである……。残念。

私個人としてはUnityのエディタスクリプトでRoslynのAdditionalFilesを指定できる部分があったので
「そのへんを頑張れば、なんとか出来る可能性があるかも」とは思いましたが、
そこまでやる気にならなかったので、後はやる気がある人に任せます……。

後、触ってみた所感、
今後SourceGeneratorを使ったUnityでいい感じのゲームFrameworkが出てきそうだと思いました。
私はFrameworkを作る側の人間じゃないのでガッツリは触らないと思いますが、
なにかおもしろい利用方法を浮かんだ人は是非教えてほしいです。

以上、UnityでRoslyn触ってみたでした。

UnityでCI Build。GitLabRunner触ってみた!

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
CIに手を出したいのですが色々考えた結果GitLabCI+Runnerが良さそうだと思ったので今回触ってみました。
今回はその時の知見を共有したいと思い記事化しました。

目次


そもそもCIとは


CI(継続的インテグレーション)の略。
本記事で言うCIはCI環境のこと。

CI環境とはビルド・テスト・デプロイという一連のソフトウェアの開発サイクルを短いスパンで行う環境のことです。
その環境を提供してくれるツールをCIツールと言います。

その数多くあるツールのうちの一つのGitLabCI+Runnerの組み合わせが今回良かったように思えたので紹介です。

CIを使うメリット


Unityなどでビルドした方は分かると思いますが、 ビルドには結構な時間がかかります。
その上、ビルドしたらビルドしなかったときとは違うエラーが出てしまうことも少なくありません。
バグを出さないためには高頻度にビルドを走らせ、エラーが出ないかチェックしたほうが良いのですが、
その間ビルドでPCを占拠してしまうのは勿体ないです。
かと言ってビルドせず最後まで放っておくと修正が大変になってしまいます。

そこで登場するのがCIです。

CIツールを使ってサーバー上や別PCでビルドを走らせることで、早くエラーに気づけるようにしようという試みが行えます。

なぜGitLabCI+Runnerなのか


UnityでのCI環境はいくつかあります。
私が知っているものがいくつかあります。

・Unity Cloud Build
Github Actions
・Jenkins
・GitLabCI
・CircleCI
・codemagic
・GitLabCI+Runner

この中でGitLabCI+Runnerを選んだ理由は一番使い勝手が良さそうだったからです。
具体的に他CIを選ばなかった理由も含めて説明を書いて行こうと思います。

Unity Cloud Buildが適さない理由


小規模プロジェクトであればUnity Cloud Buildが有効だと思います。
unity.com

しかし、Windowsでのbuildが$0.02Build minと有料です。 できれば無料でやりたいし、有料なら他ツールでも良さそうです。
後最近は大分良くなってきているようですがカスタイズ性に優れていない感じあります。

Github Actionsが適さない理由


Github Actionsを使ったCIには有名所の設定はGameCIというものがあります。
こちら企業でも使われているものなんですが、 Github Actions無料枠の場合2000分/月の無料枠&スペックそんな良くないので時間かかりがちのため
適さないと考えています。
(有料枠ならありだと思います)

Jenkinsが適さない理由


コチラはカスタマイズ性に優れていますが、こちらはサーバーの機能はないため別途ビルド用のPCをセットアップする必要があります。
過去にセットアップする記事も私が上げているのですが、設定時に2つのPCを頻繁に行き来する必要があるのと、設定が意外と面倒だったので、今回は除外です。
しかしながら、カスタマイズ性が高いため、企業では今もよく使われます。

GitLabCIが適さない理由


コチラは5GB storageとチョット容量が少なめです。
また、1 か月あたりの転送量も10 GBと少なめで大量にアセットを使うゲームでは適しません。
また、オンプレ版でないとLicence認証あたりで躓いてしまいます。

CircleCIが適さない理由


CircleCIはコレまで候補に入れていなかったのですが、
2022年に入ってから、CircleCIが無料枠を大幅拡大し、1カ月あたり6000分のビルド時間が得られるようになりました。
候補に入れるのはありだと思いました。

これまでは無料枠は1ヶ月あたり1000分のビルド時間まででGithubActionsより少なく問題だったのですが、
GithubActionsより大幅にビルド可能時間が伸びたのは嬉しいですね!

Macでの無料buildはRAMが8GB、DiskSizeが200 GBですがストレージがデフォルト2GBで
30,000 クレジット無料で使えますが正直ソチラを利用してもストレージがデフォルト2GBの関係上、残念な価格になりそうです。
バイナリをたくさん使っているプロジェクトだと余裕で超えてしまいます。

www.publickey1.jp

codemagic


codemagicを使う。
FREE版ではmacOS M1 Standard VMが使えるらしい。
サイズはなんとDisk 322GB (Free Space: 122GB)!でかい!
色々入ってない状態での話でXCodeなどが入るともう少し容量は小さくなってはしまうらしいが……。
XCodeがプリインストールされたものはディスク294GB (Free Space: 31GB)ほどらしい。(それでも十分)
月当たり500minのbuildが出来るらしい……。
buildのタイムアウトは120min。

がしかし、Unity Plusもしくは Unity Pro Licenceじゃないと使えないという欠点がある。
稼げるようになったら使いたい……。

GitLabCI+Runner


ということで、GitLabCI+Runnerの組み合わせで使うことにした。
ところでRunnerとは何かというとGitLab Runnerのことで、通常GitLabCIではGitLabが構築されたサーバーなどでのbuildが行われますが、Runnerはジョブをセルフホストで行えるものです。

docs.gitlab.com

GitHubActionsにも似たような機能がありGithubActions+Self-hostedRunnerで構築しても良いのですが、GitLFSを使いたいと考えた場合にGithubでは1GBまでしか無料でGitLFSを使えません。
GitLabならばRepositoryサイズトータル(LFS含めて)で10GBまで利用可能なので、ソチラを使ったほうが良いという判断です。
またGitLab+GithubActions+Self-hostedRunnerやGithub+LFSリポジトリサーバー+GithubActions+Self-hosedRunnerなどの組み合わせも存在しそうですが、やはり構成が簡単なGitLab+GitLabIC+Runnerが良いと判断しました。

環境


今回は

Unity2021.3.11f1
GitLab
Windows ローカルホスト

での利用を想定。

作業手順目次


早速、以下手順で導入をして行こうと思います。

(GitLabへの登録・初回コミットは済んでいる想定)

1.Runnerのダウンロード
2.Runnerの登録
3.実際にRunnerを使ってみる
4.Unity側のスクリプト
5.yml(ビルドパイプライン)の設定
+α 6. Slackへの通知
+α 7. deployGateとの連携(理想)

1.GitLabRunnerのダウンロード

今回はWindowsでの利用を想定しています。

Runnerを動かしたいビルドサーバー(Windows)にてRunnerのダウンロードを行います。
以下リンクよりダウンロードを行います。
docs.gitlab.com

Install on Windows を選択
私の手元のPCはWindows10の64bitなので64bitを選択

ダウンロード。

パワーシェルを管理者権限で立ち上げ、先程インストールした gitlab-runner.exe (ダウンロードした物によって名前が違います。以後は私の環境で行うので gitlab-runner-windows-amd64.exe とします)を指定してインストールする。
コマンド的にはこんな感じ

.\gitlab-runner-windows-amd64.exe install


↓のようなメッセージが出てきたらそれは管理者権限じゃないので昇格します。

FATAL: Failed to install gitlab-runner: Access is denied.
## ちなみに昇格コマンドは↓
Start-Process powershell -verb runas

 

Runtime platform arch=amd64 os=windows pid=xxxxxxx revision=xxxxxx version=15.4.0

などのようなメッセージが問題なく表示されてれば多分OK

2. GitLabRunnerの登録

GitLabRunnerが問題なく動いたのを確認したら、Runnerと実際のRepositoryを紐づけるためにGitLabにRunnerを登録します。

↓参考
docs.gitlab.com

Runnerは様々な単位で登録出来るのですが今回はプロジェクトと直接紐づけたいと思います。
(ちなみに他には共有ランナー、グループ ランナーという単位があります)

まず、GitLabのプロジェクトRepositoryをブラウザで開きます。

次に左のサイドバー→設定→CI/CDを選びます。
(CI/CDの項目がない人は 設定→一般→CI/CDのToggleをオンに切り替える)

Runnerの項目を見ます。

そうしたら 1. で開いたパワーシェル画面で、

.\gitlab-runner-windows-amd64.exe register
を実行

そうするとURLを登録する旨が出てくるので、ブラウザのRunnnerのSpecific runnersというこうもくの 2. に表示されているURLを打ち込みます。
ちなみに今回はオンプレじゃないウェブサービスのGitLab使ってるので https://gitlab.com/ を入力。

次はtokenの入力を迫られるので、ブラウザに出ているtokenをコピペします。
ランナーの説明を入力します。
これはブラウザ側でも後で変えられるそうなのでテキトーに。

タグをつけろと言われますコチラもテキトーに(私はWindowsというタグにしました)

ランナーのメンテナンスメモの追加テキトーに

どの形式で実行するかを聞かれます。
今回は直接PCで実行したいので shell と入力します。

これでようやく登録完了です。

ブラウザを再起動すると追加したランナーが表示されているはずです。

3.実際にRunnerを使ってみる


今回用意したRunnerは specific runner と呼ばれるものになるのですが、実際に使うためにはyamlに追加記述が必要になります。

といっても tags の項目を追加し、どのランナーで起動するか指定するだけです。
タグなしで実行する方法もあるにはあるのですが、今回は割愛。

Window11の場合、コマンドを実行しても以下のようなエラーが出て実行できません。

Job failed (system failure): prepare environment: failed to start process: exec: "pwsh": executable file not found in %PATH%. Check https://docs.gitlab.com/runner/shells/index.html#shell-profile-loading for more information

理由は.Net版のパワーシェルPowerShell Coreが入っていないためです。
Windowsで使われている標準のPowerShellWindows PowerShellと呼ばれるものです。

.Net版のパワーシェルをダウンロードして解決しても良いのですが、Win11にあった挙動ではない可能性もあると考え今回はPathを修正する様にします。
Runnerのデフォルトシェルは、Windows OSに作成したRunnerフォルダーの下に自動作成されているファイル config.toml を編集することで変更できます。
(.\gitlab-runner-windows-amd64.exe installした時に生成されてるはず)

runners セクションにあるパラメーター shell を pwsh から cmd (全部小文字)に変更することで解決します。
(PowerShellでやっても良かったのですが、ymlの構文解析が一部上手く動かなかったので今回はcmdで対応)

プロセスをスタートします。

.\gitlab-runner-windows-amd64.exe start

動かしたら
今回は一旦build Testと表示させるだけのものを動かします。
リポジトリ直下に以下のようなファイルを作成。

.gitlab-ci.yml

stages:
  - build
prepare-job:
  stage: build
  tags:
    - Windows #Runnerでつけたタグ名
  script: 
    - echo "build Test"

ファイルをアップロードすると .gitlab-ci.yml が読み込まれ既に起動してあるCIが反応して実行されます。

一時停止マークが表示されて動かない場合

ランナーが上手く登録できてないかtagsの設定が間違っており、実行できるRunnerがない状態だと思われます。(もしくはジョブを待っているマシンがネットワークから切れたとか……?)

.\gitlab-runner-windows-amd64.exe restart

を行う、ウェブ側でtagの設定をし直すなどすれば直ると思います。

4.Unity側のスクリプト


コチラは通常のビルドスクリプトで大丈夫です。
以前↓の記事で紹介したJenkinsで使ったものを流用しようと思います。

orotiyamatano.hatenablog.com

今回もHTML5でのBuildのスクリプトです。

using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

    public static class BuildScript
    {
        private static void Build()
        {
            bool isSuccess;
            const string path = "/../output";
            FolderCreate(Application.dataPath+path);

            PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.WebGL, "");
            // ビルド処理
            BuildPlayerOptions buildOption = new BuildPlayerOptions
            {
                options = BuildOptions.None,
                scenes = GetAllScenePaths(),
                target = BuildTarget.WebGL,
                locationPathName = Application.dataPath+path,
            };
            UnityEditor.Build.Reporting.BuildReport reports = BuildPipeline.BuildPlayer(buildOption);
            isSuccess = (reports.summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded);
            

            Debug.Log(Application.dataPath + path);
            Debug.Log("isSuccess:"+ isSuccess);
            EditorApplication.Exit(isSuccess ? 0 : 1);
            
        }

        /// <summary>
        /// 出力先フォルダ生成
        /// </summary>
        /// <param name="path"></param>
        private static void FolderCreate(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
        }
        //GetAllScenePath
        private static string[] GetAllScenePaths()
        {
            var levels = EditorBuildSettings.scenes
                .Where(scene => scene.enabled)
                .Select(scene => scene.path)
                .ToArray();

            return levels;
        }


        [MenuItem("Build/Build Develop App")]
        private static void BuildApp()
        {
            Build();
        }
    }

↑のようなビルドスクリプトをエディターフォルダにいれてaddしてcommit・Pushします。

5.yml(ビルドパイプライン)の設定


まず、Buildを走らせるサーバーに同じバージョンのUnityをインストールしておきます。
そして、3で追加した、ymlを変更します。

.gitlab-ci.yml

stages:
  - build
prepare-job:
  stage: build
  tags:
    - Build #Runnerでつけたタグ名
  script: 
    - echo "build Test"
    - echo %UNITY_VERSION%
    - echo %CI_PROJECT_DIR%
    - chcp 65001 #そのままだとログが文字化けしてしまうので文字コード変える
    - '"C:\unity\%UNITY_VERSION%\Editor\Unity.exe" -quit -batchmode -projectPath %CI_PROJECT_DIR% -executeMethod BuildScript.Build -logFile -' #Unityのexeとプロジェクトのフォルダを選ぶ

UNITY_VERSIONは後から自由に変えられるように設定、CD/CIの変数にプロジェクトごとに変更します。

(ProjectSettingsフォルダのProjectVersion.txtの中身を見てきて自動的にUnityのダウンロードをするみたいなこともやろうと思えば出来るのですが、面倒だったので割愛)

変更し、PushするとBuildが走ります。
問題がなければエラーなく最後までBuildが行われるはずです。

+α 6. Slackへの通知


Slackから通知が受け取れるともっと楽になります。
SlackアプリとしてIncoming Webhookを追加し、ログを吐き出したいチャンネルを指定。
するとWebhook URLが出来上がるのでソレをコピーして、
GitLabのプロジェクトの設定→インテグレーション→Slack notificationsのWebhookに設定し変更を保存します。

するとログが出るようになるはずです。
また、SlackAppとしてGitLabを追加することで便利なコマンドも使えるようになるのでソチラも追加して置くと良いかも知れません。

+α 7. deployGateとの連携(理想)


暇があれば書きます。多分。知らんけど。

その他


出力ログ上限が小さいのでバイト数をあげておく。

config.tomlの設定を変えてリスタートすればOK
runnersの項目に以下を追加する
output_limit = 40960

参考
qiita.com

まとめ


以上で、GitLabRunner触ってみたを終わります。
Jenkinsより比較的設定が楽だった感じがします。
また、この仕組を利用すればUnity Test FrameworkやRoslynアナライザなどを使って詳しいテストをしてみるなど、
色々使用方法が広がりそうです。

GameSynth使ってみた

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
サウンド(SE)を作るツールGameSynthが使いこなせれば結構良さそうだと思ったので今回記事化しました。

目次


GameSynthとは


効果音生成ツールです。
様々なゲーム会社で使われています。

GameSynth ゲーム効果音生成ツール | Tsugi

39,000円ほどでお高めですが、効果音アセットを毎回買うよりは安く済みますし、
まれに40%OFFセールとか半額セールとかやってるので、安いときを狙って買うのがオススメ。

機能紹介


様々な機能があり、プロシージャル(自動的・自動生成的)にサウンドを生成可能です。
風を切るような音のWHOOSH、打撃音のIMPACT、機械的なボタン音を生成できるRETRO
他にも、いくつも音を生成出来る機能があります。

ツールの見た目的に目を惹くのはWHOOSHやFOOTSTEPSで、
買う人もソレを目当てで買う人が多いと思います。
WHOOSH↓
www.youtube.com

しかし、個人的に一番魅力的だと思う機能は、一見見た目が地味なMODULARです。
コレまで、サウンドを加工する場合、重ねがけするような形で適用し、更に加工ということをされている方が多いと思います。

このGameSynthのMODULARは、ノードベースでパズルを組み合わせるように音の加工が可能です。
むしろ、WHOOSHなどで作れる音もMODULARで製作可能で、ぶっちゃけMODULARさえあればこのツールは十分です。
サウンドに深い理解がない私のような方でも感覚的に音を加工することができます。

視覚的ではなく、英語なため遠慮してしまいたくなるMODULARですが、
中々面白いので、今回はコチラを紹介していこうと思います。


モジュールの使い方


モジュールの使い方の参考になる記事を作ろうとして資料などを集め書いていたのですが
正直ちゃんと、調べてみると意外と公式が充実していましたので、今回はソチラを紹介します……。


(ヘルプに沢山の資料がある)

モジュール周期表


www.tsugi-studio.com

コチラはモジュールの各ノードがどのような役割を果たしているかの一覧です。
音に詳しい方はコチラをみるだけでノードが作れてしまうかも知れません。

基本的なモジュールの使い方のPDF


http://www.tsugi-studio.com/Software/GameSynth/HelpCenter/221/Manuals/JP/Modular%20Reference%20(JP).pdf

公式の使い方PDFです。
これだけでは使いこなすのは少々不十分ですが、網羅されており参考になります。

サンプルとその作り方


www.tsugi-studio.com
www.tsugi-studio.com
www.tsugi-studio.com
www.tsugi-studio.com

公式の実装サンプルと、その作り方です。
これらをみればどのように作ればいいか一目瞭然です。

実際に作る時の手順


では、あなたが実際に新規に音を作ってみようと思ったとします。
私が↓作った例に簡単に手順を説明します。

1.公式の大量のサンプルを参考にする


公式には大量にサンプルがあります。
ツールのGetから参照出来るのですが、ノード以外も含めるとゆうに100は超えます。

私は紙をペラリとめくる音を作りたかったので、紆余曲折探りましたが、
結局公式サンプルから似たような音を探して来て探すのが一番です。

サンプルにてpaperで検索するといくつかサウンドが出てきました。


紙を破る音、紙を擦る音、風に吹かれる紙の音、紙コップが落ちる音。

2.サンプルの音を開く


風に吹かれる紙の音が一番近い音だったので、ソチラを開きました。
サンプルの音を開くとこのような、ノードが表示されました。


しかしながら、風に吹かれる紙の音は音が連続しており、私の要望のペラリとめくる音には程遠いです。

3.音を加工する


そこで、先ほど紹介した、モジュール周期表が役に立ちます。
モジュール周期表を参考にしながら、音がどのような構成になっているのか、
どのように加工すればそれっぽくなるのかがなんとなく掴めます。
何階か試行錯誤を重ね、最終的には以下(↓)のようなノードになりました。

終わり!
全然説明になってないように移るかも知れませんが、それほど手軽に音が出来てしまうのです。

まとめ


サウンドアセットを買うとなると大量に入ったサンプルは1万円ほどしたりします。
無料のアセットは音の品質も怪しいですし、ほかアセットと組み合わせるとなると音質の差が気になってしまいます。

高品質で一定の音を揃えたいのなら割と低価格で済み、自作なのでオリジナリティも出せます。
そう考えるとGameSynthはかなりの低コストです。セールの際にでも購入検討してみてください。