だらだら〜自由自在〜

インディーゲーム制作チーム GAME GABURI でプログラム担当してます

悩みング

最後に書いた記事が2019年かぁ。年に一回は記事を書いてたのに途切れてしまった…。 別に何か為になるような話でもなく、何らかの覚書でも無い長文のつぶやきのような記事ですが、2021年も投稿無しってなるのも何か嫌なので書いておきます。

開発中インディーゲームのストーリーを考えて実装する役が回ってきたのですが、これで3ヶ月ぐらい悩んでいます。 シナリオ作成というやつで、そういうことをやるのは初なので全く勝手がわかりません。

今の気分を言葉に表すと。

「(このタスクが)終わらないことはない、いつか完成する、けど終わるまでの道のりは想像できない、完成してゲームをリリースする自分は何となく想像できる」

みたいな。 自分が新卒でゲーム会社入って最初にガッツリ携わったプロジェクトの開発中の気持ちに似ています。というか思い出した。

まぁ生きて取り組んでる限りいつかは達成できるんだろうけど、経験不足であったり今現在悩みまくっていてるので完成までの途中がイメージできない感じ?

散歩とかしているときにふと話が思い浮かんだりするっていうのもよく聞く気がするんですが、自分はそれができない気もしていて。

歩きながらもの考えるって出来るの?危なくない?

この記事ももちろん現実逃避95%で書いてるんですが。

さて、どうしましょうね…。

はたしてゲームがリリースされる日は来るのか!

Windowsのゴミ箱をつかってファイルやフォルダを削除する

QtでWindows向けのファイラーを作っているのですが、OSのゴミ箱に選択項目を捨てる処理を実装したくて調べました。 Qt自体もQFileSystemModel::removeなど削除機能はもっているのですが、OSのゴミ箱ではないどこか彼方へ(そういえばどこに…?)消し去ってくれるものなようです。

[QTBUG-47703] Move a file or directory to the trash - Qt Bug Tracker

ちょうどQtにもビルトインでゴミ箱送り機能つけてくれというプロポーザルがあるようで、このリンクはそれのIssueチケットらしいのですが。 そのImplementsDetailsに記載があります。

Windowsのゴミ箱に異界送りするための手段としては2つあるようです。

on Windows, SHFileOperation (FO_DELETE with FOF_ALLOWUNDO) is available, but deprecated, and it doesn't return the location of the file in the trash, which makes it a bad choice. In that case, we also have no way to prevent the deletion of the file if it is not moved to the trash. A better alternative uses IFileManager, which via a IFileOperationProgressSink allows us to learn about the location of the file after the trashing, and also allows us to cancel the operation if the file would be deleted (ie because it resides on a network share or otherwise volume that doesn't support recycling). The only drawback is that this requires an up-to-date SDK; building with MinGW has been problematic.

SHFileOperation という選択肢があるが、これは今や推奨されてないのでIFileManagerを使うほうが良いとのことです。 ということで参考サイトで色々調べて最終的に下記のようにしました。

実装コード

#include <ShObjIdl.h>
#include <ShlObj_Core.h>

int PlatformCompat::MoveToTrash(QList<QString> itemPathList)
{
    if (itemPathList.size() == 0)
        return -1;

    HRESULT hr;

    int itemNum = itemPathList.size();
    PCIDLIST_ABSOLUTE_ARRAY pIdlArray = new LPCITEMIDLIST[itemNum];
    PIDLIST_ABSOLUTE abSourcePidl;
    SFGAOF attrs;
    for (int i = 0; i < itemNum; ++i)
    {
        LPCWSTR cstr = reinterpret_cast<LPCWSTR>(itemPathList[i].replace('/', '\\').utf16());
        hr = SHParseDisplayName(cstr, NULL, &abSourcePidl, 0, &attrs);
        if (FAILED(hr))
        {
            goto EXIT;
        }
        pIdlArray[i] = abSourcePidl;
    }


    //
    // Initialize COM as STA.
    //
    hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    if (SUCCEEDED(hr))
    {
        IFileOperation* pfo;

        //
        // Create the IFileOperation interface 
        //
        hr = CoCreateInstance(CLSID_FileOperation,
            NULL,
            CLSCTX_ALL,
            IID_PPV_ARGS(&pfo));
        if (SUCCEEDED(hr))
        {
            hr = pfo->SetOperationFlags(FOF_ALLOWUNDO);
            if (SUCCEEDED(hr))
            {
                IShellItemArray* pShellItemArr = NULL;
                hr = SHCreateShellItemArrayFromIDLists(itemNum, pIdlArray, &pShellItemArr);
                if (SUCCEEDED(hr))
                {
                    hr = pfo->DeleteItems(pShellItemArr);
                    if (SUCCEEDED(hr))
                    {
                        hr = pfo->PerformOperations();
                    }
                }
                pShellItemArr->Release();
            }
            pfo->Release();
        }

        CoUninitialize();
    }

EXIT:
    if (pIdlArray)
    {
        delete[] pIdlArray;
    }

    return hr;
}

参考サイト

QWidget継承クラスでRegisterHotKey使おうとしてハマった

作成中のファイラーにグローバルホットキーを実装しようとしてハマった話。
Qt5.11を使っている。
RegisterHotKeyはWindowsでグローバルホットキーを実装するときの選択肢の一つだが、このメッセージハンドラをQtWidget継承クラスのoverrideしたnativeEventFilterで受けようとしたけどメッセージが来なかった。
最終的にqApp->installNativeEventFilterでハンドラーを登録してやって実装できた。
下記コードは実装例(※色々省いた擬似コードみたいなものなのでコンパイルは通らない)

static const int HOTKEY_EVENT_ID = 100;

class Filer : public QWidget
{
	Filer(QWidget *parent)
		: QWidget(parent)
	{
		qApp->installNativeEventFilter(&_globalShortcutEvent);
		
		//コストラクタなどで購読開始
		HWND hwnd = (HWND)winId();
		auto result = ::RegisterHotKey(
			hwnd,
			HOTKEY_EVENT_ID,
			MOD_CONTROL,
			0x32 //2キー
		);
		
		if(!result) {/*登録エラー*/}
	}
	
	void closeEvent(QCloseEvent* )
	{
		//closeEventなどで購読解除
		::UnregisterHotKey(HWND(winId()), HOTKEY_EVENT_ID);
	}

	class EventFilter : public QAbstractNativeEventFilter
	{
	public:
		bool nativeEventFilter(const QByteArray& eventType, void* message, long* result);
	};
	
	static EventFilter _globalShortcutEvent;
	
#if 0
	//最初は基底クラスにあるこの関数をOverrideしていたけど、WM_HOTKEYメッセージが来ることがなかった・・・
    virtual bool nativeEvent(const QByteArray &eventType, void *message, long *result);
#endif
}

bool Filer::EventFilter::nativeEventFilter(const QByteArray& eventType, void* message, long* result)
{
	Q_UNUSED(eventType)
	Q_UNUSED(result)
	// Transform the message pointer to the MSG WinAPI
	MSG* msg = reinterpret_cast<MSG*>(message);

	// If the message is a HotKey, then ...
	if (msg->message == WM_HOTKEY)
	{
		// ... check HotKey
		if (msg->wParam == HOTKEY_EVENT_ID)
		{
			// We inform about this to the console
			qDebug() << "HotKey worked";
			return true;
		}
	}
	return false;
}

SerializeFieldのCS0649を大体削除するための正規表現による置換

qiita.com
既存のUnityProjectを.Net4.xに変更したら未初期化変数でワーニング大量に出たので正規表現使ってdefault初期値に置換しました。という話。

//検索
\[SerializeField\]([\r\n]?)(\t+|\s+)(private|public|protected)?(\t|\s)?([0-9a-zA-Z|_|\[.*\]]+)(\t|\s)?([0-9a-zA-Z|_]+);
//置換
[SerializeField]$1$2$3$4$5$6$7 = default;

これで自分のProjectは大丈夫でした。以下備考

  • structのフィールドも置換しちゃうので、そっちはコンパイルエラーになってしまう。手動で戻してください。
  • 属性とフィールドをワンライナーで書いてるやつに対応できてるか謎。
  • アクセス修飾子には対応した
  • 配列にも対応した(2次元配列は対応できてないかも…)
  • まぁ備忘と参考にということで


何か書きにくいな、Qiitaとかにしたほうがええんやろか。

nginxでファイルが存在しないときに別ドメインから探す

仕事用メモ。

nginxのホスト上にRequestされたファイルが存在しないときに、別サーバの同Pathのものをレスポンスする。ということがやりたかった。
nginxのことをよくわかってなくてphpでファイルの存在チェックとcURLによる転送処理を書いてしまった。
でも探してみるとnginx単体でできそうなことがわかった。

location / {
root html;
index index.html index.htm;
try_files $uri $uri/ @dinamic;
}

location @dinamic {
proxy_pass http://example.com;
}

try_filesの部分と、locatoin @dinamicのところがポイント。
こんな感じで(Redirectではなく)Proxyをやってくれるようだ。

UnityCloudBuildでビルドエラー "Dex cannot parse version 52 byte code."

Androidで手元ではビルド成功するのにUCB上ではビルドエラーが出る…!という状況がまぁまぁあるのですが、そのパターンの解決策の1つとしてさっき分かったものご紹介します。

まず出るエラーはこんな

22567: [Unity] EXCEPTION: CommandInvokationFailure: Gradle build failed.
22568: [Unity] /BUILD_PATH/jdk1.8.0_121.jdk/Contents/Home/bin/java -classpath "/UNITY_PATH/Unity/Unity-2017_3_1p3/PlaybackEngines/AndroidPlayer/Tools/gradle/lib/gradle-launcher-4.0.1.jar" org.gradle.launcher.GradleMain "-Dorg.gradle.jvmargs=-Xmx2048m" "assembleDebug"
22569: [Unity] stderr[
22570: [Unity] Dex: Error converting bytecode to dex:
22571: [Unity] Cause: Dex cannot parse version 52 byte code.
22572: [Unity] This is caused by library dependencies that have been compiled using Java 8 or above.
22573: [Unity] If you are using the 'java' gradle plugin in a library submodule add
22574: [Unity] targetCompatibility = '1.7'
22575: [Unity] sourceCompatibility = '1.7'
22576: [Unity] to that submodule's build.gradle file.
22577: [Unity] UNEXPECTED TOP-LEVEL EXCEPTION:
22578: [Unity] java.lang.RuntimeException: Exception parsing classes
22579: [Unity] Caused by: com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000)
22580: [Unity] 1 error; aborting

何日か悩んだ末にビルドシステムがGradleっぽいことに気づき、UCBのConfigで「高度な設定 > Android Build System > Internal」にしてみる。そしたら治った。手元ではInternalでビルドしていたから再現しなかったのか~。
元々なんに設定していたかというとGardleでもInternalでもなかった。デフォルトの設定でビルドを作成したときはこの状態で、実際ビルドするときにはGradleが使われるということだろうか。

いやー、しかしリリース近いのにこういうことが起きると嫌だよね~。

Unity Cloud Build の Win/iOS GUIフロントエンドを作った

あくまで自分用なんですけど作ってみました。

github.com

スクリーンショット

WPF

f:id:nyakagawan:20180129130418p:plain

iOS Simulator

f:id:nyakagawan:20180129130528p:plain

機能

  • ビルド開始
  • ビルドターゲット一括作成
  • ビルドターゲット一括削除
  • Credential情報の取得

なんで作ったか

UnityCloudBuildは便利だけどビルドをパラメトリックに実行できない…。
設定やDefineSymbolが少し違うだけで同じようなビルドターゲットをその組み合わせ分作らないといけない。
これは人力でやることではない。だからプログラムした。
そしてパラメータを指定してビルドを実行できるGUIフロントエンドがほしいと思った。
あとあったら便利そうなのでiOSアプリもつくった。というのは理由の半分でXamarin使ってみたかったのでiOS/WPFなプロジェクトにした。

なにで出来ているか

Xamarinで出来ている。
初めてのXamarinだったのでわけがわからんかった。
UIはXamarinFormsというのをつかった。これはUI(XAML)部分をMultiplatform write onceにできるものらしい。
Xamarin自体はUnityほどMultiplatformにおけるコードの共通化がされておらず、Platform寄りのコードはNativeなAPIC#でラップした単純な感じのものらしい(使ってないので分からない)
ただ、XamarinFormsは今(2018/1/29)はまだWPFを正式にサポートしてない感じで。NugetのNightlyBuildで公開されているExperimentalなXamarin.WPFを使わせてもらった。
当初は「普通にWPFもいけるっしょ」みたいに思っていたので泡を食ったが、情報があってよかった。

ちなみに、はじめはUnityでツールを作ろうとおもっていたけど。
UnityCloudBuildAPIがUnity上で動いてくれなくてXamarinならいけるやろってことでこうなった。

使わせてもらったもの

guitarrapc/UnityCloudbuildApi

github.com

まずUnityCloudBuildAPIをC#からつかうために
https://build-api.cloud.unity3d.com/docs/1.0.0/index.html
この公式APIリファレンスで書かれているSwaggerCodeGeneratorをつかってC#のClientCodeを作ろう!今風にと思ってやってみた。
しかしうまくいかなかった。そもそもCodeGenコマンドが失敗したり、出てきたコードがビルド通らなかったり…。
SwaggerCodeGenはwindowでjarをつかってみたり、macでhomeblewしてみたけどNGだった。
疲れた僕はリンク先の偉大な先人のお方のありがたい御コードを拝借した。すごい、うごくしコンパイルが通る。どうやったんだ…。ありがとうございます。

Xamarin.WPF

qiita.com

こちらの記事を参考にさせていただいた。

Newtonsoft Json.NET for Unity3D

github.com

色々実装していってiOS向けにビルドしてみるとAOT絡みのビルドエラーに出くわした。
System.Reflection.EmitをJson.NETが使っているからで、Json.NETはUnityCloudAPIが使っていた。
これをどうにかするためにAOT対策が施されたJson.NETを使わせてもらった。Unity向けだが…。

使おうと思っていたもの

  • Prism
    • MVVMインフラストラクチャ(ですよね?)で、自分はLivetしか使ったことなかったがXamarinFormsならPrismがええど、という噂を目にしたので使いたかった。でも新しいものだらけでキャパオーバーしたので別の機会に。

所感など

  • Xamarinいいなー。Windows上で開発して隣のMacに接続してビルドして、WindowsのVisualStudio上からデバッガつないでってなんかかっこいいわー。ターンアラウンドタイムも全然OKな範囲だし、これはいいわー。
  • でもUnityCloudBuildAPIが動かないのさえなければUnityで作ってただろうなー。この程度のUIなら一瞬でつくれるしCloudBuildでビルドと配布までワンストップだし。今回は配布にHockeyapp使ったが利用者側のことも考えると若干手間。
  • 多分シンプルなツール程度ならiOSWPFでUIをシングルコード実装できるんだろうけど。ある程度複雑になるとまぁ無理そうだなと思った。これはもう実装がどうっていうかそもそも使いやすくするためには別レイアウトにせざる得ない気がする。そういうことであんまりXamarinForms.WPFは需要ないんかなーと思ったり。しらんけど。
  • トライアンドエラーの繰り返しでプロジェクトがぐちゃぐちゃになったのが原因なのかしらないが、XamarinFormsのXAMLのインテリセンスが変になっていて。変なコード補完が働いてめっちゃ書きにくかった。