20190724のC#に関する記事は8件です。

[VisualStudio] 「リンクとして追加」で、プロジェクト間で同じソースファイルを使う

やりたいこと

VisualStudioで、1つのソリューション(sln)に複数のプロジェクト(csproj)が含まれているときに、まったく同じクラスを両方で使いたいとなったときに、dllに分けるまでしなくても簡易的に同じクラスを使える(共有できる)ようにしたい。

前提

VisualStudio2019
Consoleアプリケーション(.net Framework)
で実験実施。

やり方

ファイルの「リンクとして追加」を使う。

  • 共有される側のプロジェクトで、共有されるクラスを作成する。
  • 共有する側のプロジェクトを右クリックする。
  • [追加]を選択する。
  • [既存の項目]をクリックする。

image.png

  • 共有されるクラスを記述したファイルを選択する。
  • その状態で、[追加]ボタンの横の[▼]を押し、[リンクとして追加]を押す。

image.png

これで、共有する側のプロジェクトに、そのcsファイルが追加される。
(リンクとして追加したファイルには、image.pngのアイコンがつく。)
image.png

注意

[リンクとして追加]で追加したファイルは、リンク元のファイルと同じファイルを見ているので、どちらかを変えるともう片方にも同じ変更が入ってしまうので注意。
(ファイルの実体(.csファイル)は、元のプロジェクトの方にだけ存在していて、リンク先のプロジェクトにはない。)
→.csprojファイルに、リンクした旨の設定が追加される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C++でC#のDLLを利用する方法

処理時間かかりすぎな手法(C++/CLIを使う)

高速化を目指し、C#で処理を書き、C++で呼ぶ構成にしたのに、
C++/CLIでdllを実行すると、処理時間が大幅に長くなりました。
実装した方法と、実行時間を短縮するために考察した事をまとめました。

C++/CLIでdllを呼び出す際の実装方法

①.プロパティーページで共通言語ランタイムサポートを有効にする。

「プロジェクト(P)」→「プロパティー(E)」→「構成プロパティ」→「詳細」→「共通言語ランタイムサポート」
の設定を
「共通言語ランタイムサポート(/clr)」
に変更する。

②.参照に使用したいDLLを指定する。

プロジェクトのフォルダにDLLを移動しておく。(移動しなくても大丈夫ですが移動しておいた方が確認しやすいので・・・)
「ソリューションエクスプローラ」内の「参照」で右クリックし「参照の追加」を選択
左の「参照」タブをクリックし、使用したいDLLを選択する。

③.使用したいDLLを呼び出す

呼び出したいソースで「::」を利用し、クラスを呼び出す。

考察

「C++/CLR」を有効にし、呼び出す事で、C++で「COMオブジェクト」を呼び出すのと同様になってしまい、処理時間が長くなっているのではないか?

実測値

ネットで「マネージ関数とネイティブ関数を1つのプロジェクト内に混在させた場合」の実行速度を計測してくれている方が居ました。

実行方式 実行時間
ネイティブ関数 → ネイティブプラグマ内のネイティブ関数 31 ms
ネイティブ関数 → マネージドプラグマ内のネイティブ関数 1047 ms
マネージ関数 → ネイティブプラグマ内のネイティブ関数 4281 ms
マネージ関数 → マネージドプラグマ内のネイティブ関数 266 ms
マネージ関数 → マネージドプラグマ内のネイティブ関数(__clrcall規約) 266 ms
マネージ関数 → マネージド関数 281 ms

処理時間をかけないために、どうするべきか?

CLRを利用せずに、C#の関数を呼び出せるようにしなければならない。
C#側で関数をDLLにエクスポートする必要がある。

候補①.DllExport.batの利用

これは、Nugetで取得すると怒られるので公式サイトから落とす。
DllExportのGitHub

候補②.Unmanaged Exportsの利用

これは、Nugetで取得できる模様
Unmanaged Exportsのページ

用語集

ネイティブコード

コンピュータのCPU(MPU/マイクロプロセッサ)が理解できる形式で記述されたコンピュータプログラム。 プロセッサに対する命令の仕様を定義した機械語(マシン語)で書かれており、人間が直接読み書きすることは困難である。

アンマネージコード(アンマネージドコード)

Microsoft . NETにおける実行可能形式のプログラムコードの形式の一つで、. NETの共通中間言語(CIL)以外の形式で記述されたもの。

マネージコード(マネージドコード)

Microsoft .NETにおける実行可能形式のプログラムコードの形式の一つで、.NETの共通中間言語(CIL)によって記述されたもの。CILを解釈・実行できるCLR(Common Language Runtime/共通言語ランタイム)が用意されている環境であれば、どこでも等しく実行することができる。

CIL

共通言語基盤 (Common Language Infrastructure、CLI) で定義された最も低水準な人間が解読可能なプログラミング言語であり、.NET FrameworkやMonoにより使用される。

ガベージコレクション(GC)

誰からも参照されなくなったオブジェクトを自動的に回収し解放する機能

マネージリソース

GCの管理下にあるリソース

アンマネージリソース

GCの管理下にないリソース
ファイルハンドル、ウィンドウハンドル(HWND)、データベース接続 など。

参考サイト

C++/CLR時に参考にしたサイト

ガベージコレクション(GC)とは?
StringとCString(gcnewについて)
C++(EXE)からC#(DLL)の関数を呼び出す

速度向上調査時に参考にしたサイト

[C++/CLI] マネージド、アンマネージド間の関数呼び出し速度調査
C#のメソッドをC++から呼ぶ方法

C#側で関数をDLLにエクスポート時に参考にしたサイト

C++からC# DLL 超超超入門
[C++/C#]C#をC++/CLIでラップしてC++アプリから呼ぶ

セキュリティの「ブロックの解除」について

依存関係の一つが読み込めないDLLエラーの対策と原因
複数ファイルの「ブロックの解除」を一括で行う方法
Windowsで、複数のファイルのブロック解除を簡単に行う(スクリプトで!)

備忘録メモ

CファイルをC++としてコンパルする場合のコンパイルオプション

①「共通言語ランタイムサポート(/clr)」を有効にする。
②「基本ランタイム チェック」を規定にする。
③「C/C++」→「詳細設定」→「コンパイラ言語の選択」を「C++ コードとしてコンパイル(/TP)」に変更

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C++からC#のDLLを呼び出す際の引数

経緯

C#で作成したDLLをC++で呼び出そうとした場合、引数の受け渡しの部分で少しはまったので、まとめておきます。

DLLと呼び出し側の構成について

引数の型によって呼び出す構成が変わります。

引数の型 データ 渡し方
プリミティブ型 数値
(固定の)文字列
論理値
null
undefined
IntPtr
そのまま受け渡す事が可能
オブジェクト型 (可変の)文字列など IntPtrでポインタとして渡し、
受け取り後に変換

上述した通り、可変の文字列などは、普通の関数のように引数を受け渡す事ができない(もしかしたらできるかも・・・)ため、一旦ポインタで渡し、受け取り側で変換することが必要です。

プリミティブ型の引数の受け渡し方

DLLでの引数の受け取り方

ポインタ型の引数を受け取り、Marshal.PtrToStringAutostring型に変換します。

DLL側.cs
        [DllExport]
        public static bool hoGE(int iTmp)
        {
            ・・・//普通にiTmpを使える。
        }

呼び出し側の引数の渡し方

今回は以下のようにヘッダーを作成しました。
クラス内に関数ポインタ型の変数を宣言し、コンストラクタで変数に関数のポインタを格納します。

呼び出し側.h
auto dll = ::LoadLibrary("EM.dll");
typedef bool (*HOGE)(int);
class EM
{
public:
    EM();
    ~EM();
    HOGE            hoge;
};
EM::EM()
{
    hoge = reinterpret_cast<HOGE>(::GetProcAddress(dll, "hoGE"));
}
EM::~EM()
{
    if (dll)
        ::FreeLibrary(dll);
}

hogeの引数の型がintなので、プリミティブ型であり、直接受け渡すことが可能です。

呼び出し側.cpp
EM EMD;
void Check()
{
      int Tmp = 25;
      EMD.hoge(Tmp);
        ・・・
        }

プリミティブ型での直接受け渡し時に注意すること

C#とC++の型の違いに注意してください。
今回、自分がはまったのが、long型の受け渡しでした。
型名は同じなのに、C#とC++でバイト数が異なるため、意図していない値になっていました。
(整数型・文字型のサイズは自身の処理系に依存します。)

バイト数 C++ C#
1 char sbyte
2 wchar_t char
2 short short
4 int int
4 long ---
8 __int64(Microsoft固有) long
※コメントでご指摘頂いた点

C++にlong型は存在しません。整数型はintであり、short/long/long longは整数型のサイズを指定する修飾子です(unsigned/signedは符号の有無を指定する修飾子)。修飾子が使用されている場合にのみintを省略することができます。

オブジェクト型の引数の受け渡し方

DLLでの引数の受け取り方

ポインタ型の引数を受け取り、Marshal.PtrToStringAutostring型に変換します。

DLL側.cs
        [DllExport]
        public static bool hoGE(IntPtr pTmp)
        {
            var sTmp = Marshal.PtrToStringAuto(pTmp);
        ・・・
        }

呼び出し側の引数の渡し方

今回は以下のようにヘッダーを作成しました。
クラス内に関数ポインタ型の変数を宣言し、コンストラクタで変数に関数のポインタを格納します。

呼び出し側.h
auto dll = ::LoadLibrary("EM.dll");
typedef bool (*HOGE)(const wchar_t*);
class EM
{
public:
    EM();
    ~EM();
    HOGE            hoge;
};
EM::EM()
{
    hoge = reinterpret_cast<HOGE>(::GetProcAddress(dll, "hoGE"));
}
EM::~EM()
{
    if (dll)
        ::FreeLibrary(dll);
}

hogeの引数の型がconst wchar_t*なので、CString型をCStringWに変換し、GetString()を行うことでポインタを渡しています。

呼び出し側.cpp
EM EMD;
void Check(CString strFunc)
{
      CStringW Tmp = strFunc;
      EMD.hoge(Tmp.GetString());
        ・・・
        }

参考サイト

参考にさせていただいたサイトです。

C#とC++の型について
プリミティブ型とオブジェクト型

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】範囲外のオブジェクトを端っこで表示させ続けるミニマップ

概要

重要な建造物はミニマップやレーダー上で範囲外になっても端っこに表示させ続けるゲームは数多く存在します、多分。
今回はそんなミニマップをUnity上で作ってみようと思います。

ちなみに完成するとこんな感じになります。
今回は範囲外であることがわかるように半透明で表示させています。
05_minimapComplete.gif

下準備

1. とりあえず動くキャラクターと適当にオブジェクトを配置する

  • とりあえず地面と動かせるものを準備する。困ったら豆腐
    • 地面はGround、動かせるものはPlayerとかわかりやすいようにしておく
  • 後はミニマップに表示するためのオブジェクトも適当に配置
    • 名前はObjectとか適当に
  • ついでにカメラもそれっぽく追尾させる

01_moveBox.gif

2. ミニマップのアイコンを用意する

それっぽいアイコンを作ります。
面倒くさかったらProjectからCreate->Spritesを使ってもいいです。
色はSpriteRendererで変えることもできるのでとりあえず白のみの画像で。
minimap_icon.png

3. ミニマップのフレームとマスク画像を用意する

今回は円形で作るので円形のフレームとマスク画像を準備します。即席で作ったので使いたい方はどうぞ。
minimap_frame.png minimap_mask.png

ミニマップを表示する

まずはシンプルな円形ミニマップを作ります。

1. ミニマップ用のカメラを作成する

  • 空のゲームオブジェクトMinimapを作る
  • Minimapの子になるようにミニマップ用のカメラMinimap Cameraを追加
    • Rotationを(90, 0, 0)にしてカメラを真下に向かせる
  • プレイヤーを中心にカメラを動かすスクリプトを作ってMinimap Cameraにアタッチする
    • InspectorからプレイヤーのTransformを設定するのを忘れずに
  • ProjectionをOrthographicにしてSizeを適宜調整する 01_minimapcamera2.png
MinimapCamera.cs
public class Minimap : MonoBehaviour {
    [SerializeField] private Player player;

    void Update () {
        var pos = player.transform.position;
        pos.y = transform.position.y;
        transform.position = pos;
    }
}

Minimap Cameraのカメラプレビューでプレイヤーを追尾していることを確認します。
02_minimapCamera.gif

2. ミニマップを円形で表示する

ミニマップカメラで映しているものを画面上に表示します。

  • ProjectからRender Textureを作成、ファイル名はMinimapRenderにする
  • Minimap Cameraを選択して、CameraのTarget Textureに作成したMinimapRenderを設定する

これでRender TextureにMinimap Cameraの映しているものを表示することができます。
次にミニマップを円形にして表示します。

  • フレーム用のImageを作成する
    • 名前はMinimap FrameにしてSource Imageにフレーム画像を設定する
  • Minimap Frameの子にマスク画像のImageを作成する
    • 名前はMinimap MaskにしてSource Imageにマスク画像を設定する
    • 設定したらMaskをアタッチしてShow Mask Graphicのチェックを外す
  • Minimap Maskの子にRaw Imageを作成する
    • 名前は'Minimap Render'にしてTextureにMinimapRenderを設定する
  • Rect TransformのWidthとHeightは適宜調整する

ヒエラルキーはこんな感じに
02_minimapdisp.png

ミニマップが円形で表示されてることを確認します。
03_minimap.gif

3. ミニマップにアイコンを表示する

現在のミニマップは上から撮ったものをそのまま映しているだけです。そのため、地面とアイコンのみを表示します。

  • 地面用とアイコン用のレイヤーを追加する
    • それぞれGround, Minimapにする
  • Main CameraのCulling MaskからMinimapを除外する
  • Minimap CameraのCullin MaskをGroundMinimapにする

これでメインカメラはMinimap以外、ミニマップカメラはGroundMinimapのレイヤーのみを映すことができます。
アイコンのレイヤーをMinimapに設定するとミニマップ上にアイコンを表示することができます。

早速プレイヤーのアイコンをミニマップに表示させてみます。
- ヒエラルキーのPlayerにミニマップ用のアイコンをD&Dする
- 名前はMinimapIconとかにリネームする
- Rotationを(90, 0, 0)にする
- レイヤーをMinimapにする

03_minimapicon.png

これでミニマップ上にプレイヤーを示すアイコンを表示することができます。
Objectに対しても同じことを行い、オブジェクトのアイコンも表示させてみるとこんな感じ。
04_minimapIcon.gif

ミニマップ範囲外のオブジェクトを半透明で表示する

アイコン表示を制御するためのスクリプトを作成してObjectMinimapIconにアタッチします。
minimapCameraはInspector上から設定していますが、ミニマップカメラにタグつけてGameObject.FindGameObjectWithTagを使ってもいいと思います。
表示範囲は何も考えずにカメラのorthographicSizeに設定しておきます(orthographicSizeはカメラ縦幅の半分の大きさ)。

MinimapIcon.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
public class MinimapIcon : MonoBehaviour {
    [SerializeField] private Camera minimapCamera;              // ミニマップ用カメラ
    [SerializeField] private Transform iconTarget;              // アイコンに対応するオブジェクト(建造物等)
    [SerializeField] private float rangeRadiusOffset = 1.0f;    // 表示範囲のオフセット

    // 必要なコンポーネント
    private SpriteRenderer spriteRenderer;

    private float minimapRangeRadius;   // ミニマップの表示範囲
    private float defaultPosY;          // アイコンのデフォルトY座標
    const float normalAlpha = 1.0f;     // 範囲内のアルファ値
    const float outRangeAlpha = 0.5f;   // 範囲外のアルファ値

    private void Start () {
        minimapRangeRadius = minimapCamera.orthographicSize;
        spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
        defaultPosY = transform.position.y;
    }

    private void Update () {
    }
}

1.オブジェクトがミニマップ範囲内か確認する

ミニマップ用カメラとオブジェクトの距離を求めて、その距離がミニマップ範囲内かチェックします。
今回はVector3.Distanceで距離を求めます。平面上の距離を求める必要があるので、Y座標は統一しておきます。

MinimapIcon.cs
    private bool CheckInsideMap() {
        var cameraPos = minimapCamera.transform.position;
        var targetPos = iconTarget.position;

        // 直線距離で判定するため、yは0扱いにする
        cameraPos.y = targetPos.y = 0;

        return Vector3.Distance(cameraPos, targetPos) <= minimapRangeRadius - rangeRadiusOffset;
    }

2.ミニマップ範囲内のオブジェクトを表示

ミニマップ範囲内の場合はSpriteRendererのアルファ値を1にしてそのまま表示すればOKです。

MinimapIcon.cs
    private void Update () {
        DispIcon();
    }

    private void DispIcon() {
        // アイコンを表示する座標
        var iconPos = new Vector3(iconTarget.position.x, defaultPosY, iconTarget.position.z);

        // ミニマップ範囲内の場合はそのまま表示する
        if (CheckInsideMap()) {
            spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, normalAlpha);
            transform.position = iconPos;
            return;
        }
    }

3.ミニマップ範囲外のオブジェクトを表示

ミニマップ端にアイコンを表示するためにベクトルを計算する必要がありますが、Vector3.ClampMagnitudeを使うことで簡単に求めることができます。
このAPIはベクトルと最大距離を指定すると最大距離までに制限したベクトルを返してくれるすごいやつです。これを紹介するためにこの記事を書いてると言っても過言ではない。
方向ベクトルをVector3.ClampMagnitudeで制限することでミニマップ端までのベクトルを簡単に求めることができます。
半透明にするにはSpriteRendererのアルファ値を0.5くらいにすればOKです。

MinimapIcon.cs
        // アイコンを半透明にする
        spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, outRangeAlpha);

        // カメラとアイコンの位置から方向ベクトルを求める
        var centerPos = new Vector3(minimapCamera.transform.position.x, defaultPosY, minimapCamera.transform.position.z);
        var offset = iconPos - centerPos;

        // 指定距離で制限した方向ベクトルを求めてアイコン位置を設定する
        transform.position = centerPos + Vector3.ClampMagnitude(offset, minimapRangeRadius - rangeRadiusOffset);

4.コード全文

MinimapIcon.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
public class MinimapIcon : MonoBehaviour {
    [SerializeField] private Camera minimapCamera;              // ミニマップ用カメラ
    [SerializeField] private Transform iconTarget;              // アイコンに対応するオブジェクト(建造物等)
    [SerializeField] private float rangeRadiusOffset = 1.0f;    // 表示範囲のオフセット

    // 必要なコンポーネント
    private SpriteRenderer spriteRenderer;

    private float minimapRangeRadius;   // ミニマップの表示範囲
    private float defaultPosY;          // アイコンのデフォルトY座標
    const float normalAlpha = 1.0f;     // 範囲内のアルファ値
    const float outRangeAlpha = 0.5f;   // 範囲外のアルファ値

    private void Start () {
        minimapRangeRadius = minimapCamera.orthographicSize;
        spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
        defaultPosY = transform.position.y;
    }

    private void Update () {
        DispIcon();
    }

    /// <summary>
    /// アイコン表示を更新する
    /// </summary>
    private void DispIcon() {
        // アイコンを表示する座標
        var iconPos = new Vector3(iconTarget.position.x, defaultPosY, iconTarget.position.z);

        // ミニマップ範囲内の場合はそのまま表示する
        if (CheckInsideMap()) {
            spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, normalAlpha);
            transform.position = iconPos;
            return;
        }

        // マップ範囲外の場合、ミニマップ端までのベクトルを求めて半透明で表示する
        spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, outRangeAlpha);
        var centerPos = new Vector3(minimapCamera.transform.position.x, defaultPosY, minimapCamera.transform.position.z);
        var offset = iconPos - centerPos;
        transform.position = centerPos + Vector3.ClampMagnitude(offset, minimapRangeRadius - rangeRadiusOffset);
    }

    /// <summary>
    /// オブジェクトがミニマップ範囲内にあるか確認する
    /// </summary>
    /// <returns>ミニマップ範囲内の場合、trueを返す</returns>
    private bool CheckInsideMap() {
        var cameraPos = minimapCamera.transform.position;
        var targetPos = iconTarget.position;

        // 直線距離で判定するため、yは0扱いにする
        cameraPos.y = targetPos.y = 0;

        return Vector3.Distance(cameraPos, targetPos) <= minimapRangeRadius - rangeRadiusOffset;
    }
}

5.動作確認

スクリプトのInspectorからminimapCamera, iconTarget, Range Radius Offsetを設定しつつ、Objectのアイコン表示が期待通りに動作すればOKです。今回はRange Radius Offsetを3.5にしてみました。
05_minimapComplete.gif

おしまい

というわけで割とお手軽に作ることができました。
作り始めたときは「画面端へのベクトル計算とかどうするねん」って思っていたのですが、Vector3.ClampMagnitudeを使えば面倒な計算とかしなくても実装することができました。

すごく簡単に作ったので「ここおかしくね?」みたいなのあったらぜひコメントください。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デザインパターン入門_Factory Methond

Javaで学ぶデザインパターン入門

結城浩「Javaで学ぶデザインパターン入門」をC#で勉強

FactoryMethod パターン

Templete Methodパターン(スーパークラスで処理の枠組みを定め、 サブクラスでその具体的内容を定めるパターン)をインスタンスの生成場面に 適用したもの。 インスタンス生成のための枠組みとインスタンス生成クラスを分けて考えることができる。

実装 (frameworkパッケージ)

// Productクラス

namespace DesignPatternLearn.FactoryMethodLearn.FrameWork
{
    public abstract class Product
    {
        public abstract void Use();
    }
}

// Factory クラス

namespace DesignPatternLearn.FactoryMethodLearn.FrameWork
{
    public abstract class Factory
    {
        public Product Create(string owner)
        {
            var product = CreateProduct(owner);
            RegisterProduct(product);
            return product;
        }

        public abstract Product CreateProduct(string owner);
        public abstract void RegisterProduct(Product product);

    }
}

実装 (idcardパッケージ)

// IdCardクラス(Productクラスの実装)

using DesignPatternLearn.FactoryMethodLearn.FrameWork;

namespace DesignPatternLearn.FactoryMethodLearn.IdCard
{
    class IDCard : Product
    {
        public string Owner { get; set; }
        public IDCard(string owner)
        {
            Console.WriteLine(owner + "のカードを作ります。");
            this.Owner = owner;
        }
        public override void Use()
        {
            Console.WriteLine(Owner + "のカードを使います。");
        }
    }
}

// IdCardFactoryクラス(Factoryクラスの実装)
using DesignPatternLearn.FactoryMethodLearn.FrameWork;

namespace DesignPatternLearn.FactoryMethodLearn.IdCard
{
    public class IDCardFactory : Factory
    {
        private List<string> owners = new List<string>();
        public List<string> Owners { get => owners; }

        public override Product CreateProduct(string owner)
        {
            return new IDCard(owner);
        }

        public override void RegisterProduct(Product product)
        {
            owners.Add(((IDCard)product).Owner);
        }
    }
}

実装(Main)

using DesignPatternLearn.FactoryMethodLearn.FrameWork;
using DesignPatternLearn.FactoryMethodLearn.IdCard;

namespace DesignPatternLearn.FactoryMethodLearn
{
    class FactoryMethodLearn
    {
        public static void Main(string[] args)
        {
            // Factoryクラス
            Factory factory = new IDCardFactory();

            /* ここが重要
            * インスタンス生成が「new 具体的なクラス名()」ではなく
            * メソッド呼び出しになっている。
            * 違うProductを使いたいとなったときは
            * 呼び出すFactoryクラスさえ切り替えればよく、以下を変更する必要がない
            */
            Product card1 = factory.Create("山田太郎");
            Product card2 = factory.Create("田中太郎");
            Product card3 = factory.Create("佐藤太郎");
            card1.Use();
            card2.Use();
            card3.Use();

            Console.Read();
        }
    }
}

重要な考え方

FlameWorkパッケージがIdCardパッケージに依存していないこと。
→ IdCradパッケージを別のものに切り替えても、FlameWorkパッケージやそれを使用する側の修正が不要。

インスタンス生成方法

Factory Methodパターンを調べていて、便利そうだと思った使い方
→ 条件によって生成するインスタンスを切り替える

/*
* FlameWorkパッケージはそのまま
*/

/*
* Animalパッケージ(上の例のIdCardパッケージに相当)
*/

using DesignPatternLearn.FactoryMethodLearn.FrameWork;
using DesignPatternLearn.FactoryMethodLearn.Animal;

namespace DesignPatternLearn.FactoryMethodLearn
{
    public class AnimalFactory : Factory
    {
        /*
        * この部分
        * 条件によって、作成するインスタンスを切り替える
        */

        public override Product CreateProduct(string owner)
        {
            if (owner.Equals("犬"))
            {
                return new Dog();
            }
            else if (owner.Equals("猫"))
            {
                return new Cat();
            }
            else if (owner.Equals("ネズミ"))
            {
                return new Mouse();
            }

            Console.WriteLine("そんな動物はいないよ");
            return null;
        }

        public override void RegisterProduct(Product product)
        {
            return;
        }
    }
}

using DesignPatternLearn.FactoryMethodLearn.FrameWork;
namespace DesignPatternLearn.FactoryMethodLearn.Animal
{
    public class Dog : Product
    {
        private readonly string cry = "ワン!";
        public string Cry { get => cry;}
        public Dog()
        {
            Console.WriteLine("犬を飼いました。");
        }
        public override void Use()
        {
            Console.WriteLine(Cry);
        }
    }

    public class Cat : Product
    {
        private readonly string cry = "にゃー";
        public string Cry { get => cry; }
        public Cat()
        {
            Console.WriteLine("猫を飼いました。");
        }
        public override void Use()
        {
            Console.WriteLine(Cry);
        }
    }

    public class Mouse : Product
    {
        private readonly string cry = "チュー";
        public string Cry { get => cry; }
        public Mouse()
        {
            Console.WriteLine("ネズミがいます");
        }
        public override void Use()
        {
            Console.WriteLine(Cry);
        }
    }
}

/*
* Mainクラス
*/

using DesignPatternLearn.FactoryMethodLearn.FrameWork;
using DesignPatternLearn.FactoryMethodLearn.IdCard;

namespace DesignPatternLearn.FactoryMethodLearn
{
    class FactoryMethodLearn
    {
        public static void Main(string[] args)
        {
            Factory factory = new AnimalFactory();
            Product card1 = factory.Create("犬");
            Product card2 = factory.Create("猫");
            Product card3 = factory.Create("ネズミ");
            card1.Use();
            card2.Use();
            card3.Use();

            Console.Read();
        }
    }
}

/*
*  ===== 実行結果 =====
*   
*   犬を飼いました。
*   猫を飼いました。
*   ネズミがいます
*   ワン!
*   にゃー
*   チュー
*
*   ====================
*/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

浮動小数点数の限界を把握する

いまやあらゆるプログラムで浮動小数点数が使われていますが、ここで改めてその性質を確認してみましょう。

フォーマットのおさらい

浮動小数点数には単精度(float)と倍精度(double)がありますが、例えばゲームプログラムであれば使用頻度が高いのはfloatでしょう。
floatのフォーマットはIEEE754という規格で
符号部:1bit
指数部:8bit
仮数部:23bit
と定められています。

Kobito.LXylcU.png

指数部には負の値が欲しいので(数値の符号とは別に)、あらかじめ127を加算した値を指数部に格納します。(追記:コメントに議論あり、規格策定の歴史的経緯に言及しているわけではありません)
よって指数部にゼロ乗を格納したい場合は上の図のように 01111111 が入ります(という規格です)。

仮数部は1.0を基準に考えたいので、あらかじめ1.0を減算した値を格納します。
よって仮数部に1.0を格納したい場合は上の図のように 00000000000000000000000 が入ります(という規格です)。

結果、たとえば 1.0 という値を表現する浮動小数点数は二進数で 0 01111111 00000000000000000000000 となります。
4つずつ区切ると
0011 1111 1000 0000 0000 0000 0000 0000
なので、16進数だと
3f800000
となります。ベテランプログラマは全員、 1.0 が 3f800000 であることをこっそり暗記しています(たぶん)。

限界を調べる

指数部は「2の何乗か」を示します。よって、指数部に1を加えるだけで二倍の値が表現できます。IEEE754によれば指数部には-126から127まで格納できるので、ものすごく小さな値からものすごく大きな値が表現できるわけです。(指数に慣れていない方は指数の負の値についておさらいしましょう。たとえば指数に-2を入れると「2の2乗分の1」つまり $\frac{1}{4}$ になり、指数に-126を入れたときは「2の126乗分の1」という、とんでもなく小さな値になります)

大きな数値も小さな数値も表現できてスバラシイ。でもここで問題になるのは、大きな数値と小さな数値が同居する場合です。例えば10進数で
10000000.00000001
のような数値ですね。有効数字は仮数部23bitで定められているので、そこに限界があるでしょうよ、という話です。

23bitの限界

仮数部は23bitあるので、指数部がゼロ(=01111111)だった場合$\frac{1}{2}^{23}$まで有効数字があります。計算してみると

$\frac{1}{2}^{23}=0.00000011920928955078125$

こんな値です。よって指数部がゼロ乗だとすれば「1.0に加算できる粒度は0.00000011920928955078125が限界」、つまり

1.00000011920928955078125

という値が、表現できる限界ということになります。

2進数ベースの理屈をここでは10進数で書いているので端数が長いんですが、何進数であっても有効数字の理屈は変わりません。
例えば全体を1000倍したら「1000に加算できるのは0.00011920929」が限界、つまり

1000.00011920928955078125

という値が限界です。1000倍が2のべき乗でないため、厳密には少し値がズレますけど。ざっくり言えば、
「1000という値には0.0001という値を加えるのがギリギリ」
と考えられるわけです。

問題が起きるパターン

距離で起きる問題

そんな限界から、ヤバいパターンを考えてみましょう。
1.0が1メートルだった場合。1000メートルは1キロメートルで、0.0001は 0.1ミリメートルです。先ほどの限界の話をここに適用すると、
「原点から1キロ離れた位置では0.1ミリを加算するのがギリギリ」
という話になります。
さらに10倍してみると、
「原点から10キロ離れた位置ではもう1ミリがギリギリ」
という話にもなります。これはけっこう無視できない値です。例えばゲームにおいて、
「西に1時間ほど歩き続けたらなんかぶっ壊れた」
なんてことは当たり前に発生しうるので、必ず意識しておく必要があります。

時間で起きる問題

距離の話で登場した数値を時間に換算すると、
「1000秒に0.1ミリ秒加算するのがギリギリ」
なわけです。では24時間経過した世界を考えると、

24H=24x60x60=86400秒

で、ここは改めて真面目にfloatの限界値を計算してみると

0.0078125

が限界です。つまり

86400.0078125

が限界なので、
「24時間経過すると、分解能は7ミリ秒程度しかない」
という話になります。60fpsのゲームにおいては、24時間後にはもうdeltaTの加算が危うい、ということです。24時間を秒に換算した 86400.0 に、1/60秒を足そうとして 0.016666 を加算しても、実際には 0.015625 しか足されません。

どう対処するか

この問題への対処ですが、ひとことで言えば「大きな値に小さな値を加算する時には気をつけようね」という話になってしまいます。
要するに具体的に言及しても責任取れませんすみません、ということですが、あえて対処法の第一候補としてオススメしたいのは、倍精度浮動小数点数(double)を使用することです。

doubleの仮数部は52bitあります。これだけあれば相当な無茶も許容されます。試しに24時間後を計算してみると、限界は

86400.000000000014551915228366851806640625

のようです。(floatの限界は 86400.0078125 でしたね!)
つまり24時間経過してもなお、約0.01ナノ秒の表現力を、doubleなら持っているわけです。例えば100日後、つまり3ヶ月動かしっぱなしでも約1ナノ秒の分解能を維持しているという、この安心感。

doubleは遅いのか

doubleについて、floatと比較してパフォーマンスが気になるかもしれません。メモリアクセスにも関連するので一概には言えませんが、現代においてはdoubleの演算はほとんど問題にならない速度で実行できます。プロセッサによってはdoubleのほうが速いことすらあります。ゲームロジックならぜんぶdoubleで良いと言ってもいいぐらいです。ただし現代のGPU(つまりシェーダ)はdoubleを扱えないので、なにもかもdoubleにするわけにはいきません。

まとめ

floatは精度がね、などとよく言われますが、具体的にどのぐらいに限界があるのか、という話でした。
特に、加算しっぱなしの時間、なんてものがプログラム中にあったら、floatは相当ヤバイです。doubleを検討しましょう。
いっぽうで累積の要素がないのであれば、floatで問題が起きることはあまりないでしょう。(なのでGPUでdoubleを扱えるようになる日は来ないのではないか、という気がします)
また物理を扱う場合は微分値を加算するケースが多いので、精度が落ちる地点はもっと近くにくるはずです。

それから、floatもdoubleも1.0が起点になっているため、1.0近辺の精度がもっとも高いことを覚えておくと良いです。
これはちょっとしたコツですが、ゲームを作るときは、自キャラを1.0ぐらいにしておく(戦艦のゲームなら全体のスケールを1/100にする)と、のちのトラブルに巻き込まれにくいと思います。

(追記:不正確な記述なので削除しました)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

バーチャルキャスト内で使用可能なVCIスライドを自動作成

a338432f-3479-4614-86c6-d52e073b644e_base_resized.jpg

概要

画像のようにバーチャルキャスト内で使用可能なVCIスライドを作成し、そのVCIスライドを画像から自動作成できるツールを作りました。苦労話の愚痴に、もしかしたら役に立つかもしれない小ネタを混ぜつつ作り方?を書きます。似たような事を初めてする人の参考になれば幸いです。
https://120byte.booth.pm/items/1297409

バージョン

Unity : 2018.4.0f1
バーチャルキャスト : 1.6.4b
VCI : 0.19

まずVCIを作る

普通にVCIを作ります。同期が地獄です。HMD付けたり外したり、SteamVRがダダこねたり、RiftSが映らなかったり、コミュ障メンタルに鞭打って凸ったりしながら動確を取ります。
image.png

ポイント

VCI Sub Itemが提供してくれる拡縮機能には下限サイズ(おそらく0.2)があります。
上記から逃げるにはVCI Sub Itemの子に可視オブジェクトを入れて任意のサイズ(xyz比が重要)に設定します。
GetPositionの値にオフセットを乗せる場合は、オブジェクトにオフセットを乗せるとluaが綺麗に書けます。
オブジェクト移動の基準位置には、アンカーのようにして使う透明オブジェクトが便利だと思います。
アップしたVCIにluaがない場合は、ローカルで更新されるまでゲスト側に反映されない時があるようです。
oculusとviveでトリガーとグリップが逆になってるので、操作を説明する時には注意が必要です。
所有権は移動できる状態の握った時に移る。という事を忘れてはいけません。
on某系の関数は所有権を持った人側でしか動きませんのでログ(print)も出ません。
not ownerというログがあるようですが、エラーではないのでビビってはいけません。
オブジェクト非表示が無いのでサイズ0にすると中間フレームが補間されて縮む様子が見えてしまいます。
上記を逃げるためには極端に遠い位置へ移動させるのが良さそうです。
同期は状態変数、共有変数、メッセージ、ダミーオブジェクトなどがあり、適材適所に使い分けましょう。
奥の手発動!の例としてupdate関数に対し、on某系から遅延フレームを設定して遅延処理ができそうです。
状態変数(vci.state)は完全に予想ですが、挙動的にはアイテム生成者が真の持ち主感です。
同期はグループID全部1で大体揃ってほしいくらい雑な捉え方してますが、複数人で掴まれると崩壊します。
確証はないんですが、更新アップロードは怪しい気がするので、作業中は削除>新規が良いような気がします。
アップロードしたVCIはバーチャルキャスト再起動しないとダメっぽいです。

詳細

image.png
image.png
0.2の下限から逃げるためスライドはSub Itemの子にします。が、これは私がスライドにCubeを使っていた頃の名残なので、Planeでやるなら要らなかったかもしれません。書いてて今気づきました。あとは、スライド自体は掴めるようにはせず、スライドを表示させる場所を取得するための透明オブジェクト(anchor)を使います。大きさや向きもこの透明オブジェクトを使ってユーザー操作を受け付け、全スライドに反映します。スライドはforの連番で取得するので連番名にします。
image.png
ページ送り用のボタンとレーザーポインタのレーザー部分は、レーザーポインタにFixed Jointでくっつけます。luaでも似た事ができますが、追従はこちらの方が綺麗に動くようです。jointの親子関係に気を付けましょう。jointを付けるとたぶん握れないので、握る方を親にした方が良さそうです。

lua

local out_pos = Vector3.__new(0, 1000, 0)

if vci.assets.isMine then
    for i = 1, 1000 do
        local item = vci.assets.GetSubItem(i)
        if item == nil then
            -- 最大ページの取得
            vci.state.Set('max', i - 1)
            break
        end
    end
    vci.state.Set('page', 0)
end

function onGrab(target)
    if target == "pointer" then
        vci.assets.GetSubItem("laser").SetLocalScale(Vector3.one)
    end
end

function onUngrab(target)
    if target == "pointer" then
        vci.assets.GetSubItem("laser").SetLocalScale(Vector3.zero)
    end
end

function onUse(use)
    if use == "back" or use == "next" then
        local max = vci.state.Get('max')
        local page = vci.state.Get('page')

        -- onUseでは共有変数の加減算のみ
        if use == "next" and page < max then
            vci.state.Set('page', page + 1)
        end
        if use == "back" and page > 0 then
            vci.state.Set('page', page - 1)
        end
    end
end

function updateAll()
    local max = vci.state.Get('max')
    local page = vci.state.Get('page')
    if max == nil or page == nil then
        return
    end

    local anchor = vci.assets.GetSubItem("anchor")
    for i = 0, max do
        -- 一旦全ページを遠くへ移動して向きと大きさを揃える
        vci.assets.GetSubItem(i).SetPosition(out_pos)
        vci.assets.GetSubItem(i).SetRotation(anchor.GetRotation())
        vci.assets.GetSubItem(i).SetLocalScale(anchor.GetLocalScale())
    end
    -- 現在ページを指定位置に移動
    vci.assets.GetSubItem(page).SetPosition(anchor.GetPosition())
end

自動作成ツール

Unityで作ります。作ったスライドVCIをexe作成用のシーンに複製してページオブジェクトを削除します。
image.png
必要な情報の入力UIを作り、ボタンを押したらその情報をVCIに適用して、スライドにしたい画像をページオブジェクトとしてC#スクリプトから生成します。

Cしゃーぷ

using System;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using VCI;
using VCIGLTF;

public class ExportVCI : MonoBehaviour
{
    [SerializeField]
    GameObject Template;

    [SerializeField]
    InputField Title;

    [SerializeField]
    InputField Version;

    [SerializeField]
    InputField Author;

    [SerializeField]
    InputField Contact;

    [SerializeField]
    InputField Reference;

    [SerializeField]
    Text ExportVCI_Text;

    [SerializeField]
    Material Mat;

    public void Export()
    {
        var title = Title.text;
        var version = Version.text;
        var author = Author.text;
        var contact = Contact.text;
        var reference = Reference.text;

        if (title == "" || author == "")
        {
            ExportVCI_Text.text = "必須の項目を入力してください";
            return;
        }
        else
        {
            ExportVCI_Text.text = "Export VCI";
        }

        var temp = Instantiate(Template);
        temp.name = title;

        var vci = temp.GetComponent<VCIObject>();
        vci.Meta.title = title;
        vci.Meta.version = version;
        vci.Meta.author = author;
        vci.Meta.contactInformation = contact;
        vci.Meta.reference = reference;

        var jpg = Directory.GetFiles(Application.dataPath + "/../IMAGE", "*.jpg");
        var png = Directory.GetFiles(Application.dataPath + "/../IMAGE", "*.png");
        var img = new string[jpg.Length + png.Length];
        jpg.CopyTo(img, 0);
        png.CopyTo(img, jpg.Length);
        Array.Sort(img);

        for (int i = 0; i < img.Length; i++)
        {
            var tex = ReadTexture2D(img[i]);
            float x = tex.width >= tex.height ? 1 : (float)tex.width / tex.height;
            float y = tex.width <= tex.height ? 1 : (float)tex.height / tex.width;

            var go = new GameObject();
            go.name = i.ToString();
            go.transform.parent = temp.transform;
            go.transform.position = new Vector3(0, 1, 0);

            var sub = go.AddComponent<VCISubItem>();
            sub.GroupId = 1;

            var rigid = go.AddComponent<Rigidbody>();
            rigid.useGravity = false;

            var plane = GameObject.CreatePrimitive(PrimitiveType.Plane);
            plane.transform.parent = go.transform;
            plane.transform.position = new Vector3(0, 1, 0);
            plane.transform.eulerAngles = new Vector3(90, 0, 0);
            plane.transform.localScale = new Vector3(x / 10, 0.1f, y / 10);
            plane.GetComponent<Renderer>().material = Mat;
            plane.GetComponent<Renderer>().material.SetTexture("_MainTex", tex);
            Destroy(plane.GetComponent<MeshCollider>());
        }

        var gltf = new glTF();
        var exporter = new VCIExporter(gltf);
        exporter.Prepare(temp);
        exporter.Export();
        var bytes = gltf.ToGlbBytes();
        var path = Application.dataPath + "/../" + title + ".vci";
        File.WriteAllBytes(path, bytes);

        Destroy(exporter.Copy);
        Destroy(temp);
    }

    Texture2D ReadTexture2D(string path)
    {
        byte[] read = ReadFile(path);
        Texture2D texture = new Texture2D(1, 1);
        texture.LoadImage(read);
        return texture;
    }

    byte[] ReadFile(string path)
    {
        FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
        BinaryReader br = new BinaryReader(fs);
        byte[] read = br.ReadBytes((int)br.BaseStream.Length);
        br.Close();
        return read;
    }
}

Export()をボタンに割り当てます。Planeのサイズってなんで他の10倍なんでしょうね。

参考

VCIスクリプトリファレンス
https://virtualcast.jp/wiki/doku.php?id=vci:script:reference

VCIをビルドしたクライアントからExportする
https://qiita.com/Nekomasu/items/5ab21c6d9359f6c18e46

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DateTime項目を含むJSONシリアライズ

例えば

public class Master {
    public Master(string ID, string Name, DateTime Join) {
        UserID = ID;
        UserName = Name;
        Joined = Join;
        Leaved = DateTime.MinValue;
    }

    public string UserID { get; set; }

    public string UserName { get; set; }

    public DateTime Joined { get; set; }

    public DateTime Leaved { get; set; }
}

こんなクラスがあったとしましょう。
ぶりぶりに使い込んで良い感じのシステムの一部として組み込まれていると思ってね。

ところで

諸々の要件が上がってきて、CS間でJSONデータのやり取り、ってのが必要になったとするじゃないですか―
            なったんですよ、いいですね?

まぁ、MasterクラスをJSON対応すれば良いんですけど。
こんな感じに…

[DataContract]
public class Master {
    public Master() { }

    public Master(string ID, string Name, DateTime Join) {
        UserID = ID;
        UserName = Name;
        Joined = Join;
        Leaved = DateTime.MinValue;
    }

    [DataMember(Order = 0)]
    public string UserID { get; set; }

    [DataMember(Order = 1)]
    public string UserName { get; set; }

    [DataMember(Order = 2)]
    public DateTime Joined { get; set; }

    [DataMember(Order = 3)]
    public DateTime Leaved { get; set; }



    public static Master CreateInstance(string json) {
        using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) {
            DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(Master));
            return (Master)serializer.ReadObject(stream);
        }
    }

    public string Serialize() {
        using (MemoryStream stream = new MemoryStream()) {
            DataContractJsonSerializer serializer = new DataContractJsonSerializer(this.GetType());
            serializer.WriteObject(stream, this);
            return Encoding.UTF8.GetString(stream.ToArray());
        }
    }
}

でも、落とし穴が

例えば、

    Console.WriteLine(new Master("0000", "Toraja", DateTime.Today).Serialize());

みたいな感じでJSONが得られると思うじゃないですか、普通。
でもでも、

image

って怒られちゃいます。

あっちこっちでそういう状況が発生しているらしいので、皆さん解説して下さっていますので、詳しくはググってみて下さい。

一応簡単に言うと、DateTimeをシリアライズする際にその値をUTCに変換しようするんですが、その値がMinValueだった場合マイナス9時間した際にアンダーフローを起こす、と云う事らしいですけどじゃぁどうすれば良いんだよって感じです。

で、識者の意見によれば、JSONの定義にはDateTime型は含まれていないので、日付をJSONに入れたい場合には文字列として扱え!
と云うのが正論らしいです。

どう対応するか

いやーでも、ぶりぶりに使い込んでるんですよ。
今更クラス定義のDateTimeをstring型に変更しちゃったら、影響範囲がどこにまで及ぶか…


って事で、考え方を反転させるのが良いのかなぁ、と。
こう云う感じ(着目部分だけ括り出しました)。

    [IgnoreDataMember]
    public DateTime Joined { get; set; }

    [IgnoreDataMember]
    public DateTime Leaved { get; set; }

    [DataMember(Order = 2, Name = "Joined")]
    private string StringJoined {
        get { return $"{Joined:yyyyMMddHHmmss}"; }
        set { Joined = DateTime.ParseExact(value, "yyyyMMddHHmmss", CultureInfo.CurrentCulture, DateTimeStyles.None); }
    }

    [DataMember(Order = 3, Name = "Leaved")]
    private string StringLeaved {
        get { return $"{Leaved:yyyyMMddHHmmss}"; }
        set { Joined = DateTime.ParseExact(value, "yyyyMMddHHmmss", CultureInfo.CurrentCulture, DateTimeStyles.None); }
    }

内側のDatetime型のプロパティはJSONには出さないようにして、JSONに出す側のプロパティをprivateのstring型として新設します。序に名前をちょこっと変えといてあげればイイ感じかも…

で、結果は以下。

{"UserID":"0000","UserName":"Toraja","Joined":"20190723000000","Leaved":"00010101000000"}

あ、ひょっとして

グリニッジより西にある諸国では、こう云うややこしい目に合わないんだろうねぇ
きっと

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む