20200724のC#に関する記事は7件です。

【Unity(C#)】3次元空間にオブジェクトを重なりなく生成(表示)する方法

デモ

まずはデモです。

特定の範囲内にオブジェクトがランダムかつ、
メッシュ同士が重なって表示されることなく出現します。

GunShot3.gif

生成位置(表示位置)をランダムにすることは簡単なのですが、
立体的なオブジェクトをメッシュの重なりなく表示するのは
少々入り組んだロジックを考える必要がありましたのでメモしときます。

コード

まずはコード全文です。
今回は最初からHierarchyにオブジェクトを非表示で配置しておきました。
GenerateRandom.PNG

using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Random = UnityEngine.Random;

/// <summary>
/// ブロックをランダムな位置にランダムなタイミングで表示
/// 位置被りナシ
/// </summary>
public class ActivateBlock : MonoBehaviour
{
    [SerializeField] private GameObject _blockParent;
    [SerializeField] private BoxCollider _blockBoxCollider;

    private const float _MIN_INTERVAL_VALUE = 0.5f;
    private const float _MAX_INTERVAL_VALUE = 2.0f;
    private const float _MIN_X_VALUE = -1.0f;
    private const float _MAX_X_VALUE = 1.0f;
    private const float _MIN_Y_VALUE = 0.5f;
    private const float _MAX_Y_VALUE = 2.0f;
    private const float _MIN_Z_VALUE = -1.0f;
    private const float _MAX_Z_VALUE = 1.0f;

    private bool _isGameStart = true;

    private bool _isSetablePositionX;
    private bool _isSetablePositionY;
    private bool _isSetablePositionZ;

    private int _randomNumber;
    private float _randomInterval;
    private float _randomValueX;
    private float _randomValueY;
    private float _randomValueZ;

    private readonly List<Vector3> _usePositionList = new List<Vector3>();

    void Start()
    {
        //空同然だけどリスト作っとく
        foreach (Transform child in _blockParent.transform)
        {
            _usePositionList.Add(child.position);
        }

        DelayInitBlock();
    }

    private async UniTask DelayInitBlock()
    {
        while (_isGameStart)
        {
            //ランダムな値
                _randomNumber = Random.Range(0, _blockParent.transform.childCount);
                _randomInterval = Random.Range(_MIN_INTERVAL_VALUE, _MAX_INTERVAL_VALUE);
                _randomValueX = Random.Range(_MIN_X_VALUE, _MAX_X_VALUE);
                _randomValueY = Random.Range(_MIN_Y_VALUE, _MAX_Y_VALUE);
                _randomValueZ = Random.Range(_MIN_Z_VALUE, _MAX_Z_VALUE);

            //選ばれたブロックの位置
            Vector3 selectedBlockPosition =
                    _blockParent.transform.GetChild(_randomNumber).gameObject.transform.position;

            //選ばれたブロックの位置は比較対象から一旦削除
            _usePositionList.Remove(selectedBlockPosition);

            //現在使用中のポジションのリストから今利用検討中のポジションが利用可能か判定
            foreach (Vector3 position in _usePositionList)
            {
                //表示位置被りがないかオブジェクトの大きさでチェック
                _isSetablePositionX =
                    Mathf.Abs(position.x - _randomValueX) > _blockBoxCollider.bounds.size.x ;
                _isSetablePositionY =
                    Mathf.Abs(position.y - _randomValueY) > _blockBoxCollider.bounds.size.y;
                _isSetablePositionZ =
                    Mathf.Abs(position.z - _randomValueZ) > _blockBoxCollider.bounds.size.z ;

                //座標のうち、全ての軸で被っていたら置けないのでやり直し
                if (!_isSetablePositionX && !_isSetablePositionY && !_isSetablePositionZ)
                {
                    _usePositionList.Add(selectedBlockPosition);
                    break;
                }
            }

            //位置被りがどれか1つの軸で無ければ実行する
            if (_isSetablePositionX || _isSetablePositionY || _isSetablePositionZ)
            { 
                Vector3 randomPosition = new Vector3(_randomValueX, _randomValueY, _randomValueZ);

                //ランダムな間隔でDelay
                await UniTask.Delay(TimeSpan.FromSeconds(_randomInterval));
                _blockParent.transform.GetChild(_randomNumber).gameObject.transform.position = randomPosition;

                //新しい使用中のポジションをリストに追加
                _usePositionList.Add(randomPosition);
            }
        }
    }
}

流れとしては下記です。
⓪"オブジェクトの座標を保存しておくリスト"を作成
①ランダムな座標を作成
②ランダムにオブジェクトを選択
③選んだオブジェクトの座標を"オブジェクトの座標を保存しておくリスト"の中から削除
④XYZの3軸それぞれで位置が被っていないか比較
⑤1軸でも座標が被っていないなら重なりなく表示可能なので表示する
⑥"オブジェクトの座標を保存しておくリスト"に今回利用した座標を追加

Bounds

オブジェクトの座標同士を比較するだけでは、オブジェクト同士の重なりを防ぐことができません。
ですので、今回はBoundsと呼ばれるオブジェクトの領域を指すもの利用することにしました。

BoxCollider.bounds.sizeでオブジェクトの持つコライダーの大きさが取得できます。

このオブジェクトの大きさを利用して、
新しく配置しようとしている座標既に利用済みの座標と比べてBounds一個分離れているか
どうかを判定すれば、位置被りをなくすロジックを組むことができます。

 //表示位置被りがないかオブジェクトの大きさでチェック
 _isSetablePositionX = Mathf.Abs(現在利用中のX座標 - ランダムに生成したX座標) > _blockBoxCollider.bounds.size.x ;
 _isSetablePositionY = Mathf.Abs(現在利用中のY座標 - ランダムに生成したY座標) > _blockBoxCollider.bounds.size.y;
 _isSetablePositionZ = Mathf.Abs(現在利用中のZ座標 - ランダムに生成したZ座標) > _blockBoxCollider.bounds.size.z ;

2020/07/26 追記
取得したいBoundsを持つオブジェクトが非アクティブな時、
Boundsの取得に失敗することなく、0を返します。(要注意)

まとめ

結構必要となるロジックだと思いましたが、意外と実装録を見つけられませんでした。
もっとスマートな方法があれば教えてください。

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

【Unity】Water Ripple For Screenを使ってみた

はじめに

WaterRippleForScreenはUnityで水の波紋を表現できるJose Hernandez氏作のアセットです。無料!!
リンクはこちら

ゲームを作成しているときに「水の波紋みたいなエフェクトできたらエモくね?」となったのですが、シェーダーの知識が全く無かったために自分で作成することができなかったのでアセットに頼ろうと調べていたときに見つけました。
このアセットに関して日本語で書かれた記事を発見できなかったので備忘録を兼ねてかいていきます。

利用方法

基本

アセットのインストール方法に関しては他の方がわかりやすく説明しているため、自分のプロジェクトにインポートできている前提で話をすすめます。
インポートすると、Assetsフォルダの中にWaterRippleForScreenというフォルダが作成されます。

まずは、WaterRippleForScreen/Scripts/に存在するRippleEffectsをエフェクトを適用したいCameraに貼り付けます。
image.png

背景の画像を適当に入れて、DetectClickにチェックを入れて実行し、ゲーム画面をクリックしてみてください(下の画像は見やすいようにWaveScaleを30にしています。)
ezgif.com-video-to-gif.gif

このように水の波紋のような効果をかけられるのがこのアセットの効果です。

スクリプトから呼び出す場合は以下のようにします。
まず、忘れては行けないのが

using WaterRippleForScreens;

今回はスペースキーの入力があったら波紋を呼び出すようにしていきます。
以下のスクリプトを適当なゲームオブジェクトにアタッチしてください。

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

public class hogehoge : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Camera cam = Camera.main;
            Vector2 target = new Vector2(0, 0);//Unity上の座標
            target = cam.WorldToScreenPoint(target);//カメラ座標に変換
            target.y = Screen.height - target.y;//WRFS用に座標を変換
            cam.GetComponent<RippleEffect>().SetNewRipplePosition(target); //新しい波紋を生成
       }
    }
}

RippleEffectのSetNewRipplePositionに画面座標を上下反転させたものを渡すことで波紋を生み出せます。
(DetectClickモードでもクリックした箇所の上下反転に波紋が生み出されるので多分上下反転はバグだと思われます。)

エフェクトの調整

以下の変数はすべての波に対して適応されますのでご注意ください。波ごとに個別の設定を行うことはできませんでした。
もしかするとCameraに波の生成ごとにスクリプトからコンポーネントを追加して設定すればうまくいくかもしれません。(未検証)

DitectClick

はじめにも利用したクリックした座標(を上下反転させた座標)に波紋を生み出します。クリックを利用するプロジェクトではどこでも生み出されてしまうため、falseにしておくことをおすすめします。

WaveCount

画面に表示させる波の最大数

TimeInfinity

trueにすると画面にずっと波が表示され続けます。

WaveTime

そろぞれの波の時間が秒単位で設定できます。

WaveAnimCurve

時間経過に伴う波の挙動を決定します。0~1に収まるようにしないとおかしなことになります。

WaveScale

波の強度。ここが大きいと画面が大きく歪むようになります。

WaveScale

波の速度。

WaveFrequency

波の周波数。値が大きいほど波と波の感覚が狭まります。

CiecleXYScale

異なる値を設定することで波を楕円形にできます。

InternalRadio & ExternalRadio

image.png
image.png
画像はWaterRippleForScreenフォルダ内部に生成されるpdfよりお借りしております。
Internalの内部には波が生まれず、Internal以上Internal+External未満に波が表示され、それ以上には波が生まれません。

その他

RippleGeneratorというスクリプトも同梱されており、こちらでは自動で波を発生させられるらしい。
このエフェクトは画面に映るもの全てを歪ませてしまうため、少々使い勝手が悪そうなので今回はパスします。

参考

Asset Storeのダウンロードページ

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

Visual Studio開発環境構築メモ

1.コーディング関連

1-1.ソースの整形

dotnet-format

インストール

  1. スタートメニューから「Developer Command Prompt for VS 2019」を起動。
  2. ソリューションフォルダに移動。
  3. マニフェストファイルを作成するため、以下を入力する。「dotnet new tool-manifest」
  4. 以下を入力し、インストールする。「dotnet tool install dotnet-format」

実行

「dotnet format」を入力し、実行する。

CodeMaid

Code Alignment

2.テスト関連

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

C# 入門

C#の学習メモです。

言語の特徴

  • C言語を始めとした各言語から継承した言語仕様(CC++JAVAC#
  • オブジェクト指向言語(クラスの定義)
  • プロジェクト単位でプログラムを管理
  • PC、スマホなど幅広いプラットフォームに対応
  • .NETフレームワーク上(windows上で動くアプリ)で動作

.NET

C#は、.NET上で動く言語。(CLI:Common Language Infrastructure規格上で動く)
.NET Coreは、クロスプラットフォームで互換性あり(LinuxやMacOSも対応)

  • CLR(Common Language Runtime)
    C# → IL Code(中間コード) → Native Code コンパイルされる
    .NET Frameworkに対応した言語のライブラリを共通して使える(e.g. C#、VB.NET、F#、C++...)

  • Class Library

開発環境

Visual Studio(IDE:統合開発環境) → Visual Studio Codeよりも高機能。

macでは、windowsアプリ(フォームなど)は作れないみたいですね...

プロジェクト

C#は、プロジェクト単位で各種ファイルを管理します。ひとつのプログラムを作るのに必要な複数のファイルとアセット(画像などの素)をひとまとめにしてプロジェクトとします。

Program.cs
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

静的型付け言語

C#は静的片付け言語なので、変数や定数を宣言する際には、型を宣言する必要があります。また、データ型によって確保するメモリのビット数が異なる為、型変換の際に大きい型から小さい型に変換しようとすると、「暗黙的に変換できません。明示的な変換が存在しません。」とエラーがでます。

データ型 説明 ビット
byte 符号なし整数 8
sbyte 符号つき整数 8
int 符号つき整数 32
uint 符号なし整数 32
short 符号つき整数 16
ushort 符号なし整数 16
long 符号つき整数 64
ulong 符号なし整数 64
float バイナリ浮動小数点数 32
double バイナリ浮動小数点数 64
char Unicode文字列 16
bool ブール値 8
string 文字列
decimal 高精度10進浮動小数点数 128

整数は、小数点を含まない整数。
浮動小数点は、小数点いかを含む実数。
符号つきは、負の値を含む。

floatdoubleだと小数点の値に誤差が生じるので、絶対に誤差を出したくない場合はdecimalを使います。(金額計算など)

Microsoft/ドキュメント/C#/型/種類

Program.cs
using System;

class Program
{
    static void Main(string[] args)
    {
        byte    val1  = 255;
        short   val2  = 10;
        short   val3  = short.MaxValue; // 32767
        short   val4  = short.MinValue; // -32768
        int     val5  = 10;
        long    val6  = 10;
        float   val7  = 1.23f; // サフィックス(数値の末尾にデータ型を示す文字列)が必要
        double  val8  = 1.23; // 1.23dと明示的に表記してもOK
        decimal val9  = 1.23m;
        bool    val10 = true;
        string  val11 = "string";
    }
}

サフィックスは、大文字でも小文字でもどちらでもOKです。

値型と参照型

  • 値型 → 数値 → int コピーした場合、値そのものが別のメモリに確保される
  • 参照型 → クラス → コピーした場合、参照先自体がコピーされる

Javascriptの変数とオブジェクトの「コピー・参照」と挙動と同じになっています。

学習項目

C#で押さえておきたい項目は下記の通りです。

  • 文法
  • コーディングルール、お作法
  • オブジェクト指向
  • デザインパターン
  • TDD(テスト駆動開発)
  • リファクタリング
  • DDD(ドメイン駆動開発)

オブジェクト指向

DAOパターン ストラテジーパターン + ファクトリーパターン

エラー

dot.netエラー

Could not execute because the specified command or file was not found.
Possible reasons for this include:
  * You misspelled a built-in dotnet command.
  * You intended to execute a .NET Core program, but dotnet-aspnet-codegenerator does not exist.
  * You intended to run a global tool, but a dotnet-prefixed executable with this name could not be found on the PATH.

dotnet-aspnet-codegeneratorが存在しないことで発生するエラー。

You can invoke the tool using the following command: dotnet-aspnet-codegenerator

dotnet-aspnet-codegeneratorをインストールするコマンド。

dotnet tool install --global dotnet-aspnet-codegenerator

プロジェクトのディレクトリ直下でコマンドを実行していない場合に起こるエラー。
Controllerや、Modelsのあるディレクトリ内でコマンドを実行します。

Scaffolding failed.
Could not find project file in /Users/tetsu/Projects/MvcMovie
To see more information, enable tracing by setting environment variable 'codegen_trace' = 1.
RunTime 00:00:00.07

やり直した結果

cd MvcMovie
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

Building project ...
Finding the generator 'controller'...
Running the generator 'controller'...
Attempting to compile the application in memory.
Attempting to figure out the EntityFramework metadata for the model and DbContext: 'Movie'
Added Controller : '/Controllers/MoviesController.cs'.
Added View : /Views/Movies/Create.cshtml
Added View : /Views/Movies/Edit.cshtml
Added View : /Views/Movies/Details.cshtml
Added View : /Views/Movies/Delete.cshtml
Added View : /Views/Movies/Index.cshtml
RunTime 00:00:13.72

参考本

リーダブルコード
.NETのクラスライブラリ設計

ドキュメント

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

C# で Json を扱う

はじめに

以前は Newtonsoft.Json を使っていたが、現在は System.Core3.0 以降であれば System.Text.Json があるとの事。
System.Text.Json 名前空間 | Microsoft Docs

とりあえず実際に使った内容などの覚書のため、今後新たに知った事は追記して行く。

記事記載時の環境など

Microsoft Visual Studio Community 2019
Version 16.6.3

Windows Forms App(.NET Core)

.NET Framework での場合、NuGetパッケージからインストールする必要があるので Json なりで検索して System.Text.Json をインストールする。

基本

次のようなクラスがあったとする。

public class Hoge
{
    public int id { get; set; }
    public string name { get; set; }
}

Jsonの内容は次の通り。

hoge.json
{
    "id": 1,
    "name": "hoge"
}

JSONデータをクラスに取り込む(デシリアライズ)

using System.Text.Json;

var json = File.ReadAllText(@"C:\hoge.json");
var hoge = JsonSerializer.Deserialize<Hoge>(json);

クラスの内容をJSON文字列にする(シリアライズ)

using System.Text.Json;

var hoge = new Hoge();
hoge.id = 1;
hoge.name = "hoge";
var json = JsonSerializer.Serialize<Hoge>(hoge);
File.WriteAllText(@"C:\hoge.json", json);

オプション指定について

必要に応じて組合せて使用する。

enum を扱えるようにする(enum を文字列として扱う)

public enum HogeType
{
    HOGE,
    GEHO
}

public class Hoge
{
    public HogeType type { get; set; }
    public int id { get; set; }
    public string name { get; set; }
}
using System.Text.Json;
using System.Text.Json.Serialization;

var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());

// デシリアライズ
var hoge = JsonSerializer.Deserialize<Hoge>(File.ReadAllText(@"C:\hoge.json"), options);

// シリアライズ
var json = JsonSerializer.Serialize<Hoge>(hoge, options);
File.WriteAllText(@"C:\hoge.json", json);
オプション未指定時
{"type":0,"id":1,"name":"hoge"}
オプション指定時
{"type":"HOGE","id":1,"name":"hoge"}

文字をエスケープしない

通常、シリアライズすると次のよう日本語などはエスケープされる。

var hoge = new Hoge();
hoge.type = HogeType.HOGE;
hoge.id = 1;
hoge.name = "ほげ";
var json = JsonSerializer.Serialize<Hoge>(hoge);
File.WriteAllText(@"C:\hoge.json", json);
hoge.json
{"type":0,"id":1,"name":"\u307B\u3052"}

次のオプション指定を行う。

using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;

var options = new JsonSerializerOptions();
options.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);

var json = JsonSerializer.Serialize<Hoge>(hoge, options);
File.WriteAllText(@"C:\hoge.json", json);

hoge.json
{"type":0,"id":1,"name":"ほげ"}

結果をフォーマットする(インデントする)

options.WriteIndented = true;

参考にしたサイトなど

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

海外のレストランのメニューには写真がない問題を解決しよう

海外旅行や海外出張はどちらもあまり好きではないのですが、たま~~~~に行くくらいならいいものです。
最近はコロナのせいで絶対いけない感じになってしまいましたが…。

海外でご飯を食べるときに困ることとして、レストランのメニューが英語という問題があります。
英語なのは、まぁ当たり前なのでいいのですが海外のメニューはテキストのみのケースが多く、名前から料理がわからないと詰むという問題があります。どんな料理ですか?と聞くだけのコミュ力も英語力もない。

つまり、円滑にレストランでご飯をするには、こういうアプリが必要になります。

  1. レストランのメニューを写真撮影
  2. OCR でテキストを認識
  3. 認識した料理名で画像検索
  4. どんな料理かイメージがわく

Windows デスクトップ アプリで作るよ

こういうのはモバイルでも使えるアプリ(Web かネイティブアプリ) として作ると使い勝手がいいのですが、今回は先日紹介した UWP 版の Prism を使うためにでっちあげたネタなので、Windows デスクトップ アプリとして作ります。
ただ、クライアント側は画像開いて後述する Azure Cognitive Services に分析や検索をお願いしているだけなので、Android/iOS/Web のどれでも、そんなに苦労せずに作れると思います。

画像から文字を読み取る処理や文字列から画像を検索する処理には Microsoft Azure の Cognitive Services もつかいます。

作ったアプリ

以下のような動きになります。起動直後。

image.png

文字しかない英語のレストランのメニューの画像を開いて認識した文字列がリストに表示されます。

image.png

因みに読ませた画像は、こちらのサイトにあるレストランのメニューの画像です。
そしてリストの文字列を選択すると、その内容で画像検索した結果が表示されます。検索結果の画像は選択すると大きな画像が表示されます。

image.png

image.png

ソースコードは GitHub に上げておきました。

PrismUnoSampleApp

使ってるもの

  • Prism.Uno
    • UWP (厳密には Uno Platform)向けの MVVM アプリケーション開発用のライブラリ
    • Uno Platform についてはこちらを見てみてください
      Uno Platform 入門
  • Azure Cognitive Services Computer Vision
    • Azure の AI 系の API が使えるサービスの Cognitive Services の Computer Vision の中に OCR の機能があるので、ファイルやカメラの画像を食わせて文字列を抽出するのに使っています。
    • ドキュメント:光学式文字認識 (OCR)
  • Bing Search API
    • Bing の検索を API として使える Bing Search API の中にある画像検索機能を使ってテキストから画像を検索して表示するのに使っています。
    • ドキュメント:Bing Image Search API とは

実行方法

Azure で Cognitive Services の Computer Vision と、Bing Search のリソースを作ります。どちらも無料枠があります。
そして、そのキーとエンドポイントを PrismUnoSampleApp\appsettings.json の中に設定します。

appsettings.json
{
  "ocr": {
    "subscriptionKey": "your computer vision key",
    "endpoint": "your computer vision endpoint"
  },
  "imageSearch": {
    "subscriptionKey": "your bing image search key"
  }
}

あとはビルドして実行すれば動くと思います。

アプリの構造

オニオンアーキテクチャあたりを意識しつつ、なるべく外部リソースに依存するところはインターフェースを切っておいて、最悪あとで単体テスト出来たりするようにしてあります。
ただ、途中でこんな小さなアプリでこんなにしてもあんまりメリットはないなぁと思いちょっと妥協してるところもあります。

こんな雰囲気を目指しています。

image.png

プロジェクトは 3 つで、Prism の Shell があるメインのプロジェクトと、モジュールのプロジェクトがあります。Prism では、複数のモジュールを組み合わせて、モジュールから Shell に定義された Region に View を登録していくように作れるのですが、今回はシンプルにモジュール 1 つだけです。

写真撮って AI にかけて表示するだけだしね。

image.png

画面

Shell は以下のような XAML で定義しています。

Shell.xaml
<Page
    x:Class="PrismUnoSampleApp.Views.Shell"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:PrismUnoSampleApp.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:regions="using:Prism.Regions"
    xmlns:infra="using:PrismUnoSampleApp.Infrastructures"
    xmlns:mvvm="using:Prism.Mvvm"
    mvvm:ViewModelLocator.AutoWireViewModel="True"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <Page.Resources>
        <infra:RegionNamesForXAML x:Key="regionNames" x:Name="regionNames" />
    </Page.Resources>
    <RelativePanel>
        <ContentControl regions:RegionManager.RegionName="{x:Bind regionNames.TopMenuRegion}" 
                        x:Name="topMenuRegion"
                        RelativePanel.AlignLeftWithPanel="True"
                        RelativePanel.AlignRightWithPanel="True"
                        RelativePanel.AlignTopWithPanel="True" 
                        HorizontalAlignment="Stretch"
                        HorizontalContentAlignment="Stretch"
                        VerticalContentAlignment="Stretch"/>
        <ContentControl regions:RegionManager.RegionName="{x:Bind regionNames.MasterRegion}" 
                        x:Name="masterRegion"
                        RelativePanel.Below="topMenuRegion"
                        RelativePanel.AlignLeftWithPanel="True"
                        RelativePanel.Above="statusBar"
                        MaxWidth="250" />
        <ContentControl regions:RegionManager.RegionName="{x:Bind regionNames.DetailsRegion}" 
                        x:Name="detailsRegion"
                        RelativePanel.Below="topMenuRegion"
                        RelativePanel.RightOf="masterRegion"
                        RelativePanel.AlignRightWithPanel="True" 
                        RelativePanel.AlignBottomWith="masterRegion"
                        RelativePanel.Above="statusBar"
                        HorizontalContentAlignment="Stretch"
                        VerticalContentAlignment="Stretch" />
        <Grid x:Name="statusBar"
              RelativePanel.AlignRightWithPanel="True"
              RelativePanel.AlignLeftWithPanel="True"
              RelativePanel.AlignBottomWithPanel="True"
              VerticalAlignment="Bottom"
              Padding="5">
            <TextBlock Text="{x:Bind ViewModel.StatusbarMessage.Value, Mode=OneWay}" 
                       HorizontalAlignment="Right" />
        </Grid>
    </RelativePanel>
</Page>

大体以下のような区画が区切られていて、モジュールからこの区画に View を表示していく感じです。

image.png

PrismUnoSampleApp.EnglishRestaurant プロジェクトの Views 名前空間では以下のような View を定義しています。

  • TopView
  • MenuListView
  • ImageListView
  • CommandBarView
  • ImageDialogView

初期状態で表示されている画面が TopView です。

image.png

画像を選択すると、TopViewModel から裏の処理を呼び出し、成功すると各 Region に View を追加していっています。以下の画像のオレンジが CommandBarView で、青色が MenuListView で、黄色が ImageListView になります。

image.png

画像を選択したときに出てくるのが ImageDialogView です。Prism の IDialogService を使って出しています。

image.png

ViewModel では ReactiveProperty を使って Rx を使ってさくっとモデルとつないでいます。
大体以下のような感じになっています。

ImageListViewModel.cs
public class ImageListViewModel : ViewModelBase
{
    private readonly IDetectMenuTextUseCase _detectMenuTextUseCase;
    private readonly IDialogService _dialogService;

    public ReadOnlyReactivePropertySlim<ReadOnlyObservableCollection<ImageInfo>> Images { get; }

    public ReactivePropertySlim<ImageInfo> SelectedImage { get; } = new ReactivePropertySlim<ImageInfo>();

    public ImageListViewModel(IDetectMenuTextUseCase detectMenuTextUseCase, IDialogService dialogService)
    {
        _detectMenuTextUseCase = detectMenuTextUseCase ?? throw new ArgumentNullException(nameof(detectMenuTextUseCase));
        _dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
        Images = _detectMenuTextUseCase
            .RestaurantMenu
            .CurrentText
            .Select(x => x?.Images)
            .ToReadOnlyReactivePropertySlim()
            .AddTo(Disposables);

        SelectedImage.Where(x => x != null)
            .Subscribe(x =>
            {
                _dialogService.ShowDialog(ViewNames.ImageDialogView, 
                    new DialogParameters { { "image", x } }, 
                    _ => { });
            });
    }
}

アプリ上では 1 画面に見えるものも Region で分割して作れるので、割と一つ一つはシンプルな感じになっています。

ReactiveProperty を使うときのコツというか、やっておくと便利なことの一つとして、複数のイベントが飛んでくるようなものは、IObservable に変換するような拡張メソッドを作っておくとスムーズに ReactiveProperty とつなげることが出来ます。

Prism だと IEventAggregator と PubSubEvent がモジュール間でのイベントのやりとりに使えるのですが、PubSubEvent の拡張メソッドを定義して以下のように IObservable にしておくといいです。

PubSubEventExtensions.cs
using Prism.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PrismUnoSampleApp.Infrastructures.Events
{
    public static class PubSubEventExtensions
    {
        public static IObservable<TPayload> ToObservable<TPayload>(this PubSubEvent<TPayload> self) =>
            Observable.Create<TPayload>(ox =>
            {
                var token = self.Subscribe(x => ox.OnNext(x));
                return () => token.Dispose();
            });
        public static IObservable<Unit> ToObservable(this PubSubEvent self) =>
            Observable.Create<Unit>(ox =>
            {
                var token = self.Subscribe(() => ox.OnNext(Unit.Default));
                return () => token.Dispose();
            });
    }
}

このサンプルでは UpdateGlobalMessageEvent というイベントを定義していて、このイベントを投げるとステータスバーのメッセージを書き換えるようにしています。

UpdateGlobalMessageEvent.cs
using Prism.Events;

namespace PrismUnoSampleApp.Infrastructures.Events
{
    public class UpdateGlobalMessageEvent : PubSubEvent<string>
    {
    }
}

Shell の ViewModel では、以下のように IEventAggregator からシームレスに ReadOnlyReactivePropertySlim にしています。

ShellViewModel.cs
using Prism.Events;
using Prism.Mvvm;
using PrismUnoSampleApp.Infrastructures.Events;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;

namespace PrismUnoSampleApp.ViewModels
{
    public class ShellViewModel : BindableBase
    {
        private readonly IEventAggregator _eventAggregator;

        public ReadOnlyReactivePropertySlim<string> StatusbarMessage { get; }

        public ShellViewModel(IEventAggregator eventAggregator)
        {
            _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
            StatusbarMessage = _eventAggregator.GetEvent<UpdateGlobalMessageEvent>()
                .ToObservable()
                .ObserveOnUIDispatcher()
                .ToReadOnlyReactivePropertySlim();
        }
    }
}

ReadOnlyReactivePropertySlim は Dispose しないの?と思うかもですが、Shell は 1 つしかなくて、アプリ終了まで居続けるので、アプリの死ぬ時が Shell の死ぬときなので、まぁいいかなと。
この ReadOnlyReactivePropertySlim の Value をバインドしておくと、誰かが UpdateGlobalMessageEvent を Publish うるとステータスバーのテキストが書き換わります。

UWP で作ると…

機能面では Win32 アプリと比べるとセキュリティの縛りが強いぶん出来ないこともありますが、UWP からじゃないと呼べない API も現段階ではいくつかあったりするので、何を使って Windows アプリを作るのか悩ましいところです。
アプリの直接的な機能に役立つものではないですが UWP で作っておくと、何も考えなくてもテーマに対応できたりします。また、アクリルブラシなどで半透明感のある見た目もサクッとできます。

白テーマ(アクセントカラー赤)

image.png

黒テーマ(アクセントカラー紫)

image.png

ハイコントラスト黒

ハイコントラストは UWP じゃなくても対応していますが一応念のため。(自分で色指定してたりするとハイコントラストでうまく表示されないこともある。UWP は、そこらへんをいい感じにしてくれる仕組みもある。)

image.png

今後 Windows UI Library 3.0 がリリースされると UWP でも Win32 でも、ここら辺も対応可能になるので個人的に楽しみにしています。

まとめ

UWP 版の Prism である Prism.Uno を試すためにやってみました。Uno Platform は UWP のコードを Android, iOS, WebAssembly などに対応可能なのですがファイルいたりカメラを使うところは絶対 OS ごとに固有処理になってコード量が増えるので今回は UWP に絞って使ってみました。
機能以外にも小さな画面を意識した UI も組まないといけなかったりしますしね…出来るけどコード量は増える…。

とりあえず試してみた感じ、現段階で動きが怪しいところは見つからなかったので Prism 8.0 のリリースが楽しみです。

あと Cognitive Services を組み合わせるとアイデア次第では面白いものも作れそうだなぁと思いました。

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

Unity + Epic Online Services でオンラインゲームを作ろう ~②マルチFPS編~

概要


Github:unity_eos_fps
Unity + Epic Online Services(以下 EOS) でマルチプレイ FPS を作ってみました。
前回は「ゲームを作ろう」というタイトルなのに成果物がチャットでしたが、今回は正真正銘のゲームです。

残念:ゲーム公開ができない

ネット上の多数の方にプレイしてもらいたく開発していたのですが、
「さぁ公開だ」となったタイミングで気づきました。
Epic Online Services のゲームの公開はまだ許可されていません。
公開用のための審査提出ボタンが「COMMING SOON」となっています。
そのため、開発チーム以外のアカウントでゲームにログインするとはじかれます。
Edp37coUYAAqOrP.png
・・・

残念ですが現在はみんなでプレイができず、ビルドしてローカルでテストしかできません。
また進展があったら報告します。?

ロビー検索のバグの対処

前回と同じく 7/23 現在 EOS のロビー検索はまだバグで動きません。
バグ報告したスレッド の情報提供によると内部で通信エラーが発生しているようです。
同じロビーにいないと P2P 通信が始められないため、
対処として Glitch で簡易ロビー検索サーバーを作りました。
ロビーID を保持・配信するだけの API です
Glitch : eos-lobby

コードの工夫

SDK の呼び出しはコード量が増える一方だったので、拡張メソッドをどんどん書いて整理しました。

EOS の C# SDK は C++ SDK と書き方を統一するためか、インターフェースのメソッドは下記のようになってます。

  • 引数に直接値を書かず、データ受け渡し用クラスのインスタンスで渡す ⇒ インスタンスを new する記述が増える
  • 戻り値は処理結果(Result)の Enum ⇒ 呼び出すたびに処理が成功したか判定する必要がある
  • 処理結果はコールバックで返ってくる ⇒ ネストがどんどん深くなる

なので下記のような拡張メソッドを EOS の使用するメソッド分、追加してます。

  • 引数だけで呼び出せるように置き換え
  • 戻り値を UniTask にしてコールバックを async で受け取る

ビルド

著者環境

OS : Windows 10 Pro
Unity : Unity 2020.1.0f1

1. Github からプロジェクトチェックアウト

2. Epic Online Services デベロッパーポータルで登録

  1. 新しいプロダクト作成
  2. Epic Account Services で 3 項目を全て入力して、Configured にする image.png
  3. Product Settings の内容を Unity でプロジェクトを開いて Assets/ScriptableObjects/EOSSettings.asset に記載

3. Glitch でサーバー作成

  1. eos-lobby を開いて View Sorce -> Remix Edit で自身のアカウントにクローン
  2. .env にシークレットのキーと値を設定して
    • Variable Name:SECRET
    • Valiable Value:適当な文字列 image.png
  3. Unity プロジェクトの Assets/ScriptableObjects/EOSSettings.asset に記載
    • Api Url : https://【クローン後のプロジェクト名】.glitch.me/kvs
    • Api Securet : .env の SECRET に設定した文字列 image.png

4. Unity ビルド

Windows Standalone でビルド

終わりに

公開できないのは残念です。
EOS は正式リリースされているかのように宣伝されているので、
私のように勘違いして開発している人いそうです。
ロビー検索が動かない件も含めて、公式には早急に対応していただきたいです。

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