Unityのネイティブプラグインをはじめました

昨日に引き続き、今日はネイティブプラグインを扱います。

ここではUnity 2018 1.3f、Visual Studio 2017を使用しています。

おかげさまで絶好調です。

 

ネイティブプラグインとは

多分公式マニュアルを見た方が早いでしょうか?
docs.unity3d.com

既存のC,C++で書かれたライブラリを、 C#で扱うための仕組みです。

Visual Studioなど他のコンパイラでdllファイルを作成し、Unityでこのdllの関数を呼び出します。 

 

ネイティブプラグインの作成

dllの作成

まずはVisual Studio 2017でdllを作成するプロジェクトを作成します。

Visual C++ > Windows デスクトップ > ダイナミック リンク ライブラリ(.dll)のプロジェクトを作成します。

f:id:chirotec:20180617201500p:plain

 

初期状態では、以下のようなプロジェクトになっています。

 

f:id:chirotec:20180617201456p:plain

ここでは「NativePluginSample.cpp」のみを編集します。
 さらにdllをincludeするためのヘッダーファイル「NativePluginSample.h」をプロジェクトに追加します。

 

NativePluginSample.h

#pragma once
#ifdef NATIVEPLUGINSAMPLE_EXPORTS
#define SAMPLE_API __declspec(dllexport)
#else
#define SAMPLE_API __declspec(dllimport)
#endif

extern "C" {
	// 値渡し
	SAMPLE_API int SampleAPIInt(int i);
	SAMPLE_API float SampleAPIFloat(float f);
	SAMPLE_API double SampleAPIDouble(double d);

	// 参照渡し
	SAMPLE_API void SampleAPIInt2(int& i);
	SAMPLE_API void SampleAPIFloat2(float& f);
	SAMPLE_API void SampleAPIDouble2(double& d);

	// 配列の参照渡し
	SAMPLE_API void SampleAPIIntArray(int intArray[], int intArraySize);
	SAMPLE_API void SampleAPILongArray(long longArray[], int longArraySize);
	SAMPLE_API void SampleAPIFloatArray(float floatArray[], int floatArraySize);
	SAMPLE_API void SampleAPIDoubleArray(double doubleArray[], int doubleArraySize);

	// 文字列
	SAMPLE_API const char* SampleAPIString1();
	SAMPLE_API const char* SampleAPIString2(const char* str);
}

NativePluginSample.cpp

#include "stdafx.h"
#include "stdlib.h"
#include "NativePluginSample.h"

SAMPLE_API int SampleAPIInt(int i)
{
	return 123 + i;
}

SAMPLE_API float SampleAPIFloat(float f)
{
	return 123.456f + f;
}

SAMPLE_API double SampleAPIDouble(double d)
{
	return 123.456 + d;
}

SAMPLE_API void SampleAPIInt2(int& i)
{
	i = 123 + i;
}

SAMPLE_API void SampleAPIFloat2(float& f)
{
	f = 123.456f + f;
}

SAMPLE_API void SampleAPIDouble2(double& d)
{
	d = 123.456 + d;
}

SAMPLE_API void SampleAPIIntArray(int intArray[], int intArraySize)
{
	for (int i = 0; i < intArraySize; i++)
	{
		intArray[i] = i;
	}
}

SAMPLE_API void SampleAPILongArray(long longArray[], int longArraySize)
{
	for (int i = 0; i < longArraySize; i++)
	{
		longArray[i] = i;
	}
}

SAMPLE_API void SampleAPIFloatArray(float floatArray[], int floatArraySize)
{
	for (int i = 0; i < floatArraySize; i++)
	{
		floatArray[i] = 0.5f + i;
	}
}

SAMPLE_API void SampleAPIDoubleArray(double doubleArray[], int doubleArraySize)
{
	for (int i = 0; i < doubleArraySize; i++)
	{
		doubleArray[i] = 0.5 + i;
	}
}

SAMPLE_API char* MallocString(const char* str)
{
	size_t bufSize = strlen(str) + 1;
	char* buf = (char*)malloc(bufSize);
	strcpy_s(buf, bufSize, str);
	return buf;
}

SAMPLE_API const char* SampleAPIString1()
{
	return MallocString("Hello!");
}

SAMPLE_API const char* SampleAPIString2(const char* str)
{
	// 文字列の後ろに"Hello!"をくっ付けて返す
	const int bufferSize = 256;
	static char buffer[bufferSize];
	const char hello[] = "Hello!";

	strcpy_s(buffer, bufferSize, str);
	size_t n = strnlen(buffer, bufferSize);
	strcpy_s(buffer + n, bufferSize - 7, hello);
	return MallocString(buffer);
}

このプロジェクトを作成したときに、プロジェクトのプロパティ内で、プリプロセッサの定義に NATIVEPLUGINSAMPLE_EXPORTS が定義されています。これを利用して dllexport 属性またはdllimport 属性を使用出来ます。

正しくdllexport属性が使用できていれば、ビルド時にインポートライブラリ(*.libファイル)が*.dllファイルと共に作成されます。

注意するべき点は、const char* 型をC#側でstring型として扱う場合、malloc()でヒープ領域にメモリを確保して返す必要があることです。うっかりスタック領域を返してしまうと、最悪Unity Editorごと落ちます。malloc()で確保したメモリは、stringが解放されるタイミングで開放されます。

ポインタを扱う場合、C#側ではIntPtr型で扱えますが、マーシャライズなどを行う必要があるため今回説明は省略します。

 

dllのテストプログラムの作成

dllが正しく作られたかテストを行うために、先ほどのソリューションに追加する形で、Windows コンソールアプリケーションを作成します。

f:id:chirotec:20180617201452p:plain

 テストプログラムのプロジェクトが追加されたら、テストプログラムのプロジェクト側の「参照」に、先ほどのdllのプロジェクトを追加します。

f:id:chirotec:20180617201449p:plain

 構成はこのようになります。テストプログラムの方をスタートアッププロジェクトに設定しておけば、デバッグ実行を行うことが出来ます。

f:id:chirotec:20180617201443p:plain

 インクルードディレクトリにdllプロジェクトのヘッダファイルが入ったフォルダを追加しておきます。

f:id:chirotec:20180617201603p:plain

以下のようなチェック用プログラムを作成しました。

NativePluginSampleTest.cpp

#include "stdafx.h"
#include "stdlib.h"
#include "NativePluginSample.h"

int main()
{
	printf("int:    %d\n", SampleAPIInt(111));
	printf("float:  %f\n", SampleAPIFloat(111.111f));
	printf("double: %f\n", SampleAPIDouble(111.111));

	int    i = 222;
	float  f = 222.f;
	double d = 222.0;
	SampleAPIInt2(i);
	SampleAPIFloat2(f);
	SampleAPIDouble2(d);
	printf("int:    %d\n", i);
	printf("float:  %f\n", f);
	printf("double: %f\n", d);

	int    is[3] = { 111, 222, 333 };
	long   ls[3] = { 111, 222, 333 };
	float  fs[3] = { 111.f, 222.f, 333.f };
	double ds[3] = { 111.0, 222.0, 333.0 };
	SampleAPIIntArray(is, 3);
	SampleAPILongArray(ls, 3);
	SampleAPIFloatArray(fs, 3);
	SampleAPIDoubleArray(ds, 3);
	printf("int:    %d,%d,%d\n", is[0], is[1], is[2]);
	printf("long:   %d,%d,%d\n", ls[0], ls[1], ls[2]);
	printf("float:  %f,%f,%f\n", fs[0], fs[1], fs[2]);
	printf("double: %f,%f,%f\n", ds[0], ds[1], ds[2]);

	const char st[] = "MyTest ";
	const char* p1 = SampleAPIString1();
	const char* p2 = SampleAPIString2(st);
	printf("string1: %s\n", p1);
	printf("string2: %s\n", p2);
	free((void*)p1);
	free((void*)p2);

	return 0;
}

 このプログラムを実行すると、dllの関数が呼び出されていることが分かります。

f:id:chirotec:20180617201600p:plain

 

Unity側での使用

Unityのプロジェクトを作成します。 

f:id:chirotec:20180617210619p:plain

”Plugins”フォルダを作成し、中にReleaseビルドしたdllファイルを入れます。

(64bit版と32bit版を用意し、Platform settingsで振り分けます。)

f:id:chirotec:20180617211100p:plain

C#の関数にdllの関数を割り当てます。
lib.cs

using System.Runtime.InteropServices;

public class Lib {

    // 値渡し
    [DllImport("NativePluginSample")]
    public static extern int SampleAPIInt(int i);
    [DllImport("NativePluginSample")]
    public static extern float SampleAPIFloat(float f);
    [DllImport("NativePluginSample")]
    public static extern double SampleAPIDouble(double d);

    // 参照渡し
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIInt2(ref int i);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIFloat2(ref float f);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIDouble2(ref double d);

    // 配列の参照渡し
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIIntArray(int[] intArray, int intArraySize);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPILongArray(int[] longArray, int longArraySize);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIFloatArray(float[] floatArray, int floatArraySize);
    [DllImport("NativePluginSample")]
    public static extern void SampleAPIDoubleArray(double[] doubleArray, int doubleArraySize);

    // 文字列
    [DllImport("NativePluginSample")]
    public static extern string SampleAPIString1();
    [DllImport("NativePluginSample")]
    public static extern string SampleAPIString2(string str);
}

テストプログラムを作成し、適当なオブジェクトにアタッチして実行します。

内容は先ほどのC++版のほぼ等価です。

TestObject.cpp

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestObject : MonoBehaviour {

	// Use this for initialization
	void Start () {
        Debug.Log("int:    " + Lib.SampleAPIInt(111));
        Debug.Log("float:  " + Lib.SampleAPIFloat(111.111f));
        Debug.Log("double: " + Lib.SampleAPIDouble(111.111));

        int i = 222;
        float f = 222.0f;
        double d = 222.0;
        Lib.SampleAPIInt2(ref i);
        Lib.SampleAPIFloat2(ref f);
        Lib.SampleAPIDouble2(ref d);
        Debug.Log("int:    " + i);
        Debug.Log("float:  " + f);
        Debug.Log("double: " + d);

        int[]    i_s = new int[] { 111, 222, 333 };
        int[]    l_s = new int[] { 111, 222, 333 };
        float[]  f_s = new float[] { 111.0f, 222.0f, 333.0f };
        double[] d_s = new double[] { 111.0, 222.0, 333.0 };
        Lib.SampleAPIIntArray(i_s, i_s.Length);
        Lib.SampleAPILongArray(l_s, l_s.Length);
        Lib.SampleAPIFloatArray(f_s, f_s.Length);
        Lib.SampleAPIDoubleArray(d_s, d_s.Length);
        Debug.LogFormat("int:    {0},{1},{2}", i_s[0], i_s[1], i_s[2]);
        Debug.LogFormat("long:   {0},{1},{2}", l_s[0], l_s[1], l_s[2]);
        Debug.LogFormat("float:  {0},{1},{2}", f_s[0], f_s[1], f_s[2]);
        Debug.LogFormat("double: {0},{1},{2}", d_s[0], d_s[1], d_s[2]);

        string st = "MyTest ";
        string s1 = Lib.SampleAPIString1();
        string s2 = Lib.SampleAPIString2(st);
        Debug.Log("string1: " + s1);
        Debug.Log("string2: " + s2);
    }
}

 

f:id:chirotec:20180617201553p:plain

 動作が確認できました。

 

Unityエディタ拡張はじめました

個人的にはやや不調ですので、リハビリに手短に行かせていただきます。m(__)m

少しは役に立つかな? という小ネタを書いてみます。 

 

Unityのエディタ拡張

UnityのEditorは、スクリプトで機能を追加することが出来ます。

Assetsフォルダの下のどこかに"Editor"という名前のフォルダを作成し、エディタ用のスクリプトをその中に置けばOKです。

今回は下の内容の"PageView.cs"ファイルを置きました。 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using UnityEngine.SceneManagement;

// ページ切り替えを行うエディタ拡張
// メニューのEditor/PageViewerから呼び出します。
public class PageViewer : EditorWindow
{
    [MenuItem("Editor/MyPageViewer")]
    private static void Create()
    {
        // 生成
        GetWindow("MyPageViewer");
    }

    // MyPageViewerウィンドウが描画されるときの処理
    private void OnGUI()
    {
        // scene中のルートオブジェクトを全て取得
        Scene scene = SceneManager.GetActiveScene();
        GameObject[] rootObjects = scene.GetRootGameObjects();

        List pageObjects = new List();
        foreach (var rootObj in rootObjects)
        {
            // 子オブジェクトをすべて取得
            Transform[] childObjectsTransform = rootObj.GetComponentsInChildren(true);
            List childObjects = childObjectsTransform.Select(x => x.gameObject).ToList();
            // このうち、PageTagタグが付いているGameObjectを取得
            List taggedObjects = childObjects.Where(x => x.tag == "PageTop").ToList();
            // リストに追加
            pageObjects.AddRange(taggedObjects);
        }

        // PageTagタグが付いたGameObject一覧を表示
        foreach(GameObject pageObject in pageObjects)
        {
            // ボタンを表示
            if (GUILayout.Button(pageObject.name))
            {
                // ボタンが押された時の処理
                // ボタンに対応したpageObjectをアクティブに、それ以外のpageObjectを非アクティブにする
                pageObjects.ForEach(x => x.SetActive(x == pageObject));
            }
        }
    }
}

今回はUnityEditor.EditorWindowクラスを継承して、ウィンドウを作成しております。

関数に[MenuItem("Editor/MyPageViewer")]という属性をつけていますが、これにより"Editor"および"MyPageViewer"という名前の項目が追加され、これを選択した時にこの関数が呼び出されます。

f:id:chirotec:20180617003609p:plain

また、OnGUI()はウィンドウが描画されるたびに呼び出されます。

仕組みとしては昔のUnityのGUIに近く、GUILayout.Buttonなどの関数でGUIのボタンなどの部品を表示します。

f:id:chirotec:20180617003542p:plain

このスクリプトでは、シーン中のルートにあるオブジェクトより下の階層にあるゲームオブジェクトの中から、「"PageTop"」という名前のタグが付いたゲームオブジェクトを検索し、該当するオブジェクトの名前をボタンとして一覧表示しています。このボタンが押されたとき、ボタンに対応したオブジェクトがアクティブになり、その他のボタンに対応したオブジェクトが非アクティブ状態となります。

 

上記の画像の例では、「Page1」~「Page5」ボタンにPageTopタグを割り当てております。

「Page2」ボタンを押すと「Page2」がアクティブになり、その他の「Page1」などが非アクティブになります。

 

この例の検索の条件などを色々変えてみると、作業に役に立つ機能が作れるかもしれません。

誰でもVTuberになれる?「3tene(ミテネ)」を使ってみました(1)

色々試してみたいことがありますが時間が取れない昨今ですが、

今日は「3tene」を試してみました。

 

3tene(ミテネ)とは

公式サイト様より

http://3tene.com/

>これからバーチャルユーチューバーを目指す方に、簡単に手軽に扱えることを目的として作られたアプリです。

公開されたばかりのアプリケーションですが、具体的には以下の機能があります。

VRM形式で作られたアバターを表示できます。

・ウェブカメラがあれば、顔認識で頭の移動、まばたき、リップシンクを行えます。

・キーボードやゲームパッド入力で表情やポーズを変更できます。

・クロマキー合成用のブルーバック表示や、エフェクト機能があります。

・録画、スクリーンショット機能などがあります。

OBS Studioなどの配信ソフトと連携して、YouTubeで配信を行うことが出来ます!

起動

現時点ではWindows用のFree版のみが公開されています。

zipファイルをダウンロード、展開して、exeファイルを実行すればOKです。

6/3時点で、私のメインPC(Windows10)では何故か起動が出来ませんでした。ユニティちゃんライセンスのロゴが表示された後にエラーメッセージの表示が何も無く閉じてしまいます。何かのドライバかセキュリティソフトが悪さしてるのでしょうか……? 今後の修正に期待致します。ノートPC(実はこちらの方がスペックが少し良い)では無事に起動しました。

起動に成功したところ、背景に何もないスカイボックスと、左右にメニューが表示されています。

ボタンの説明は、公式サイト様の最初のスクリーンショットに書かれております。

f:id:chirotec:20180604002556p:plain

VRMファイル

VRMとは

VRMとは、2018年4月にドワンゴが提唱した、VRアプリケーション向けの人型3Dアバターデータを扱うためのファイルフォーマットです。

VRM - dwango on GitHub

人型に特化した統一データ形式、プラットフォーム非依存、ライセンスファイルを埋め込めるといった特徴があります。

VRMファイルの入手

ドワンゴが提唱した形式ですので、ニコニコ動画の関連サービス「ニコニ立体」では当然いち早く対応しております。

3d.nicovideo.jp

ニコニ立体でのダウンロードは、ニコニコ動画のアカウント(無料で可)が必要です。

利用条件、ライセンスは必ず確認しましょう。

この記事では、利用条件が非常に緩い「ニコニ立体ちゃん」を利用させて頂きます。

3d.nicovideo.jp

また、VRMファイルは自作することも出来ます。機会があれば取り扱いたいと思います。

アバターの読み込み

先ほどダウンロードしたVRMファイルを分かりやすい所(マイドキュメントの中など)に置いておきます。

3tene画面の右列・上から2番目のファイル選択ボタンを押し、このVRMファイルを選択します。

ライセンスに同意すると、アバターが読み込まれます。

f:id:chirotec:20180604003217p:plain

WebCamの起動

ウェブカメラがPCに接続されていれば、右上端のボタンを押すと、ウェブカメラを起動できます。

ウェブカメラの表示は、右列一番下のボタンでON/OFFが出来ます。放送中にうっかり押さないように気を付けましょう。

首やまぶたを動かすとちゃんと追随してくれます。可愛い。気が付くと本人の顔が緩んでいます。

 

スペックについて

Core i7-7700HQ、GeForce GTX 1050TiのスペックのノートPCでも、アバターのみ表示時には60fpsをキープ、カメラ起動時には30~50fpsが出ていました。 

ただし録画機能を使うと3~15fps程度になります。録画する場合はShadowPlayを使った方がいいかも。

f:id:chirotec:20180604005215p:plain

まとめ

このように非常にお手軽に試すことが出来ます。

興味ある方は、是非とも触ってみて下さい。

【雑記】Unityのアセットを買ってみました

いそがし

多忙で更新できず、本当にすみません。

更新ペースを回復しようと、リハビリに雑記を書こうかなーとしております。

 

セールで沢山買ってみた

というわけで近況報告です。

現在は既に終わってますが、5月上旬に行われたUnity Assete StoreのMADNESS SALEで多数のアセットを購入してみました。

昔は毎日日替わりでセールが行われていたのですが、去年あたりから日替わりセールは無くなってしまいました。(´・ω・`) NGUIとか安く買えたのにねー

基本的に半年に一度行われるこのセールが、アセットを安く購入できる手段になってしまいました。次回は秋の終わり頃かなぁ、今後もあるとは限りませんが……

 

当時のセールの情報およびアセットの情報は、こちらのブログを参考に致しました。

いつも読ませていただいてありがとうございます! 

www.asset-sale.net

 

今回は以下のアセットを購入しました。

Dynamic Bone

OpenCV for Unity 

Final IK

Bolt

Mesh Effects

Realistic Nature Environment

Ultimate Fantasy Creator

Low Poly Series: Landscape

Toony Colors Pro 2

Universal Sound FX

Graph And Chart

Vegetation Studio

 Riko

Camera Filter Pack

SPACE for Unity - Space Scene Construction Kit

 30%~50%割引とはいえ、今回は多少お高いアセットが多いので、全部で$400を超えていて結構な出費です。もちろん全部自腹(´・ω・`) 

Graph And Chart とか何で買ったんだろと思いましたが……物欲って素敵ですね☆(正直に申しますと、エディタ拡張などの参考にしようとも思っています)

 

この中で一番欲しかったのはOpenCV for Unity ですかね……御存じオープンソースの画像処理ライブラリが元となっていますが、こちらはなんとWebGL版にも対応しています。(どこまで使えるかはまだ検証していませんが、デモを見ると可能性を感じました)

assetstore.unity.com

Final IKとかDynamic Boneなども併用すると、今流行のばーちゃるゆーちゅーばーっぽいことが出来るかもしれません。

 

Vegetation Studioは新しいアセットでお値段が張りますが、可能性を感じるので購入。

草木を大量に配置したいですよねー(*´・ω・)(・ω・`*)ネー

assetstore.unity.com

 

SPACE for Unityで、宇宙をつくってみたいですよね。

これから宇宙を舞台にしたゲームを作るかどうかは分かりませんが……

assetstore.unity.com

 

Rikoちゃんは可愛いから買いました!(危険な発言) 以上!

assetstore.unity.com

こういった感じで欲しいアセットを買ったら、合計$471.40になってしまいました。某ゲームの石が852個買えるよヤバいヤバい

とは言いつつ、有意義な買い物かなーと思います。

終わり

本当はアセットの使用感をレポートしたかったですが、まだそれほど触ってないのでまた次の機会にしたいと思います。

よろしくお願いします。

Unityのゲーム作りをさくっと解説(4)

久しぶりの更新です(二度目)

やっと時間がとれそうです……m(__)m

今回はクリックしたときの判定など、よりゲームらしくしていきます。 

 

GUIを先に作ってしまう(uGUI)

説明が前後してしまいますが、GUI関連のオブジェクトだけを先に置かせていただきます。(クリック判定にイベントシステムを使いたいので)

UnityのGUIの歴史

昔のUnityのバージョンにもGUIシステムは一応ありましたが、それがゲーム中で使うにはあまりにもショボかったため昔は皆 NGUI 等の有料アセットを利用していました。

しかし、Unity4.6あたりで公式に新しいGUIが実装されました。このシステム公式では「Unity の新しい UI システム 」等と書かれていますが、一般的には「uGUI」と呼ばれています。(古い方のGUIシステムはエディタ等で使うため IMGUI として現在でも残っています。)

uGUIをシーンに追加

さっそくuGUIをシーンに追加してみましょう。

GameObject => UI => Text

を選択すると、CanvasEventSystemがまだない場合にはシーンに追加され、TextオブジェクトがCanvasの下に追加されます。とりあえず追加したTextオブジェクトの名前を分かりやすくScoreTextに変更しておきます。

(先にイベントシステムだけ触りたい場合は単にEventSystemだけを追加しても良いです。なお、EventSystemはシーン中に複数同時に作成することは出来ません。)

f:id:chirotec:20180428172141p:plain

f:id:chirotec:20180428172137p:plain

CanvasとTextの設定

Canvasの設定を少しだけ触ってみます。

Canvasでは、UIをシーン内のどこに描画するかを設定します。Render ModeをScreen Space - Cameraとすると、カメラに合わせて描画します。

  • Render Cameraで描画する基準となるカメラの指定を忘れずに。
  • Plane DistanceではUIを描画するカメラからの距離を設定します。普通はオブジェクトの手前にUIが描画されるようにします。

Canvas Scalerでは、画面のサイズが変更されたときにどのようにスケーリングするかを設定します。UI Scale ModeをScale With Screen Sizeにすると、画面サイズに合わせてスケーリングします。

  • Reference Resolutionで、基準となる解像度を指定します。

f:id:chirotec:20180428194641p:plain

また、先ほど作られた"ScoreText"オブジェクト内のTextコンポーネントで、表示するテキストを設定することが出来ます。

現在は仮のテキストを置いておきます。

f:id:chirotec:20180428194638p:plain

 

GameManagerのシングルトン化

GameManagerを他のオブジェクトのプログラム中から触りたいので、シングルトンとして扱います。この場合GameManagerもしくはそのアタッチされたGameObjectを一つしか使わない前提となります。シングルトンって何?と気になる方は「シングルトンパターン」等で調べてみて下さい。

基本はGameManagerクラスのstatic変数を用意して、作成時に自身を入れる、これだけです。

多重作成を防ぐために、代入時に判定し、すでにある場合はDestroyします。

また、外部からはgetのみを呼べるようにし、setは呼べないようにしておきます。

現在はまだ行いませんが、シーン遷移した場合もこのGemeObjectを残すようにDontDestroyOnLoad()を呼び出しておきます。

    // シングルトン
    private static GameManager _instance = null;
    public static GameManager Instance
    {
        get{return _instance;}
    }
    (略)
    private void Awake()
    {
        if (Instance == null)
        {
            _instance = this;
        }
        else
        {
            Destroy(gameObject);
            return;
        }
        DontDestroyOnLoad(gameObject);
    }
    

 

クリックイベントの処理

ここまでで前準備が終わりました。

ピースをクリックした判定を、 イベントシステム で受け取り、処理を行います。

ピース側の処理

まず下記のようなPieceControl.csスクリプトを作り、各ピースのプレハブにアタッチしておきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems; // ← 必要

public class PieceControl : MonoBehaviour, IPointerClickHandler{
    // ピースの種類(色)
    public int PieceType;

    // 選択されたピースのフラグ
    public bool SelectFlag;
    // 選択されたピースに接続しているかのフラグ
    public bool JoinFlag;
    (略)
    public void OnPointerClick(PointerEventData eventData)
    {
        GameManager.Instance.OnClickPiece(this);
    }
}

 ついでに自身の色の情報も割り当てます。各ピースのプレハブのPieceTypeに、異なる数値を割り当てておきます。

SelectFlag, JoinFlagはこの記事の下の方で扱います。

f:id:chirotec:20180428194644p:plain

 

イベントシステムからクリックイベントを受け取るために、IPointerClickHandlerインターフェースを継承しています。

Colliderは既に(衝突判定を行うために)追加済みですので、実行中にGame画面をクリックするとOnPointerClick()関数がイベントシステムより呼び出されます。 

クリックされたことを検知したときに、GameManager側のOnClickPiece()を呼び出します。GameManagerはシングルトン化しているので、FindComponent()等で検索しなくても済みます。

 

GameManager側の処理 

GameManagerクラスに、次の処理を追加・変更します。

まずは、毎回ピースをシーン内から検索しなくても済むように、リストで管理します。

Update()の生成処理も書き換えます。

    // 生成したピースのインスタンス
    public List<PieceControl> pieceList;// ピースがクリックされたときに呼ばれる
    (略)
    void Update () {
        // ------------- ピース生成処理 ---------------
        // 一定回数呼ばれる毎にピース生成処理を行う
        tCount++;
        if (tCount >= genFrame)
        {
            if (pieceList.Count < pieceCountMax) // ←リストから判定
            {
                // 存在するピースの数が一定数以下の場合、新しくピースを生成する
                int pieceColor = Random.Range(0, 4);
                float pieceX = Random.Range(-2.0f, 2.0f);
                float pieceY = 6.0f;
                GameObject pieceObject = Instantiate(piecePrefab[pieceColor], new Vector3(pieceX, pieceY), Quaternion.identity);
                pieceList.Add(pieceObject.GetComponent<piececontrol>()); // ←追加
            }
            tCount = 0;
        }
    }
    // 対象のピースを削除する
    public void EracePiece(PieceControl piece)
    {
        // リストから除去
        pieceList.Remove(piece);
        Destroy(piece.gameObject);
    }

単純に C#のListコレクション で管理しています。生成時にリストに追加し、削除時にリストから除去しています。

次にGameManagerにクリック処理が呼ばれたときの関数を追加します。前段落のGameManager.Instance.OnClickPiece(this);で呼び出しているところです。

    // ピースがクリックされたときに呼ばれる
    public void OnClickPiece(PieceControl _piece)
    {
        EracePiece(_piece);
    }

これでクリックすると、ピースが一個消え、新しいピースが一個降るようになります。

f:id:chirotec:20180428203828g:plain

繋がったピースの判定 

クリックしたピースと同じ色で繋がったピースを消すようにしたいと思います。

この辺りからやや難解になりそうですが、

まずはソース一式を貼ります。

    public void OnClickPiece(PieceControl _piece)
    {
        // クリックしたピースとの接続判定を行います。
        OnSelectPiece(_piece);

        // 接続されたピースを全て消します。
        pieceList.FindAll(x => x.JoinFlag).ForEach(x =>
        {
            EracePiece(x);
        });
    }
    // ピースが選択されたときに呼ばれる
    public void OnSelectPiece(PieceControl _piece)
    {
        // フラグを一旦クリア
        pieceList.ForEach(x => { x.SelectFlag = false; x.JoinFlag = false; });
        pieceList.Find(x => x == _piece).SelectFlag = true;

        // ------------接続されたピースを探索します。------------------
        // 接続されたピースのリスト
        List<PieceControl> joinedPieceList = new List<PieceControl>();
        joinedPieceList.Add(_piece);
        // 接続判定対象の残りのピース
        List<PieceControl> fieldPieceList = new List<PieceControl>(pieceList);
        fieldPieceList.Remove(_piece);

        // 接続されたピースのフラグをON
        while (true)
        {
            List<PieceControl> addJoinedList = new List<PieceControl>();
            foreach (var joinedPiece in joinedPieceList)
            {
                foreach (var fieldPiece in fieldPieceList)
                {
                    // 色が違うか
                    if (fieldPiece.PieceType != joinedPiece.PieceType) continue;
                    // ピース間の距離が一定以上か
                    float d = Vector3.Distance(fieldPiece.transform.position, joinedPiece.transform.position);
                    if (d <= pieceRadius * 3.0f) // 半径1個分のスペースは許容する
                    {
                        // これも接続する。接続リストの末尾に追加する。
                        addJoinedList.Add(fieldPiece);
                    }
                }
            }
            // もう接続ピースが無い場合は抜けます。
            if (addJoinedList.Count == 0) break;
            // 接続リストに追加します。
            joinedPieceList.AddRange(addJoinedList);
            // 未接続リストから除外します。
            addJoinedList.ForEach(x=> fieldPieceList.Remove(x));
        }

        // 接続フラグを立てます。
        joinedPieceList.ForEach(x => x.JoinFlag = true);
    }
    

 

特に

 pieceList.FindAll(x => x.JoinFlag).ForEach(x => EracePiece(x););

て何だ! と感じる方が多いと思われます。

これはラムダ式と言います。上の例では最初のFindAll()関数ではpieceListの要素のうちからJoinFlagが立ったオブジェクトを集めたものをリストとして取得して、次のForEach()関数では前述のリストの各要素毎に、EracePiece()関数を呼び出しています。

そもそもListって何? コレクションって何? なC#やプログラムの初心者さん向けでやる内容じゃないと思いますが

許してください(´・ω・`)

代わりにラムダ式を使わないでforeach文などを使って書くと以下のようになります。

    public void OnClickPiece(PieceControl _piece)
    {
        // クリックしたピースとの接続判定を行います。
        OnSelectPiece(_piece);

        // 接続されたピースを全て消します。
        List<PieceControl> listJoinedPiece = new List<PieceControl>();
        foreach(var x in pieceList)
        {
            if (x.JoinFlag) listJoinedPiece.Add(x);
        }
        foreach(var x in listJoinedPiece)
        {
            EracePiece(x);
        }
    }
    // ピースが選択されたときに呼ばれる
    public void OnSelectPiece(PieceControl _piece)
    {
        // フラグを一旦クリア
        foreach (PieceControl p in pieceList)
        {
            p.SelectFlag = false;
            p.JoinFlag = false;
        }
        // 選択したピースのフラグをON
        foreach (PieceControl p in pieceList)
        {
            if (p == _piece)
            {
                p.SelectFlag = true;
                break;
            }
        }

        // ------------接続されたピースを探索します。------------------
        // 接続されたピースのリスト
        List<PieceControl> joinedPieceList = new List<PieceControl>();
        joinedPieceList.Add(_piece);
        // 接続判定対象の残りのピース
        List<PieceControl> fieldPieceList = new List<PieceControl>(pieceList);
        fieldPieceList.Remove(_piece);

        // 接続されたピースのフラグをON
        while (true)
        {
            List<PieceControl> addJoinedList = new List<PieceControl>();
            foreach (var joinedPiece in joinedPieceList)
            {
                foreach (var fieldPiece in fieldPieceList)
                {
                    // 色が違うか
                    if (fieldPiece.PieceType != joinedPiece.PieceType) continue;
                    // ピース間の距離が一定以上か
                    float d = Vector3.Distance(fieldPiece.transform.position, joinedPiece.transform.position);
                    if (d <= pieceRadius * 3.0f) // 半径1個分のスペースは許容する
                    {
                        // これも接続する。接続リストの末尾に追加する。
                        addJoinedList.Add(fieldPiece);
                    }
                }
            }
            // もう接続ピースが無い場合は抜けます。
            if (addJoinedList.Count == 0) break;
            // 接続リストに追加します。
            joinedPieceList.AddRange(addJoinedList);
            // 未接続リストから除外します。
            foreach(var x in addJoinedList)
            {
                fieldPieceList.Remove(x);
            }
        }

        // 接続フラグを立てます。
        foreach (var x in joinedPieceList)
        {
            x.JoinFlag = true;
        }
    }
    

どちらが分かりやすいでしょうか?

初心者向けではないとは思いますが、この考え方は色々な言語で役に立つと思います。多分。

 

話を戻します。

上記の処理のうちOnSelectPiece()では、選択したピースと同色で繋がっているピース全てをリストアップし、JoinFlag を立てています。全てのピースの繋がりを調べて、繋がっているピースをjoinedPieceListに貯め、最後にJoinFlag を立てます。

次に JoinFlag が立ったピースを全て削除します。

f:id:chirotec:20180428205428g:plain

 

基本のゲームシステムが出来上がりました。

次回は、演出を加えて豪華にしていきます!

姿勢推定とVRのお勉強

突然ですが、アニメとかゲームのキャラを自由に動かせたら楽しいと思いませんか?

バーチャルYouTuberさんもいいですね。私はシロさんが好きです。それ以外にも自由自在にキャラを動かせたら……と思うと夢が広がりそうです。

 

色々な手法があるようです。大まかには

モーションキャプチャーシステムやモーションセンサーでキャプチャ

・カメラ映像からそれっぽく生成

・あらかじめ大量のモーションを事前登録してその中から再生 ……など

 

機材用意する手間や費用を考えると、カメラを使えるとお手軽そうです。

カメラ映像からモーションを生成する手法のひとつについて調べてみました。

 

姿勢推定

CMUカーネギーメロン大学)で公開された

[1611.08050] Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields

という論文があります。

姿勢推定、画像中の複数の人の2D姿勢を効率的に検出する手法とのことです。

映像から、リアルタイムに人間の姿勢を取得するのですね。

内容

こちらの方のメモを参考にさせて頂きました。

yusuke-ujitoko.hatenablog.com

構造はCNN(畳み込みニューラルネットワーク)で、

・まず一つ、Part Confidence Mapsを作り、体部位の位置を予測する

・もう一つ、Part Affinity Fieldsを作り、体部位間の繋がりを予測する

・これらを連結して、結果を出力する。

これをひとつのstageとし、これを繰り返して、精度を高める。

 

ただPart Affinity Fieldsを解くのはNP困難なので、問題を緩和する。

・エッジの数が最小になるように条件を決定する

・隣接する部位の間のみを解く

 

と問題を単純にするようです。

 

うーん、まだ正直よく分かってない、もう少し調べないと……

もっと機械学習を勉強して、プログラムを作ってみるのが良いのでしょうか。

 

実装例

CMUではOpenPoseというC++ライブラリを公開されています。研究用、非商用だと無料で扱えるとのことです。

 

また、DeNAの研究開発の方がPythonとChainerで実装されていたりします。

こちらの記事もとても参考になります。

engineer.dena.jp

サンプルプログラムを実行しましたが、顔の輪郭や体の姿勢がリアルタイムで取れていました。素晴らしい。

 

おわり 

あまりまとまってなくてすみません。

近いうちに実際に組んでみたいかと思います。

Unityのゲーム作りをさくっと解説(3)

久しぶりの更新です……

多忙により前回の更新より少し間が開いてしまいました、すみません。

 

Unityの初心者向けさくっと解説です。

今回はスクリプトを導入します。

出来ることが大幅に広がり、いよいよゲームらしくなってきます。

 

コンポーネントの追加

現在、シーン内に機能を持たない玉オブジェクトがぽつんと置かれている状態です。(※厳密にはスプライトを描画する機能だけを持っています)

Unityでは、このオブジェクトコンポーネントを足して機能を追加します。

参考:ゲームオブジェクト - Unity マニュアル

物理エンジン関連コンポーネントの追加

HierarkeyウインドウまたはSceneウインドウで玉オブジェクトを選択すると、Inspectorウインドウにコンポーネントが表示されます。

現在の玉オブジェクトには「Transforrm」と「Sprite Renderer」コンポーネントのみがあります。

f:id:chirotec:20180407175243p:plain

玉オブジェクトに、コンポーネントを追加します。 InspectorウインドウのAdd Componentボタンよりコンポーネントを追加(アタッチ)出来ます。

ここではPhysics 2Dカテゴリーの「RigidBody 2D」を追加してみます。 f:id:chirotec:20180407175237p:plain

動かしてみます。「Play」ボタンを押すとエディタ内で動作します。

f:id:chirotec:20180407175531g:plain

玉が落ちていきました。RigidBody 2Dにより、このようにオブジェクトが物理エンジンによる影響を2D空間内で受けるようになります。

次は、玉に「Circle Collider 2D」を、周囲の壁3つに「Box Collider 2D」をアタッチします。

f:id:chirotec:20180407175524g:plain

接触判定が行われるようになりました。

 

プレハブ化

次は玉を大量に生成したいですが、その前に「プレハブ化」を行います。

オブジェクトをプレハブに格納しておくことで、複製が容易になります。

プレハブの作成

上のメニューからAssets -> Create -> Prefab

またはAssetsウインドウで右クリックしてCreate -> Prefabを選択します。

f:id:chirotec:20180407175233p:plain

空のプレハブ「New Prefab」がプロジェクト内に作られます。分かりやすいように名前を付けておきます。

f:id:chirotec:20180407191235p:plain

Hierarchyウインドウのオブジェクトをプレハブにドラッグアンドドロップすると、オブジェクトの内容がプレハブに格納されます。

f:id:chirotec:20180407192715g:plain

また、これまでシーン中にあったオブジェクトはプレハブに関連付けされ、このプレハブのインスタンス(実体)となります。

試しにプレハブをシーン中にドラッグアンドドロップしてみると、プレハブの複製が新しいインスタンスとしてシーン中に配置されるのが分かると思います。

プレハブのインスタンスはHierarchyウインドウ内では青文字となり、Inspectorウインドウでは「Prefab」の項目が表示されます。

f:id:chirotec:20180407193825p:plain

例えばシーン中に配置したインスタンスを更新した後にApplyボタンを押すと、プレハブが同じ状態に更新されます。
なお、メニューの「GameObject -> Break Prefab Instance」を選択すると、このオブジェクトとプレハブの関連付けを解除できます。

スクリプト

ここではゲームのシーン全体を管理するスクリプトを追加します。

スクリプトの導入

ここでは空のオブジェクトを作ってスクリプトを置きます。どこに追加するかは迷うところですが、例えばカメラのオブジェクトに追加してもいいと思います。

HierarchyウインドウかGameObjectメニューの「Create Empty」で空のオブジェクトを生成します。

InspectorウインドウのAdd ComponentボタンからNew Scriptを選択すると、新しいスクリプトがAssetsフォルダに追加されると同時にオブジェクトにアタッチされます。

f:id:chirotec:20180407175230p:plain

Projectウインドウのスクリプト(歯車のアイコン)をダブルクリックすると、スクリプトエディタが起動します。Windows版のUnity2017の標準的なインストール設定だとVisual Studio 2017 Communityが起動するはずです。(標準的にはMonoDevelopがあります。Visual Studio Codeなどを使用することも出来ます。)

f:id:chirotec:20180407175543p:plain

※右上のアカウント情報は伏せてます

スクリプトの編集

C#の解説は少な目です(すみません……他サイト様のC#の解説なども参照願います)

Unityのスクリプトは基本的にはMonoBehaviourクラス ※リファレンス の派生クラスとなります。ここにゲームオブジェクトの振る舞いを実装します。

(画像)

f:id:chirotec:20180407175540p:plain

(テキスト)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {

    // フレームのカウンター
    private int tCount = 0;
    // ピース(現在はボール)
    public GameObject peace;

    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        tCount++;
        if (tCount>=30)
        {
            // 新しくピースを発生させる
            Instantiate(peace, new Vector3(1.8f, 5.0f), Quaternion.identity);
            // カウントを0に戻す
            tCount = 0;
        }
    }
}

Update()関数は毎フレーム呼び出される関数です。30フレームごとにInstantiate()関数 ※リファレンス を呼び出し、ボールのプレハブのインスタンスを生成します。

 

メンバ変数のpeaceが初期化されてないように見えますが、これはInspectorで行うためです。 後で気が付きましたが、pieceのスペルを間違ってました

publicの変数はインスタンスが生成されるときに初期化されます。GameObject型の変数にはプレハブやシーン中のオブジェクトを入れられます。ここで先ほど作成したボールのプレハブを指定しましょう。 

f:id:chirotec:20180407175537p:plain

publicの変数はインスタンスが生成されるときに初期化されます。ここで先ほどのボールが入ったプレハブを指定しましょう。 

 

実行してみます。

f:id:chirotec:20180407220259g:plain

上から降り続けるようになりました。

スクリプトの改良

もう少し修正してみます。

  • 4色ランダムで降るようにする
  • 降ってくる位置をランダムにする
  • 降ってくる頻度を変更できるようにする
  • シーン中に存在する玉数の上限を決め、一定以上降らないようにする

生成元のプレハブを配列にし、複数の中からランダムで選ばれるようにします。

Random.Range()は乱数を生成する関数です。引数が整数(int)型のときと小数(float)型のときは振る舞いが少し異なります。※リファレンス Random.Range

降ってくる玉の種類を0以上4未満のうちからランダムで選択し、生成するx座標を2.0f以上2.0f以下からランダムで決定します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {

    // フレームのカウンター
    private int tCount = 0;
    // ピースを一個生成するフレーム数
    public int genFrame;

    // これまでに生成したピースの数
    private int pieceCount = 0;
    // ピースの最大生成数
    public int pieceCountMax;

    // 生成するピースのプレハブ
    public GameObject[] piecePrefab;

    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        tCount++;
        if (tCount >= genFrame)
        {
            if (pieceCount >= pieceCountMax)
            {
                int pieceColor = Random.Range(0, 4);
                float pieceX = Random.Range(-2.0f, 2.0f);
                // 新しくピースを発生させる
                Instantiate(piecePrefab[pieceColor], new Vector3(0, pieceX), Quaternion.identity);
                pieceCount++;
            }
            // カウントを0に戻す
            pieceCount = 0;
        }
    }
}

 

色違いのボールのプレハブを3種類用意します。

f:id:chirotec:20180407210917p:plain

配列となったGameObject型変数pieceのサイズに4を入力して、プレハブを入れます。他のパラメータにも値を入れましょう。ついでにスペルミスも直しました

f:id:chirotec:20180407210923p:plain

実行

 

f:id:chirotec:20180407214158g:plain

非常にそれっぽくなりましたね。 降ってくる最大数のパラメータを変更すると、その通りになるのが確認出来ると思います。

 

次回予告 

次回はユーザーの操作も入れて、よりゲームらしくします。 今度はなるべく早いうちに……