20201216のC#に関する記事は14件です。

今年業務で使ったc#のスキルまとめ

C# Advent Calendar 2020の17日目の記事です。

はじめに

今年は業務でwindowsアプリを開発したので、今回は備忘録的に今年使ったスキルを書きます。
多分基礎的なことばっかりです。

  • 開発環境
    • VisualStudio 2019

レジストリ操作

インストーラーを使って開発したwindowsアプリをインストールしてもらう想定だったため、インストール時にインストーラーがレジストリ登録したバージョン情報などをアプリで表示する必要などがありました。

インストーラーとは?

windowsでレジストリを開く場合は win + R キーを押してregeditと入力する方法が一般的だと思います。

image.png

image.png

次はc#でHKEY_USERSのサブキーの一覧(.DEFAULT, S-1-5-XX)を取得します。
using Microsoft.Win32を追加することに注意です。

            // サブキーの一覧を取得
            var keys = Registry.Users.GetSubKeyNames();
            foreach (var key in keys)
            {
                Console.WriteLine($"取得したサブキー : {key}");
            }

Registry.Users でHKEY_USERSを読み込んで、GetSubKeyNames()でサブキーの一覧(string型の配列)を取得しています。

取得したサブキー : .DEFAULT
取得したサブキー : S-1-5-19
取得したサブキー : S-1-5-20
取得したサブキー : S-1-5-21-3102848314-395816088-1389192899-1001
取得したサブキー : S-1-5-21-3102848314-395816088-1389192899-1001_Classes
取得したサブキー : S-1-5-18

次はキーに書かれている値を取得してみましょう。
HKEY_CURRENT_USER のサブキー Software\Microsoft\Edge\BLBeaconversion の値をc#で読み込みます。
image.png

            using (var regkey2 = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Edge\BLBeacon", false))
            {
                var ver = (string)regkey2.GetValue("version");
                Console.WriteLine($"BLBeacon ver : {ver}");
            }

先ほどと違うのはRegistry.CurrentUserHKEY_CURRENT_USERを指定して、OpenSubKey()Software\Microsoft\Edge\BLBeacon のサブキーを開きます。

GetValue()の引数に取得したい名前を渡します。今回はREG_SZの値が欲しいので、stringでcastします。

BLBeacon ver : 87.0.664.60

最後にサブキーを作る例です。
32bit/64bitアプリでサブキーを作成する場所が変わったりするので注意が必要です。

            try
            {
                using(var prevKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64))
                using(var error = prevKey.OpenSubKey("Software", true))
                {
                    var _ = error?.CreateSubKey("test");
                    Console.WriteLine("レジストリに書き込み出来ました。");
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }

レジストリへのサブキー・値の作成(Create)には管理者権限が必要なので、上記のコードを管理者権限以外で実行するとCreateSubKeyで例外が発生することになります。

System.Security.SecurityException: 要求されたレジストリ アクセスは許可されていません。
   場所 System.ThrowHelper.ThrowSecurityException(ExceptionResource resource)
   場所 Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable)

管理者権限で実行した場合 HKEY_LOCAL_MACHINE\SOFTWARE\testが作成されます。
レジストリへの書き込みを行うアプリを作る場合は、管理者権限が必要になることを注意しましょう。

レジストリへの読み込み、書き込み

json操作

設定画面でパラメータを反映できるアプリでは、起動するたびに初期値を表示するのではなく、前回アプリ終了時の設定をそのまま表示させたいと思います。
そのような場合に、どのように設定値を保存するかでJSON(JavaScript Object Notation)形式でデータを保持しておいて、アプリ終了時にファイルへ書き込みし、アプリ起動時にファイルを読み込むという手法がよくとられます。(少なくとも私の会社のアプリはjsonにデータもたせてます。)

jsonのライブラリはいくつかありますが、Nugetから簡単に使用できるNewtonsoft.Jsonで例を出します。

    [JsonObject("target")]
    public class TargetJson
    {
        [JsonProperty("happy")]
        public bool Boolean { get; set; }

        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("pi")]
        public double Number { get; set; }

        [JsonProperty("list")]
        public List<int> List { get; set; }

        [JsonProperty("object")]
        public ObSample ObS { get; set; }
    }

    public class ObSample
    {
        [JsonProperty("currency")]
        public string Name { get; set; }

        [JsonProperty("value")]
        public double Value { get; set; }
    }

TargetJsonクラスはbool、string、double、List<int>、stringとdoubleのプロパティをもつObSampleクラスの プロパティをもつクラスになります。

このようなオブジェクトをjsonに書き込むようにする処理をシリアライズとよぶそうです。

            var t = new TargetJson()
            {
                Boolean = true,
                Name = "arisugawa",
                Number = 3.14,
                List = new List<int> { 3, 3, 4 },
                ObS = new ObSample()
                {
                    Name = "USB",
                    Value = 33.45,
                },
            };
            // 第二引数でインデントをつけれる
            string j = Newtonsoft.Json.JsonConvert.SerializeObject(t, Newtonsoft.Json.Formatting.Indented);
            // file書き込み
            File.WriteAllText(@"write.json", j);

変数tにTargetJsonクラスのインスタンスを代入し、Newtonsoft.Json.JsonConvert.SerializeObject()で変数tのオブジェクトをjsonに書き込める形式に変換(string型へ)してあげています。
最後に、File.WriteAllText() でwrite.json という名前のファイルに書き込んでいます。

image.png

上の画像がwrite.jsonに書き込まれている内容です。
JSONは key : value の組み合わせになり、 valueは各プロパティにセットした値ですが、keyの名前(例えばbool型のhappy)はどこで設定しているのでしょうか。

        [JsonProperty("happy")]
        public bool Boolean { get; set; }

Booleanプロパティに JsonPropertyという属性がついていますね。ここで設定した値がjsonのkeyになります。

ファイルなどに書かれたjsonオブジェクトを読み込んで活用したい場合はデシリアライズとよばれる動作が必要になります。

            var sr = new StreamReader("write.json");
            // ファイルの内容をすべて読み込みます。
            string j = sr.ReadToEnd();
            // string型の文字列をもとにTargetJson型のオブジェクトにデシリアライズ
            TargetJson s = Newtonsoft.Json.JsonConvert.DeserializeObject<TargetJson>(j);
            // オブジェクトのプロパティにアクセス
            Console.WriteLine(s.ObS.Name);

バージョニング

個人でつくるHelloWorldやサンプルアプリならバージョンについて深く考えることはないかと思いますが、お客様提供するアプリの場合は障害発生したときの解析や問い合わせ対応のためにバージョンを付与するのは必須かと思います。

image.png

VisualsStudio2019(devenv.exe)のファイルバージョン

バージョニングのルールやどのタイミングでどの番号をいくつ上げるかなどは所属する組織で違うはずですが、
windowsアプリの場合はMicrosoftのドキュメントに倣っているところが多いのではないでしょうか。

// AssemblyInfo.csの内容
// アセンブリのバージョン情報は次の 4 つの値で構成されています:
//
//      メジャー バージョン
//      マイナー バージョン
//      ビルド番号
//      リビジョン
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

バージョン情報の更新は、Properties\AssemblyInfo.csの"1.0.0.0"の内容を直接書き換えても反映されます。が、プロジェクト-右クリック、プロパティ-アプリケーション-アセンブリ情報で表示されるGUI上で変更するほうが楽かと思います。ちなみにこのGUIで変更した内容はAssemblyInfo.csに反映されます。

image.png

アプリのアイコン設定

visualstudioで作成したアプリはデフォルトのアイコンになります。
image.png

アイコンを変更する場合は
プロジェクト-右クリック、プロパティ-アプリケーション-リソース-アイコンとマニフェストicoファイルを指定します。

icoファイルはjpgやpngをコンバートして作成しましょう。
アイコンファイル(*.ico)作成

icoファイルを設定後は、プロジェクトにicoファイルが追加されます。
image.png

ビルドしてexeを生成・実行すると指定したicoがアイコンとして表示されているはずです。

まとめ

コーディング的なスキルというより、リリースまでの設定に近い部分が多くなりました。
windowsアプリだとUWPによるアプリ開発が主流と思いますが、今回はいわゆるレガシーアプリ開発で、開発のルールなどもUWP開発とは少し違うことになりました。(UWPだとマニフェストの設定やstoreへの登録などが必要ですね。)

正直、ドキュメントがチームに充実しているかで変わってきますね。
以上になります。

備考

インストールシールド12
業務ではIS12を使ってインストーラーを開発してます。

きまま研究所(WOW64で起動した際に64bitレジストリへアクセスする)

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

UnityとFirebaseでアプリ開発する際にユーザーデータの取り扱い方まとめ

こちらは Firebase Advent Calendar 2020 の 16日目の記事です。

概要

本記事は、Unity #2 Advent Calendar 2020 の 8日目の記事(前半)Unity 超初心者が Firebase でアプリ開発する際に必要になるスキル の後半内容となります。
そのため、全て内容を引き継いで解説を進めるため、本記事を読む前に前半をご覧くださいmm

ふりかえり

前半では、iOS/Android アプリを開発する際にユーザーデータを Firebase Realtime Database (Firebase Database) で保存取得しましょうと話し、その際にセキュリティ面を考えて rule でユーザー毎でアクセスできる設定のために Firebase Authentication (Firebase Auth) を使うことを解説しましたが、実装に関する解説をこれから解説していきます。

Firebase Auth

前半で 認証方法をメアド/パスワードで取り扱う 解説をしました。
もう少し詳しく話すと、例えばユーザーデータを取り扱っているようなカジュアルゲームでニックネームなどは設定するもののメアドやパスワードは設定しないと思います。全てのアプリがそうではないですが、あの裏側ではよしなにユーザー情報が作成されており、ユーザー ↔︎ アプリを勝手に紐づけています。

この方法でFirebase Authのメアド/パスワードで実装するとしたら、具体的に次の内容を考えてました。

  1. @前半をUUID/UDIDなどを用いて@後半はFirebaseが勝手に作っているドメインを扱ってメアド生成メソッドを作る
  2. 英数字記号をランダムで作ってくれるメアド生成メソッドを作る
  3. 2と3をCreateUserWithEmailAndPasswordAsyncに渡してアカウント生成
  4. FirebaseAuth.DefaultInstance.CurrentUserでユーザー情報が取得できたらログイン中で取得できない場合は自動ログイン
  5. 自動ログインの際にSignInWithEmailAndPasswordAsyncのために2と3を PlayerPrefs で保存取得できるようにしておく

ですが、匿名認証 を使うことで1~3を飛ばして4と5を実現することができます。

 

アプリの要件にもよりますが、アプリ初起動時に裏側で勝手に匿名認証のユーザー作成を行い、ユーザー設定のようなメニューを設けて「アプリ引き継ぎ設定」を作ることで上記1~3はやらなくてよくなります。
このように一般的なWeb/Mobileのアプリ開発とは異なり、ゲームやコンテンツのアプリはどれだけユーザーに面白い・楽しいと思ってもらいとにかく面倒な要素を取っ払うか重要になります。

FirebaseAuth.SignInAnonymouslyAsync

早速、匿名証明でユーザー作成をやってみましょう。
公式のサンプルコードを参考にし、delegateを用いてFirebaseAuthだけを管理するクラスで以下のように実装するとよいでしょう。

FirebaseAuthManager.cs
using UnityEngine;
using Firebase.Auth;

public class FirebaseAuthManager : MonoBehaviour
{
    FirebaseAuth _auth;
    FirebaseUser _user;
    public FirebaseUser UserData { get { return _user; } }
    public delegate void CreateUser(bool result);

    void Awake()
    {
        // 初期化
        _auth = FirebaseAuth.DefaultInstance;
        // すでにユーザーが作られているのか確認
        if (_auth.CurrentUser.UserId == null)
        {
            // まだユーザーができていないためユーザー作成
            Create((result) =>
            {
                if (result)
                {
                    Debug.Log($"成功: #{_user.UserId}");
                }
                else
                {
                    Debug.Log("失敗");
                }
            });
        }
        else
        {
            _user = _auth.CurrentUser;
            Debug.Log($"ログイン中: #{_user.UserId}");
        }
    }

    /// <summary>
    /// 匿名でユーザー作成
    /// </summary>
    public void Create(CreateUser callback)
    {
        _auth.SignInAnonymouslyAsync().ContinueWith(task => {
            if (task.IsCanceled)
            {
                Debug.LogError("SignInAnonymouslyAsync was canceled.");
                callback(false);
                return;
            }
            if (task.IsFaulted)
            {
                Debug.LogError("SignInAnonymouslyAsync encountered an error: " + task.Exception);
                callback(false);
                return;
            }

            _user = task.Result;
            Debug.Log($"User signed in successfully: {_user.DisplayName} ({_user.UserId})");
            callback(true);
        });
    }
}

 
delegateは、上記のようにdelegateの型に合うメソッドを用意すれば他クラスにメソッドの引数として渡すことができ、その他クラスのメソッドの処理終わりのタイミングで渡したメソッドが呼び出される仕組みとなります。++C++; // 未確認飛行 C さんが解説されている記事 がとてもわかりやすいので、こちらを参考にされてくださいmm

「匿名アカウントを永久アカウントに変換する」

先ほども解説した通り、最初に匿名でユーザーを作り、後からメアド/パスワードもしくはSNS連携に切り替えることができます。
方法としては、サンプルコードで解説されている通りFirebaseAuth.DefaultInstance.CurrentUser.LinkWithCredentialAsyncを扱うようです。

FirebaseAuthManager.cs
    /// <summary>
    /// メアド/パスワードでユーザー作成
    /// </summary>
    public void Create(string email, string password, CreateUser callback)
    {
        // すでにユーザーが作られているのか確認
        if (_auth.CurrentUser.UserId == null)
        {
            // 新規でユーザー作成
            _auth.CreateUserWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
            {
                if (task.IsCanceled)
                {
                    Debug.LogError("CreateUserWithEmailAndPasswordAsync was canceled.");
                    callback(false);
                    return;
                }
                if (task.IsFaulted)
                {
                    Debug.LogError("CreateUserWithEmailAndPasswordAsync encountered an error: " + task.Exception);
                    callback(false);
                    return;
                }

                _user = task.Result;
                Debug.Log($"Firebase user created successfully: {_user.DisplayName} ({_user.UserId})");

                callback(true);
            });
        }
        else
        {
            // 認証方法追加
            Credential credential = EmailAuthProvider.GetCredential(email, password);
            _auth.CurrentUser.LinkWithCredentialAsync(credential).ContinueWith(task => {
                if (task.IsCanceled)
                {
                    Debug.LogError("LinkWithCredentialAsync was canceled.");
                    callback(false);
                    return;
                }
                if (task.IsFaulted)
                {
                    Debug.LogError("LinkWithCredentialAsync encountered an error: " + task.Exception);
                    callback(false);
                    return;
                }

                _user = task.Result;
                Debug.Log($"Credentials successfully linked to Firebase user: {_user.DisplayName} ({_user.UserId})");

                callback(true);
            });
        }
    }

 
これでAuthの対応は一旦終わりで、目的のDatabaseの実装に進んでいきます。

Firebase Database

最後にDatabaseでは、「Authで作ったユーザー情報をもとにruleの設定」「データの保存と取得」の2点を解説して終わりとなります。
Authでユーザー情報を扱えるようになり、ruleでAuthのユーザーだけDatabaseとやりとりできるようになりました。

ruleの設定

設定はとても簡単で、公式にユーザー認証した際のルール設定方法の内容をそのまま以下のように設定すれば大丈夫です。

 

databaseを初めて使うときにテストモードにしていると上記スクショのようにコメントアウトします。

また、今回はサンプルとしてusersの下にデータを保存と取得する内容でまとめていますが、他にもランキング情報やチャット情報を取り扱う場合は、rankingschatsなどのように別keyになると思います。
ポイントは、$uidが先ほどのFirebaseAuth.CurrentUser.UserIdと同じものでなければ許可しないように設定することです。

ユーザーデータの保存と取得

Startメソッドに保存と取得の処理を書いてますが、本来は別クラスから処理を呼び出すことになります。例として、ゲームのクリアタイムをニックネームとともに保存するサンプルを用意してみました。

FirebaseDatabaseManager.cs
using UnityEngine;
using Firebase.Database;

public class UserPlayData
{
    public string username;
    public float time;

    public UserPlayData(string username, float time)
    {
        this.username = username;
        this.time = time;
    }
}

public class FirebaseDatabaseManager : MonoBehaviour
{
    readonly string USER_DATA_KEY = "users";
    DatabaseReference reference;
    [SerializeField]
    FirebaseAuthSample _auth;
    public delegate void GetUserDataCallback(UserPlayData result);

    void Start()
    {
        reference = FirebaseDatabase.DefaultInstance.RootReference;

        // サンプル: 保存
        var userData = new UserPlayData("gremito", 10.5f);
        SaveUserData(userData);

        // サンプル: 取得
        GetUserData((result) => {
            if(result == null)
            {
                Debug.LogWarning("失敗");
            }
            else
            {
                Debug.Log($"username: {result.username}");
                Debug.Log($"time: {result.time}");
            }
        });
    }

    /// <summary>
    /// ユーザーデータをJson化してdatabaseに保存(SetRawJsonValueAsync)
    /// </summary>
    public void SaveUserData(UserPlayData data)
    {
        // 公式サンプル方法: https://firebase.google.com/docs/database/unity/save-data?authuser=0#write_update_or_delete_data_at_a_reference
        var json = JsonUtility.ToJson(data);
        reference.Child(USER_DATA_KEY).Child(_auth.UserData.UserId).SetRawJsonValueAsync(json);
    }

    /// <summary>
    /// ユーザーデータを取得
    /// </summary>
    public void GetUserData(GetUserDataCallback callback)
    {
        FirebaseDatabase.DefaultInstance.GetReference(USER_DATA_KEY)
            .Child(_auth.UserData.UserId).GetValueAsync().ContinueWith(task =>
            {
                if (task.IsFaulted)
                    callback(null);

                else if (task.IsCompleted)
                    callback(new UserPlayData(
                        task.Result.Child("username").Value.ToString(),
                        float.Parse(task.Result.Child("time").Value.ToString())));
            });
    }
}

  

右スクショのFirebaseコンソールのDatabaseで保存したデータが反映されているようにUnityのデータを保存取得することができました。
これは、最低限の実装なのでAppStore/GooglePlayのストアにアプリをリリースする品質を考えるとトランザクションイベント並べ替えとフィルタリングなども上手く扱う必要が出てくるかもしれません。

オフライン機能

Realtime Databaseにはオフライン機能があり、最後に通信した最新のデータをFriebase SDK側でデータを保持して取り扱うことができます。

アプリの要件に応じて最新情報が必須な場面は、意図的にアプリを使用できなくする機能実装が必要になります。
反対に最新情報が必要でなくてもアプリを操作できる場面は、特に対応しなくていいかもしれません。

例えば、カジュアルゲームのアプリでランキング機能の場合、理想は最新情報が欲しいけど別に後から同期させて最新のランキング情報にすればいいから、オフラインでもゲームを楽しめるケースが考えられます。

まとめ

これでFirebaseのAuthとDatabaseを組み合わせて、しっかりセキュリティを担保したユーザーデータの取り扱い方を知れたと思います。

先ほども解説した通り、ここまでの内容は最低限のサンプルでFirebaseには他にも機能が豊富で、アプリの要件に応じてここで解説していない使える機能があると思います。

これをきっかけにさらにFirebaseを知ることでサーバーサイドの知識も得られますし、一石二鳥以上のスキルアップになると思うのでぜひ活用していきましょう!

 

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

Xamarin Community Toolkit カタログ(ビヘイビア・コンバーター編)

この記事は、Xamarin Advent Calendar 2020 16日目の記事です。

はじめに

Xamarin Community Toolkit は、Xamarin.Formsで使用できる便利ツール群です。ビヘイビア、コンバーター、エフェクトなどの汎用的な機能が揃っています。
この記事では、Xamarin Community Toolkitの機能を紹介していきます。執筆時点でのバージョンは、1.0.0-pre5 です。

記事内のコードは、GitHubにあげてあります。
XamarinCommunityToolkitCatalog

使い方

プレビュー版ですが、NuGetでインストールできます。
Xamarin.CommunityToolkit

xamlでのネームスペースは、以下のように指定します。

xmlns:xct="http://xamarin.com/schemas/2020/toolkit"

ビヘイビア

AnimationBehavior

タップ時にアニメーションが行われるようにするビヘイビアです。設定できるアニメーションの種類は以下の通りです。

  • FadeAnimation (フェード)
  • FlipHorizontalAnimation (横フリップ)
  • FlipVerticalAnimation (縦フリップ)
  • RotateAnimation (回転)
  • ScaleAnimation (スケール)
  • ShakeAnimation (シェイク)

Commandで、アニメーション終了時に実行するコマンドを設定できます。ただし、仕様なのかバグなのか分かりませんが、2回実行されるので注意が必要です。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.AnimationBehaviorPage"
             Title="AnimationBehavior">
    <StackLayout VerticalOptions="Center"
                 Padding="32, 0"
                 Spacing="16">
        <Label Text="Fade" HorizontalTextAlignment="Center" >
            <Label.Behaviors>
                <xct:AnimationBehavior Command="{Binding FinishedCommand}">
                    <xct:AnimationBehavior.AnimationType>
                        <xct:FadeAnimation />
                    </xct:AnimationBehavior.AnimationType>
                </xct:AnimationBehavior>
            </Label.Behaviors>
        </Label>

        <Label Text="FlipHorizontal" HorizontalTextAlignment="Center" >
            <Label.Behaviors>
                <xct:AnimationBehavior Command="{Binding FinishedCommand}">
                    <xct:AnimationBehavior.AnimationType>
                        <xct:FlipHorizontalAnimation />
                    </xct:AnimationBehavior.AnimationType>
                </xct:AnimationBehavior>
            </Label.Behaviors>
        </Label>

        <Label Text="FlipHorizontal" HorizontalTextAlignment="Center" >
            <Label.Behaviors>
                <xct:AnimationBehavior Command="{Binding FinishedCommand}">
                    <xct:AnimationBehavior.AnimationType>
                        <xct:FlipVerticalAnimation />
                    </xct:AnimationBehavior.AnimationType>
                </xct:AnimationBehavior>
            </Label.Behaviors>
        </Label>

        <Label Text="Rotate" HorizontalTextAlignment="Center" >
            <Label.Behaviors>
                <xct:AnimationBehavior Command="{Binding FinishedCommand}">
                    <xct:AnimationBehavior.AnimationType>
                        <xct:RotateAnimation Rotation="360" Duration="1000" />
                    </xct:AnimationBehavior.AnimationType>
                </xct:AnimationBehavior>
            </Label.Behaviors>
        </Label>

        <Label Text="Scale" HorizontalTextAlignment="Center" >
            <Label.Behaviors>
                <xct:AnimationBehavior Command="{Binding FinishedCommand}">
                    <xct:AnimationBehavior.AnimationType>
                        <xct:ScaleAnimation Scale="2" />
                    </xct:AnimationBehavior.AnimationType>
                </xct:AnimationBehavior>
            </Label.Behaviors>
        </Label>

        <Button Text="Shake">
            <Button.Behaviors>
                <xct:AnimationBehavior EventName="Clicked"
                                       Command="{Binding FinishedCommand}">
                    <xct:AnimationBehavior.AnimationType>
                        <xct:ShakeAnimation />
                    </xct:AnimationBehavior.AnimationType>
                </xct:AnimationBehavior>
            </Button.Behaviors>
        </Button>
    </StackLayout>
</ContentPage>

EventToCommandBehavior

指定したイベントの発火時にコマンドを実行するビヘイビアです。EventNameでイベント名を、Commandでコマンドを設定します。また、CommandParameterでコマンドのパラメータ、EventArgsConverterEventArgsのコンバーターの設定もできます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.EventToCommandBehaviorPage"
             Title="EventToCommandBehavior">
    <Button Text="Button"
            VerticalOptions="Center"
            Margin="32, 0">
        <Button.Behaviors>
            <xct:EventToCommandBehavior EventName="Clicked"
                                        Command="{Binding ClickedCommand}" />
        </Button.Behaviors>
    </Button>
</ContentPage>

ImpliedOrderGridBehavior

GridGrid.ColumnGrid.Rowを指定しなくても、暗黙的に設定を行うビヘイビアです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.ImpliedOrderGridBehaviorPage"
             Title="ImpliedOrderGridBehavior">
    <Grid>
        <Grid.Behaviors>
            <xct:ImpliedOrderGridBehavior />
        </Grid.Behaviors>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Label Text="0,0" />
        <Label Text="1,0" />
        <Label Text="2,0" />

        <Label Text="0,1" />
        <Label Text="1,1" />
        <Label Text="2,1" />
    </Grid>
</ContentPage>

MaskedBehavior

入力した文字列を指定したパターンに合わせるビヘイビアです。Maskでパターンを設定します。パターンでの任意の文字はデフォルトではXで表します。この文字は、UnMaskedCharacterで変更することが可能です。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.MaskedBehaviorPage"
             Title="MaskedBehavior">
    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:MaskedBehavior Mask="XXXX-XX" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

MaxLengthReachedBehavior

EntryEditorの最大入力文字(MaxLength)に達した時にイベントのやコマンドを実行するビヘイビアです。イベントはMaxLengthReached、コマンドはCommandで設定します。また、ShouldDismissKeyboardAutomaticallytrueにすることで、最大入力文字に達した時に、キーボードを閉じるようにすることもできます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.MaxLengthReachedBehaviorPage"
             Title="MaxLengthReachedBehavior">
    <Entry MaxLength="6"
           VerticalOptions="Center"
           Margin="32, 0">
        <Entry.Behaviors>
            <xct:MaxLengthReachedBehavior Command="{Binding MaxLengthReachedCommand}"
                                          ShouldDismissKeyboardAutomatically="True" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

UserStoppedTypingBehavior

EntryEditorで入力を終えてから一定時間後にコマンドを実行するビヘイビアです。StoppedTypingTimeThresholdでコマンド実行までの時間(ミリ秒)を、Commandで実行するコマンドを設定します。また、MinimumLengthThresholdでコマンドの実行を行う最小文字数を設定できたり、ShouldDismissKeyboardAutomaticallytrueにすることで、コマンド実行時にキーボードを閉じるようにすることもできます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.UserStoppedTypingBehaviorPage"
             Title="UserStoppedTypingBehavior">
    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:UserStoppedTypingBehavior Command="{Binding UserStoppedTypingCommand}"
                                           StoppedTypingTimeThreshold="500"
                                           MinimumLengthThreshold="3"
                                           ShouldDismissKeyboardAutomatically="True" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

ValidationBehavior

バリデーションを行うためのビヘイビアです。ValidStyleで有効時、InvalidStyleで無効時のスタイルを設定できます。また、IsValidで、有効かどうかを取得できます。
ValidationBehaviorは抽象クラスで、以下が実装してあるクラスです。

CharactersValidationBehavior

指定した種類の文字が指定数分含まれているかどうかのバリデーションを行うビヘイビアです。CharacterTypeで文字の種類、MinimumCharacterCountで最小数、MaximumCharacterCountで最大数を指定します。
指定できる文字の種類は以下の通りです。

  • LowercaseLetter (小文字)
  • UppercaseLetter (大文字)
  • Letter (LowercaseLetter または UppercaseLetter)
  • Digit (数字)
  • Alphanumeric (Letter または Digit)
  • Whitespace (空白文字)
  • NonAlphanumericSymbol (文字、数字、空白文字以外)
  • LowercaseLatinLetter (a〜z)
  • UppercaseLatinLetter (A〜Z)
  • LatinLetter (LowercaseLatinLetter または UppercaseLatinLetter)
  • Any (Alphanumeric または NonAlphanumericSymbol または Whitespace)

フラグなので組み合わせることもできます。ただし、LowercaseLetterUppercaseLetterでは日本語の文字は含まれないので、必然的にLetterでも含まれないので注意が必要です。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.CharactersValidationBehaviorPage"
             Title="CharactersValidationBehavior">
    <ContentPage.Resources>
        <Style x:Key="invalidEntryStyle" TargetType="Entry">
            <Setter Property="TextColor" Value="Red" />
        </Style>
    </ContentPage.Resources>

    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:CharactersValidationBehavior InvalidStyle="{StaticResource invalidEntryStyle}"
                                              CharacterType="Letter"
                                              MinimumCharacterCount="2"
                                              MaximumCharacterCount="5" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

EmailValidationBehavior

Eメールの書式になっているかどうかのバリデーションを行うビヘイビアです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.EmailValidationBehaviorPage"
             Title="EmailValidationBehavior">
    <ContentPage.Resources>
        <Style x:Key="invalidEntryStyle" TargetType="Entry">
            <Setter Property="TextColor" Value="Red" />
        </Style>
    </ContentPage.Resources>

    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:EmailValidationBehavior InvalidStyle="{StaticResource invalidEntryStyle}" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

MultiValidationBehavior

複数のValidationBehaviorを組み合わせてバリデーションを行うビヘイビアです。バリデーション毎にErrorで無効時の値を設定でき、Errorsで無効と判定されているバリデーションのErrorの値を取得できます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.MultiValidationBehaviorPage"
             Title="MultiValidationBehavior">
    <ContentPage.Resources>
        <Style x:Key="invalidEntryStyle" TargetType="Entry">
            <Setter Property="TextColor" Value="Red" />
        </Style>
    </ContentPage.Resources>

    <StackLayout VerticalOptions="Center" Margin="32, 0">
        <Entry>
            <Entry.Behaviors>
                <xct:MultiValidationBehavior x:Name="multiValidationBehavior"
                                             InvalidStyle="{StaticResource invalidEntryStyle}" >
                    <xct:CharactersValidationBehavior CharacterType="Digit"
                                                      MinimumCharacterCount="1"
                                                      xct:MultiValidationBehavior.Error="数字1文字以上" />
                    <xct:CharactersValidationBehavior CharacterType="UppercaseLetter"
                                                      MinimumCharacterCount="1"
                                                      xct:MultiValidationBehavior.Error="大文字1文字以上" />
                    <xct:CharactersValidationBehavior CharacterType="Any"
                                                      MinimumCharacterCount="8"
                                                      xct:MultiValidationBehavior.Error="8文字以上" />
                </xct:MultiValidationBehavior>
            </Entry.Behaviors>
        </Entry>
        <Label Text="{Binding Errors[0], Source={x:Reference multiValidationBehavior}}" />
    </StackLayout>
</ContentPage>

NumericValidationBehavior

数値の範囲や小数点以下の桁数のバリデーションを行うビヘイビアです。MinimumValueで最小値、MaximumValueで最大値、MinimumDecimalPlacesで小数点以下の最小桁数、MaximumDecimalPlacesで小数点以下の最大桁数を設定します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.NumericValidationBehaviorPage"
             Title="NumericValidationBehavior">
    <ContentPage.Resources>
        <Style x:Key="invalidEntryStyle" TargetType="Entry">
            <Setter Property="TextColor" Value="Red" />
        </Style>
    </ContentPage.Resources>

    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:NumericValidationBehavior InvalidStyle="{StaticResource invalidEntryStyle}"
                                           MinimumValue="1.0"
                                           MaximumValue="10.0"
                                           MinimumDecimalPlaces="1"
                                           MaximumDecimalPlaces="3" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

RequiredStringValidationBehavior

指定した文字列が入力されているかどうかのバリデーションを行うビヘイビアです。RequiredStringで対象の文字列を設定します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.RequiredStringValidationBehaviorPage"
             Title="RequiredStringValidationBehavior">
    <ContentPage.Resources>
        <Style x:Key="invalidEntryStyle" TargetType="Entry">
            <Setter Property="TextColor" Value="Red" />
        </Style>
    </ContentPage.Resources>

    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:RequiredStringValidationBehavior InvalidStyle="{StaticResource invalidEntryStyle}"
                                                  RequiredString="hoge" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

TextValidationBehavior

文字列の長さや、正規表現によるバリデーションを行うビヘイビアです。MinimumLengthで最小の長さ、MaximumLength最大の長さ、RegexPatternで正規表現のパターンを設定できます。また、DecorationFlagsを設定することで、文字列を加工した後にバリデーションを行うことが可能です。DecorationFlagsは以下のものが用意されています。

  • TrimStart (先頭の空白文字を削除)
  • TrimEnd (末尾の空白文字を削除)
  • Trim (先頭と末尾の空白文字を削除)
  • NullToEmpty (nullを空文字に変換)
  • ReduceWhiteSpaces (空白文字を削除)

ちなみに、CharactersValidationBehaviorEmailValidationBehaviorUriValidationBehaviorは、TextValidationBehaviorのサブクラスです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.TextValidationBehaviorPage"
             Title="TextValidationBehavior">
    <ContentPage.Resources>
        <Style x:Key="invalidEntryStyle" TargetType="Entry">
            <Setter Property="TextColor" Value="Red" />
        </Style>
    </ContentPage.Resources>

    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:TextValidationBehavior InvalidStyle="{StaticResource invalidEntryStyle}"
                                        MinimumLength="4"
                                        MaximumLength="8"
                                        DecorationFlags="Trim"
                                        RegexPattern="^\w+$" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

UriValidationBehavior

URIのフォーマットであるかどうかのバリデーションを行うビヘイビアです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.UriValidationBehaviorPage"
             Title="UriValidationBehavior">
    <ContentPage.Resources>
        <Style x:Key="invalidEntryStyle" TargetType="Entry">
            <Setter Property="TextColor" Value="Red" />
        </Style>
    </ContentPage.Resources>

    <Entry VerticalOptions="Center" Margin="32, 0">
        <Entry.Behaviors>
            <xct:UriValidationBehavior InvalidStyle="{StaticResource invalidEntryStyle}" />
        </Entry.Behaviors>
    </Entry>
</ContentPage>

コンバーター

BoolToObjectConverter

bool値を任意の値に変換するコンバーターです。TrueObjectにはtrueの時の値、FalseObjectにはfalseの時の値を設定します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.BoolToObjectConverterPage"
             Title="BoolToObjectConverter">
    <ContentPage.Resources>
        <xct:BoolToObjectConverter x:Key="boolToObjectConverter"
                                   x:TypeArguments="x:String"
                                   TrueObject="真"
                                   FalseObject="偽"/>
        <x:Boolean x:Key="value">true</x:Boolean>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource boolToObjectConverter}}" />
</ContentPage>

ByteArrayToImageSourceConverter

byte配列をImageSourceに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.ByteArrayToImageSourceConverterPage"
             Title="ByteArrayToImageSourceConverter">
    <ContentPage.Resources>
        <xct:ByteArrayToImageSourceConverter x:Key="byteArrayToImageSourceConverter" />
    </ContentPage.Resources>

    <Image VerticalOptions="Center" HorizontalOptions="Center"
           Source="{Binding ImageBytes,
                    Converter={StaticResource byteArrayToImageSourceConverter}}" />
</ContentPage>

DateTimeOffsetConverter

DateTimeOffsetDateTimeに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamarinCommunityToolkitCatalog.Views.DateTimeOffsetConverterPage"
             Title="DateTimeOffsetConverter">
    <ContentPage.Resources>
        <xct:DateTimeOffsetConverter x:Key="dateTimeOffsetConverter" />
        <sys:DateTimeOffset x:Key="value">
            <x:Arguments>
                <x:Int32>2020</x:Int32>
                <x:Int32>12</x:Int32>
                <x:Int32>16</x:Int32>
                <x:Int32>0</x:Int32>
                <x:Int32>0</x:Int32>
                <x:Int32>0</x:Int32>
                <x:TimeSpan />
            </x:Arguments>
        </sys:DateTimeOffset>
    </ContentPage.Resources>

    <DatePicker VerticalOptions="Center" HorizontalOptions="Center"
                Date="{Binding ., Source={StaticResource value},
                       Converter={StaticResource dateTimeOffsetConverter}}" />
</ContentPage>

DoubleToIntConverter

doubleintに変換するコンバーターです。Math.Roundで丸められます。パラメーターで、乗数を指定することもできます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.DoubleToIntConverterPage"
             Title="DoubleToIntConverter">
    <ContentPage.Resources>
        <xct:DoubleToIntConverter x:Key="doubleToIntConverter" />
        <x:Double x:Key="value">1.25</x:Double>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource doubleToIntConverter},
                  ConverterParameter=10}" />
</ContentPage>

EqualConverter

パラメーターの値と等しければtrue、違っていればfalseに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.EqualConverterPage"
             Title="EqualConverter">
    <ContentPage.Resources>
        <xct:EqualConverter x:Key="equalConverter" />
        <x:String x:Key="value">value</x:String>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource equalConverter},
                  ConverterParameter=value}" />
</ContentPage>

IndexToArrayItemConverter

パラメーターに設定したArrayのインデックスの値に変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.IndexToArrayItemConverterPage"
             Title="IndexToArrayItemConverter">
    <ContentPage.Resources>
        <xct:IndexToArrayItemConverter x:Key="indexToArrayItemConverter" />
        <x:Int32 x:Key="value">1</x:Int32>
        <x:Array x:Key="array" Type="{x:Type x:String}">
            <x:String>value0</x:String>
            <x:String>value1</x:String>
            <x:String>value2</x:String>
        </x:Array>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource indexToArrayItemConverter},
                  ConverterParameter={StaticResource array}}" />
</ContentPage>

IntToBoolConverter

0ならfalse、0以外ならtrueに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.IntToBoolConverterPage"
             Title="IntToBoolConverter">
    <ContentPage.Resources>
        <xct:IntToBoolConverter x:Key="intToBoolConverter" />
        <x:Int32 x:Key="value">0</x:Int32>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource intToBoolConverter}}" />
</ContentPage>

InvertedBoolConverter

trueならfalsefalseならtrueに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.InvertedBoolConverterPage"
             Title="InvertedBoolConverter">
    <ContentPage.Resources>
        <xct:InvertedBoolConverter x:Key="invertedBoolConverter" />
        <x:Boolean x:Key="value">true</x:Boolean>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource invertedBoolConverter}}" />
</ContentPage>

IsNotNullOrEmptyConverter

null、空文字、空白文字のみではない場合はtrue、それ以外はfalseに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.IsNotNullOrEmptyConverterPage"
             Title="IsNotNullOrEmptyConverter">
    <ContentPage.Resources>
        <xct:IsNotNullOrEmptyConverter x:Key="isNotNullOrEmptyConverter" />
        <x:String x:Key="value"> </x:String>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource isNotNullOrEmptyConverter}}" />
</ContentPage>

IsNullOrEmptyConverter

null、空文字、空白文字のみである場合はtrue、それ以外はfalseに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.IsNullOrEmptyConverterPage"
             Title="IsNullOrEmptyConverter">
    <ContentPage.Resources>
        <xct:IsNullOrEmptyConverter x:Key="isNullOrEmptyConverter" />
        <x:String x:Key="value"> </x:String>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource isNullOrEmptyConverter}}" />
</ContentPage>

ItemSelectedEventArgsConverter

ListViewItemSelectedイベントの引数であるSelectedItemChangedEventArgsを、そのプロパティであるSelectedItemの値に変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.ItemSelectedEventArgsConverterPage"
             Title="ItemSelectedEventArgsConverter">
    <ContentPage.Resources>
        <xct:ItemSelectedEventArgsConverter x:Key="itemSelectedEventArgsConverter" />
    </ContentPage.Resources>

    <ListView>
        <ListView.ItemsSource>
            <x:Array Type="{x:Type x:String}">
                <x:String>value0</x:String>
                <x:String>value1</x:String>
                <x:String>value2</x:String>
            </x:Array>
        </ListView.ItemsSource>
        <ListView.Behaviors>
            <xct:EventToCommandBehavior EventName="ItemSelected"
                                        Command="{Binding ItemSelectedCommand}"
                                        EventArgsConverter="{StaticResource itemSelectedEventArgsConverter}" />
        </ListView.Behaviors>
    </ListView>
</ContentPage>

ItemTappedEventArgsConverter

ListViewItemTappedイベントの引数であるItemTappedEventArgsを、そのプロパティであるItemの値に変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.ItemTappedEventArgsConverterPage"
             Title="ItemTappedEventArgsConverter">
    <ContentPage.Resources>
        <xct:ItemTappedEventArgsConverter x:Key="itemTappedEventArgsConverter" />
    </ContentPage.Resources>

    <ListView>
        <ListView.ItemsSource>
            <x:Array Type="{x:Type x:String}">
                <x:String>value0</x:String>
                <x:String>value1</x:String>
                <x:String>value2</x:String>
            </x:Array>
        </ListView.ItemsSource>
        <ListView.Behaviors>
            <xct:EventToCommandBehavior EventName="ItemTapped"
                                        Command="{Binding ItemTappedCommand}"
                                        EventArgsConverter="{StaticResource itemTappedEventArgsConverter}" />
        </ListView.Behaviors>
    </ListView>
</ContentPage>

ListIsNotNullOrEmptyConverter

nullではない、もしくは、IEnumerableが空ではない場合はtrue、それ以外はfalseに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             xmlns:scg="clr-namespace:System.Collections.Generic;assembly=mscorlib"
             x:Class="XamarinCommunityToolkitCatalog.Views.ListIsNotNullOrEmptyConverterPage"
             Title="ListIsNotNullOrEmptyConverter">
    <ContentPage.Resources>
        <xct:ListIsNotNullOrEmptyConverter x:Key="listIsNotNullOrEmptyConverter" />
        <scg:List x:Key="value" x:TypeArguments="x:Int32">
            <x:Int32>0</x:Int32>
        </scg:List>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource listIsNotNullOrEmptyConverter}}" />
</ContentPage>

ListIsNullOrEmptyConverter

null、もしくは、IEnumerableが空である場合はtrue、それ以外はfalseに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             xmlns:scg="clr-namespace:System.Collections.Generic;assembly=mscorlib"
             x:Class="XamarinCommunityToolkitCatalog.Views.ListIsNullOrEmptyConverterPage"
             Title="ListIsNullOrEmptyConverter">
    <ContentPage.Resources>
        <xct:ListIsNullOrEmptyConverter x:Key="listIsNullOrEmptyConverter" />
        <scg:List x:Key="value" x:TypeArguments="x:Int32">
            <x:Int32>0</x:Int32>
        </scg:List>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource listIsNullOrEmptyConverter}}" />
</ContentPage>

ListToStringConverter

IEnumerableの各値を文字列にして、Separatorもしくはパラメーターで指定した区切り文字を使用して連結した文字列に変換するコンバーターです。区切り文字は、パラメーターで指定した方が優先されます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             xmlns:scg="clr-namespace:System.Collections.Generic;assembly=mscorlib"
             x:Class="XamarinCommunityToolkitCatalog.Views.ListToStringConverterPage"
             Title="ListToStringConverter">
    <ContentPage.Resources>
        <xct:ListToStringConverter x:Key="listToStringConverter"
                                   Separator=", " />
        <scg:List x:Key="value" x:TypeArguments="x:String">
            <x:String>value0</x:String>
            <x:String>value1</x:String>
            <x:String>value2</x:String>
        </scg:List>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource listToStringConverter}}" />
</ContentPage>

MultiConverter

複数のコンバーターを順番に適用して変換するコンバーターです。MultiConverterParameterで、適用するコンバーター毎にパラメーターを設定することができます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.MultiConverterPage"
             Title="MultiConverter">
    <ContentPage.Resources>
        <xct:MultiConverter x:Key="multiConverter" >
            <xct:EqualConverter />
            <xct:BoolToObjectConverter x:TypeArguments="x:String"
                                       TrueObject="同じ"
                                       FalseObject="違う" />
        </xct:MultiConverter>
        <x:Array x:Key="multiParams"
                 Type="{x:Type xct:MultiConverterParameter}" >
            <xct:MultiConverterParameter
                ConverterType="{x:Type xct:EqualConverter}"
                Value="value" />
        </x:Array>
        <x:String x:Key="value">value</x:String>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource multiConverter},
                  ConverterParameter={StaticResource multiParams}}" />
</ContentPage>

NotEqualConverter

パラメーターの値と違っていたらtrue、等しければfalseに変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.NotEqualConverterPage"
             Title="NotEqualConverter">
    <ContentPage.Resources>
        <xct:NotEqualConverter x:Key="notEqualConverter" />
        <x:String x:Key="value">value</x:String>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource notEqualConverter},
                  ConverterParameter=value}" />
</ContentPage>

StateToBooleanConverter

StateToCompareや、パラメーターで指定したLayoutStateと等しければtrue、違っていればfalseに変換するコンバーターです。StateToCompareとパラメーター両方指定した場合は、パラメーターが優先されます。LayoutStateは、Xamarin Community Toolkitの機能であるStateLayoutで使用されます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.StateToBooleanConverterPage"
             Title="StateToBooleanConverter">
    <ContentPage.Resources>
        <xct:StateToBooleanConverter x:Key="stateToBooleanConverter"
                                     StateToCompare="Success" />
        <x:Static x:Key="value" Member="xct:LayoutState.Error" />
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource stateToBooleanConverter}}" />
</ContentPage>

TextCaseConverter

文字列を大文字や小文字に変換するコンバーターです。Typeまたは、パラメーターで、大文字にする場合はUpper、小文字にする場合はLowerと指定します。Typeとパラメーター両方指定した場合は、パラメーターが優先されます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             x:Class="XamarinCommunityToolkitCatalog.Views.TextCaseConverterPage"
             Title="TextCaseConverter">
    <ContentPage.Resources>
        <xct:TextCaseConverter x:Key="textCaseConverter"
                               Type="Upper" />
        <x:String x:Key="value">Value</x:String>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource textCaseConverter}}" />
</ContentPage>

TimeSpanToDoubleConverter

TimeSpanをそれが表す秒数の合計(TotalSeconds)に変換するコンバーターです。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamarinCommunityToolkitCatalog.Views.TimeSpanToDoubleConverterPage"
             Title="TimeSpanToDoubleConverter">
    <ContentPage.Resources>
        <xct:TimeSpanToDoubleConverter x:Key="timeSpanToDoubleConverter" />
        <sys:TimeSpan x:Key="value">
            <x:Arguments>
                <x:Int32>12</x:Int32>
                <x:Int32>30</x:Int32>
                <x:Int32>10</x:Int32>
            </x:Arguments>
        </sys:TimeSpan>
    </ContentPage.Resources>

    <Label VerticalOptions="Center" HorizontalOptions="Center"
           Text="{Binding ., Source={StaticResource value},
                  Converter={StaticResource timeSpanToDoubleConverter}}" />
</ContentPage>

おわりに

Xamarin Community Toolkitのビヘイビア、コンバーターを紹介しました。
Xamarin Community Toolkitには、エフェクトやコントロールなど紹介していない機能がまだあります。続きは、Xamarin Advent Calendar 2020 20日目で公開予定です。

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

.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter1)

※本記事は下記のエントリから始まる連載記事となります。
.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter0)

Chapter1 画面上に図形を描画するための仕組みを作ろう

実装イメージ

具体的な実装に入る前に、まずは図形を描画する部分のクラス構成をざっくりと考えてみます。

image.png

  • 四角形や楕円などの図形は、IShapeインターフェースを実装するものとします。今は描画処理Draw()のみが実装されています。
  • 描画処理の実装部分はIGraphicsインターフェースをDraw()メソッドで受け取ることで外部から注入します(Method Injecton)
  • IGraphicsのSkiaSharp用の実装はCoreShape.Extensions.SkiaSharpに用意します。

ひとまずはこの構成にしたがって実装を進めてみたいと思います。

Chapter1.1 四角形を描画してみよう

それでは四角形(矩形)を描画する処理を例に具体的な実装について見てみましょう。

RectangleShape

IShapeインターフェースを継承したRectangleShapeクラスを作成します。
Draw()メソッドの実装はひとまず空のままにしておきます。

public class RectangleShape : IShape
{
    public void Draw(IGraphics g)
    {
    }
}

座標や色情報を格納する構造体を用意する

図形描画のためのパラメータとして以下が必要になります。

  • 位置(Point)
  • サイズ(Size)
  • 色(Color)

これらを構造体として定義します。また、位置(Point)とサイズ(Size)を組み合わせて四角形を表す構造体(Rectangle)も定義しておきます。
これらは構造体のデザインのガイドラインにしたがってIEquatableを実装した書き換え不可な構造体として定義します。

今回は「C#9.0 SourceGeneratorでReadonly構造体を生成するGeneratorを作ってみました。」で紹介したReadonlyStructGeneratorを使って実現します。
下記のように構造体を定義し、必要なプロパティをinitアクセサー付きで追加します。
IEquatable<T>とその他諸々の実装はGeneratorによって自動生成されます。

[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Point
{
    public float X { get; init; }
    public float Y { get; init; }
}
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Size
{
    public float Width { get; init; }
    public float Height { get; init; }
}
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Rectangle
{
    public Point Location { get; init; }
    public Size Size { get; init; }

    public float Left  => Location.X;
    public float Top => Location.Y;
    public float Right { get; }
    public float Bottom { get; }

    public Rectangle(Point location,Size size)
    {
        Location = location;
        Size = size;
        Right = location.X + size.Width;
        Bottom = location.Y + size.Height;
    }

    public Rectangle(float left, float top, float width, float height)
        : this(new Point(left, top), new Size(width, height)) { }
}
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Color
{
    public byte R { get; init; }
    public byte G { get; init; }
    public byte B { get; init; }
    public byte A { get; init; }

    public Color(byte r, byte g, byte b, byte a = 255) => (R, G, B, A) = (r, g, b, a); 
}

プロパティの追加

  • 四角形の位置、サイズの情報はRectangle構造体(Boundsプロパティ)で管理します。
  • 輪郭の色や太さはStrokeクラス、塗りつぶしの色はFillクラスで管理します。
public class RectangleShape : IShape
{
    public Rectangle Bounds { get; protected set; }
    public Stroke? Stroke { get; set; }
    public Fill? Fill { get; set; }

    public RectangleShape(Rectangle bounds)
    {
        Bounds = bounds;        
    }

    public void Draw(IGraphics g)
    {
    }
}
public class Stroke
{
    public Color Color { get; set; } = Color.Black;
    public float Width { get; set; } = 1f;

    public Stroke(Color color , float width)
    {
        Color = color;
        Width = width;
    }
}
public class Fill
{
    public Color Color { get; set; } = Color.White;
    public Fill(Color color)
    {
        Color = color;
    }
}

これで四角形の描画に必要なパラメータは揃いました。

IGraphics の実装

次に、実際に輪郭や塗りつぶしを描画する処理を定義するIGraphicsインターフェースに四角形を描画するためのメソッドを追加します。
輪郭はDrawRectangle()メソッド、塗りつぶしはFillRectangle()メソッドを使って描画します。
パラメータには位置とサイズを表すRectangleと、輪郭の色とサイズを指定するStroke または 塗りつぶしの色を指定するFillを受け取ります。

public interface IGraphics
{
    void DrawRectangle(Rectangle rectangle, Stroke stroke);
    void FillRectangle(Rectangle rectangle, Fill fill);
}

RectangleShape.Draw()メソッドの実装

IGraphicsにメソッドを追加したので、RectangleShapeDraw()メソッドが記述できるようになりました。
輪郭を描かない(線なし)、塗りつぶさない(塗りつぶしなし)の場合に対応するためにStrokeFillをNULL許容としておきます。
(参照型も既定でNULL非許容となっているので、クラス名の後に?を付けてNULL許容を明示する必要があります。)

public class RectangleShape : IShape
{
    public Rectangle Bounds { get; protected set; }
    public Stroke? Stroke { get; set; }
    public Fill? Fill { get; set; }

    public virtual void Draw(IGraphics g)
    {
         if (Fill is not null)
        {
            g.FillRectangle(Bounds, Fill);
        }
        if (Stroke is not null)
        {
            g.DrawRectangle(Bounds, Stroke);
        }
    }
}        

ここまででCoreShape側での実装は完了です。
続いてCoreShapeRectangleShape)を利用する側の実装に移ります。

IGraphicsSkiaSharp実装SkiaGraphicsクラス

CoreShape.Extensions.SkiaSharpのプロジェクトへ移動し、IGraphicsインターフェースを継承したSkiaGraphicsクラスを作成します。

SkiaSharpではSKCanvasクラスが図形描画を行うメソッドを持っています。
コンストラクタでSKCanvasのオブジェクトを受け取って利用するようにします。

後は DrawRectangle()FillRectangle() でSkCanvasのDrawRect() を実行するだけです。

public class SkiaGraphics : IGraphics
{
    protected virtual SKCanvas Canvas { get; set; }

    public SkiaGraphics(SKCanvas canvas)
    {
        Canvas = canvas;
    }

    public virtual void DrawRectangle(Rectangle rectangle, Stroke stroke)
    {
        var rect = new SKRect(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom);
        var paint = new SKPaint()
        {
            Style = SKPaintStyle.Stroke,
            Color = new SkColor(stroke.Color.R, stroke.Color. G,stroke. Color.B, stroke.Color.A),
            StrokeWidth = stroke.Width
        }
        Canvas.DrawRect(rect, paint);
    }

    public virtual void FillRectangle(Rectangle rectangle, Fill fill)
    {
        var rect = new SKRect(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom);
        var paint = new SKPaint()
        {
            Style = SKPaintStyle.Fill,
            Color = new SkColor(fill.Color.R, fill.Color.G, fill.Color.B, fill.Color.A)
        }
        Canvas.DrawRect(rect,paint);
    }
}

型変換用拡張メソッドの定義

SkiaGraphicsの実装はこれで良いのですが、1つだけ(ちょっとした)問題があります。
SkiaSharpの描画メソッドを利用する場合、座標や色などの指定にSkRectSkColorといったSkiaSharp独自のオブジェクトを利用する必要があります。

CoreShape でも独自にRectangleColor構造体を定義しています。また、描画パラメータStrokeFillに相当する値をSkiaSharpではSkPaintで指定する必要があります。

したがって、CoreShapeから渡ってきたこれらの値を一旦SkiaSharp用の値に変換してあげる処理が必要になります。これらの処理を色々なところで毎回行うのは非常に面倒なので、拡張メソッドとしてまとめて定義してしまいましょう。

CoreShape.Extensions.SkiaSharpのプロジェクトに以下のクラスを追加します。

(型変換用)

public static class TypeConvertExtensions
{
    public static SKPoint ToSk(this Point p) => new SKPoint(p.X, p.Y);
    public static SKSize ToSk(this Size s) => new SKSize(s.Width, s.Height);
    public static SKRect ToSk(this Rectangle rect) => new SKRect(rect.Left, rect.Top, rect.Right, rect.Bottom);
    public static SKColor ToSk(this Color color) => new SKColor(color.R, color.G, color.B, color.A);
}

(SkPaintにStroke,Fillの値を設定)

public static class SkPaintExtensions
{
    public static SKPaint SetStroke(this SKPaint paint, Stroke stroke)
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.Color = stroke.Color.ToSk();
        paint.StrokeWidth = stroke.Width;
        return paint;
    }
    public static SKPaint SetFill(this SKPaint paint, Fill fill)
    {
        paint.Style = SKPaintStyle.Fill;
        paint.Color = fill.Color.ToSk();
        return paint;
    }
}

これらを使って先ほどのDrawRectangle()FillRectangle()を書き換えると...

public virtual void DrawRectangle(Rectangle rectangle, Stroke stroke)
{
    Canvas.DrawRect(rectangle.ToSk(), new SKPaint().SetStroke(stroke));
}

public virtual void FillRectangle(Rectangle rectangle, Fill fill)
{
    Canvas.DrawRect(rectangle.ToSk(), new SKPaint().SetFill(fill));
}

なんということでしょう!こんなにすっきりと記述できました!
長い道のりでしたが、画面に四角形を描画するまであと一息です!

Windowに四角形を描画する

SampleWPFのプロジェクトへ移動します。
MainWindowのコードビハインド MainWindow.xaml.cs に追加した sKElement_PaintSurface イベントハンドラに以下のコードを記述します。

  • RectangleShapeを生成し、FillStrokeを設定します
  • イベント引数のSKPaintSurfaceEventArgsから取得できるSKCanvasオブジェクトを渡して SkiaGraphicsを生成します。
  • 生成したSkiaGraphicsのインスタンスを渡して RectangleShapeDraw()メソッドを実行します。
public partial class MainWindow : Window
{
    private void sKElement_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
    {
        var shape = new RectangleShape(100, 100, 200, 150)
        {
            Stroke = new Stroke(CoreShape.Color.Red, 2),
            Fill = new Fill(CoreShape.Color.LightSkyBlue)
        };

        var g = new SkiaGraphics(e.Surface.Canvas);
        shape.Draw(g);
    }
}

ここまで来れば後は実行するのみです!

image.png

はい、ようやくWindowに四角形を描くことに成功しました!

これで図形を描画する仕組みが整いましたので、後はこの仕組みに乗っかって他の図形を描画するクラスを追加していけば良いですね。

例えば楕円を描画するクラスを追加したい場合は下記のようにするだけで完了です。

  • RectangleShapeを継承したOvalShapeのクラスを用意
  • IGraphics に楕円描画用のメソッド(DrawOval() FillOval()のような)を追加
  • OvalShapeDraw() メソッド内にDrawOval() FillOval()を実行する処理を記述
  • IGraphicsを継承したクラスでDrawOval() FillOval()の実装を記述

次回

次回の予定は

  • Chapter2 ドラッグ操作で図形を動かしてみよう

です。

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

.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter0)

.NET5.0も正式リリースされ、C#言語バージョンも9.0となりました。
とはいえ、私自身C#の知識が7.0くらいの知識からアップデートされていなかったりします。

そこで今回はC#8、9辺りの新しい機能を意識しつつ、.NET5.0のアプリケーションを一から作ってみようと思います。

作るもの

さて、何を作るかですが
「ほとんどの人がどのようなものか知っている(仕様の説明が要らない)」
「実装の結果が視覚的にわかりやすい」
との理由からWord、Excel等についているオートシェイプ機能のような図形描画のライブラリを作ることとしました。

  • オートシェイプ機能のような図形描画のライブラリを.NET5.0クラスライブラリとして作ります。
  • このライブラリを利用したサンプルアプリケーションをWPFで作成します。
  • サンプルでは2DグラフィックスエンジンのSkiaSharpを使います
  • (それ以外はノープランです)

※ 行き当たりばったりで設計しながら作る過程を連載記事として公開してゆく予定ですので、「それあまり良くない設計ですよ」とか「もっといい方法ありますよ」などフィードバックをいただけますと大変うれしいです。

ソースコード

本記事のソースコードはGitHub上に置いておきます。

https://github.com/pierre3/CoreShape

(「blog/cahpter0-2」のように章ごとにブランチを切っていく予定です)

記事一覧(予定)

それでは、まずは開発の準備から始めましょう!

Chapter0 ソリューションの作成

Visual Studio2019 の最新バージョン(12/13現在はVersion 16.8.3)を使用して開発します。

プロジェクトの構成

以下のような構成でプロジェクトを作成します。

  • CoreShape: 今回作成するメインのプロジェクト (.NET5.0 クラスライブラリ)
  • CoreShape.Extensions.SkiaSharp: SkiaSharp向けのCoreShape拡張。インターフェースの実装や拡張メソッドなどを定義(.NET5.0クラスライブラリ)
  • SkiaSharp: 2Dグラフィックスエンジン(NuGetライブラリ参照)
  • SampleWPF: サンプルアプリ(WPF(.NET5.0)App)

CoreShapeライブラリはどのプラットフォーム、ライブラリにも依存しないポータブルな状態を保つようにします。

image.png

プロジェクトの作成

.NET5.0プロジェクトテンプレートの表示

.NET5.0 対応のプロジェクトテンプレートを利用するには今のところVisual Studioのオプション設定を変更する必要があります。

  • メニューバーで「ツール」-「オプション」を選択
  • オプションダイアログの左側で「環境」-「プレビュー機能」を選択
  • 「「新しいプロジェクト」ダイアログに全ての.NET Coreテンプレートを表示する」にチェックを付ける

image01

.NET5.0クラスライブラリの作成

下記の手順で「CoreShape」及び「CoreShape.Extensions.SkiaSharp」のプロジェクトを作成します。

  • メニューバーで「ファイル」-「新規作成」-「プロジェクト」を選択
  • 「新しいプロジェクトの作成」ダイアログでテンプレート「Class library」を選択して「次へ」
    image02

  • プロジェクト名「CoareShape」を入力して「次へ」

  • 「追加情報」でターゲットフレームワークで「.NET5.0」を選択して「作成」
    img03

  • メニュー「ファイル」-「追加」-「新しいプロジェクト」から「CoreShape.Extensions.SkiaSharp」プロジェクトも同様に作成します。

WPF(.NET5.0)Appの作成

続けてWPFアプリケーション(SampleWpf)のプロジェクトを作成します。

  • メニューバーで「ファイル」-「追加」-「新しいプロジェクト」を選択
  • 「新しいプロジェクトの追加」ダイアログでテンプレート「WPF Application」を選択して「次へ」
    img04

  • プロジェクト名を入力して「次へ」

  • 「追加情報」でターゲットフレームワークで「.NET5.0」を選択して「作成」
    img04

SkiaSharpのNuGetパッケージを追加

「ツール」-「NuGetパッケージマネージャ」-「ソリューションのNuGetパッケージの管理」を開き、「SkiaSharp」のパッケージを検索し、プロジェクトに追加します。

  • 「SampleWPF」プロジェクトに「SkiaSharp.Views.WPF」の最新安定バージョンを追加します
  • 「CoreShape.Extensions.SkiaSharp」プロジェクトに「SkisSharp」の最新安定バージョンを追加します。

プロジェクトの参照

下記の通りにプロジェクト間の参照設定を行います。

  • SampleWPF
    → CoreShape、CoreShape.Extensions.SkiaSharp を参照
  • CoreShape.Extensions.SkiaSharp
    → CoreShape を参照

NULL許容コンテキストを有効にする

作成されたcsprojファイルに <Nullable>enable</Nullable> を追加します。
こうすることで参照型もNULL非許容が前提となり、NULL以外の値に初期化しないと警告が出るようになります。(作成した全てのプロジェクトでこの設定を行います)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Null許容参照型 - C#リファレンス

描画面の設定

SampleWPFでMainWindow.xamlを開き、SkiaSharpで図形を描画するためのコントロールSKElementを追加します。

<Window x:Class="SampleWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:skiaSharp="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF"
        xmlns:local="clr-namespace:SampleWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <skiaSharp:SKElement x:Name="skElement" 
                             PaintSurface="sKElement_PaintSurface" />
    </Grid>
</Window>

そしてコードビハインド(MainWindow.xaml.cs)には描画面を更新する際のイベント PaintSurfaceのイベントハンドラを追加しておきます。

private void sKElement_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
    //ここに図形描画の処理を記述します。
}

これで実装前の準備は完了です。

次回

次回は

です。

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

NuGet Source がなぜか消えない

今までDisktopを使っていたが、旅先なので、LapTopを持っていくことにした。環境の移行とかはしたのだが、現地で、VSを使うと、何とリストアがVSで、こける。なんでやねん、、、

image.png

明らかに、先日削除した ServiceFabric のパッケージソースがあって、それが悪さをしてそう。ちなみに、VS のパッケージソースからは削除済みなのにいまだに出てしまう。気を取り直して、先日学んだコマンドラインの方式でやってみる。

dotnet restore
  Determining projects to restore...
C:\Program Files\dotnet\sdk\5.0.101\NuGet.targets(131,5): error : The local source 'C:\Program Files\Microsoft SDKs\Service Fabric\packages' doesn't exist. [C:\Users\tsushi\source\repos\ScaleControllerSpike\ScaleControllerSpike\ScaleControllerSpike.csproj]

同じですな。ではパッケージソースは?

dotnet nuget list source
Registered Sources:
  1.  Local Package Source [Disabled]
      C:\LocalNuGet
  2.  nuget.org [Enabled]
      https://api.nuget.org/v3/index.json
  3.  azure_app_service [Enabled]
      https://www.myget.org/F/azure-appservice/api/v2
  4.  Microsoft Visual Studio Offline Packages [Enabled]
      C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\

まだおるがな。じゃあ、コマンドで削除や。

dotnet nuget remove source "Microsoft Azure Service Fabric SDK"
Package source with Name: Microsoft Azure Service Fabric SDK removed successfully.

うむ。確認。

dotnet nuget list source
Registered Sources:
  1.  Local Package Source [Disabled]
      C:\LocalNuGet
  2.  nuget.org [Enabled]
      https://api.nuget.org/v3/index.json
  3.  azure_app_service [Enabled]
      https://www.myget.org/F/azure-appservice/api/v2
  4.  Microsoft Visual Studio Offline Packages [Enabled]
      C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\
  5.  Microsoft Azure Service Fabric SDK [Enabled]
      C:\Program Files\Microsoft SDKs\Service Fabric\packages

まだおるがな、、、

多分これは、nuget.config のデフォルトの問題と思い調査すると次のスタックオーバーフローがヒット。

スコープ NuGet.Config の場所 説明
Solution カレントフォルダ VSのソリューションのフォルダ
User Windows: %appdata%\NuGet\NuGet.config, Mac/Linux: ~/.config/NuGet/NuGet.Config もしくは ~/.nuget/NuGet/NuGet.Config Solutionのレベルで上書きされる
Computer Windows: %ProgramFiles(x86)%\NuGet\Config, Mac/Linux: $XDG_DATA_HOMEつまり ~/.local/share もしくは /usr/local/share User もしくは Solution レベルで上書きされる

なるほど。だから、ユーザーレベルのを消しても、それが、Computerレベルのものだったら、消えないわけか。

image.png

コンピューターレベルのデフォルトにいたので、ServiceFabricSDK.config を消すことで無事解決。

Resource

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

SQLiteUnityKitの読み込み先をResourcesフォルダからにしてみる

この記事は大阪工業大学 Advent Calendar 2020の18日目の記事です。

はじめに

はじめましての方ははじめまして、それ以外の人はこんにちは。
今年度大学に行かなさ過ぎてただのニートと化してるnihuといいます。
軽く自己紹介でもした方がいいかなと思ったんですけど、僕のTwitterのホーム画見たら大体わk
需要がない気がしたので割愛します。
あと今回のはやってること自体は難しくないですがunityちょっと知らないとわからないかもしれない...申し訳ない...

1.目的

UnityでSQLを扱うのに便利なSQLiteUnityKitというものがあります。
詳しくは他にわかりやすい記事かいっぱいあると思うのでここでは詳しく書きませんが、
これを導入すると簡単にデータベースを扱えるようになるアプリです。

で、このアプリ、データベースファイルを読み込む先がstreamingAssetsからに限定されています。
streamingAssets以外に置くとビルド後に場所が変わってしまったりするから当然と言われれば当然なのですが、
streamingAssetsに置くと作成したアプリがデスクトップ向けのものだとフォルダの中にデータベースファイルがそのまま入っていて
少し知っている人なら簡単に中身を見れるという問題があります。(まだ初心者で知らないだけの場合もあるので隠蔽とかする方法あるなら誰か教えてください泣いて喜びます)
前使ったときは見られても問題ないなーぐらいに思ってたんでスルーしてたんですが、
今後作りたいと考えてるものの中で見られると嫌だなというケースが出てきたので、なんか方法ないかなと思って考えたのがResourcesフォルダから読み込む方法です。
(unity詳しい人ならAssetBundleじゃないのって思うと思うんですけどまだ使ったことないのでResourcesからで大目に見てください...)

とりあえず考えたはいいけど実際できるのかと調べた結果できたのでこれからやった方法を纏めていきます。

2.方法の模索

まずSQLiteUnityKitのSqliteDatabaseのコンストラクタを見てデータベースのファイルをどうやって読み込んでいるか見てみました。

SqliteDatabase.cs
public SqliteDatabase (string dbName){

        pathDB = System.IO.Path.Combine (Application.persistentDataPath, dbName);
        //original path
        string sourcePath = System.IO.Path.Combine (Application.streamingAssetsPath, dbName);

        //if DB does not exist in persistent data folder (folder "Documents" on iOS) or source DB is newer then copy it
        if (!System.IO.File.Exists (pathDB) || (System.IO.File.GetLastWriteTimeUtc(sourcePath) > System.IO.File.GetLastWriteTimeUtc(pathDB))) {

            if (sourcePath.Contains ("://")) {
                // Android  
                WWW www = new WWW (sourcePath);
                // Wait for download to complete - not pretty at all but easy hack for now 
                // and it would not take long since the data is on the local device.
                while (!www.isDone) {;}

                if (String.IsNullOrEmpty(www.error)) {                  
                    System.IO.File.WriteAllBytes(pathDB, www.bytes);
                } else {
                    CanExQuery = false;                                     
                }   

            } else {
                // Mac, Windows, Iphone

                //validate the existens of the DB in the original folder (folder "streamingAssets")
                if (System.IO.File.Exists (sourcePath)) {

                    //copy file - alle systems except Android
                    System.IO.File.Copy (sourcePath, pathDB, true);

                } else {
                    CanExQuery = false;
                    Debug.Log ("ERROR: the file DB named " + dbName + " doesn't exist in the StreamingAssets Folder, please copy it there.");
                }   

            }           

        }
    }

色々書かれてますが要約するとandroid以外はデータベースをPersistentDataPathにコピーしてコピーしたものを参照している仕組みだとわかります。
つまりResourcesフォルダからデータを読み込んで同じところにファイルを生成できればあとは全く同じでも問題ないということになります。
ただResourcesフォルダ内のものはよくわからん形式に圧縮されてるので何かいい方法はないかと調べてると以下のサイトをみつけました。

【Unity】ファイルをバイナリとして扱う方法

これを見るとTextAssetというクラスを使うとResourcesそのままのデータを読み込めるらしいのでつまり

ファイルを読み込む→パス作ってSystem.IO.File.WriteAllBytesで読み込んだバイナリデータをファイルに書き込む→ファイル作れた^q^

という流れを汲めば恐らく上手く動作するはずだと考え、次にSqliteDatabaseの書き換えに移りました。

3.ソースコードの書き換え

ほんとは上の方法で正しくコピーできるかとかテストしたんですが、問題なく動いたのと書く時間がないので割愛して書き換え後のSqliteDatabaseのコンストラクタのコードを記述します。

SqliteDatabase.cs
public SqliteDatabase (string dbName){

        pathDB = System.IO.Path.Combine (Application.persistentDataPath, dbName+".db");
        //original path
        //string sourcePath = System.IO.Path.Combine (Application.streamingAssetsPath, dbName);

        //if DB does not exist in persistent data folder (folder "Documents" on iOS) or source DB is newer then copy it
        if (!System.IO.File.Exists (pathDB) /*|| (System.IO.File.GetLastWriteTimeUtc(sourcePath) > System.IO.File.GetLastWriteTimeUtc(pathDB))*/) {

            TextAsset textAsset=(TextAsset)Resources.Load(dbName);
            System.IO.File.WriteAllBytes(pathDB, textAsset.bytes);

            /*if (sourcePath.Contains ("://")) {
                // Android  
                WWW www = new WWW (sourcePath);
                // Wait for download to complete - not pretty at all but easy hack for now 
                // and it would not take long since the data is on the local device.
                while (!www.isDone) {;}

                if (String.IsNullOrEmpty(www.error)) {                  
                    System.IO.File.WriteAllBytes(pathDB, www.bytes);
                } else {
                    CanExQuery = false;                                     
                }   

            } else {
                // Mac, Windows, Iphone

                //validate the existens of the DB in the original folder (folder "streamingAssets")


                if (System.IO.File.Exists (sourcePath)) {

                    //copy file - alle systems except Android
                    System.IO.File.Copy (sourcePath, pathDB, true);

                } else {
                    CanExQuery = false;
                    Debug.Log ("ERROR: the file DB named " + dbName + " doesn't exist in the StreamingAssets Folder, please copy it there.");
                }   

            }*/     

        }
    }

元々のソースコードでいらない部分はコメントアウトして残しています。色々適当だったり殆ど原形なかったりしますがまあこれで行けると思います(適当)。

そしてテスト用に書いたソースコードはこちらです。

test.cs
void Start()
    {
        string test="1";

        SqliteDatabase sqliteDatabase=new SqliteDatabase("testdata");
        string testQuery=string.Format("select * from test where id = '{0}'",test);
        DataTable dataTable=sqliteDatabase.ExecuteQuery(testQuery);

        string name="";

        foreach (DataRow dr in dataTable.Rows){
            name=(string)dr["name"];
            Debug.Log(name);
        }

    }

あとテスト用に作ったデータベースはこんな感じ
スクリーンショット 2020-12-17 225121.jpg

これで必要なものはそろったのでtest.csを適当なオブジェクト作ってアタッチしてみてテストプレイを実行してみます。
スクリーンショット 2020-12-17 225754.jpg

ヤッターーー!!!
というわけでうまく作動できました。

おわりに

絶対他にいい方法ある気しかしない(震え)
初め調べたとき似たようなの出てこなかったので作るかってなったけど後で調べてたらしっかり見てはないけどバインドして隠蔽云々書かれている記事見つけて「これ...いるんか?」ってなったけど見なかったことにしました()
けど今の自分ができるなかでやってみて一応うまくできたので良かったかなと思います(難しいことはしてないけど)
上にも書きましたけどこんなことしなくてもファイル隠せるよなど詳しい人いたら教えてください...

(おわり)

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

C#でフロントエンド開発ができる「Blazor WebAssembly」を試してみた

はじめに

本記事はBlazor WebAssembly使ってみて得た知見をまとめることを目的としています。

Blazor WebAssemblyとは

BlazorとはC#、Razor(後ほど説明します)、およびHTMLをベースにしたWeb UIフレームワークのことを指します。
Blazor WebAssemblyとは今年の5月にマイクロソフトから正式リリースされたC#と.NET Coreを用いてWebアプリケーションの開発を可能にするフレームワークです。
私のようなフロントエンド言語は苦手だけどC#ならわかる人におすすめです。

開発してみる

Visual StudioでBlazorアプリを選択し、新規プロジェクトを立ち上げます。Blazor WebAssembly Appを選択して作成ボタンを押します。
プロジェクトをビルドして実行すると、ブラウザが勝手に立ち上がり以下のような画面が表示されます。今回は画面の赤枠部分について解説します。
clip-20200716200702.png

Razor構文

上記図の部分はRazorファイルというもので作られています。RazorファイルではHTMLページ内にRazor構文でC#のコードを埋め込んで開発することができます。変数をHTMLコードとして動的に出力したい場合には、変数名の先頭に”@”を付け、@code内でC#でメソッドやプロパティ、フィールド変数を定義することができます。
currentCountという変数とcurrentCountをインクリメントするメソッドIncrementCount()@Code内で定義されています。HTMLページ内にそれらが埋め込まれており、Click meボタンを押すと、メソッドが実行され、currentCountの値が更新されます。

@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
}

さいごに

Javascriptが分からなくてもRazor構文というものを使えばC#でロジックを書くことができるのはとてもお手軽だと思いました。まだ新しいので日本語の情報が少なく開発し辛い場面もありますが、Webでちょっと遊んでみたいという人にはとても良いと思いました。
最後までお読みいただきありがとうございました。

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

Explicit Interface

自分が見ているレポのコードリーディングで次のような構文が出てきて何だろう?と思ったので調べてみた。同じメソッド名でこんな定義が出来るのが知らなかった。調べてみると、Explicit Interface という構文っぽい。

        async Task<ScaleMetrics> IScaleMonitor.GetMetricsAsync()
        {
            return await GetMetricsAsync();
        }

        public Task<KafkaTriggerMetrics> GetMetricsAsync()
                             :

Explicit Interface の構文を使うと、複数のインターフェイスで同じ名前のメソッドがあっても、インターフェイスを明示した方が使われる。つまりこのケースだと、次のサンプルがわかりやすいかもしれない。同じ GetSome() のメソッド。1つは、戻り値が int もう一つの Explicit Interface がついている方の戻りは、string インターフェイスを介してアクセスすると、該当の Interface の メソッドが呼ばれる。

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

            var p = new Program();
            // int GetSome() is for common usage.
            int a = p.GetSome();
            IInterface i = new Program();
            // If you use interface, it will explicit interface. 
            string b = i.GetSome();
        }

        public int GetSome()
        {
            return 1;
        }

        string IInterface.GetSome()
        {
            return "hello";
        }
    }

    interface IInterface
    {
        string GetSome();
    }

一つ実験してみる。インターフェイスに、Hello メソッドを足す。

    interface IInterface
    {
        string GetSome();
        void Hello();
    }

Program に、Hello メソッドを別途定義する

        void Hello()
        {
            Console.WriteLine("Hello from the class.");
        }

        void IInterface.Hello()
        {
            Console.WriteLine("Hello from the interface.");
        }

メインメソッドを 変更して、それぞれ Hello() が呼ばれるようにする。

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            var p = new Program();
            // int GetSome() is for common usage.
            int a = p.GetSome();
            p.Hello();
            IInterface i = new Program();
            // If you use interface, it will explicit interface. 
            string b = i.GetSome();
            i.Hello();
        }

Result

予想通り!

Hello World!
Hello from the class.
Hello from the interface.

リソース

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

【2020年度版】Unityで初学者から中級者までを駆け抜けるためのおすすめ書籍【中学生から大人まで】

はじめに

Unityの書籍紹介は色々あるのですが、初学者へのおすすめ書籍まとめという記事が多い印象です。それ自体はいいのですが、
初学者が、一通り読んで学んだ、その先に何ををすればいいのかというところまで幅広くまとめている記事はあまり見かけないなと思ったので作ります。
Life is Tech ! のCampやSchoolで中高生という初学者と日々コミュニケーションをしているので初学者から中級者がやっぱりメインになりますが、頑張って上級者になるための道のりも引いてみます。

読む上での注意

基本的には、やっと中級にやってきた筆者がこれまで読んできた書籍が中心です。
上級向けとして、ゲームエンジンのアーキテクチャや、ゲームエンジンに頼らない実力が必要と考えている方もいるかもしれませんが、対象から外れていますのであしからず。

また、個人の好みによって同系統の別の方がおすすめ、ということもあるかと思います。
そして著者が読んでいないUnity本も数多あります。
「この場合はこの書籍がおすすめ」ということがあればぜひコメント欄で教えて下さい。
また、書籍画像とリンクはAmazonサイトから主に引用しています。

対象者

  • 初学者:これからUnityを覚えたい!プログラミングを学んでみたい!という人
    • いわゆるプログラミング初心者
  • 中級者:Unityの基本的な動作は覚えられた。ゲームを作ってみた人
    • ゲームは作ってみてからの伸びしろが大きいですよね!
  • 上級者:自分で1つのゲームはすらすら作れるようになって、さらにレベルアップしたい人
    • いわゆるデザイン領域もこのあたりから入ってくるのですが、1人で作ることを今回は想定しています。

初学者から中級者

初学者から中級者までは一直線に駆け抜けていきましょう!

初学者向け(Unityとプログラミングの基本)

いわゆる初心者向けまとめ記事では、この入門教科書を色々紹介してくれていることが多いのですが、
初心者にとって大事なことは、様々な参考書使うより1冊を信じてしっかりやり遂げることだと思っています。
その思いを信じて、今回は思い切って1つのみしか紹介しません!

Unityの教科書 Unity 2020完全対応版

image.png

いわゆる「ねこの本」です。
2Dの内容が多めとなっているが、基本的なUnityの操作からプログラミングまでを覚えることができる。
とくにいいな、この本を紹介したいなと思ったのが、ゲームの作り方を簡潔に説明している点
わかりやすいゲームでのアクションのギミックだけでなく、「監督役、工場」などのわかりやすい単語で、
ゲームを管理する役割までを説明されている点は、自分が人に教えるときに見習いたいと思っています。

中級者向け(1冊を信じてやり遂げた人に贈るもう1冊)

初学者向けよりは詳しく記載されているが、UnityやC#についての基本から一気通貫で網羅している書籍をもう一度やりましょう。
いわゆる「強くてニューゲーム」です。ここでの一冊目は著者がまさにUnityを勉強し直すときに信じてやり遂げた書籍から紹介します。

Unity5 3D/2Dゲーム開発実践入門 Unity2019対応版

image.png

去年辺りまで、Unity5の本だったので紹介しづらかったのですが、2019対応版が発売されたのでこれからは堂々と紹介できます。
ゲームロジックの考え方がわかりやすくて有用だったり、GI(グローバルイルミネーション)など演出として有用な部分も
丁寧に説明されているので、もう一冊基本から初めて、だけど基本だけじゃなく更にゲームを作るための方法を学ぶことができる
素晴らしい書籍だと思っています。

中級者から上級者になるための三種の神器!

さてさて、「それでは上級編!」といきたいところですが、、
Unityは、そしてゲームづくりというものは、とても奥が深いのです。
そのため、もう少しだけ、ゆっくりしっかりとレベルアップをしていきましょう!
それぞれ、下記で紹介する上級向けのジャンルを代表する書籍です。
まずは、この3種類の方向性に進むとといいのかなと思います。

上級者向け

上級編からは、様々なジャンルに分かれて様々な書籍があります。
この記事では、筆者の考えから学びやすいだろうという順番に並べていますが、本質的に順番はないと考えています。
順番を来にせず、興味があるジャンルから学んでいってください。

そして…ここからは遠慮なく物量作戦です。
後半の分量の多さにびっくりさせてしまったかもしれません。
それだけゲームづくりとは複合的で総合的なモノづくりだと思っています。
だからこそ創ることが面白いのです。
それでは上級者への道のりを覗いてみましょう!

ゲームメカニクス

ゲームメカニクスというのは幅広く様々なな意味を持つ単語なのですが、ここでは主にプログラミングやUnityでの操作を多く含む
ゲームの仕組みづくりというものをゲームメカニクスと表しています。

Unityゲーム プログラミング・バイブル

image.png

Unity2018までで、できることを利用した、まさに機能のバイブル。
章ごとに完結しているので、気になった技術から試すことができます。使ったことがない機能を使ってみるという点でもおすすめ。
ただ、Unityは最近の進化が早いので、Unity標準でできることがAssetで紹介されていたりするものもあります(Anima2Dなど)。

Unity デザイナーズバイブル

image.png

プログラミングバイブルが出版されたときの感想は上記の説明に加えて実は、「ProBuilderがあればベストだった。。」と思っていました。
が、まさにそのような声に応えるようなカタチで出版されたプログラミング・バイブルの姉妹編が出版されました!
デザイナーと名前にあるように、プログラマーだけじゃなく幅広い人にUnityの使い方を紹介してくれています。
こちらも素晴らしい書籍なのですが、一点だけ注意点があります。
それは、Amazonnoレビューでもある通り、誤植が多い点です。
ここまでいろんな書籍を通して作品を作っていれば自己解決できるレベルなのですが、再販が行われる際にはブラッシュアップされているとうれしいですね!

ゲームの作り方 改訂版 Unityで覚える遊びのアルゴリズム

image.png

そろそろ、Unityの基本だけではなく、ゲームメカニクスの作り方も気になってきます。
実際ありそうなゲームサンプルにて以下のようなの実現方法を学ぶことができます。

  • タッチ操作
  • 迷路のCSVファイル管理
  • 複数カメラの利用
  • ミスタードリラー的ブロックゲーム
  • シューティングのロックオン管理

サンプル的な意味合いが強いので、考え方を理解して自分で作ってみることをおすすめします!

ゲームデザイン

こちらでは、ゲームメカニクスに比べ、ゲームという仕組みそのものを面白くするための書籍を紹介します。
ゲームデザインという言葉はいろいろな使われ方をしますが、ここではまずはゲームをより面白くするためのゲーム全体の仕組みと考えてみてください。

ゲームデザインバイブル

image.png

これまで、ゲームデザインを学びたいというときに紹介する本は迷っていることが多かったです。
ゼビウスの遠藤さんによるゲームデザイン講義実況中継や、オライリーによるレベルアップのゲームデザインなどを伝えてきましたが、2019年についにこの書籍が紹介されました。
バイブルという名前

ゲームプランとデザインの教科書 ぼくらのゲームの作り方

image.png

この後出てくる、リーダブルコードの部分でも触れているのですが、日本の書籍と海外の書籍は、本というものの作り方が少し違うなぁと感じる時があります。
これは個人的な意見なのですが、日本の書籍の方がよりストーリー的でリスト的で、より具体的な印象を受けます。
ゲームデザインバイブルはシステマチックすぎるなぁと感じた人には、こちらの書籍を読んでみることをおすすめします。
後半にある実際のゲームクリエイターの方々によるインタビューとノウハウ紹介には、刺激を受けると思います。

3Dゲームをおもしろくする技術 実例から解き明かすゲームメカニクス・レベルデザイン・カメラのノウハウ

image.png

こちらの本には、プログラミングは一切出てきません。
ただひたすらにデジタルゲームをゲーム設計としてのメカニクス(格闘ゲームのタイミング設計など)、レベルデザイン、カメラテクニックなどから
分析しています。このような書籍が他にないからこそ、そして実際のゲームが題材だからこそ自分のゲーム体験を省みることにも繋がります。

ゲームメカニクス大全

image.png

ゲームという長い歴史の中で、デジタルゲームが生まれたのはほんの最近に過ぎません。
ゲームというものは、テーブルゲーム、ボードゲームと一緒に発展してきました。
この本では、そのようないわゆるアナログゲームに現れるメカニクスを網羅し分類に挑戦しているという意欲的な一冊です。

おもしろいゲームシナリオの作り方 ―41の人気ゲームに学ぶ企画構成テクニック

image.png

ゲームデザインの中には、もちろんゲームのフィクションというものも含まれます。
この本は、その中でもゲームシナリオだけに注目している本です。
しかし、この中で出てくるヒーローズジャーニーという考え方は確実に学びにつながるのでそこだけでも読む価値があります。
そして、本当に様々な面白いゲームの面白いシナリオを紹介してくれるのであなたのインプットも広げてくれることでしょう。

ゲームデザインについてその他の書籍

まだまだ紹介したい本はたくさんあるぐらい、ゲームデザインとは奥深いのですが一旦ここまで。

  • HALF-REAL
    • ビデオゲームとは何かを考える
  • ゲームの企画書
    • ゲームを作ったクリエイター達は何を考えていたのか
  • 中ヒットに導くゲームデザイン
    • この本の目的は原題にあらわれています。
    • Game Design Workshop: A Playcentric Approach to Creating Innovative Games:革新的ゲームを創るためのプレイ中心アプローチ
      • つまり、ゲーム作りのプロトタイピングについて考えられているものです。

プログラミングとして、C#を学ぶ

プログラミングは、深く知れば知るほど面白い世界です。
一緒にC#という言語と考え方を知っていきましょう。

新・標準プログラマーズライブラリ なるほどなっとく C#入門

image.png

スラスラわかるC#

image.png

前述のなるほどなっとくなどで、クラスやポリモーフィズムなどを理解したら、もう少し深くC#について学んでみましょう。
匿名関数やデリゲートなど、プロパティなど、もう少し深ぼってC#を改めて知ること画できます。

実戦で役立つ C#プログラミングのイディオム/定石&パターン

image.png

上記2冊でC#という言語を学んだら、一歩進んだC#の書き方を身に着けましょう。
ゲーム制作のための書籍ではないのですが、C#らしいコードを学ぶことができます。

UniRx/UniTask完全理解 より高度なUnity C#プログラミング

image.png
上級以上のレベルになってしまうと思われますが、ゲーム制作で非常に便利な考え方はリアクティブプログラミングです。
Unityでは、実質的にUniRXが当てはまります。さらには、C#のTaskをUnity向けに拡張されたUniTaskについても合わせられた、とりすーぷ先生の書籍も紹介されてください。
(筆者もこれから読んで、まずは完全理解したと言えるようにがんばります。)

コーディング一般

ゲームプログラマのためのコーディング技術

image.png

そろそろ、コードが煩雑になってきて管理が難しくなってくる頃。
リーダブルコードももちろんおすすめだけど、こちらのほうが実践的内容から始めるので初学者にはわかりやすいと考えています。
(また、リーダブルコードはTHEアメリカン書籍という体裁なため、普段本を読まない日本系の方だと読みづらいのではないかとも感じています。)

この本では、命名の原則からオブジェクト指向のクラス設計などまで、実践的にプログラミングの設計を学ぶことができます。
特にゲーム制作に役立つ形でのSOLID原則のわかりやすい説明は多くの方に紹介したいです。

プリンシプル オブ プログラミング3年目までに身につけたい一生役立つ101の原理原則

image.png

KISS?DRY?SOLID?YAGNI?疎結合??
などなどプログラミングを調べていると出てくる様々な先人たちの研鑽の結果を手早く知ることができます。
この本にはコードは出てきません。コードを書くプログラマーがどのように考えているのかを学びましょう。

リーダブルコード

image.png
もちろん、リーダブルコードをおすすめさせてください。この本に関しては、様々な方が紹介をしているので、詳しくは調べてみてください。
命名についての項目などとても有益なことばかりが記載されています。

アルゴリズムとデータ構造

image.png

ゲームを作っていると、Webアプリケーションやアプリ開発に比べて早い段階で、数多くのデータやオブジェクトを管理する、アルゴリズムを構築する
必要に迫られると感じています。
(もちろんWebアプリケーションなどでもありますが、ライブラリがとても有用なため中級者あたりまでは意識することが少ないと感じます)
そのため、どこかでしっかりアルゴリズムやデータ構造を学びたいとなった場合はこちらです。
そして、穴掘り法による迷路やA*によるその解法などを実装してみましょう!

デザインパターン

さて、デザインパターンです。ここまで学習を進めていると、デザインパターンという言葉は様々なところで聞いたことがあるはずです。
デザインパターン自体は、建築家アレグザンダーのパターンランゲージという考え方をモノづくりに応用しようとする試みがあり、
そしてGoFによるオブジェクト指向における再利用のためのデザインパターンに繋がります。
(参考:Wikipedia
デザインパターン自体の歴史が長くなってきていて今のゲーム開発に即時に活用できるかというと難しいのですが、生き残る古典というものにはやはり価値があると思います。

増補改訂版 Java言語で学ぶデザインパターン入門

image.png
日本におけるデザインパターンの入門書としてはもはや右に出るものはないかと思います。
非常にわかりやすくデザインパターンを知ることができます。Javaで説明されていますが、基本の部分はC#とはほぼ一緒なので違和感なく読めます。

Game Programming Patterns ソフトウェア開発の問題解決メニュー

image.png

デザインパターンを実際にゲームに応用するためにはという視点ではこちらの書籍を紹介させてください。
この本の前半では、書籍内でよく参照するデザインパターンのおさらいから始まるので、この本から読んでも頑張れば読み進めることができます。

ゲーム数学、物理

ゲームを創る中では数学と物理が非常に多く出てきます。さらには、より高度なことをしたいと考えたらシェーダーというものを実装することもあるでしょう。
そのようなときのために、ゲームで使われる数学、物理、そしてゲームの動かされ方を知っておくことも有用です。

ゲームアプリの数学 Unityで学ぶ基礎からシェーダーまで

image.png
数学や物理の説明を行っている書籍は様々なので、自分が読みやすいと思ったものを読み進めることが重要です。
この書籍では、実際にUnityを使って試すことができるので、手を動かしたいという方におすすめです。

ゲームを動かす技術と発想R

image.png

ゲーム内のための数学だけではなく、Unityを使っているだけならなかなか触れることがない、
ゲームがそもそもどう動いているのかというより低レイヤー名部分までわかりやすく説明してくれています。

Unityシェーダープログラミングの教科書 ShaderLab言語解説編

※こちらのみ、Boothです。
ゲームづくりをしている中で、演出を深めようとしたら現れる、シェーダーというもの。
シェーダーとはなにか、そしてどの様に創ることができるのかを知ることができます。
image.png

ゲーム演出

Unity ゲームエフェクト マスターガイド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153766/d647789c-d6c1-067b-ab19-e56ccef8956e.png

備考:ゲームAI

ゲームAIについても色々と触れられたらいいのですが、まだ著者が挑戦できていないので、2021年にご期待ください。
人工知能について、一般とゲーム関係を1冊づつ。そしてUnityの強化学習についてを紹介するに留めさせてください。

インターネットにもまだまだたくさんの情報が

書籍以外にも様々な情報源があるのが、この令和時代のいいところ!インターネットって素晴らしいですね。
ただ、あまりに情報量が多くなるので、今回は泣く泣く割愛させていただきます。

終わりに

初学者が選ぶべき本は、ここで紹介した書籍である必要はありません。繰り返しますが、
様々な参考書使うより1冊を信じてしっかりやり遂げることが何よりも大事です。
どの書籍も著者の方や編集者の方が考えに考えて構成を考えています。信じて1冊やり遂げてください。

中級者になった方も、もう1ランク上の本を信じてやり遂げましょう。
学び直しは最速のインプットとアウトプットの回転です。考えながらまとめながらやり遂げてください。

そして、上級者になりたいという方は、ゲーム作りの奥深さを受け止めましょう。
どんな旅も一歩一歩歩くことで成し遂げられます。
一緒にゲーム作りという壮大な世界を歩いていきましょう!

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

Unityで初学者から中級者までを駆け抜けるためのおすすめ書籍

はじめに

Unityの書籍紹介は色々あるのですが、初学者へのおすすめ書籍まとめという記事が多い印象です。それ自体はいいのですが、
初学者が、一通り読んで学んだ、その先に何ををすればいいのかというところまで幅広くまとめている記事はあまり見かけないなと思ったので作ります。
Life is Tech ! のCampやSchoolで中高生という初学者と日々コミュニケーションをしているので初学者から中級者がやっぱりメインになりますが、頑張って上級者になるための道のりも引いてみます。

読む上での注意

基本的には、やっと中級にやってきた筆者がこれまで読んできた書籍が中心です。
上級向けとして、ゲームエンジンのアーキテクチャや、ゲームエンジンに頼らない実力が必要と考えている方もいるかもしれませんが、対象から外れていますのであしからず。

また、個人の好みによって同系統の別の方がおすすめ、ということもあるかと思います。
そして著者が読んでいないUnity本も数多あります。
「この場合はこの書籍がおすすめ」ということがあればぜひコメント欄で教えて下さい。
また、書籍画像とリンクはAmazonサイトから主に引用しています。

対象者

  • 初学者:これからUnityを覚えたい!プログラミングを学んでみたい!という人
    • いわゆるプログラミング初心者
  • 中級者:Unityの基本的な動作は覚えられた。ゲームを作ってみた人
    • ゲームは作ってみてからの伸びしろが大きいですよね!
  • 上級者:自分で1つのゲームはすらすら作れるようになって、さらにレベルアップしたい人
    • いわゆるデザイン領域もこのあたりから入ってくるのですが、1人で作ることを今回は想定しています。

初学者から中級者

初学者から中級者までは一直線に駆け抜けていきましょう!

初学者向け(Unityとプログラミングの基本)

いわゆる初心者向けまとめ記事では、この入門教科書を色々紹介してくれていることが多いのですが、
初心者にとって大事なことは、様々な参考書使うより1冊を信じてしっかりやり遂げることだと思っています。
その思いを信じて、今回は思い切って1つのみしか紹介しません!

Unityの教科書 Unity 2020完全対応版

image.png

いわゆる「ねこの本」です。
2Dの内容が多めとなっているが、基本的なUnityの操作からプログラミングまでを覚えることができる。
とくにいいな、この本を紹介したいなと思ったのが、ゲームの作り方を簡潔に説明している点
わかりやすいゲームでのアクションのギミックだけでなく、「監督役、工場」などのわかりやすい単語で、
ゲームを管理する役割までを説明されている点は、自分が人に教えるときに見習いたいと思っています。

中級者向け(1冊を信じてやり遂げた人に贈るもう1冊)

初学者向けよりは詳しく記載されているが、UnityやC#についての基本から一気通貫で網羅している書籍をもう一度やりましょう。
いわゆる「強くてニューゲーム」です。ここでの一冊目は著者がまさにUnityを勉強し直すときに信じてやり遂げた書籍から紹介します。

Unity5 3D/2Dゲーム開発実践入門 Unity2019対応版

image.png

去年辺りまで、Unity5の本だったので紹介しづらかったのですが、2019対応版が発売されたのでこれからは堂々と紹介できます。
ゲームロジックの考え方がわかりやすくて有用だったり、GI(グローバルイルミネーション)など演出として有用な部分も
丁寧に説明されているので、もう一冊基本から初めて、だけど基本だけじゃなく更にゲームを作るための方法を学ぶことができる
素晴らしい書籍だと思っています。

中級者から上級者になるための三種の神器!

さてさて、「それでは上級編!」といきたいところですが、、
Unityは、そしてゲームづくりというものは、とても奥が深いのです。
そのため、もう少しだけ、ゆっくりしっかりとレベルアップをしていきましょう!
それぞれ、下記で紹介する上級向けのジャンルを代表する書籍です。
まずは、この3種類の方向性に進むとといいのかなと思います。

上級者向け

上級編からは、様々なジャンルに分かれて様々な書籍があります。
この記事では、筆者の考えから学びやすいだろうという順番に並べていますが、本質的に順番はないと考えています。
順番を来にせず、興味があるジャンルから学んでいってください。

そして…ここからは遠慮なく物量作戦です。
後半の分量の多さにびっくりさせてしまったかもしれません。
それだけゲームづくりとは複合的で総合的なモノづくりだと思っています。
だからこそ創ることが面白いのです。
それでは上級者への道のりを覗いてみましょう!

ゲームメカニクス

ゲームメカニクスというのは幅広く様々なな意味を持つ単語なのですが、ここでは主にプログラミングやUnityでの操作を多く含む
ゲームの仕組みづくりというものをゲームメカニクスと表しています。

Unityゲーム プログラミング・バイブル

image.png

Unity2018までで、できることを利用した、まさに機能のバイブル。
章ごとに完結しているので、気になった技術から試すことができます。使ったことがない機能を使ってみるという点でもおすすめ。
ただ、Unityは最近の進化が早いので、Unity標準でできることがAssetで紹介されていたりするものもあります(Anima2Dなど)。

Unity デザイナーズバイブル

image.png

プログラミングバイブルが出版されたときの感想は上記の説明に加えて実は、「ProBuilderがあればベストだった。。」と思っていました。
が、まさにそのような声に応えるようなカタチで出版されたプログラミング・バイブルの姉妹編が出版されました!
デザイナーと名前にあるように、プログラマーだけじゃなく幅広い人にUnityの使い方を紹介してくれています。
こちらも素晴らしい書籍なのですが、一点だけ注意点があります。
それは、Amazonnoレビューでもある通り、誤植が多い点です。
ここまでいろんな書籍を通して作品を作っていれば自己解決できるレベルなのですが、再販が行われる際にはブラッシュアップされているとうれしいですね!

ゲームの作り方 改訂版 Unityで覚える遊びのアルゴリズム

image.png

そろそろ、Unityの基本だけではなく、ゲームメカニクスの作り方も気になってきます。
実際ありそうなゲームサンプルにて以下のようなの実現方法を学ぶことができます。

  • タッチ操作
  • 迷路のCSVファイル管理
  • 複数カメラの利用
  • ミスタードリラー的ブロックゲーム
  • シューティングのロックオン管理

サンプル的な意味合いが強いので、考え方を理解して自分で作ってみることをおすすめします!

ゲームデザイン

こちらでは、ゲームメカニクスに比べ、ゲームという仕組みそのものを面白くするための書籍を紹介します。
ゲームデザインという言葉はいろいろな使われ方をしますが、ここではまずはゲームをより面白くするためのゲーム全体の仕組みと考えてみてください。

ゲームデザインバイブル

image.png

これまで、ゲームデザインを学びたいというときに紹介する本は迷っていることが多かったです。
ゼビウスの遠藤さんによるゲームデザイン講義実況中継や、オライリーによるレベルアップのゲームデザインなどを伝えてきましたが、2019年についにこの書籍が紹介されました。
バイブルという名前

ゲームプランとデザインの教科書 ぼくらのゲームの作り方

image.png

この後出てくる、リーダブルコードの部分でも触れているのですが、日本の書籍と海外の書籍は、本というものの作り方が少し違うなぁと感じる時があります。
これは個人的な意見なのですが、日本の書籍の方がよりストーリー的でリスト的で、より具体的な印象を受けます。
ゲームデザインバイブルはシステマチックすぎるなぁと感じた人には、こちらの書籍を読んでみることをおすすめします。
後半にある実際のゲームクリエイターの方々によるインタビューとノウハウ紹介には、刺激を受けると思います。

3Dゲームをおもしろくする技術 実例から解き明かすゲームメカニクス・レベルデザイン・カメラのノウハウ

image.png

こちらの本には、プログラミングは一切出てきません。
ただひたすらにデジタルゲームをゲーム設計としてのメカニクス(格闘ゲームのタイミング設計など)、レベルデザイン、カメラテクニックなどから
分析しています。このような書籍が他にないからこそ、そして実際のゲームが題材だからこそ自分のゲーム体験を省みることにも繋がります。

ゲームメカニクス大全

image.png

ゲームという長い歴史の中で、デジタルゲームが生まれたのはほんの最近に過ぎません。
ゲームというものは、テーブルゲーム、ボードゲームと一緒に発展してきました。
この本では、そのようないわゆるアナログゲームに現れるメカニクスを網羅し分類に挑戦しているという意欲的な一冊です。

おもしろいゲームシナリオの作り方 ―41の人気ゲームに学ぶ企画構成テクニック

image.png

ゲームデザインの中には、もちろんゲームのフィクションというものも含まれます。
この本は、その中でもゲームシナリオだけに注目している本です。
しかし、この中で出てくるヒーローズジャーニーという考え方は確実に学びにつながるのでそこだけでも読む価値があります。
そして、本当に様々な面白いゲームの面白いシナリオを紹介してくれるのであなたのインプットも広げてくれることでしょう。

ゲームデザインについてその他の書籍

まだまだ紹介したい本はたくさんあるぐらい、ゲームデザインとは奥深いのですが一旦ここまで。

  • HALF-REAL
    • ビデオゲームとは何かを考える
  • ゲームの企画書
    • ゲームを作ったクリエイター達は何を考えていたのか
  • 中ヒットに導くゲームデザイン
    • この本の目的は原題にあらわれています。
    • Game Design Workshop: A Playcentric Approach to Creating Innovative Games:革新的ゲームを創るためのプレイ中心アプローチ
      • つまり、ゲーム作りのプロトタイピングについて考えられているものです。

プログラミングとして、C#を学ぶ

プログラミングは、深く知れば知るほど面白い世界です。
一緒にC#という言語と考え方を知っていきましょう。

新・標準プログラマーズライブラリ なるほどなっとく C#入門

image.png

スラスラわかるC#

image.png

前述のなるほどなっとくなどで、クラスやポリモーフィズムなどを理解したら、もう少し深くC#について学んでみましょう。
匿名関数やデリゲートなど、プロパティなど、もう少し深ぼってC#を改めて知ること画できます。

実戦で役立つ C#プログラミングのイディオム/定石&パターン

image.png

上記2冊でC#という言語を学んだら、一歩進んだC#の書き方を身に着けましょう。
ゲーム制作のための書籍ではないのですが、C#らしいコードを学ぶことができます。

UniRx/UniTask完全理解 より高度なUnity C#プログラミング

image.png
上級以上のレベルになってしまうと思われますが、ゲーム制作で非常に便利な考え方はリアクティブプログラミングです。
Unityでは、実質的にUniRXが当てはまります。さらには、C#のTaskをUnity向けに拡張されたUniTaskについても合わせられた、とりすーぷ先生の書籍も紹介されてください。
(筆者もこれから読んで、まずは完全理解したと言えるようにがんばります。)

コーディング一般

ゲームプログラマのためのコーディング技術

image.png

そろそろ、コードが煩雑になってきて管理が難しくなってくる頃。
リーダブルコードももちろんおすすめだけど、こちらのほうが実践的内容から始めるので初学者にはわかりやすいと考えています。
(また、リーダブルコードはTHEアメリカン書籍という体裁なため、普段本を読まない日本系の方だと読みづらいのではないかとも感じています。)

この本では、命名の原則からオブジェクト指向のクラス設計などまで、実践的にプログラミングの設計を学ぶことができます。
特にゲーム制作に役立つ形でのSOLID原則のわかりやすい説明は多くの方に紹介したいです。

プリンシプル オブ プログラミング3年目までに身につけたい一生役立つ101の原理原則

image.png

KISS?DRY?SOLID?YAGNI?疎結合??
などなどプログラミングを調べていると出てくる様々な先人たちの研鑽の結果を手早く知ることができます。
この本にはコードは出てきません。コードを書くプログラマーがどのように考えているのかを学びましょう。

リーダブルコード

image.png
もちろん、リーダブルコードをおすすめさせてください。この本に関しては、様々な方が紹介をしているので、詳しくは調べてみてください。
命名についての項目などとても有益なことばかりが記載されています。

アルゴリズムとデータ構造

image.png

ゲームを作っていると、Webアプリケーションやアプリ開発に比べて早い段階で、数多くのデータやオブジェクトを管理する、アルゴリズムを構築する
必要に迫られると感じています。
(もちろんWebアプリケーションなどでもありますが、ライブラリがとても有用なため中級者あたりまでは意識することが少ないと感じます)
そのため、どこかでしっかりアルゴリズムやデータ構造を学びたいとなった場合はこちらです。
そして、穴掘り法による迷路やA*によるその解法などを実装してみましょう!

デザインパターン

さて、デザインパターンです。ここまで学習を進めていると、デザインパターンという言葉は様々なところで聞いたことがあるはずです。
デザインパターン自体は、建築家アレグザンダーのパターンランゲージという考え方をモノづくりに応用しようとする試みがあり、
そしてGoFによるオブジェクト指向における再利用のためのデザインパターンに繋がります。
(参考:Wikipedia
デザインパターン自体の歴史が長くなってきていて今のゲーム開発に即時に活用できるかというと難しいのですが、生き残る古典というものにはやはり価値があると思います。

増補改訂版 Java言語で学ぶデザインパターン入門

image.png
日本におけるデザインパターンの入門書としてはもはや右に出るものはないかと思います。
非常にわかりやすくデザインパターンを知ることができます。Javaで説明されていますが、基本の部分はC#とはほぼ一緒なので違和感なく読めます。

Game Programming Patterns ソフトウェア開発の問題解決メニュー

image.png

デザインパターンを実際にゲームに応用するためにはという視点ではこちらの書籍を紹介させてください。
この本の前半では、書籍内でよく参照するデザインパターンのおさらいから始まるので、この本から読んでも頑張れば読み進めることができます。

ゲーム数学、物理

ゲームを創る中では数学と物理が非常に多く出てきます。さらには、より高度なことをしたいと考えたらシェーダーというものを実装することもあるでしょう。
そのようなときのために、ゲームで使われる数学、物理、そしてゲームの動かされ方を知っておくことも有用です。

ゲームアプリの数学 Unityで学ぶ基礎からシェーダーまで

image.png
数学や物理の説明を行っている書籍は様々なので、自分が読みやすいと思ったものを読み進めることが重要です。
この書籍では、実際にUnityを使って試すことができるので、手を動かしたいという方におすすめです。

ゲームを動かす技術と発想R

image.png

ゲーム内のための数学だけではなく、Unityを使っているだけならなかなか触れることがない、
ゲームがそもそもどう動いているのかというより低レイヤー名部分までわかりやすく説明してくれています。

Unityシェーダープログラミングの教科書 ShaderLab言語解説編

※こちらのみ、Boothです。
ゲームづくりをしている中で、演出を深めようとしたら現れる、シェーダーというもの。
シェーダーとはなにか、そしてどの様に創ることができるのかを知ることができます。
image.png

ゲーム演出

Unity ゲームエフェクト マスターガイド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153766/d647789c-d6c1-067b-ab19-e56ccef8956e.png

備考:ゲームAI

ゲームAIについても色々と触れられたらいいのですが、まだ著者が挑戦できていないので、2021年にご期待ください。
人工知能について、一般とゲーム関係を1冊づつ。そしてUnityの強化学習についてを紹介するに留めさせてください。

インターネットにもまだまだたくさんの情報が

書籍以外にも様々な情報源があるのが、この令和時代のいいところ!インターネットって素晴らしいですね。
ただ、あまりに情報量が多くなるので、今回は泣く泣く割愛させていただきます。

終わりに

初学者が選ぶべき本は、ここで紹介した書籍である必要はありません。繰り返しますが、
様々な参考書使うより1冊を信じてしっかりやり遂げることが何よりも大事です。
どの書籍も著者の方や編集者の方が考えに考えて構成を考えています。信じて1冊やり遂げてください。

中級者になった方も、もう1ランク上の本を信じてやり遂げましょう。
学び直しは最速のインプットとアウトプットの回転です。考えながらまとめながらやり遂げてください。

そして、上級者になりたいという方は、ゲーム作りの奥深さを受け止めましょう。
どんな旅も一歩一歩歩くことで成し遂げられます。
一緒にゲーム作りという壮大な世界を歩いていきましょう!

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

今PCが手元にないなら絶対に見ないで下さい。記事が優良すぎて、ほぼ100%その場でGraphViewしてしまいます

この記事はUnity #3 Advent Calendar 2020 18日目の記事です

動機

ノベルゲーム最近少なくて寂しいなぁ
データが簡単に作れたらもしかして誰か作るかもしれないなぁ
...せや!シンプルなノードベースのシナリオエディタ作ったろ!

環境

Mac
Unity 2019.4.8.f1

最終的にこんなのを作ります

プロジェクト一式
https://github.com/dwl398/GraphViewSample

どんな人に向けた記事か

・シンプルなノベルシステムを作りたすぎる人

この記事で紹介する内容

・GraphViewの基本的な使い方
・GraphViewで作ったノードの保存、読込(つまづきポイントのみ)

GraphViewの基本的な使い方

1. EditorWindowを作成する

GraphViewを使うためにはまずEditorWindowを用意します

 ScriptGraphView.cs
public class ScriptGraphWindow : EditorWindow
{
    [MenuItem("Tool/ScriptGraph")]
    public static void Open()
    {
        ScriptGraphWindow window = GetWindow<ScriptGraphWindow>();
        window.Show();
    }
}

2. GraphViewを作る

GraphViewを作成します
※UnityEngine.UIElementsに依存した機能が各所に使われているのでusingミスに注意(1敗)

ScriptGraphView.cs
using UnityEngine.UIElements;
using UnityEditor.Experimental.GraphView;

public class ScriptGraphView : GraphView
{
    public ScriptGraphView() : base()
    {
        // 親のサイズに合わせてサイズを設定
        this.StretchToParentSize();
        // ズームインアウト
        SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
        // ドラッグで描画範囲を移動
        this.AddManipulator(new ContentDragger());
        // ドラッグで選択した要素を移動
        this.AddManipulator(new SelectionDragger());
        // ドラッグで範囲選択
        this.AddManipulator(new RectangleSelector());
    }
}
 ScriptGraphWindow.cs
private void OnEnable()
{
    var scriptGraph = new ScriptGraphView();
    this.rootVisualElement.Add(scriptGraph);
}

これでエディタのメニューからTools/ScriptGraphを選んで開いてみます


なんもでません そりゃそうだ
このまま次のノード作成に行く前にちょっとおしゃれにしたいので寄り道します
エディタの背景が殺風景だと殺風景なシナリオしか出てこないんです
無駄じゃないんです

2_a. 背景を付ける

Resourcesに以下のファイルを追加します

GraphViewBackGround.uss
GridBackground {
    --grid-background-color: #282828;
    --line-color: rgba(193,196,192,0.1);
    --tick-line-color: rgba(193,196,192,0.1);
    --spacing: 20
}![Something went wrong]()

ファイルを読み込んで背景を追加します

ScriptGraphView.cs
public class ScriptGraphView : GraphView
{
    public ScriptGraphView() : base()
    {
        // 省略

        // ussファイルを読み込んでスタイルに追加
        this.styleSheets.Add(Resources.Load<StyleSheet>("GraphViewBackGround"));

        // 背景を一番後ろに追加
        this.Insert(0, new GridBackground());
    }
}

グリッドっぽいものが追加されました

作成したussファイルをいじれば即反映されるのでカスタマイズも簡単です

3. Nodeを作る

次は作成したGraphViewに載せるノードを作ります

MessageNode.cs
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.Experimental.GraphView;

public class MessageNode : Node
{
    private TextField textField;

    public MessageNode()
    {
        // ノードのタイトル設定
        this.title = "Message";

        // ポート(後述)を作成
        var inputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(Port));
        inputPort.portName = "In";
        inputContainer.Add(inputPort);

        var outputOort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
        outputOort.portName = "Out";
        outputContainer.Add(outputOort);

        // メイン部分に入力欄追加
        textField = new TextField();
        // 複数行対応
        textField.multiline = true;
        // 日本語入力対応
        textField.RegisterCallback<FocusInEvent>(evt => { Input.imeCompositionMode = IMECompositionMode.On; });
        textField.RegisterCallback<FocusOutEvent>(evt => { Input.imeCompositionMode = IMECompositionMode.Auto; });

        this.mainContainer.Add(textField);
    }
}

これをGraphViewに追加してみます

ScriptGraphView.cs
public ScriptGraphView() : base()
{
    // 省略

    this.Add(new MessageNode());
}

ノードが生成されて配置されました
 

これで全てのノードをプログラムで追加しまくりのハードコーディングしまくりで
色々な賞も受賞しまくりです

4. Nodeをエディタから作れるようにする

もちろん嘘なのでShaderGraphの右クリックで出てくるアレを作ります

ScriptGraphSearchWindowProvider.cs
using System;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

public class ScriptGraphSearchWindowProvider : ScriptableObject, ISearchWindowProvider
{
    private SctiptGraphWindow _window;
    private ScriptGraphView _graphView;

    public void Init(ScriptGraphView graphView,ScriptGraphWindow window)
    {
        _window = window;
        _graphView = graphView;
    }

    public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
    {
        var entries = new List<SearchTreeEntry>();
        entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node")));

        entries.Add(new SearchTreeEntry(new GUIContent(nameof(MessageNode))) { level = 1, userData = typeof(MessageNode)});

        return entries;
    }

    public bool OnSelectEntry(SearchTreeEntry SearchTreeEntry, SearchWindowContext context)
    {
        var type = SearchTreeEntry.userData as Type;
        var node = Activator.CreateInstance(type) as Node;

        // ノードの生成位置をマウスの座標にする
        var worldMousePosition = _window.rootVisualElement.ChangeCoordinatesTo(_window.rootVisualElement.parent, context.screenMousePosition - _window.position.position);
        var localMousePosition = _graphView.contentViewContainer.WorldToLocal(worldMousePosition);

        node.SetPosition(new Rect(localMousePosition, new Vector2(100, 100)));

        _scriptGraphView.AddElement(node);
        return true;
    }
}

これをScriptGraphView側で生成して設定します

ScriptGraphView.cs
public ScriptGraphView(ScriptGraphWindow window) : base()
{
    // 省略

    // 右クリックでノード作成するウィンドウ追加
    var searchWindowProvider = ScriptableObject.CreateInstance<ScriptGraphSearchWindowProvider>();
    searchWindowProvider.Init(this, window);
    this.nodeCreationRequest += context =>
    {
        SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
    };
}

これで右クリックでノード生成できるようになりました

ここまでやって受賞しまくりです

4_a. Nodeを作るたびにメニューに追加するのは辛い

entries.Add(new SearchTreeEntry(new GUIContent(nameof(MessageNode))) { level = 1, userData = typeof(MessageNode)});
何度もこんなコードを書くのは辛いので少し楽にします

ScriptGraphNode.cs
public class ScriptGraphNode : Node
{
}
MessageNode.cs
public class MessageNode : ScriptGraphNode
{
    // 省略
}
ScriptGraphSearchWindowProvider.cs
public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
{
    var entries = new List<SearchTreeEntry>();
    entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node")));

    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
        foreach (var type in assembly.GetTypes())
        {
            if (type.IsClass == false) continue;

            if (type.IsAbstract) continue;

            if (type.IsSubclassOf(typeof(ScriptGraphNode)) == false) continue;

            entries.Add(new SearchTreeEntry(new GUIContent(type.Name)) { level = 1, userData = type });
        }
    }

    return entries;
}

これでScriptGraphNodeを継承したNodeが自動でノード作成メニューに表示されるようになります

5. Nodeを繋ぐ

ノードのついているIn Out のポートを接続します
ポートの接続に関する条件付けができる関数がGraphViewに用意されているので
オーバーライドして条件を記載します

ScriptGraphView.cs
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
    var compatiblePorts = new List<Port>();

    foreach (var port in ports.ToList())
    {
        // 同じノードは繋げない
        if (startPort.node == port.node) continue;

        // Input - Input , Output - Outputは繋げない
        if (startPort.direction == port.direction) continue;

        // ポートタイプが違うものは繋げない
        if (startPort.portType != port.portType) continue;

        compatiblePorts.Add(port);
    }

    return compatiblePorts;
}

ノードが増えてきた頃に重要になりそうです

GraphViewの基本的な機能はここまでになります
なまじノードの拡張性が高いせいで保存、読み込みなどは各自で用意する必要があります

GraphViewで作ったデータの保存、読込

私のつまづきポイントのみの解説となります
冒頭に記載したgithubにコードがあるので詳細はそちらで確認お願いします

手順としては以下になります

1.保存するデータ(ScriptableObject)を用意
2.編集がScriptableObjectに反映されるように
3.ノードのシリアライザ、デシリアライザを用意
4.ProjectウィンドウのScriptableObject選択でEditorWindowが開くように
5.開いた際にノードが生成されるように
6.ノードをつなぐエッジが保存されるように
7.ノードをつなぐエッジが生成されるように

2.編集がScriptableObjectに反映されるように

保存するデータはこんな感じです

ScriptGraphAsset.cs
[CreateAssetMenu(fileName = "scriptgraph.asset", menuName ="ScriptGraph Asset")]
public class ScriptGraphAsset : ScriptableObject
{
    public List<ScriptNodeData> list = new List<ScriptNodeData>();
}
ScriptNodeData.cs
[Serializable]
public class ScriptNodeData
{
    public int id;

    public NodeType type;

    public Rect rect;

    public int[] outIds;

    public byte[] serialData;
}

ノード作成時にスクリプタブルオブジェクトにデータを追加すればとりあえずの保存はできます
罠です

ScriptGraphSearchWindowProvider.cs
public bool OnSelectEntry(SearchTreeEntry SearchTreeEntry, SearchWindowContext context)
{
    // 省略

    node.SetPosition(new Rect(localMousePosition, new Vector2(100, 100)));

    _scriptGraphView.AddElement(node);

    ScriptGraphData data = Serialize(node);

    // ここで追加しよう!
    _scriptGraphAsset.list.Add(data);

    return true;
}

なぜか、これでは正しいpositionが保存されません

_scriptGtaphView.AddElement()と同じフレームでnodeのpositionを取得しようとすると
Rect(0,0,float.Nan,float.Nan)が帰ってきます

対策としてパッケージマネージャのEditorCoroutineなどでノード作成の次のフレームで保存するように調整しましょう

終わりに

かなりざっくりとですが今graphViewを扱う上で最低限の情報を書きました

実際にこのシステムでノベルゲームを作るとなると拡張が必要になりますが
拡張のためのサンプルもあるので(BranchNode)プログラマの人なら割と誰でも拡張ができると思います

ストーリー、グラフィック、サウンド、さまざまな拡張に対応し、いずれノベルゲームが完成する

そんな未来を信じています

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

Maya の MeshSync を少し便利にする

はじめに

これは Maya Advent Calendar 2020 16日目の記事です。

小ネタかつ既知かもしれませんが、MeshSyncの導入や紹介の手引きをする上で気になっていた箇所がありましたのでまとめてみました。文中はかなりUnity色が強いです。ご了承ください。

MeshSync って何?

雑にまとめるとアーティスト作業にフォーカスしたプロセス間通信のUnity版です。アセットのクオリティはイテレーション回数に比例しますが、最近のスマホアプリ製作はUnity側で調整しながらアセット編集を行わずしてクオリティを上げることが出来ない要素が多分に増えたため、MeshSyncのようなリアルタイムプレビュー機能は必須です。

MeshSyncを用いた作業の導線を考えた時、
1. PlayMode中に修正したいオブジェクトを見つける
2. PrefabにあるFBXのパスを調べる
3. Mayaで上記のFBX(ma)を開いて、MeshSync関連コマンドの実行
の3ステップを踏む必要がアセット毎に毎回発生するので、これを1つにまとめたいと考えました。

エディタ拡張スクリプト

MayaのuserSetup.pyのような下準備を不要にしつつ、C#スクリプト1ファイルに機能をまとめてみます。一部ハードコードしている部分がありますので、ご自身の環境に読み替えてください。

ポイントとして以下の条件で処理を分岐させています。

  • Mayaが起動していない
    • コマンドライン引数にMELを渡して起動+更新
  • Mayaが起動している
    • commandportに直接MELを渡して更新

余談として、MeshSyncコンポーネントのプロパティをMayaに送りたいのですが、それらメンバーはinternalです。

アクセスする方法がなくもないのですが、それらを含めるとアドカレ内容の範囲を逸脱してUnityの方に書かないといけなくなりそうなので、ここでは省略します。

using System.Text;
using System.Diagnostics;
using System.Net.Sockets;
using UnityEditor;
using UnityEngine;

public class RemoteControll
{
    public static void Send(dynamic request)
    {
        using (var client = new TcpClient("127.0.0.1", 7001))
        {
            var data = Encoding.UTF8.GetBytes(request);
            using (var stream = client.GetStream())
            {
                stream.Write(data, 0, data.Length);
            }

            client.Dispose();
        }
    }
}

[InitializeOnLoad]
public static class SceneViewCameraTool
{
    static SceneViewCameraTool()
    {
        SceneView.onSceneGUIDelegate += (sceneView) =>
        {
            Handles.BeginGUI();
            using (new GUILayout.VerticalScope(GUI.skin.box, GUILayout.Width(100)))
            {
                GUILayout.Label("Tool");

                if (GUILayout.Button("MeshSync", GUILayout.Width(100)))
                {
                    var app = "/Applications/Autodesk/maya2017/Maya.app/Contents/bin/maya";
                    var port = "if(`commandPort -q \\\":7001\\\"` == false) commandPort -name \\\":7001\\\";";
                    var cmd = "UnityMeshSync_Export;";

                    if (Process.GetProcessesByName("maya").Length > 0)
                    {
                        RemoteControll.Send(
                            cmd +
                            $"viewPlace -p -an true -eye {sceneView.camera.transform.position.x} {sceneView.camera.transform.position.y} {sceneView.camera.transform.position.z} -fov {sceneView.camera.fieldOfView} `lookThru -q`;");
                        return;
                    }

                    var selection = Selection.activeGameObject;
                    if (selection != null)
                    {
                        var meshsync = GameObject.Find("MeshSyncServer");
                        if (meshsync == null)
                        {
                            EditorApplication.ExecuteMenuItem("GameObject/MeshSync/Create Server");
                            Selection.activeGameObject = selection;
                        }

                        var fbx = AssetDatabase.GetAssetPath(
                            PrefabUtility.GetCorrespondingObjectFromOriginalSource(selection));
                        fbx = Application.dataPath.Replace("Assets", "") + fbx;

                        var setup = $"evalDeferred \\\"UnityMeshSync_Settings -p 8080; {cmd}\\\" -lp;";
                        var args = $"-file \"{fbx}\" -hideConsole -nosplash -command \"{port} {setup}\"";

                        var process = new Process
                        {
                            StartInfo =
                            {
                                FileName = app, Arguments = args,
                                WindowStyle = ProcessWindowStyle.Hidden | ProcessWindowStyle.Minimized,
                                CreateNoWindow = true
                            }
                        };
                        process.Start();
                    }
                }
            }

            Handles.EndGUI();
        };
    }
}

普段はデュアルディスプレイで作業されている方も多いと思いますが、キャプチャの都合上1画面で収録しました。SceneView上での拡張に関しては完全に個人的な好みです。

ezgif.com-gif-maker.gif

補足事項

UnityMeshSync_~ コマンドはpluginがロードされた後になるので、evalDeferredで遅延実行します。

viewPlaceコマンドはUnity側の画角を再現してみるために、おまけ程度に使ってみました。

Prefabから直接FBXのパスを参照したりしていますが、実際のところ各社各プロジェクトによって対象を書き換える必要があるかと思います。

おわりに

内製エンジン環境だと(気の利く社内の開発者が作り込んでくれているので)あまり気にしていなかったことも、商用エンジンはそういう痒いところを自分で作って便利にするか、札束で叩いたアセットで解決するしかないです。

細かい処理でもめんどくさがらずにやっておくと、時間短縮になって後が楽になりますね。


TODO

  1. Cameraの同期
  2. Material参照
  3. Component付替
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでプラットフォームごとにUIの判定エリアを変える

この記事はクラスター Advent Calendar 2020の16日目です。前回はulaphさんによる「SwiftでViewの状態をenumで管理する」でした。C#からSwiftへ軽やかに転身していてカッコいい。

はじめに

こんにちは。クラスター株式会社でデザイナーとして主にUIデザインをやっています。

バーチャルSNS cluster(クラスター)

Unityでマルチプラットフォームに対応したUIを作る場合、同じにできる機構はなるべく共通になるよう努めます。とは言え、無闇矢鱈にすべてを共通化するわけではありませんし、プラットフォームごとの対応が簡単に出来るものは対処しておきたいです。マウスイベントやタップ判定などを行う判定のエリアもその1つです。

プラットフォームごとの判定エリアのサイズ

一般的にマウスカーソルデバイス……ここではPCですが、これのホバーやクリックを判定するエリアのサイズは、UIの見た目と一致していることがほとんどです。

一方スマートフォンでは操作する指よりもUIが小さいため、判定エリアのサイズは見た目以上のサイズになっているのが一般的です。例えばiOSのソフトウェアキーボードは入力する内容を予測して動的に判定エリアのサイズが変化しています。すごい。

参考: ソシオメディア | iPhone の当たり判定を検証した

入力を予測して……はさすがに困難を極めるので、プラットフォームごとでオン/オフする程度の簡単な対処を考えてみました。

Unity UIでボタンを組む

Buttonコンポーネントがアタッチされたオブジェクト以下に背景や画像などのゲームオブジェクトが設置されたヒエラルキーの図

自分がUnityでよく組むタイプのボタンです。Unityのプリセットのようにコンポーネントを1つのオブジェクトにまとめ過ぎず、ビジュアルの責任も適度に分けたほうがデザインの柔軟性があり、かつメンテナンス性も高いです。

ボタンの判定エリアを広げる

判定エリアが広がっていることがわかる実行時のGIFアニメーション

操作時にフィードバックを返すTargetGraphic以下に、Imageコンポーネントをアタッチして、アルファ値を0にしたTargetAreaというゲームオブジェクトを配置しています。このオブジェクトのサイズをボタンよりも広げ、Raycast Targetをオンにすることで判定エリアを広げています。

PCプラットフォームでTargetAreaを削除する

プラットフォームを判別する方法を調べると2つ出てきました。

  1. プラットフォーム依存コンパイル - Unity マニュアル
  2. RuntimePlatform - Unity スクリプトリファレンス

2のRuntimePlatformで以下のよう書かれています。

プラットフォーム別コンパイルを使用するほうが、実行時にチェックする必要がないため軽くて高速なコードを生成できます。

使用しているデバイスが途中で変わる……などということは起きないので、スマートフォン向けに追加したTargetAreaをPCの場合はプラットフォーム依存コンパイルを使って削除するのが簡単そうです。

Unityが用意してくれているUNITY_STANDALONEを使います。囲んだコードが、すべてのPCプラットフォームでコンパイルに含まれます。

DestroysSelfWhenStandalone.cs
using UnityEngine;

public class DestroysSelfWhenStandalone: MonoBehaviour
{
    void Start()
    {
#if UNITY_STANDALONE
        Destroy(this.gameObject);
#endif
    }
}

プラットフォームを切り替えて実行してみる

PCプラットフォーム実行時のみ判定エリアを広げていたゲームオブジェクトが削除されている様子のGIFアニメーション

PCプラットフォームに切り替えて実行した時のみTargetAreaが削除されました。

まとめ

共通化はUnityの大きな利点の1つではありますが、プラットフォームに向けたこういうちょっとした最適化は今後も積み重ねていきたいものです。

心残りが1点。そもそも対象のゲームオブジェクトを環境ごとでビルドに含めない方法を取れれば良かったなと思います(スマートフォン側には虚無のスクリプトが残ることに)。自分で調べた範囲ではその方法は見つけられませんでした。残念。

次回は2tatuki4さんによる「ワールド制作でメモリが大変なことになった時に見るやつを書く」です。お楽しみに!

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