- 投稿日:2019-08-21T23:05:19+09:00
[DesktopBridge] 非UWP(WPF等)をパッケージする方法のまとめ
やりたいこと
WPFアプリも、UWPアプリと同じようにappxなどのパッケージにして、ストアに登録したりできるらしい。
が、なんだかそのやり方が複数あって、しかも今はもう推奨しないやり方とかもあるらしく、どれがどういうやり方なのか、ちょっとネット見たくらいではわかりづらい。勉強がてら、どんな方法があるのか、どういう特徴があるのかまとめたい。
非UWPアプリをパッケージする方法
非UWPのアプリをappxやmsixにパッケージすることを「デスクトップブリッジ」という。
DACはWindows 10, version 1607以降をサポートしており、
Windows 10 Anniversary Update (10.0; Build 14393)以降をターゲットにしたプロジェクトでのみ使用できる。で、その非UWPアプリ(WinformやWPFなど)をパッケージする方法は、下記のようなものがあるらしい。
〇DAC(Desktop App Converter)
Desktop App Converterというアプリ、もしくはコマンドラインの場合、DesktopAppConverter.exeを使ってappxを作るやり方。
19/08/21現在、MSの推奨外となっていた。
https://docs.microsoft.com/ja-jp/windows/msix/desktop/desktop-to-uwp-run-desktop-app-converter〇Windows Application Packaging Projectプロジェクト
Windows Application Packaging Projectプロジェクトを使用して、デスクトップアプリのパッケージを生成する方法。
https://docs.microsoft.com/ja-jp/windows/msix/desktop/desktop-to-uwp-packaging-dot-net〇MakeAppx
MakeAppx.exeを使い、appxmanifest.xmlを作成してappxまたはmsixをつくるやり方。
Visual StudioやMSIX Packaging Toolなどのツールを使用せずにUWP/非UWPのアプリケーションをパッケージするためのやり方。
MakeAppxは、非UWPアプリをパッケージするためだけのものではなく、UWPアプリのappxやmsixからバンドルファイルを作ったりもする。
https://docs.microsoft.com/ja-jp/windows/msix/desktop/desktop-to-uwp-manual-conversion#make-appx
https://docs.microsoft.com/ja-jp/windows/msix/package/create-app-package-with-makeappx-tool〇MSIXパッケージツール
MSIXパッケージツール、もしくはコマンドラインの場合、MsixPackagingTool.exeを使ってmsixを作るやり方。(appxは作れないっぽい)
Windows 10, version 1809以降が必要。
概要:https://docs.microsoft.com/ja-jp/windows/msix/package/manual-packaging-root
ツール:https://docs.microsoft.com/ja-jp/windows/msix/packaging-tool/create-app-package-msi-vm
CLI:https://docs.microsoft.com/ja-jp/windows/msix/packaging-tool/package-conversion-cliその他
Windows Application Packaging Projectプロジェクトについてはこちらの記事でも、少し調べた。参照。
あと、ここ読んだ方がよさそう
パッケージの仕組み
https://docs.microsoft.com/ja-jp/windows/msix/desktop/desktop-to-uwp-behind-the-scenesおかしなことを言っていたら、ご指摘頂けたら幸いです。
- 投稿日:2019-08-21T18:33:02+09:00
【C#】C#言語仕様を翻訳してみた Part1
経緯
公式ドキュメントを読むことが苦手なため、克服するためにC#の言語仕様を読み始めた。
しかし、機械翻訳があまり読みやすいものでなかったため、英語で読みながら翻訳して読むことにした。翻訳の方針
もともと書かれている機械翻訳より分かりやすくなるように翻訳することを目標とする。
ただし、機械翻訳でも読めそうな部分はそのまま使用する。
また、Google翻訳などの翻訳アプリもフルに活用することとする。
間違いやもっと良い翻訳など指摘があれば、コメントを頂ければ幸いだ。この記事の翻訳対象
セクションごとに記事を分けて書くこととする。
この記事ではIntroductionを翻訳する。翻訳文
はじめに
C#(読み方は"シーシャープ")はシンプル、モダン、オブジェクト指向かつタイプセーフなプログラミング言語です。
C#はC言語系がルーツであり、C、C++、Javaのプログラマーにすぐ馴染みます。
C#はECMA InternationでECMA-334として、ISO/IECでISO/IEC 23270として標準化されています。
.NET Framework用のMicrosoftのC#コンパイラはこれら両方の標準に準拠した実装です。C#はオブジェクト指向言語ですが、さらにコンポーネント指向のプログラミングのサポートも含まれています。
最近のソフトウェア設計では、機能を自己完結型および自己記述型パッケージの形式にしたソフトウェアコンポーネントにますます依存するようになっています。
そのようなコンポーネントの鍵となるのは、プロパティ、メソッド、イベントを使用してプログラミングモデルを表すこと、コンポーネントについての宣言的な情報を提供する属性があること、独自のドキュメントが組み込まれていることです。
C# はこれらの概念を直接サポートする言語構造を提供しているので、非常に自然にソフトウェアコンポーネントを作成して使用することができます。いくつかのC#の機能は堅牢で耐久性のあるアプリケーションの構築を支援します。
ガベージコレクションは使用していないオブジェクトによって占領されたメモリを自動的に再利用します。
例外処理は、エラーの検出と回復に構造化された拡張可能なアプローチを提供します。
タイプセーフな設計により、初期化されていない変数の読み込み、配列の境界を超えたアクセスまたは型チェックされていない型へのキャストはできません。C#は統一された型システムを持っています。
C#の型はint
やdouble
のようなプリミティブな型を含め、すべてobject
型から派生しています。
そのため、すべての型は共通の操作を共有し、任意の型の値を一貫した方法で保存、転送、および操作できます。
さらに、C#はユーザ定義の参照型、値型の両方をサポートしており、オブジェクトを動的に割り当てることも、軽量の構造体をインラインで格納することもできます。C#のプログラムとライブラリが互換性を保ちながら進化できることを保証するために、バージョン管理に重点が置かれています。
多くのプログラミング言語がこの問題を軽視しており、その結果、それらの言語で書かれたプログラムは依存しているライブラリの最新バージョンが導入されると必要以上に壊れます。
バージョン管理の考慮に影響を受けたC#の設計の側面として、virtual
とoverride
修飾子が分かれている、メソッドのオーバーロードの解決ルール、明示的なインターフェースメンバの宣言のサポートがあります。この章の残りの部分では、C#の重要な機能を説明します。
後の章はルールと例外を詳細に、ときどき数学的な方法で説明しますが、この章では完璧さを犠牲にして、明確さと簡潔さを追求しています。
目的は最初のプログラムの作成方法と後の章の読みを促進するための言語の導入部を読み手に提供することです。次回
Hello Worldを翻訳予定。
それではまた。
TomoProg
- 投稿日:2019-08-21T15:11:53+09:00
Autodesk Inventor API Hacking (自作Control開発の注意点)
0. はじめに
自作のWinFormsなControlを使おうとして、ハマることがあるよ、という記事です。
具体的なトラブルとして、VisualStudioのDesignerで自作Controlを配置できなくなる現象に遭遇することがあります。
そもそも、どういった場面で自作Controlを作る必要に迫られるかというと、例えば、以前の記事で書いたとおり、Dockable WindowでEnter
,ESC
キー入力を受け取るには、標準のControlを継承して自作Controlを作る必要があります。1. 失敗 その1
これは有名ですが、自作Controlは対象プラットフォームがx64だとDesignerで表示されません。
なぜならば、VisualStudio自体はx86
アプリケーションなので、x64
向けにCompileされたAssemblyをloadできないからです。
回避するには、対象プラットフォームを一時的にx64
からAnyCPU
に変更します。
DebugだけAnyCPU
にして、Releaseはx64
のままでも良いかもしれませんね。
AnyCPU
でrebuildしてもDesigner画面のエラーが解消されなければ、Project内のbin, obj directoryを削除するとか、VisualStudioを再起動するとか、試してみてください。2. 失敗 その2
Assemblyに対してsignしていると、Designerで読み込まないことがあるようです。
私の場合は、AssemblyInfo.cs
で遅延署名の指定をし、プロジェクト
→プロパティ
→ビルドイベント
→ビルド後イベントのコマンドライン
にて、sn.exe
で署名しています。
AnyCPU
でrebuildしてもダメな場合は、一時的に署名の設定を解除してみてください。99. 親の記事に戻る
- 投稿日:2019-08-21T14:41:50+09:00
Unity IAPを試してみた (Yet Another Purchaser.cs)
前提
- Unity 2018.4.5f1
- Unity IAP 1.22.0
- Apple App Store、Google Play Store
- この記事では、Unity IAPの一部機能を限定的に使用し、汎用性のない部分があります。
- サーバレス、消費/非消費タイプ使用、購読タイプ未使用
- この記事のソースは、実際のストアでテストしていますが、製品版での使用実績はありません。
- ソース中のIDは実際と異なります。
公式ドキュメント
- マニュアル
- スクリプトリファレンス
- 2019/08/02時点で、バージョンによって記述があったりなかったりします。
- 記述あり 5.6、2017.1、2017.2、2017.3、2018.1、2018.2
- 記述なし 2017.4、2018.3、2018.4、2019.1
ネームスペース
UnityEngine.Purchasing
- 必須のネームスペースです。
UnityEngine.Purchasing.Security
- レシートの検証で必要なネームスペースです。
- スクリプトリファレンスに記述が見つかりません。
UnityEngine.Purchasing.Extension
- この記事では扱いません。
初期化
初期化の開始
UnityPurchasing.Initialize ()
を呼ぶことで、初期化を開始します。UnityPurchasing.Initialize (Purchaser instance, ConfigurationBuilder builder);
- 初期化の要求はブロックされず、後に、結果に応じたコールバックがあります。
イベントハンドラ
- コールバックを受け取るために、
IStoreListener
を継承したクラスのインスタンスが必要です。
- 必ずしも
MonoBehaviour
を継承する必要はありません。- インターフェイス
IStoreListener
では、OnInitialized ()
、OnInitializeFailed ()
、OnPurchaseFailed ()
、ProcessPurchase ()
の4つのイベントハンドラが必要になります。準備
Initialize ()
を呼ぶためには、ConfigurationBuilder builder
のインスタンスを得る必要があります。ConfigurationBuilder.Instance ()
を呼ぶためには、IPurchasingModule
を継承したクラスのインスタンスが必要なようですが、この辺りを記載したドキュメントが見つかりません。- 付属のDemoでは、
StandardPurchasingModule
がそのクラスにあたるようで、そのインスタンスを得て使われています。- 得られたインスタンス
module
にストアの設定を行い、さらにbuilder
インスタンスを得ます。- 得られたインスタンス
builder
に製品を登録し、検証器を生成して、初期化を開始します。- ここでは、クラスのコンストラクタで、準備から初期化の開始までを行っています。
- コンストラクタが
private
なのは、シングルトンで使うためです。Purchaser.cs/// <summary>コンストラクタ</summary> private Purchaser (IEnumerable<ProductDefinition> products) { Debug.Log ("Purchaser.Construct"); var module = StandardPurchasingModule.Instance (); module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser; isGooglePlayStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay; isAppleAppStoreSelected = Application.platform == RuntimePlatform.IPhonePlayer && module.appStore == AppStore.AppleAppStore; validator = new CrossPlatformValidator (GooglePlayTangle.Data (), AppleTangle.Data (), Application.identifier); var builder = ConfigurationBuilder.Instance (module); builder.AddProducts (products); UnityPurchasing.Initialize (this, builder); }製品定義
- 先のコンストラクタが受け取って
builder
に登録した製品定義は、製品のIDとタイプのセットです。Sample.csvar products = new [] { new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item1", ProductType.Consumable), new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item2", ProductType.NonConsumable), new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item3", ProductType.NonConsumable), };
- これらはストア・ダッシュボードでの設定と正しく呼応している必要があります。
- Apple App Storeでは、IDと製品タイプの双方が設定されます。
- Google Play Storeでは、IDが設定されますが、消費の有無についての設定はありません。
- 消費タイプでは、アプリを消費したことを申告するだけです。
- この記事では、消費タイプ
Consumable
と非消費タイプNonConsumable
だけを扱い、購読タイプは扱いません。初期化の完了
- 初期化に成功したら、得られた
IStoreController
とIExtensionProvider
を保存します。Purchaser.cs/// <summary>初期化完了</summary> public void OnInitialized (IStoreController controller, IExtensionProvider extensions) { Debug.Log ($"Purchaser.Initialized {controller}, {extensions}"); appleExtensions = extensions.GetExtension<IAppleExtensions> (); appleExtensions.RegisterPurchaseDeferredListener (OnDeferred); googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions> (); this.controller = controller; this.extensions = extensions; Inventory = new Inventory { }; foreach (var product in controller.products.all) { Inventory [product] = possession (product); } } /// <summary>初期化失敗</summary> public void OnInitializeFailed (InitializationFailureReason error) { Debug.LogError ($"Purchaser.InitializeFailed {error}"); Unavailable = true; }
- iOSの'Ask to buy'関連、
OnDeferred
はテストできていません。Inventory
については、後述します。製品目録
- 初期化に成功すると、
controller.products.all
で、製品目録を得ることができます。Sample.csforeach (var product in Purchaser.Products.all) { Debug.Log (Purchaser.GetProductProperties (product)); }Purchaser.cs/// <summary>製品目録 初期化時の製品IDに対してストアから得た情報</summary> public static ProductCollection Products => Valid ? instance.controller.products : null;Purchaser.cs/// <summary>製品諸元</summary> public static string GetProperties (this Product product) { return string.Join ("\n", new [] { $"id={product.definition.id} ({product.definition.storeSpecificId})", $"type={product.definition.type}", $"enabled={product.definition.enabled}", $"available={product.availableToPurchase}", $"localizedTitle={product.metadata.localizedTitle}({product.metadata.shortTitle ()})", $"localizedDescription={product.metadata.localizedDescription}", $"isoCurrencyCode={product.metadata.isoCurrencyCode}", $"localizedPrice={product.metadata.localizedPrice}", $"localizedPriceString={product.metadata.localizedPriceString}", $"transactionID={product.transactionID}", $"Receipt has={product.hasReceipt}", $"Purchaser.Valid={Purchaser.Valid}", $"Receipt validation={Purchaser.ValidateReceipt (product)}", $"Possession={Purchaser.Inventory [product]}", }); }目録の謎
※以下は、Google Play StoreとApple App Store (Sandbox)で確認した内容です。製品でのテストではありません。
- もし、初期化の際に製品定義を渡さなかったらどうなるのでしょうか?
- その場合、製品目録は基本的に空になります。ただし、購入済みの非消費製品は取得されます。
- ストアから製品IDのカタログが得られるわけではありません。つまり、ストアに新製品を登録しただけでは、製品に組み込めないのです。
ProductDefinition.enabled
は、スクリプトリファレンスでは"This flag indicates whether a product should be offered for sale. It is controlled through the cloud catalog dashboard."と説明されています。
- これを見る限り、ストアのダッシュボードで設定されている有効/無効状態を取得できるように読めますが、実際には常に
true
になります。- 例え、ストアに登録されていないIDを指定した場合でも
true
です。全く役に立ちません。
- ストアにない場合は、
Product.availableToPurchase
はFalse
になります。- Play Storeで無効にされている製品を購入しようとすると「原因不明の購入エラー」になります。
- App Storeで無効にされている製品でも、Sandboxでは購入できてしまいます。
- つまり、以下の制約が生じます。
- ストアに登録されている未知の製品を取得することはできません。
- ストアでの状態(有効/無効)を取得することはできません。
- 購入の失敗が、ストアでの無効設定によるものと判別できません。
- その結果、以下のような使い方になります。
- ストアとは別の手段(あらかじめ組み込む、自前のサーバから取得するなど)で製品定義を保持する必要があります。
- ストアでの製品の有効/無効は、アプリの使用する製品定義に連動させます。
- 緊急時以外は、ストア独自に製品を無効化しないようにします。
購入
購入の開始
IStoreController.InitiatePurchase ()
にProduct
を渡すことで、購入が開始されます。Purchaser.cs/// <summary>課金開始</summary> private bool purchase (Product product) { if (product != null && product.Valid ()) { Debug.Log ($"Purchaser.InitiatePurchase {product.definition.id} {product.metadata.localizedTitle} {product.metadata.localizedPriceString}"); controller.InitiatePurchase (product); return true; } return false; }
- 購入の要求はブロックされず、後に、結果に応じたコールバックがあります。
購入の完了
- 課金結果のコールバックでは、購入に関わる処理が全て完了したら、
PurchaseProcessingResult.Complete
を返します。
- 消費タイプの場合は、消費が実行されます。
Purchaser.cs/// <summary>課金失敗</summary> public void OnPurchaseFailed (Product product, PurchaseFailureReason reason) { Debug.LogError ($"Purchaser.PurchaseFailed Reason={reason}\n{product.GetProperties ()}"); } /// <summary>課金結果 有効な消耗品なら保留、それ以外は完了とする</summary> public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs eventArgs) { var validated = ValidateReceipt (eventArgs.purchasedProduct); Debug.Log ($"Purchaser.ProcessPurchase {(validated ? "Validated" : "ValidationError")} {eventArgs.purchasedProduct.GetProperties ()}"); Inventory [eventArgs.purchasedProduct] = validated; return (validated && eventArgs.purchasedProduct.definition.type == ProductType.Consumable) ? PurchaseProcessingResult.Pending : PurchaseProcessingResult.Complete; }
- このコードでは、消費タイプでは
PurchaseProcessingResult.Pending
を返し、それ以外ではComplete
を返します。
Pending
を返すと、消費は保留されます。この保留状態は、(謎の)クラウドで保持されるためアプリが中断しても失われず、起動毎に
ProcessPurchase ()
へのコールバックが繰り返されます。
- 保留状態を終わらせるには、
ProcessPurchase ()
でComplete
を返すか、別途IStoreController.ConfirmPendingPurchase (product)
を呼びます。
Product.hasReceipt
は、起動直後に未購入または消費済みであればfalse
となり、購入完了時にはtrue
に変化します。
- しかし、
Complete
を返した場合も、消費を促すConfirmPendingPurchase (product)
を行おうとも、その場ではfalse
には戻りません。- つまり、
hasReceipt
を見て消費完了を知ることはできません。- また、
ConfirmPendingPurchase (product)
には、結果を知らせるコールバックがありません。従って、保留と消費の状態を判別するためには、Unity-IAPの外側で所持状態を管理する必要があります。
なお、非消費タイプでは、購入済みの
hasReceipt
は常にtrue
になります。所有状態の管理
- このコードでは、
Inventory
というDictionary
派生クラスを用意して、製品所有状態を管理しています。
- 初期化完了のコールバック中で初期化しています。
Inventory [string 製品ID]
またはInventory [Product 製品]
で真偽値を得ることができます。Purchaser.cs/// <summary>productID基準でProductの在庫を表現する辞書</summary> public class Inventory : Dictionary<string, bool> { /// <summary>Productによるアクセス</summary> public bool this [Product product] { get { return base [product.definition.id]; } set { base [product.definition.id] = value; } } }復元
- Appleの既定では、ユーザーがこの処理を明示的に行える必要があるのですが、これを呼ばなくてもUnity-IAPが自動的に復元をしているようなので、それ以上の意味はないように思われます。正直よく分かりません。
Purchaser.cs/// <summary>復元</summary> private void restore (Action<bool> onRestored = null) { Debug.Log ("Purchaser.Restore"); Action<bool> onTransactionsRestored = success => { OnTransactionsRestored (success); onRestored?.Invoke (success); }; if (isGooglePlayStoreSelected) { googlePlayStoreExtensions.RestoreTransactions (onTransactionsRestored); } else if (isAppleAppStoreSelected) { appleExtensions.RestoreTransactions (onTransactionsRestored); } }Purchaser.cs/// <summary>復元完了</summary> private void OnTransactionsRestored (bool success) { Debug.Log ($"Purchaser.Restored {success}"); }コード全容
Purchaser.cs// Copyright© tetr4lab. using System; using System.Text.RegularExpressions; using System.Collections.Generic; using UnityEngine; using UnityEngine.Purchasing; using UnityEngine.Purchasing.Security; /// <summary>UnityIAPを使う</summary> namespace UnityInAppPuchaser { /// <summary>課金処理</summary> public class Purchaser : IStoreListener { #region Static /// <summary>シングルトン</summary> private static Purchaser instance; /// <summary>在庫目録 製品の課金状況一覧、消費タイプは未消費を表す</summary> public static Inventory Inventory { get; private set; } /// <summary>有効 初期化が完了している</summary> public static bool Valid => (instance != null && instance.valid); /// <summary>使用不能 初期化に失敗した</summary> public static bool Unavailable { get; private set; } /// <summary>製品目録 初期化時の製品IDに対してストアから得た情報</summary> public static ProductCollection Products => Valid ? instance.controller.products : null; /// <summary>クラス初期化 製品のIDとタイプの一覧を渡す</summary> public static void Init (IEnumerable<ProductDefinition> products) { if (instance == null || Unavailable) { instance = new Purchaser (products); } } /// <summary>所有検証 有効なレシートが存在する</summary> private static bool possession (Product product) { return product.hasReceipt && Purchaser.ValidateReceipt (product); } /// <summary>レシート検証</summary> public static bool ValidateReceipt (string productID) { return (!string.IsNullOrEmpty (productID) && instance.validateReceipt (instance.controller.products.WithID (productID))); } /// <summary>レシート検証</summary> public static bool ValidateReceipt (Product product) { return (instance != null && instance.validateReceipt (product)); } /// <summary>課金 指定製品の課金処理を開始する</summary> public static bool Purchase (string productID) { if (!string.IsNullOrEmpty (productID) && Valid) { return instance.purchase (instance.controller.products.WithID (productID)); } return false; } /// <summary>課金 指定製品の課金処理を開始する</summary> public static bool Purchase (Product product) { if (product != null && Valid) { return instance.purchase (product); } return false; } /// <summary>保留した課金の完了 消費タイプの指定製品の保留していた消費を完了する</summary> public static bool ConfirmPendingPurchase (string productID) { if (!string.IsNullOrEmpty (productID) && Valid) { return instance.confirmPendingPurchase (instance.controller.products.WithID (productID)); } return false; } /// <summary>保留した課金の完了 消費タイプの指定製品の保留していた消費を完了する</summary> public static bool ConfirmPendingPurchase (Product product) { if (product != null && Valid) { return instance.confirmPendingPurchase (product); } return false; } /// <summary>復元 課金情報の復元を行い、結果のコールバックを得ることができる</summary> public static void Restore (Action<bool> onRestored = null) { if (Valid) { instance.restore (onRestored); } } #endregion /// <summary>コントローラー</summary> private IStoreController controller; /// <summary>拡張プロバイダ</summary> private IExtensionProvider extensions; /// <summary>Apple拡張</summary> private IAppleExtensions appleExtensions; /// <summary>Google拡張</summary> private IGooglePlayStoreExtensions googlePlayStoreExtensions; /// <summary>AppleAppStore</summary> private bool isAppleAppStoreSelected; /// <summary>GooglePlayStore</summary> private bool isGooglePlayStoreSelected; /// <summary>検証機構</summary> private CrossPlatformValidator validator; /// <summary>有効</summary> private bool valid => (controller != null && controller.products != null); /// <summary>コンストラクタ</summary> private Purchaser (IEnumerable<ProductDefinition> products) { Debug.Log ("Purchaser.Construct"); var module = StandardPurchasingModule.Instance (); module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser; isGooglePlayStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay; isAppleAppStoreSelected = Application.platform == RuntimePlatform.IPhonePlayer && module.appStore == AppStore.AppleAppStore; validator = new CrossPlatformValidator (GooglePlayTangle.Data (), AppleTangle.Data (), Application.identifier); var builder = ConfigurationBuilder.Instance (module); builder.AddProducts (products); UnityPurchasing.Initialize (this, builder); } /// <summary>レシート検証</summary> private bool validateReceipt (Product product) { if (!valid || !product.hasReceipt) { return false; } #if UNITY_EDITOR return true; #else try { var result = validator.Validate (product.receipt); Debug.Log ("Purchaser.validateReceipt Receipt is valid. Contents:"); return true; } catch (IAPSecurityException ex) { Debug.LogError ($"Purchaser.validateReceipt Invalid receipt {product.definition.id}, not unlocking content. {ex}"); return false; } #endif } /// <summary>課金開始</summary> private bool purchase (Product product) { if (product != null && product.Valid ()) { Debug.Log ($"Purchaser.InitiatePurchase {product.definition.id} {product.metadata.localizedTitle} {product.metadata.localizedPriceString}"); controller.InitiatePurchase (product); return true; } return false; } /// <summary>保留した課金の完了</summary> private bool confirmPendingPurchase (Product product) { if (product != null && Inventory [product] && possession (product)) { controller.ConfirmPendingPurchase (product); Inventory [product] = false; Debug.Log ($"Purchaser.ConfirmPendingPurchase {product.GetProperties ()}"); return true; } return false; } /// <summary>復元</summary> private void restore (Action<bool> onRestored = null) { Debug.Log ("Purchaser.Restore"); Action<bool> onTransactionsRestored = success => { OnTransactionsRestored (success); onRestored?.Invoke (success); }; if (isGooglePlayStoreSelected) { googlePlayStoreExtensions.RestoreTransactions (onTransactionsRestored); } else if (isAppleAppStoreSelected) { appleExtensions.RestoreTransactions (onTransactionsRestored); } } #region Event Handler /// <summary>復元完了</summary> private void OnTransactionsRestored (bool success) { Debug.Log ($"Purchaser.Restored {success}"); } /// <summary>iOS 'Ask to buy' 未成年者の「承認と購入のリクエスト」 承認または却下されると通常の購入イベントが発生する</summary> private void OnDeferred (Product product) { Debug.Log ($"Purchaser.Deferred {product.GetProperties ()}"); } /// <summary>初期化完了</summary> public void OnInitialized (IStoreController controller, IExtensionProvider extensions) { Debug.Log ($"Purchaser.Initialized {controller}, {extensions}"); appleExtensions = extensions.GetExtension<IAppleExtensions> (); appleExtensions.RegisterPurchaseDeferredListener (OnDeferred); googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions> (); this.controller = controller; this.extensions = extensions; Inventory = new Inventory { }; foreach (var product in controller.products.all) { Inventory [product] = possession (product); } } /// <summary>初期化失敗</summary> public void OnInitializeFailed (InitializationFailureReason error) { Debug.LogError ($"Purchaser.InitializeFailed {error}"); Unavailable = true; } /// <summary>課金失敗</summary> public void OnPurchaseFailed (Product product, PurchaseFailureReason reason) { Debug.LogError ($"Purchaser.PurchaseFailed Reason={reason}\n{product.GetProperties ()}"); } /// <summary>課金結果 有効な消耗品なら保留、それ以外は完了とする</summary> public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs eventArgs) { var validated = ValidateReceipt (eventArgs.purchasedProduct); Debug.Log ($"Purchaser.ProcessPurchase {(validated ? "Validated" : "ValidationError")} {eventArgs.purchasedProduct.GetProperties ()}"); Inventory [eventArgs.purchasedProduct] = validated; return (validated && eventArgs.purchasedProduct.definition.type == ProductType.Consumable) ? PurchaseProcessingResult.Pending : PurchaseProcessingResult.Complete; } /// <summary>破棄</summary> ~Purchaser () { Debug.Log ("Purchaser.Destruct"); if (instance == this) { instance = null; Inventory = null; Unavailable = false; } } #endregion } /// <summary>製品拡張</summary> public static class ProductExtentions { /// <summary>製品諸元</summary> public static string GetProperties (this Product product) { return string.Join ("\n", new [] { $"id={product.definition.id} ({product.definition.storeSpecificId})", $"type={product.definition.type}", $"enabled={product.definition.enabled}", $"available={product.availableToPurchase}", $"localizedTitle={product.metadata.localizedTitle}({product.metadata.shortTitle ()})", $"localizedDescription={product.metadata.localizedDescription}", $"isoCurrencyCode={product.metadata.isoCurrencyCode}", $"localizedPrice={product.metadata.localizedPrice}", $"localizedPriceString={product.metadata.localizedPriceString}", $"transactionID={product.transactionID}", $"Receipt has={product.hasReceipt}", $"Purchaser.Valid={Purchaser.Valid}", $"Receipt validation={Purchaser.ValidateReceipt (product)}", $"Possession={Purchaser.Inventory [product]}", }); } /// <summary>有効性 製品がストアに登録されていることを示すが、ストアで有効かどうかには拠らない</summary> public static bool Valid (this Product product) { return (product.definition.enabled && product.availableToPurchase); } /// <summary>アプリ名を含まないタイトル</summary> public static string shortTitle (this ProductMetadata metadata) { return (metadata != null && !string.IsNullOrEmpty (metadata.localizedTitle)) ? (new Regex (@"\s*\(.+\)$")).Replace (metadata.localizedTitle, "") : string.Empty; } } /// <summary>productID基準でProductの在庫を表現する辞書</summary> public class Inventory : Dictionary<string, bool> { /// <summary>Productによるアクセス</summary> public bool this [Product product] { get { return base [product.definition.id]; } set { base [product.definition.id] = value; } } } }Sample.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.Purchasing; using UnityInAppPuchaser; public class Sample : MonoBehaviour { [SerializeField] private Transform CatalogHolder = default; [SerializeField] private Button RestoreButton = default; /// <summary>製品目録</summary> private readonly ProductDefinition [] products = new [] { new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item1", ProductType.Consumable), new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item2", ProductType.NonConsumable), new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item3", ProductType.NonConsumable), }; /// <summary>起動</summary> private void Start () { StartCoroutine (initPurchaser ()); } /// <summary>開始処理</summary> private IEnumerator initPurchaser () { RestoreButton.interactable = false; Purchaser.Init (products); yield return new WaitUntil (() => Purchaser.Valid || Purchaser.Unavailable); // 初期化完了を待つ if (Purchaser.Valid) { Catalog.Create (CatalogHolder); foreach (var product in Purchaser.Products.all) { CatalogItem.Create (Catalog.ScrollRect.content, product); } } RestoreButton.interactable = true; } /// <summary>復元ボタン</summary> public void OnPushRestoreButton () { if (Purchaser.Unavailable) { StartCoroutine (initPurchaser ()); } else if (Purchaser.Valid) { Purchaser.Restore (success => { if (!success) { ModalDialog.Create (transform.parent, "リストアに失敗しました。\nネットワーク接続を確認してください。"); } }); } } }
- 投稿日:2019-08-21T14:11:01+09:00
【C#】RSSフィードを読み込む
備忘録。
RSSフィードを読み込む。D01tsumaTask2.csusing System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; namespace D01tsumathTask2 { class Program { static void Main(string[] args) { string url = @"http://d01tsumath.hatenablog.com/rss"; Console.WriteLine("ブログRSS取得します!"); try { // RSS読み込み XElement element = XElement.Load(url); // channelの取得 XElement channelElement = element.Element("channel"); //itemの取得 IEnumerable<XElement> elementItems = channelElement.Elements("item"); for (int i = 0; i < 5; i++) { XElement item = elementItems.ElementAt(i); Console.WriteLine($" タイトル : <a href='{item.Element("link").Value}'>{item.Element("title").Value}</a>"); } Console.WriteLine("完了"); } catch (Exception e) { Console.WriteLine(e.Message); } } } }
- 投稿日:2019-08-21T14:07:03+09:00
【C#】HttpListenerで簡易サーバー作成する
備忘録とMarkdown記法に慣れる練習兼ねて。
D01tsumaTask1.csusing System; using System.Diagnostics; using System.Net; using System.Text; namespace D01tsumaTask1 { class Program { static void Main(string[] args) { try { // HTTPリスナー作成 HttpListener listener = new HttpListener(); // リスナー設定 listener.Prefixes.Clear(); listener.Prefixes.Add(@"http://+:8080/"); // リスナー開始 listener.Start(); while (true) { // リクエスト取得 HttpListenerContext context = listener.GetContext(); HttpListenerRequest request = context.Request; // レスポンス取得 HttpListenerResponse response = context.Response; // HTMLを表示する if (request != null) { byte[] text = Encoding.UTF8.GetBytes("<html><head><meta charset='utf-8'/></head><body><h1>どいつま.com</h1></body></html>"); response.OutputStream.Write(text, 0, text.Length); } else { response.StatusCode = 404; } response.Close(); } } catch (Exception e) { Console.WriteLine(e.Message); } } } }
- 投稿日:2019-08-21T14:03:35+09:00
【Xamarin】Microsoft謹製のクロスプラットフォーム開発を触って遊んでみた
ユニバーサルWindowsプラットフォームアプリケーション(UWP)という仕組みを、
前々から触ってみたいと思っていまして。
たまたまXamarinに関わるきっかけがあったので、触りがてら遊んでみました備忘録です。1. プロジェクトの作成
開発環境
- OS...Windows10
- IDE...VisualStudio2019 Professional (他エディションでも問題なし)
VisualStudio Installerを使用して、[.NETによるモバイル開発]のインストールを行っておきます。
プロジェクトファイルの作成
VisualStudioを起動し、[テンプレートの検索(Alt+S)]欄に「xamarin」と入力します。
[モバイルアプリ(Xamarin.Forms)]を選択します。
適当なプロジェクト名を設定します。今回は「xamarin_sample」を設定しました。
VisualStudioによりxamarin_sampleプロジェクトの初期化が行われます。
完成後のイメージは以下の通り。
デバッグしてみる
初期状態でF5キーを押して実行すると、初期画面が起動しました。
マテリアルデザインちっくな、クロスプラットフォーム向けのデザインですね~。Macのアプリみたい。
適当なコードにブレークポイントを貼ってやれば、難なくデバッグも可能です。さすがVS!
2. カスタマイズ
デフォルトのままだとなんとなく個性がない‥。没個性社会のたまもの‥。
というところで、アプリケーション全体のテーマを変更したいと思います。テーマ
XAMLを読んでいると、背景色が事前に定義されたリソースとバインドされているようです。
これは、テーマを変更すると一括でアプリケーションのデザインが変えられるのでは?
テーマの設定個所を探すと、ありました。
以下の通りXAMLを書き換え、テーマを変更します。xamarin_sample.UWP/App.xaml<Application x:Class="xamarin_sample.UWP.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:xamarin_sample.UWP" RequestedTheme="Dark"> <!-- LightからDarkに変更 --> </Application>xamarin_sample.UWP/MainPage.xaml<forms:WindowsPage x:Class="xamarin_sample.UWP.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:forms="using:Xamarin.Forms.Platform.UWP" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:xamarin_sample.UWP" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <!-- 背景色にバインドされているブラシのカラーが、テーマに適したものに変更される --> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> </Grid> </forms:WindowsPage>実行すると、テーマカラーがDarkテーマに変更になっている! イカしますね~。
3. MVVMモデルを利用した画面描画
ところで、XAMLを使用しているということは、MVVMモデル(ModelViewViewModel)で構築されているのでは?
ViewModelClassを探したところ、設定されているページがありました。
さっそくMVVMモデルを使用して画面上のコンテンツを変更してみます。表示箇所設定
以下の[About]タブのアプリケーション名称を変更したいと思います。
[AboutPage]にバインドされている[AboutViewModel]の継承元である[BaseViewModel]に、AppNameプロパティを追加します。
最終的にはこのプロパティを画面上にバインドするイメージですね。xamarin_sampleusing System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using Xamarin.Forms; using xamarin_sample.Models; using xamarin_sample.Services; namespace xamarin_sample.ViewModels { public class BaseViewModel : INotifyPropertyChanged { public IDataStore<Item> DataStore => DependencyService.Get<IDataStore<Item>>() ?? new MockDataStore(); // ☆ AppNameプロパティの定義を追加する。↓ string appName = string.Empty; public string AppName { get { return appName; } set { SetProperty(ref appName, value); } } // ☆ AppNameプロパティの定義の追加 ここまで bool isBusy = false; public bool IsBusy { get { return isBusy; } set { SetProperty(ref isBusy, value); } } string title = string.Empty; public string Title { get { return title; } set { SetProperty(ref title, value); } } protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName]string propertyName = "", Action onChanged = null) { if (EqualityComparer<T>.Default.Equals(backingStore, value)) return false; backingStore = value; onChanged?.Invoke(); OnPropertyChanged(propertyName); return true; } #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = "") { var changed = PropertyChanged; if (changed == null) return; changed.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion } }続いて、[AboutPage]にバインドされている[AboutViewModel]のコンストラクタに、
AppNameプロパティの値を設定する記述を追加します。xamarin_sample/AboutViewModel.csusing System; using System.Windows.Input; using Xamarin.Forms; namespace xamarin_sample.ViewModels { public class AboutViewModel : BaseViewModel { public AboutViewModel() { Title = "About"; // ☆ 画面上に表示するアプリケーション名を指定する。 // ☆ AppNameプロパティの定義は継承元であるBaseViewModelに追加する。 AppName = "アプリケーション名を入力します。"; OpenWebCommand = new Command(() => Device.OpenUri(new Uri("https://xamarin.com/platform"))); } public ICommand OpenWebCommand { get; } } }AboutPageViewModelのAppNameプロパティをアプリケーション名称にバインドするよう、XAMLを書き換えます。
xamarin_sample/AboutPage.xaml<!-- 前略 --> <!-- コンテントページのバインディングコンテキストにAboutViewModelが設定されている。--> <ContentPage.BindingContext> <vm:AboutViewModel /> </ContentPage.BindingContext> <!-- 中略 --> <Label FontSize="22"> <Label.FormattedText> <FormattedString> <FormattedString.Spans> <!-- ☆ AppNameプロパティをバインドする。 --> <!-- ☆ <Span Text="AppName" FontAttributes="Bold" FontSize="22" /> から以下の通り書き換え --> <Span Text="{Binding AppName}" FontAttributes="Bold" FontSize="22" /> <Span Text=" " /> <Span Text="1.0" ForegroundColor="{StaticResource LightTextColor}" /> </FormattedString.Spans> </FormattedString> </Label.FormattedText> </Label> <!-- 後略 -->すると、[About]欄のアプリケーション名称が設定した値で表示されるようになりました!
値の書き換え
今回は[About]ページでボタンをクリックしたときにアプリケーション名を変更するよう、コードを書き換えます。
ふつうあり得ないですけどね、アバウトページの情報がボタンクリックするたびに書き換わったらいやじゃないですか。ワイはいやです。
でも今回はその仕様で組んでみます。xamarin_sample/AboutPage.xaml<Label FontSize="22"> <Label.FormattedText> <FormattedString> <FormattedString.Spans> <Span Text="{Binding AppName}" FontAttributes="Bold" FontSize="22" /> <Span Text=" " /> <Span Text="1.0" ForegroundColor="{StaticResource LightTextColor}" /> </FormattedString.Spans> </FormattedString> </Label.FormattedText> </Label> <!-- ☆ 以下の1行を追加してボタンを定義する。--> <Button Text="アプリケーション名を設定する" Clicked="Button_Clicked" />MVVMモデルの場合、画面上のオブジェクトを直接書き換えるのではなく、バインドされているViewModelを書き換えるお作法です。
お作法に乗っ取り、AboutPageにバインドされているViewModelを取得・値を書き換えるようにコードを編集します。AboutPage.xaml.csusing System; using System.ComponentModel; using Xamarin.Forms; using Xamarin.Forms.Xaml; using xamarin_sample.ViewModels; namespace xamarin_sample.Views { // Learn more about making custom code visible in the Xamarin.Forms previewer // by visiting https://aka.ms/xamarinforms-previewer [DesignTimeVisible(false)] public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } // ☆ ボタンクリックイベントのハンドラ private void Button_Clicked(object sender, EventArgs e) { // 自身にバインドされているViewModelを取得する。 var viewModel = BindingContext as AboutViewModel; // AppNameを書き換える。 viewModel.AppName = "ザマリンサンプルアプリケーション"; } } }すると、ボタンをクリックした時、ViewModelに設定された値を書き換えることで、バインドされている画面上のラベルも書き換わる
クソプログラムが完成しました!
(GIF画像)
4. まとめ
今回は簡単にですが、Xamarinのテンプレートプロジェクトを触ってみました。
最近は.NET Coreの対応やSQLServer on Linuxなど、Microsoftからクロスプラットフォーム向けのしくみが続々提供されていますね。
WPFの経験がある方であれば、UWPもかなり触りやすいのではないかな、と思います。引き続きなにか一本プログラムを組んでみたいと思いますので、落ち着き次第報告させていただきます!
- 投稿日:2019-08-21T02:19:39+09:00
【Unity】MonoBehaviour.Reset()の使い方
始めに
自作スクリプトにインスペクタでコンポーネントをぽちぽち割り当てていくのが面倒くさい…。
そんなときに使えるのがMonoBehaviourにあるReset()というメッセージ。
意外と融通が利いて便利なのですが、あまり使われていない?気がしますので、使い方の例を含めて紹介させて頂きます。Reset()とは
https://docs.unity3d.com/ja/current/ScriptReference/MonoBehaviour.Reset.html
要するにエディタでコンポーネント初期化のときに呼ばれるメソッドです。
具体的にはインスペクタで「Add Component」したときだったり、
右クリック or 歯車アイコンをクリックして出てくるメニューから「Reset」を選択したときに呼ばれます。
使い方
主にSerializeFieldの値を初期化するのに使います。
といっても宣言で値を代入しておけばAdd Component/Resetしたときに勝手にその値にしてくれるので、
それで事足りるならReset()を用意する必要はありません。[SerializeField] private int _testValue = 100; // 特に何もしなくてもResetしたら100に戻る宣言で代入できない値での初期化に使う
宣言で代入できない値をデフォルトにしたい(そしてそれを任意でインスペクタから変更できるようにもしたい)、そんなときがReset()の出番です。
ResetTest.csusing UnityEngine; public class ResetTest : MonoBehaviour { [SerializeField] private Rigidbody _rigidbody = null; [SerializeField] private Transform _child = null; [SerializeField] private Camera _mainCamera = null; [SerializeField] private Light _lightInScene = null; [SerializeField] private Texture _textureInResources = null; [SerializeField] private Texture _textureInAssets = null; [SerializeField] private float _rigidbodyMass = 0f; private void Reset() { // 一緒に付いているコンポーネントをセットする _rigidbody = GetComponent<Rigidbody>(); // 子オブジェクト「Child」をセットする _child = transform.Find("Child"); // メインカメラをセットする _mainCamera = Camera.main; // シーン中のLightコンポーネントをセットする _lightInScene = GameObject.FindObjectOfType<Light>(); // Resources中のTextureアセットをセットする _textureInResources = Resources.Load<Texture>("texture"); #if UNITY_EDITOR // Resources外のTextureアセットをセットする _textureInAssets = UnityEditor.AssetDatabase.LoadAssetAtPath<Texture>("Assets/Textures/texture.png"); #endif // 参照取得済み他コンポーネントの値を使ってセットする if (_rigidbody != null) { _rigidbodyMass = _rigidbody.mass; } } }これをAdd Conponentしてやると、こんな感じで勝手に設定済みの状態にしてくれます。
もちろん、この後各フィールドを変更するのも自由ですし、また上記の状態にResetすることもできます。他コンポーネントの設定値の変更に使う
さらには、他のコンポーネントの設定値を変更することもできます。
private void Reset() { // Transformの位置を変更 transform.position = new Vector3(100f, 100f, 100f); // Rigidbodyのconstraintsを変更 var rigidbody = GetComponent<Rigidbody>(); if (rigidbody != null) { rigidbody.constraints = RigidbodyConstraints.FreezeRotation; } }ただこれは相手側のコンポーネントからすれば、非実行時なのに外部から突然設定を書き換えられてしまうということですから、意図しない挙動に繋がる可能性もあります。
使いどころは要検討と言えるでしょう。それ以外に使う
そもそも、エディタモードで実行できることなら大体できるみたいです。
private void Reset() { // Add Componentしたと思ったらエディタが終了するトラップみたいなコンポーネント // !!!絶対やめましょう!!! #if UNITY_EDITOR UnityEditor.EditorApplication.Exit(0); #endif }注意点
と、便利なReset()ですが一つ注意点があります。
それは呼び出されるのはエディタモードのときのみということ。
つまり、ランタイムでスクリプトからAddComponent()した場合は呼び出されません。gameObject.AddComponent<ResetTest>(); // ResetTest.Reset()は呼び出されないこの場合、SerializeField各値は宣言での代入値(上のResetTest.csの例で言うとnull(None)とか0fとか)になります。
なお参考までに、やろうと思えばReset()を直接呼び出すことはできます。
(publicにすればクラス外からでも呼び出し可能)gameObject.AddComponent<ResetTest>().Reset(); // Reset()はpublicで定義ただ、これはReset()をどこでも自由に呼び出せるということになりますから、
もし別の誰かが内部値のクリアメソッドか何かと勘違いして呼び出したりして、それ以降インスペクタでやった設定がリセットされた状態で動作していることに気づかなかったりすると、バグに繋がりそうな気がしないでもないです。
私見ではインスペクタ設定の補助程度の範囲で使った方が無難かなと思います。参考
- 投稿日:2019-08-21T01:00:57+09:00
.NET Coreでテスト用のWebサーバを構築する
概要
HTTPクライアントを用いてテストを行う際、ダミーのエンドポイントとなるWebサーバを構築します。
.NET CoreにはKestrelというWebサーバがあるため、折角なので利用してみました。環境
- Windows10
- Visual Studio 2019
- .NET Core 2.2
- Docker for Windows
コードサンプル
サンプルコードを Github にアップしています。
https://github.com/tYoshiyuki/dotnet-core-mock-web-server解説
コンソールアプリケーションをベースにASP.NET Coreでお馴染みのIWebHostBuilderを作成します。
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>();HTTPのパイプライン処理にロジックを追記します。
HTTPアクセス時のヘッダやボディ情報等を出力します。public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Run(async (context) => { var req = context.Request; var output = new List<string> { "-------------Request Information Start-------------", $"Time[{DateTime.Now}]", $"Scheme[{req.Scheme}]", $"Path[{req.Path}]", $"QueryString[{req.QueryString}]", $"Method[{req.Method}]", $"ContentLength[{req.ContentLength}]", $"ContentType[{req.ContentType}]", "-------------Body-------------" }; using (var reader = new StreamReader(req.Body)) { var body = reader.ReadToEnd(); output.Add(body); } output.Add("-------------Headers--------------"); output.Add($"Headers Count[{req.Headers.AsEnumerable().Count()}]"); foreach (var h in req.Headers.AsEnumerable()) { output.Add($"{h.Key}[{h.Value}]"); } output.Add("-------------Request Information End-------------"); _logger.LogInformation(string.Join(Environment.NewLine, output)); await context.Response.WriteAsync("Request Success"); await context.Response.WriteAsync(Environment.NewLine); await context.Response.WriteAsync(string.Join(Environment.NewLine, output)); }); }デバッグ起動しブラウザでアクセスすると、アクセス時の情報が表示されます。
Dockerイメージ
DockerイメージをDocker Hubに登録しました。Dockerfileの生成はVisual Studioの機能を利用して実施しています。
上記、Githubのページにdocker-compose.ymlを同梱しています。docker-compose up -d30080ポートでアクセスすると、アクセス時の情報が表示されます。
- 投稿日:2019-08-21T00:55:15+09:00
【Unity】【初中級者向け】単純な写経に疲れたあなたへ
写経とは?
プログラミング修行僧による瞑想のような行為である(ちがう)。
はじめに
本記事は、僕が Unity を始めるにあたり、 Unityの教科書 Unity2019完全対応版 2D&3Dスマートフォンゲーム入門講座 (Entertainment&IDEA) を扱った勉強法を紹介する記事となります。
ただ写すだけだとどうにも身にならないし退屈なんだよなぁという方におすすめです。
対象としては、ある程度プログラミングは書けるし、ゲーム作りにも興味あるけど、何から手を付けたらいいかよくわからんのよねー。という人。環境
- Windows 10
- Unity Hub 2.1.0
- Unity 2019.1.14f1
- Visual Studio 2017
- GitHub Desctop
今回の対象の教科書について
とりあえず Unity 始めたいなと思って、最初は何もなくてもいけるやろとタカをくくっていたわけですが、撃沈しました。
それで適当に検索したところ評判の良いこちらの教科書にいきついた流れです。
- 章立てで別のゲームを作るようになってる
- ゲーム作りに必要な概念が学べる
- 挿絵があって読みやすい
評判が高いだけあってかなり良書です。
この教科書にマッチした勉強法
章立てで作ることになるテーマのゲームを 「教科書を見ずに」自分で作る 以上です。
最初の教科書のテーマは「占いルーレット」でした。
そのままゲームテーマを丸パクリというのもコンプライアンス的にまずいのかなぁという思いもあり、数字当てルーレットに変えました。成果物
レポジトリ
https://github.com/mentol310/MyRouletteGame手順
1. クラス設計する
まず何よりも設計が大事です。
設計の見通しが立っているだけで最後までやりきれるかがだいぶ変わってきます。↓の記事を参考にしました。
グローバルゲームジャムでクラス設計をやった話2019最終的に、 Input, Output namespace はクラス化しない方が今回はシンプルかなと思ってクラス化を避けたりしましたがおおよそ最初に立てたこの設計通りに出来上がりました。
クラス図には、web 版の plantuml かけるツール planttext を利用しました。
ユーザー登録もできて uml 管理機能もありとても便利です![]()
2. git レポジトリを作成する
基本 git は cli 操作でやってたんですが、今回 GUI の GitHub Desktop を使ってみました。
GitHub のアカウントでログインしたら後は New Repository から生成先ディレクトリを選択して、レポジトリ名を入力します。
Unity テンプレートの .gitignroe も選択可能なので便利でした。3. ゲームを作っていく
あとは作っていくのみです。
今回は以下の流れでようやっと出来上がりました。
(実装にあたり UniRX を使ったためアセットストアからDLしました。)
- 画面中央に円(ルーレットホイール相当)を置く(ここから詰みそうで泣きました)
- ホイールの中に円(ボール相当)を置く
- ボールがホイールの中で回るようにする
- ボールの状態(止まってる/回ってる)をホイールで購買できるように
- テキスト入力欄を置く
- テキスト入力状態が購買できるように
- テキスト入力状態によりボタン制御を行う
- ボタン制御によりボールが回るように
- 四角形の上に数字が表示されるように(出目相当)
- 出目をプレハブ化
- ホイールの子要素として、プレハブ化した出目を動的に生成する
- ボールに一番近い出目を取得できるように
- ボールに一番近い出目出力をボール位置の変動に伴い変動するように
- 止まった出目と予想値が当たってたら当たり、ハズレてたらハズレが表示されるように
todo として最初やろうかなと思ってた掛け金の処理とか回す対象を入れ替えれるようにする対応を行おうと思いましたが、一旦このテーマでこれ以上の知見を得るのは難しそうなので完としました。
ほぼほぼ 0 からのスタートだったとはいえだいぶかかりました。。。
詰まったところは以下です。
画面中央に円(ルーレットホイール相当)を置く
Asset > Sprites > Circle から作成できます。
Unity で何かしらのオブジェクト = 全て「GameObject」という先入観が何故かあり、GameObject メニューをずっといじってたんですが、「ないじゃん!」となって泣いてました。ホイールの中に円(ボール相当)を置く
オブジェクトが重なっているため、Order in Layer で描画順を調整する必要があります。
さっき円の作り方はわかったしこれは楽勝やろ!と思ってたんですが、「ホイール相当の円しか表示されない」という状態になりました。
「手前に写したいオブジェクトの Order 数値を上げる。」で解決しました。ボールの状態(止まってる/回ってる)をホイールで購買できるように
クラス設計にて 「ディーラーがホイールを監視してホイールの上で転がってるボールが止まったら~」と Dealer から Wheel 越しに Ball.State を Subscribe する必要がありましたが、なんか色々エラーが出て参ってました。
この辺の話になります。四角形の上に数字が表示されるように
「壁に文字を表示」だったり「オブジェクトに文字を貼り付ける」だったり検索すると情報はたくさん出てくるんですが、どうもうまくいきいませんでした。
調べてみると、「World Space 設定にした Canvas 毎オブジェクトの子要素とする。その後、大きさと位置を調整する。」というのがありました。
この「大きさと位置」ではっきりした数値が書かれてるところが中々なくて認識不足が原因でした。
ひとまず位置は全部 0, scale は x, y を 0.02 へ width, height を 680 x 480 へとすれば見えるようになります。その他得た知見
詰まったところも多々あれば素直に調べた記事通りやると実行できた!というのも多かったです。
- [SerializeField] をつけるとクラスとしては private で閉じてるけど UI からは設定できるというのができて安全
- ReactiveCommand への BindTo でボタンと紐付けできる
- RotateAround 第二引数で回転方向を選べる
- Time.deltaTime は前フレームから経った時間の取得
- Visual Studio のインテリセンスはかなり高い(昔 2007 とか扱ってた時の重い、扱いづらいのイメージがあったので革新的でした)
- 文字列変換埋め込み便利 $""
- Linq, UniRX の基本的な知見
...... etc
最後に
何気なくやってみるかでやってみた割にはかなり多くの学びがありました。
「何か」を始めるスイッチを探してる人にホントおすすめです!あとC#たのしい
![]()