GitHub Actionsからpushする際に権限でエラーが出た場合に確認するポイント【GitHub Actions】

初めに

個人で自動化する際にGitHub Actionsを使用しています(*1)。動かす際に権限周りでハマったので,メモしておきます

(*1) GitHub Actionsをサーバーレスで使うことは,以下の範囲であれば2023/2/4時点での利用規約上大丈夫みたいです。 使い過ぎには気をつけましょう

当社のサーバーに負担をかける行為で、その負担がユーザーに提供する利益と不釣り合いなもの(例えば、コンテンツ配信ネットワークやサーバーレスアプリケーションの一部としてActionsを使用しないこと、ただし、低負荷であれば低利益のActionでもよい)

docs.github.com

実行内容

まず処理内容ですが,以下のような処理を 特定の GitHub Actionsフロー(yml)内で行っています。普通にcommitしてpushするだけです。

      - name: Commit And Push contents info file
        run: |
          git remote set-url origin https://github-actions:${GITHUB_TOKEN}@github.com/~
          git config --global user.name "name"
          git config --global user.email "email" 
          git add コミットするファイル
          git commit -m "Update file"
          git push
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

エラー内容

上記の処理を実行した際に以下のようなエラーが表示されていました。

remote: Write access to repository not granted.
fatal: unable to access 'https://github.com/~': The requested URL returned error: 403
Error: Process completed with exit code 128.

対応方法

Setting → Actions → General → Workflow permissions から Actionsの権限を Read and Write に変更します

uGUIのスクリーン座標の取得方法【Unity】

環境

  • Unity 2021.3.16f1

やりたいこと

画面に配置されている uGUI(Buttom,Image) のスクリーン座標の取得

取得方法

取得には RectTransform.GetWorldCorners(Vector3[] v) を使用します。

docs.unity3d.com

取得した値は Vectotor3[4] で RectTransformの各角のスクリーン座標が返ってきます

サンプルコード

        private static Vector3[] RectTransformEndPositions(RectTransform rectTransform)
        {
            var fourCorners = new Vector3[4];
            rectTransform.GetWorldCorners(fourCorners);
            return fourCorners;
        }

EMNISTのデータを学習してONNXを出力する【Python】【ML】

はじめに

UnityのBarracudaでMLを使ったプロジェクトを作成したいので、準備としてデータを作成しています。 MLに関してはちょっと調べてる使っているレベルなので、コード等は間違っていることがあります。 また今回はChatGPTを試しに使ってプログラムを作成しているため、ご了承ください

やりたいこと

  • EMISTのデータをほかのプラットフォーム(今回はUnity)で使えるようにONNXとして出力する

環境

  • Docker version 20.10.22
  • Docker Desktop 4.16.1 (95567) : Windows 10

環境構築

  1. 今回は Docker上にPythonで学習用の環境を構築します。 昔はAnacondaを使用していましたが、環境を共有しやすいように dockerを採用しました。

前提として、DockerとDocker Wesktopがインストールされているものとします。 以下の二つのファイルを用意して、Dockerを起動します

  1. docker compose up -d --build
  2. docker compose exec python3 bash

Dockerfile

FROM python:3.10
USER root

RUN apt-get update
RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
RUN python -m pip install torchvision
RUN python -m pip install torch

docker-compose

version: '3'
services:
  python3:
    restart: always
    build: .
    container_name: 'python3'
    working_dir: '/root/'
    tty: true
    volumes:
      - ./opt:/root/opt

実装

MLに関してはよくわからないので、ChatGPTにお願いしていろいろ出してもらいました。 (ちゃんと使ったのは初めてなので、ChatGPTへの返答も記事のメモしてあります)

Dockerの起動が終わったら以下のファイルを opt/ に作成します。

作成したプログラムでデータセットから学習を行い、ONNXを作成します

python opt/xxx.py

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
import torch.onnx
import torch.nn.functional as F

# load EMNIST dataset
emnist_data = datasets.EMNIST(
    './EMNIST',
    # split='letters',
    split='balanced',
    train=True, download=True,
    transform=transforms.ToTensor())

data_loader = torch.utils.data.DataLoader(emnist_data, batch_size=2, shuffle=True)

print("EMNIST dataset loaded")


# create and train model
# create and train model
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 128, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(128 * 7 * 7, 256)
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, 47)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2(x), 2))
        x = x.view(-1, 128 * 7 * 7)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)


model = Net()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
criterion = nn.NLLLoss()

print("Model created")

for epoch in range(10):
    for data, target in data_loader:
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
#
# convert to ONNX format
print("Converting to ONNX format")
torch.onnx.export(model, torch.randn(1, 1, 28, 28), "model.onnx")

# split train and validation data
train_size = int(len(emnist_data) * 0.8)
val_size = len(emnist_data) - train_size
train_dataset, val_dataset = torch.utils.data.random_split(emnist_data, [train_size, val_size])

val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=4, shuffle=True)

# Evaluate on validation set
val_loss = 0
correct = 0
with torch.no_grad():
    for data, target in val_loader:
        output = model(data)
        val_loss += criterion(output, target).item()
        pred = output.argmax(dim=1, keepdim=True)
        correct += pred.eq(target.view_as(pred)).sum().item()

val_loss /= len(val_dataset)
val_acc = correct / len(val_dataset)

print('Validation set: Average loss: {:.4f}, Accuracy: {:.2f}%'.format(val_loss, val_acc * 100))

結果

実行結果(処理の時間と精度)は以下です

また生成したONNXがdocker環境内になるので、ローカルPC側にコピーする際に以下で任意のパスにコピーしてきます

docker cp docker-container-id:file-path local-path

Unity側にONNXをコピーすれば、Unityで Barracudaなどを使って開発を行うことができます。

参考サイトおよびChatGPTの返答内容

データセット引用元

作成者: Gregory Cohen, Saeed Afshar, Jonathan Tapson, and André van Schaik タイトル: EMNIST: an extension of MNIST to handwritten letters 公開日: 2017年2月17日(初版)/2017年5月1日(第2版) URL: http://arxiv.org/abs/1702.05373

参考サイト

atmarkit.itmedia.co.jp

www.nist.gov

qiita.com

zenn.dev

ChatGPT

EMNISTを学習してONNXに出力するプログラムを作成するときにChatGPTに聞いた内容

GoogleSheetのプルダウン選択肢を特定のフォルダ内のファイル一覧から自動更新する【GoogleAppsScript】

はじめに

とあるプロジェクト(インディーズゲーム)で、Unity向けのADV開発基盤を作成しています。
ここでは Googel Sheetを使ったシナリオデータの管理を行っていますが、セリフに紐づく素材データをより簡単に管理・設定しやすいようにしたいと思い機能を拡張しました。

デモ

デモ画像では以下を行っています。 * セリフに紐づくキャラ画像の選択をキャラ画像フォルダ内のファイル一覧と同じものをプルダウンから選べる * Google SheetのMenuから手動で更新する

またこちらの更新は トリガーにて一定時間ごとに更新を行っているため、ファイルを更新するたびに手動で更新する必要はありません。

環境

  • Google Sheet(2023/1/9時点での機能を使用)
  • Google Apps Script(2023/1/9時点での機能を使用)

実装方法

特定のフォルダからプルダウンを更新

まずは特定のフォルダからファイル一覧を取得します。

function GetAllFiles(folderId){
  let folder = DriveApp.getFolderById(folderId);
  let files = folder.getFiles();

  let fileNames = []
  while(files.hasNext()){
    var file = files.next();
    fileNames.push([file.getName()])
  }
  console.log(fileNames)
  return fileNames;
}

次にファイル一覧から プルダウンの入力規則を作成・更新します。

function UpdatePulldownValues(sheet,folderId,range){
  let fileName = GetAllFiles(folderId);
  
  //入力規則を作成
  const rule = SpreadsheetApp.newDataValidation().requireValueInList(fileName).build();
  
  //リストをセットするセル範囲を取得
  const cell = sheet.getRange(range);
  
  //セルに入力規則をセット
  cell.setDataValidation(rule);

}

こちらのプロジェクトでは3つの素材を指定したいので、以下のようにまとめる関数を作成しました。

function UpdateAllPulldownValues(){
    const sheet = SpreadsheetApp.getActiveSpreadsheet();
    UpdatePulldownValues(sheet,characterImageFolder,'D2:D100')
    UpdatePulldownValues(sheet,backgroundImageFolder,'E2:E100')
    UpdatePulldownValues(sheet,soundFolder,'F2:F100')
    
}

Custom Menuを作成

以下のような Googel Sheet上のCustom Menuの追加は Google Sheetが開いたときに関数を読み込んでUIの追加ができる onOpen を使用しました。

function onOpen() {
  const customMenu = SpreadsheetApp.getUi()
  customMenu.createMenu('ADV Menu') //メニューバーに表示するカスタムメニュー名
      .addItem('プルダウンの更新', 'UpdateAllPulldownValues') //メニューアイテムを追加
      .addToUi()
}

参考サイト

FungusのFlowchart内にあるセリフデータをGoogle Sheetに反映させる【Unity】【GoogleAppsScript】

はじめに

Unityで会話実装を行う際にFungusを使用した会話機能の実装を以下のゲームで行いました。
しかし、セリフデータをFungus内のflowchartに入れてしまうとローカライズ対応やボイス実装の際に毎回確認や修正をするたびにUnityを開ける必要があります。手動だと大変なので一覧化してみようと思いましたので、実装方法を記事にしました

store.steampowered.com

Demo

Inspectorにあるボタンを押すことで、ヒエラルキー上のすべてのflowchart内のセリフを Google Sheet上に反映することができます。

またキャラ数が数十体あるため、各シートの一覧化及びシートリンクも自動的に追加されるようになっています。

やりたいこと

  • シーン上に存在する会話するNPCのセリフを取得
  • セリフデータを Google Sheetへの反映
    • 新規キャラがいる場合の、新規シート作成
    • キャラクターシートを保護モードにして、変更できないようにする
  • 各セリフデータやキャラの分析及び一覧化
    • 一番上にパラメータ名の追加と行の固定

環境

実装

1. シーン上に存在する会話NPCのセリフの取得

こちらは以下の記事で fungusのセリフデータが格納されているコンポーネント(flowchart)をjson形式に出力する方法について詳しく書いていますので、以下をご覧ください

ayousanz.hatenadiary.jp

2. Google Sheetにセリフデータを反映させる

1にてセリフデータを以下のようなjsonクラスに変換できました。

次に以下のような流れで Unity側のセリフデータクラスを Google Sheetに反映させていきます。

Unity側の実装

Unity側でデータをGAS側にPostする関数を作成します。

        public static async UniTask<bool> PostGameInfo(string url, string sheetName, string postData)
        {
            var request = new UnityWebRequest($"{url}?sheetName={sheetName}", "POST");
            var data = Encoding.UTF8.GetBytes(postData);
            request.uploadHandler = new UploadHandlerRaw(data);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");

            await request.SendWebRequest();
            Debug.Log(request.downloadHandler.text);
            return request.result == UnityWebRequest.Result.Success;
        }

前回のjsonに変換する関数と組み合わせたUnity側の関数は以下のようになります。

        private async UniTask ExportGoogleSheet()
        {
            foreach (var dialogue in _cardEventDialogueList)
            {
                var sheetName = dialogue.dialogueNo;
                var json = JsonExtension.ListClassToJson(dialogue, sheetName);
                await GoogleSheetExtension.PostGameInfo(GoogleSheet.CardEventDialogueSheet, sheetName, json);
            }
        }

Google Apps Script側の実装

外部からPostできる API doPost(e)を以下のように作成します。

以下では、シート名を指定して処理を行えるようにパラメータに sheetNameを入れています。 またpost dataのbodyには キャラセリフデータが入ります。

function doPost(e){
  const sheetName = e.parameter.sheetName;
  const postContent = e.postData.getDataAsString();
  const params = JSON.parse(postContent)["Data"]["dialogues"];
}

以下で postされた セリフデータを GASで扱える Object側に変換して、必要なデータ(セリフが入っているリスト部分)を 取得しています。

  const params = JSON.parse(postContent)["Data"]["dialogues"];

今後はこの関数内に処理を追加していきます。

次に Postしたキャラデータのシートがまだ作成されていない場合、新規シートを作成します。

  let sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  if(sheet == null){
    sheet = NewSheet(sheetName);
  }

function NewSheet(sheetName){
  //スプレッドシートに新しいシートを追加挿入
  let newSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
  //追加挿入したシートに名前を設定
  newSheet.setName(sheetName);
  return newSheet;
}

次に前回のシート情報から差分があった場合、シート内容がおかしくならないように初期化を行います。

  //前回の情報を初期化する
  sheet.clearContents();

シートの作成やセリフデータが取得できたので、シートに書き込むための処理を行います。
まずは 書き込む範囲の指定を sheet.getRange で行います。
その後 セリフ分をfor文で回してリストに入れていきます。

function SetDialogueData(sheet,dialogue,setHeaderSheetInfo){
  const dialogueLength = dialogue.length;
  let range = sheet.getRange(1,1,dialogueLength + 1,setHeaderSheetInfo.length);
  let dataList = []

  // シートの一番上に列の情報を入力する
  dataList.push(setHeaderSheetInfo);
  for(let i = 0;i < dialogueLength;i++){
    var data = new Array();
    var d = dialogue[i];
    data.push(d["characterName"],d["characterViewName"],d["text"],d["commandIndex"],d["dialogueCount"])
    dataList.push(data)
  }
  range.setValues(dataList);
}

書き込むための処理を行った後に、見た目を整えるために以下の処理を行います * 一行目の固定 * 各列の幅を自動で揃える

function SetSheetStyle(sheet,sheetRowCount){
  //一番上の行を固定にする
  sheet.setFrozenRows(1);

  //必要のない列を非表示にする
  sheet.hideColumns(sheetRowCount + 1,26 - sheetRowCount);

  //行の幅をそろえる
  sheet.autoResizeColumns(1,sheetRowCount);
}

Unity側の更新が優先されるために間違えてシートの変更がユーザーによってされないようにシートに保護モードもつけておきます

function SetProtect(sheet){
  //Unity側しか更新できないように保護モードに変更
  const protect = sheet.protect();
  //保護内容の説明文章を設定
  protect.setDescription("Unity側のみ更新可能です");
  //保護を入力不可ではなく、入力時に警告を表示
  protect.setWarningOnly(true);

}

最後に Unity側で処理を行った後に、正常に終わったかわからないのでResponseを返すようにします。

以下のようにレスポンスが返ってくるようにGAS側でレスポンスを作成します。

function ReturnResponse(sheetName){
  // レスポンスとしてJsonを返す
  const response = JSON.stringify({ message: "success for " + sheetName });
  let output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(response);
  return output;
}

以上の内容は以下のようなプログラムになっています。 (Unity側からPostして、シート作成後データを書き込むコード)

function doPost(e){
  const sheetName = e.parameter.sheetName;
  const postContent = e.postData.getDataAsString();
  const params = JSON.parse(postContent)["Data"]["dialogues"];

  const sheetHeaderInfo = ["character name","character view name","dialogues","commnadIndex","dialogue count"];

  let sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  if(sheet == null){
    sheet = NewSheet(sheetName);
  }

  //前回の情報を初期化する
  sheet.clearContents();
  sheet.showColumns(1,26);
  
  SetDialogueData(sheet,params,sheetHeaderInfo);
  SetSheetStyle(sheet,sheetHeaderInfo.length);
  SetProtect(sheet);
  return ReturnResponse(sheetName);
}

function SetProtect(sheet){
  //Unity側しか更新できないように保護モードに変更
  const protect = sheet.protect();
  //保護内容の説明文章を設定
  protect.setDescription("Unity側のみ更新可能です");
  //保護を入力不可ではなく、入力時に警告を表示
  protect.setWarningOnly(true);

}

function SetDialogueData(sheet,dialogue,setHeaderSheetInfo){
  const dialogueLength = dialogue.length;
  let range = sheet.getRange(1,1,dialogueLength + 1,setHeaderSheetInfo.length);
  let dataList = []

  // シートの一番上に列の情報を入力する
  dataList.push(setHeaderSheetInfo);
  for(let i = 0;i < dialogueLength;i++){
    var data = new Array();
    var d = dialogue[i];
    data.push(d["characterName"],d["characterViewName"],d["text"],d["commandIndex"],d["dialogueCount"])
    dataList.push(data)
  }
  range.setValues(dataList);
}

function SetSheetStyle(sheet,sheetRowCount){
  //一番上の行を固定にする
  sheet.setFrozenRows(1);

  //必要のない列を非表示にする
  sheet.hideColumns(sheetRowCount + 1,26 - sheetRowCount);

  //行の幅をそろえる
  sheet.autoResizeColumns(1,sheetRowCount);
}

function NewSheet(sheetName){
  //スプレッドシートに新しいシートを追加挿入
  let newSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
  //追加挿入したシートに名前を設定
  newSheet.setName(sheetName);
  return newSheet;
}

function ReturnResponse(sheetName){
  // レスポンスとしてJsonを返す
  const response = JSON.stringify({ message: "success for " + sheetName });
  let output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(response);
  return output;
}

3. セリフデータの分析及び一覧化

キャラシートが数十枚あるので、以下のように一覧にしてわかりやすく見れるシートを作成します。

処理の流れは以下のようになっています 1. 自分自身以外のすべてのシートの取得 2. 各シートからキャラ名・表示名・セリフ文字を取得してグローバル変数に追加 3. (行と列が逆になっているため)辞書型リストからSheetに書き込むためのArrayListに変換

//character nameとcharacte view nameを紐づいている変数
let characterName = {}

// charcter nameとセリフ文字数を紐づいている変数
let characterDialogueCount = {}

// character nameとハイパーリンク
let characterSheetLinkList = {}

//行と列を入れ替える関数
const transpose = a=> a[0].map((_, c) => a.map(r => r[c]));

//自身のスプレッドシートid
let ssId = "";

function UpdateSummary(){
  var allSheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
  const sheet = SpreadsheetApp.getActiveSpreadsheet();
  ssId = sheet.getId();
  let summarySheet;
  // 一覧シートに書き込む範囲
  let editRange;

  var sheetCount = allSheets.length;
  for(let i = 0;i < sheetCount;i++){
    var sheetName = allSheets[i].getName();
    var targetSheet = sheet.getSheetByName(sheetName);
    if(sheetName === "キャラ一覧"){
      editRange = targetSheet.getRange(2,1,targetSheet.getLastRow(),3);
      editRange.clearContent();
      summarySheet = targetSheet;
    }else
    {
      // 各カードイベントのセリフシートからデータの取得と更新
      GetData(targetSheet);
    }
  }

  //データの書き込み
  let [nameList,characterViewName,link] = [Object.keys(characterName),Object.values(characterName),Object.values(characterSheetLinkList)]
  let count = Object.values(characterDialogueCount);
  let nameAndCountData = []
  nameAndCountData.push(nameList)
  nameAndCountData.push(characterViewName)
  nameAndCountData.push(count)
  nameAndCountData.push(link)
  editRange = summarySheet.getRange(2,1,nameList.length,nameAndCountData.length);
  editRange.setValues(transpose(nameAndCountData))
}

function GetData(sheet){
  if(sheet.getLastRow() <= 1) return;
  console.log("sheet name:" + sheet.getName())
  var npcInfoListInSheet = sheet.getRange(2,1,sheet.getLastRow() - 1,5).getValues();
  for(var i = 0;i < npcInfoListInSheet.length;i++){
    //キャラ名と表示名の更新処理
    var characterInfo = npcInfoListInSheet[i];
    var name = characterInfo[0];
    if(!characterName[name]){
      characterName[name] = characterInfo[1]
    }
    // キャラ名とセリフ文字数更新処理
    if(characterDialogueCount[name]){
      characterDialogueCount[name] = characterDialogueCount[name] + characterInfo[4];
    }else{
      characterDialogueCount[name] = characterInfo[4];
    }

    // シートのURLからハイパーリンク文字列を組み立て
    if(characterSheetLinkList[name] == null){
      let url = "https://docs.google.com/spreadsheets/d/" + ssId + "/edit#gid=" + sheet.getSheetId();
      characterSheetLinkList[name] = '=HYPERLINK("' + url + '","' + name + '")'
    }
  }
}

FungusのFlowchart内にあるセリフデータをJSONに出力する【Unity】【Fungus】

はじめに

インディーズゲームを作っている中でNPCの会話を実装をすることがあります。はじめから外部にセリフデータを分離した実装を行っていればいいのですが、Unity内部で持ってしまうことがあります。

今回は以下のDreamIslandにて Fungusを使用してFlowchart内に入れた実装にしてしまいました。

store.steampowered.com

やりたいこと

以下の画像のように FungusのFlowchart内部にもっているセリフデータをjson形式にすべて一括でexportする (会話するNPCは100体以上存在するため、一つ一つだと時間がかかってしまいます)

Demo

Inspectorの Export Dialogue To Json のボタンを押すことで、以下の画像のように Inspector及びローカルのjsonに一覧化して見やすくしました

追加の拡張機能宣伝

以下では セリフデータを Google Sheetに保存する機能を作りました。 Unityを見ずらいjsonやInspectorではなく、使い慣れた Sheet形式で見れるのでかなり便利だと思います。 //別記事として作成中

環境

実装内容

一セリフデータを格納するクラスの作成

Flowchart内でセリフデータが入っているComponentは、以下画像のSayになります。
こちらのコンポーネントを見ると、以下の情報を持っていることが分かります。

  • ItemId(Commandの順番、本来は非表示)
  • Story Text (セリフ)
  • Character(セリフを話しているcharacterコンポーネント)

上の情報に セリフの文字数と都合上 nameの二つを追加したクラスを作成します。

    [Serializable]
    public class Dialogue
    {
        public string characterName;
        public string characterViewName;
        public string text; //セリフデータ
        public int commandIndex;
        public int dialogueCount;
    }

一キャラのセリフデータを格納するクラスの作成

一つのNPCに対して(FlowchartがアタッチされているNPC) 、セリフデータは以下のように複数存在しています。

そのため、複数のセリフを格納するために先ほどの Dialogue をリストにして NPCの名前と紐づけた 一キャラのセリフデータクラスを作成します。

    [Serializable]
    public class CardEventDialogue
    {
        public string dialogueNo;
        public List<Dialogue> dialogues;
    }

Flowchartコンポーネントから情報を取得する

クラスの作成は終わったので、一キャラのflowchart内から情報を取得していきます。

        private static CardEventDialogue GetCardEventDialogues(Component flowchart)
        {
            var dialogueList = new CardEventDialogue
            {
                dialogueNo = flowchart.name,
                dialogues = new List<Dialogue>()
            };
            var sayList = flowchart.GetComponents<Say>();
            foreach (var say in sayList)
            {
                if (say._Character == null) continue;
                var dialogue = new Dialogue
                {
                    characterName = say._Character.name,
                    characterViewName = say._Character.NameText,
                    text = say.GetStandardText(),
                    dialogueCount = say.GetStandardText().Length,
                    commandIndex = say.ItemId,
                };
                dialogueList.dialogues.Add(dialogue);
            }

            dialogueList.dialogues.Sort((a, b) => (a.commandIndex - b.commandIndex));

            return dialogueList;
        }

またセリフデータを取得する際に、セリフの順序がバラバラになると困るので ItemId でソートを行います

dialogueList.dialogues.Sort((a, b) => (a.commandIndex - b.commandIndex));

シーン上のすべての会話NPCの取得

ボタンを押したときにシーン(ヒエラルキー上)に存在する flowchartがアタッチされている NPCを取得します。

        private void GetAllDialogues()
        {
            _cardEventDialogueList.Clear();
            var dialogues = FindObjectsOfType<Flowchart>();
            foreach (var dialogue in dialogues)
            {
                _cardEventDialogueList.Add(GetCardEventDialogues(dialogue));
                _cardEventDialogueList.Sort((a, b) => (GetCardEventInt(a.dialogueNo) - GetCardEventInt(b.dialogueNo)));
            }
        }

Jsonにクラスを保存

クラス内がリスト型のため、JsonUtility.ToJson ではうまく変換ができないため以下のような関数を作り疑似的に保存できるように変更しています。

    public static class JsonExtension
    {
        public static string ListClassToJson<T>(T listClass, string className)
        {
            var tmpClass = new TempClass<T>
            {
                ClassName = className,
                Data = listClass
            };
            return JsonUtility.ToJson(tmpClass, true);
        }

        private class TempClass<T>
        {
            public string ClassName;
            public T Data;
        }
    }

取得してきたセリフデータをjsonに保存します。

        private async UniTask ExportJson()
        {
            var json = JsonExtension.ListClassToJson(_cardEventDialogueList, nameof(_cardEventDialogueList));
            Debug.Log("json: " + json);
            var writer = new StreamWriter(JsonSavePath, false);
            await writer.WriteAsync(json);
            await writer.FlushAsync();
            writer.Close();
        }

Riderで変数変更時にFormerlySerializedAsが自動的に付かない設定に変更する【Unity】【Rider】

はじめに

Unity で開発しているときにいつも JetBrainsのRiderを使用させていただいています。こちらの IDEはものすごく便利なのですが、作業中に困ることが多々あるので設定をメモしておきます。

環境

  • Unity 2022.2.0f1
  • JetBrains Rider 2022.3.1

解決したい問題

変数を IDEの機能の一つである Rename (*1) で変更した際に、FormerlySerializedAs が自動的につくことがあります。こちらありがたい機能でありますが、そもそも普段使わないため手動で削除する手間があります。

(*1)

任意の変数を選択した状態で、Refactor → Renameから選択ができます。

設定項目

Languages & Frameworks → Unity → Refactoringから変更できます。