20211123のUnityに関する記事は6件です。

Unity Test Runnerの使い方を理解する

動作確認環境 Windows10 Unity 2019.4.13f1 Unity Test Runner とは? Unityのテスト実行ツール NUnit という.NET用テスティングフレームワークが使われている EditMode, PlayMode のテスト実行環境がある Unity Test Runner のインストール方法 Unity2019.1 以前ならば、標準でインストールされているので対応不要 Unity2019.3 以降ならば、Package Manager から Test Framework と検索してインストールする Windowの開き方 Window > General > Test Runner を選択するとウィンドウを開くことができる 以下のようなウィンドウが開けばOK Create Playmode Test Assembly Folder ボタンを押すと、 TestフォルダとAssembly Definition Fileが作成される 作成された Test フォルダ配下にスクリプトを配置していく EditMode プレイモードを経由せずに、Unityエディタで即実行できる すぐ実行できる Start() や Update() など MonoBehavior の関数は呼ばれない メソッドに [Test] アトリビュートをつけるとテストメソッドと認識される スクリプトの配置場所は… Editor のチェックを入れた Assembly Definition File(.asmdefファイル) を配置したフォルダ配下 PlayMode Unityエディタのプレイモードで実行できる MonoBehaviour を組み合わせたテストができる テスト用の Scene が生成・実行される Unity エディタがクラッシュすると、 Scene ファイルが残ってしまう スクリプトの配置場所は… Create Playmode Test Assembly Folder ボタンを押して作成したフォルダ Assembly Definition Fileの Assemply Definition References の UnityEditor.TestRunner を消す(画像の選択部分を消す) フォルダ構成例 Assets/ └ Tests/ ┝ PlayMode.asmdef ┝ TestCodeInPlayMode.cs ┝ Editor/ │ ┝ Editor.asmdef │ └ TestCodeInEditorMode.cs テストの書き方 通常は Test アトリビュート、コルーチンの場合は UnityTest アトリビュートを付けたメソッドを定義 判定の書き方は Constraint Model と Classic Model がある using System.Collections; using NUnit.Framework; using UnityEngine.TestTools; public class EditorModeExample { [Test] public void ExampleTest() { var a = 10; var b = 10; Assert.That(a == b); } [UnityTest] public IEnumerator ExampleTestEnumerator() { Assert.That(1 < 10); yield return null; Assert.That(2 < 10); yield return null; Assert.That(3 < 10); } } Classic Model Assert.True() や Assert.AreEqual() などが使える古い書き方 こっちはもう使わない 基本的には後述の Constraint Model を使う Constraint Model Assert.That() を使う Assert.That() には多くのオーバーライドがある Classic Model より Constraint Model を使う理由 複雑な条件が来た場合、Classic Model より柔軟に対応できる テストコードを結果と期待値を記述するという内容に画一化できる 旧モデルはサポートされなくなってきている テストの実行方法 実行したい関数かクラスを選択して、ダブルクリックもしくは右クリック > Run をクリックする 緑のチェックマークになれば、テスト成功 赤いバツマークになれば、テスト失敗 テスト成功時 テスト失敗時 テストの前後処理の書き方 対応するアトリビュートを使用することで可能 書き方 説明 [SetUp] 各テスト実行前に1回ずつ呼ばれる [TearDown] 各テスト実行前に1回ずつ呼ばれる [UnitySetUp] 各テスト実行前に1回ずつ呼ばれる(コルーチン) [UnityTearDown] 各テスト実行前に1回ずつ呼ばれる(コルーチン) [OneTimeSetUp] 最初のテスト実行前に1回だけ呼ばれる [OneTimeTearDown] 最後のテスト実行後に1回だけ呼ばれる コード例 // このクラスに定義された各テストが実行される前に、テストごとに一回ずつ呼ばれる [SetUp] public void Setup() { Debug.Log("SetUp"); } // このクラスに定義された各テストの実行終了後に、テストごとに一回ずつ呼ばれる [TearDown] public void TearDown() { Debug.Log("TearDown"); } // このクラスに定義された各テストが実行される前に、テストごとに一回ずつ呼ばれる(コルーチン) [UnitySetUp] public IEnumerator UnitySetup() { Debug.Log("UnitySetup"); yield break; } // このクラスに定義された各テストの実行終了後に、テストごとに一回ずつ呼ばれる(コルーチン) [UnityTearDown] public IEnumerator UnityTearDown() { Debug.Log("UnityTearDown"); yield break; } // このクラスに定義されたテストのうち最初のテストが実行される前に一回呼ばれる [OneTimeSetUp] public void OneTimeSetUp() { Debug.Log("OneTimeSetUp"); } // このクラスに定義されたテストのうち最後のテストが実行された後に一回呼ばれる [OneTimeTearDown] public void OneTimeTearDown() { Debug.Log("OneTimeTearDown"); } 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AvatarをセットアップするEditorを作る(FX編)|VRC用の拡張Editorを作ってみよう(3)

この記事は前回の続きになります VRC用の拡張Editorを作ってみよう(2) 目次 1.不要なコードを削除する 2.VRCAvatarDescriptorが入ってない場合自動でコンポーネントを追加するようにする 3.FXLayerが入っているか確認して入っていなければ追加するコードを書く 4.ボタンを押すと処理が行われるようにする 5.動作確認をする 6.次回 1.不要なコードを削除する 削除するコード InsertObjectメソッドはすべて消します //すべて消す private void InsertObject() { gameObject.transform.SetParent(avatar.transform); } if(GUILayout.Button)もすべて消す if (GUILayout.Button("オブジェクトを入れる")) { InsertObject(); //中のInsertObject();も消す } 前回作成したGameObjectの変数とObjectFieldを削除する //gameObjectはすべて消す private GameObject gameObject; gameObject = EditorGUILayout.ObjectField("gameObject", gameObject, typeof(GameObject), true) as GameObject; 全て消すとこのようになると思います public class TestEditor : EditorWindow { [MenuItem("TestEditor/TestEditor")] public static void ShowWindow() { EditorWindow.GetWindow<TestEditor>(true, "TestEditor"); } private GameObject avatar; private void OnGUI() { avatar = EditorGUILayout.ObjectField("Avatar", avatar, typeof(GameObject), true) as GameObject; } } これでコードの削除ができました 2.VRCAvatarDescriptorが入ってない場合自動でコンポーネントを追加するようにする AvatarのObjectFieldをVRCAvatarDescriptorからGameObjectに書き換える 前回書いたVRCAvatarDescriptorタイプの変数AvatarをGameObjectにしていきます まずは変数から変更してきます //変更前 private VRCAvatarDescriptor avatar; //変更後 private GameObject avatar; VRCAvatarDescriptorをGameObjectに書き換えるだけで変更できると思います 次にObjectFieldを変更していきます //変更前 avatar = EditorGUILayout.ObjectField("Avatar", avatar, typeof(VRCAvatarDescriptor), true) as VRCAvatarDescriptor; //変更後 avatar = EditorGUILayout.ObjectField("Avatar", avatar, typeof(GameObject), true) as GameObject; オブジェクトタイプとキャストをVRCAvatarDescriptorからGameObjectに変更しました これで書き換えができました VRCAvatarDescriptorを追加する用のメソッドを作る 前回と同じようにメソッドを作っていきます 今回も値を返さないメソッドを作ります 名前は適当につけて・・・ private void AddDescriptor() { } こんな感じになりました VRCAvatarDescriptorが入っているかを確認して、入っていなければ追加するコードを書く コードは先ほど作成したAddDescriptorの中に書いていきます まずはVRCAvatarDescriptorが入っているかを確認します if文で真・偽を判定し、Avatar.GetComponentを使ってコンポーネントを取得します コンポーネントを取得できなければ真を返すようにしたいので、GetComponentの先頭に 否定「!」 を付けます GetComponentで取得するコンポーネントはVRCAvatarDescriptorです 今回のコードはこうなりました if (!avatar.GetComponent<VRCAvatarDescriptor>()) { } これで確認できるようになりました 次にVRCAvatarDescriptorを追加していきます Avatar.AddComponentを使ってコンポーネントを追加できるようにします AddComponentで追加するコンポーネントはVRCAvatarDescriptorです 今回のコードはこうなりました if (!avatar.GetComponent<VRCAvatarDescriptor>()) { avatar.AddComponent<VRCAvatarDescriptor>(); } これでコンポーネントがなければ追加するコードが完成しました 3.FXLayerが入っているか確認して入っていなければ追加するコードを書く FXLayerを保管するフォルダーを作成する 準備としてFXLayerを保管するフォルダーを作っていきます フォルダーを作る場所は自由です 名前は分かりやすいようにFxLayerなどつけておきましょう フォルダーが完成したらフォルダーのパスを取得します FXLayerフォルダーを右クリックしてパスをコピーします パスのコピーができたら新しく変数を宣言していきます 今回はOnGUIの上で宣言します privateなstring型の変数createFXPathを宣言します 宣言したcreateFXPathに、先ほどコピーしたパスを代入します 宣言ができるとこんな感じになると思います private string createFXPath = "Assets/test/FxLayer"; private void OnGUI() { FXLayerを保管するフォルダーができました FXLayerをコピーするためのパスを取得する FXLayerをコピーするパスの変数を作っていきます 今回もOnGUIの上で宣言します privateなstring型の変数copyFXPathを宣言します 宣言したcopyFXPathに、FXLayerが入っている Assets/VRCSDK/Examples3/Animation/Controllers/vrc_AvatarV3HandsLayer.controller を代入します 宣言ができるとこんな感じになると思います private string copyFXPath = "Assets/VRCSDK/Examples3/Animation/Controllers/vrc_AvatarV3HandsLayer.controller"; これで準備はできました FXLayerを追加する用のメソッドを作る FXLayerを追加する用のメソッドを作っていきます 今回も値を返さないメソッドを作ります 名前は適当につけて・・・ こんな感じのコードになると思います private void SetupFXLayer() { } これでメソッドが完成しました Playeble LayersのCustomizeが有効になっているかを確認して無効になっていた場合有効にする 先ほど作成したSetupFXLayerの中に書いていきます 最初に変数を作る 後から使いやすいように最初に変数を作っていきます VRCAvatarDescriptorタイプのavatarDescriptor変数を作って・・・ avatarDescriptor変数にavatar.GetComponentで取得したVRCAvatarDescriptorを代入していきます 今回のコードはこうなります VRCAvatarDescriptor avatarDescriptor = avatar.GetComponent<VRCAvatarDescriptor>(); これで変数はできました 次にPlayeble LayersのCustomizeが有効になっているかを確認する Playeble LayersのCustomizeが有効になっているかを確認するコードを書いていきます 有効になっているかの確認はVRCAvatarDescriptor.customizeAnimationLayersで確認できます if文でcustomizeAnimationLayersが偽の場合、真に変更するコードを書いてきます 先ほど作成した変数を使用して・・・ 今回のコードはこうなりました if (!avatarDescriptor.customizeAnimationLayers) { avatarDescriptor.customizeAnimationLayers = true; //trueで偽から真に変える } これでPlayeble LayersのCustomizeが有効になっているかを確認して、無効になっていた場合有効にするコードが完成しました FXLayerがデフォルトになっているか確認して、デフォルトになっていたらカスタムできるようにする SetupFXLayer()の中に書いていきます FXLayerの取得はVRCAvatarDescriptor.baseAnimationLayers[4]を使用することで取得できます FXLayer以外にも、配列の番号を変えるとほかのLayerが取得できます Base Additive Gesture Action FX [0] [1] [2] [3] [4] デフォルトの確認はbaseAnimationLayers[].isDefaultを使用して確認できます 今回はデフォルトになっているかの確認をしたいので、if文を使います 確認用のコードが完成したら、if文の中にisDefaultが真のときのコードを書きます カスタムする場合、isDefaultをfalseに変えてあげるとカスタムできるようになります 今回のコードはこうなりました if (avatarDescriptor.baseAnimationLayers[4].isDefault) //avatarのFXLayerがデフォルトになってるか判定する { avatarDescriptor.baseAnimationLayers[4].isDefault = false; //デフォルトになっていたらデフォルトを真から偽に変える } これでFXコントローラーのカスタムができるようになりました FXコントローラーを追加する 先ほどカスタム状態にしたFXLayerの中にFXコントローラーを追加していきます コードは先ほど作ったif文の中に書きます 初めにFXコントローラーをコピーする コピーはAssetDatabase.CopyAssetを使用します 使い方はこの通りです AssetDatabase.CopyAsset("コピー元のパス" , "コピー後のパス"); これに先ほど準備したパス変数を書いていきます コピー元のパスはcopyFXLayer コピー後のパスはcreateFXLayer ですがコピー後のパスcreateFXLayerはこのままでは使用できないのでアバターごとに作成されるようにアバターの名前を追加して作成するようにします 今回はこのようにします コピー後のパス + "アバターの名前" + コントローラーの拡張子 createFXPath + "/" + avatar.name + "_HandsLayer.controller" これを使用してコードを書いていきます 今回のコードはこうなります AssetDatabase.CopyAsset(copyFXPath, createFXPath + "/" + avatar.name + "_HandsLayer.controller"); これでFxLayerにコントローラーが作成されました FXコントローラーを追加する FXLayer.Controllerの取得はavatarDescriptor.baseAnimationLayers[4].animatorControllerを使用して取得できます 取得したFXコントローラーにコントローラーを入れる 取得したFXコントローラーにコントローラーを入れるにはAssetからロードする必要があるので AssetDatabase.LoadAssetAtPathを使用します 書き方はこの通りです AssetDatabase.LoadAssetAtPath("ロードするファイルのパス", "オブジェクトのタイプ") ロードするファイルのパスは先ほど書いた createFXPath + "/" + avatar.name + "_HandsLayer.controller" を利用します オブジェクトタイプはRuntimeAnimatorControllerです 上記のFXコントローラーを追加する、を組み合わせて書いていきます 今回はこのようになりました avatarDescriptor.baseAnimationLayers[4].animatorController = AssetDatabase.LoadAssetAtPath(createFXPath + "/" + avatar.name + "_HandsLayer.controller", typeof(RuntimeAnimatorController)) 前回と同じようにこのままでは変換ができないのでキャストを用意します コードの末尾にas RuntimeAnimatorControllerを追加します これでコードが完成しました 4.ボタンを押すと処理が行われるようにする ボタンに条件を付ける 条件が真・偽であればボタンを表示させるコードをOnGUIの中に書いていきます 今回はまずavatarが入っているかをif文で判定し、真だった場合にVRCAvatarDescriptorが入っているか、いないかをif文で判定してボタンを表示できるようにします 真の場合はSetupFXLayer()を実行 偽の場合はAddDescriptor()を実行します 判定用のif文ができたらボタンの表示名を適当につけてボタンを作ります ボタンができたらコードはこんな感じになると思います if (avatar) { if (avatar.GetComponent<VRCAvatarDescriptor>()) { if (GUILayout.Button("アバターのFXLayerをセットアップする")) { } } else { if (GUILayout.Button("アバターにVRCAvatarDescriptorを入れる")) { } } } ボタンでメソッドを呼び出すようにする 今回作ったメソッド二つをボタンのif文の中に追加していきます 今回作ったメソッドは AddDescriptorとSetupFXLayerなので引数なしで呼び出していきます コードはこうなります if (avatar) { if (avatar.GetComponent<VRCAvatarDescriptor>()) { if (GUILayout.Button("アバターのFXLayerをセットアップする")) { SetupFXLayer(); } } else { if (GUILayout.Button("アバターにVRCAvatarDescriptorを入れる")) { AddDescriptor(); } } } これで呼び出せるようになりました 5.動作確認をする まずはCtrl + Sを押してスクリプトを保存しましょう 次にUnityに戻って動作確認します 準備 準備として、空のオブジェクトをヒエラルキーに作成します 分かりやすくするために名前はAvatarなどに変更しましょう これで準備ができました 動作確認 TestEditorを開きます EditorのAvatarにゲームオブジェクトのAvatarをセットしましょう セットができたらアバターにVRCAvatarDescriptorを入れるを押してみましょう 押したら一度AvatarのInspectorに移りVRCAavatarDescriptorが入っているのが確認します VRCAavatarDescriptorが入っているのを確認できたと思います 確認ができたらもう一度Editorに戻りアバターのFXLayerをセットアップするを押します 押したらもう一度AvatarのInspectorに移りFXLayerをセットアップされているか確認します 確認ができたら今回は終了です ※連続で押すとVRCAvatarDescriptorが生成される前に操作してしまいエラーが出ることがあります 6.次回 次回はExMenuとExParameterを自動でセットアップするEditorを作っていこうかなと思います VRC用の拡張Editorを作ってみよう(4)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新卒2年目までに学んだ、コーディングで意識すること

本記事はCraft Egg Advent Calendar 2021の12/3の記事です。 12/2の記事は@ishiguro_takuyaさんの[Unity] UniversalRenderPipelineについて調べた覚え書き でした。 はじめに 株式会社Craft EggでUnityクライアントエンジニアをしている豊田です。 今回は私が新卒入社してから現在の2年目までに、プルリクエストでチームからもらったコメントや技術書をなどの学びを元に、コーディングで気をつけていることをまとめてみました。 before/afterのコードの例を出すことで、新卒エンジニアが遭遇しやすいコーディングアンチパターンとその改善例の参考になれば幸いです。 コーディング規約やしきたりはチームによって異なりはしますが、できるだけ一般的な内容を取り上げました。 ※本記事で取り上げるコードはサンプルです。重要でない部分の命名は「hoge」としたり、簡略化しています。 表面上の改善 リーダブルコードには、第1部に「表面上の改善」が紹介されています。 命名規則やコードの体裁など、第三者(未来の自分自身も)がコードを理解するために最初に意識できる観点だと思います。 適切な変数名にする 意味が分かる命名にする boolは「is~」「should~」「can~」「exists~」「has~」など コールバックは「onCloseDialog」など「on~」 略しすぎない。一般的な略称ならOK(× s_pos → ◯ startPosや、startPosition) 英文法に誤りが無いか気をつける(× onClosedDialog → ◯ onCloseDialog) など 適切な関数名にする 基本的に動詞始まりにする 初期化は「Initialize」「Setup」 生成は「Create~」 登録は「Register~」 変換は「ConvertHogeToFoo」 「Add◯◯」なら「Remove◯◯」というように、名前と処理を対にして理解しやすくする 「~IfNeed」「Check~」のような命名の関数を避ける(処理を分けられないか検討する) 英文法に誤りが無いか気をつける など 一次変数でも分かりやすい命名にする 一次変数などスコープが小さくてもできるだけ意味の分かる命名にします。 before UserData u = GetUserData(userId); after UserData userData = GetUserData(userId); ※ラムダ式の中でも同様 before filterUsers = userList .Where(x => x.Id % 2 == 0) .ToList(); after filterUsers = userList .Where(user => user.Id % 2 == 0) .ToList(); ネストを浅くする 例外は先に早期returnすることで思考をクリアにできます。 before private void Hoge() { if (hoge != null) { Foo(); if(foo != null) { // メインの処理が続く } } } after private void Hoge() { if (hoge == null) { return; } Foo(); if (foo == null) { return; } // メインの処理が続く } なお、早期returnは関数の出口が複数でき、利用側の期待する処理が行われず罠にハマることがあるため、何でも早期returnするのではなく、処理を分離できないか検討するなどしてケースバイケースで用いるのが良いと思います。 三項演算子を使う before if (IsHoge()) { foo = bar; } else { foo = null; } after foo = IsHoge() ? bar : null; foreachをLINQに置き換える before private List<ItemData> CreateItemDataList(List<uint> itemIds) { List<ItemData> itemDataList = new List<ItemData>(); foreach (uint id in itemIds) { itemDataList.Add(new ItemData(id)); } return itemDataList; } after private List<ItemData> CreateItemDataList(List<uint> itemIds) { return itemIds .Select(id => new ItemData(id)) .ToList(); } 配列やListに対してforeachで処理する場合の多くはLINQで書けます。 文字列補間を使う C#の6.0以上で使える文字列補間は、書きやすく見やすいです。 https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/tokens/interpolated before Debug.LogError("userId : " + userId + ", userName : " + userName + "のユーザでエラー"); after Debug.LogError($"userId : {userId}, userName : {userName}のユーザでエラー"); コメントアウト 自明なコメントは書かない Initialize(); // 初期化 と書かれていてもコメントの有無で情報量が変わりません(むしろノイズになりうる)。 できるだけコードで説明することを心がけます。 処理の内容ではなく理由を書く 特殊な実装をしていたら、なぜそうしているかのコメントがあると、読んだ人をびっくりさせません。 なお、低レベルのコードを要約したり、処理のまとまりを説明する場合など、ケースによって処理の内容は書くことはあると思います。 問題のあるコードにコメントを書く こちらも読んだ人をびっくりさせることを防ぎます。 やむを得ず綺麗じゃない実装をしているのなら、しれっとコードに紛れ込ませるのではなくTODOやHACKなどを添えて問題点を説明します。 安全性、Nullチェック Get関数は失敗を考慮する before UserData userData = GetUserData(userId); userData.HogeMethod(); この例だと、userDataの取得に失敗した時にNullエラーになりかねないので、Nullチェックを挟みます。 after UserData userData = GetUserData(userId); if (userData == null) { Debug.LogError( "UserData is null. userId : " + userId ); // 場合に合わせて例外処理を行う return; } userData.HogeMethod(); Null演算子を使って簡潔にする before if (userData != null && userData.Card != null) { // userData.Cardにアクセスする処理 } after if (userData?.Card != null) { // userData.Cardにアクセスする処理 } ※ リストのNull、空チェックも同様に簡潔にできます before if (userList != null && userList.Count != 0) { // userListにアクセスする処理 } after if ((userList?.Count ?? 0 ) != 0) { // userListにアクセスする処理 } なお、MonoBehaviourなどUnityEngine.Objectを継承したものに対してのnullチェックは以下のような問題もあるので、Null演算子は使わずに== nullを使います。 エラー通知は必要な時にはっきり行う ログは、エラーが起きた時にエラー出力をさせるなど、必要な時に行います。 正常挙動である時はログは出力させません。 なお、追いにくい低レイヤの処理には正常ログを入れる場合もあるかと思います(問題が起きた時に追いやすくする)。 制御の改善 処理の対象を絞ってLoop処理を回す before foreach (var hoge in hogeList) { if(!hoge.IsFoo) { continue; } hoge.HogeMethod(); } 先にIsFooで絞ってからforを回したほうが、どういうデータを処理したいループなのかが分かりやすいです。 after foreach (var hoge in hogeList.Where(hoge => hoge.IsFoo)) { hoge.HogeMethod(); } 状態を保持する変数はenumで定義する フラグを乱立すると状態が分かり辛く、ソースを読み解くのが大変になります。 before private bool isProcessing; private bool isInterrupted; private bool isInitialized; after private enum StateType { None, Processing, Interrupted, Initialized } private StateType currentState; 設計の改善 モジュールを「純粋」にして、モジュール間を「疎遠」にすることを意識します。 凝集度と結合度の話ですが、以下の要素は基本的に凝集度を高く結合度を低くするための実践例だと思います。 長すぎるメソッド、クラスを書かない 一つの関数が長くなっているのであれば、まず分離できないかを検討します。 before public void Setup() { // UI、コンポーネントの初期化が数行に渡って書かれる ... // ロードの準備が数行に渡って書かれる ... // ロード処理が数行に渡って書かれる ... } after public void Setup() { SetupComponents(); PrepareLoad(); Load(); } 必要ないものはpublicにしない クラスが、利用側の知る必要のない内部の詳細部分を隠蔽すれば、やりとりがシンプルになりコード全体の複雑性を下げることができます。利用側からみても、使い方がシンプルになり使い勝手が良くなります。 まずprivateで書いてみて、公開する必要があればpublicにする、がいいかもしれません。 ロジックとデータは近くに置く before(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } // コンストラクタは省略 } before(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.Price * purchaseData.Count}円です") このbeforeの例では、データとなるPurchaseDataクラスと、利用側の2つのコードがあります。 データのクラスは、DB由来のモデルクラスと捉えてもいいです。 改善したいのは、Debug.Log内の金額の計算を利用側で行っている点で、ロジックがデータの外側に実装されてしまっています。 after(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } public uint GetAmount() { return Price * Count; } // コンストラクタは省略 } after(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.GetAmount()}円です") afterでは、合計金額を計算するロジックをデータのクラス側に移動しました。 例えばこれに、「軽減税率を適用するか」という「データ」を増やし、消費税計算の「ロジック」の実装が必要になった時に、改修の対象はPurchaseData内のみですので、利用側はロジックの変更を知らなくて済みます。 引数で処理の内容が変わる関数を避ける 以下のように引数を元に処理が変わる関数だと、利用側が関数のロジックを把握する必要があり、ブラックボックス化できません(制御結合になっている)。 制御結合になっている例 public void hoge(bool flag) { if(flag) { // 処理Aが続く } else { // 処理Bが続く } } ただし、システムによっては、制御結合を避けられない場合もあるので、集まっている機能を精査しながらより結合度を下げられないか検討します。 無駄な引数を送らない before例ではスタンプ結合になっています。関数にはできるだけ使うものだけ送るようにします。 before public void SetUserNameLabel(UserData userData) { userNameLabel.text = userData.Name; } after public void SetUserNameLabel(string userName) { userNameLabel.text = userName; } Unity関連 GetComponent、Find系の関数を使わない パフォーマンスに悪いので、事前にSerializeFieldで参照を持たせます。 Update()は使わない コールバックやコルーチンで実現できないか検討をします Animatiorの引数はhash値を使う Tips的な項目ですが、animatorの引数はhash値を使った方がパフォーマンス的に良いです。 Animator.StringToHash で取得した値を保持しておくのがミソ(先輩の言葉を引用) before private void PlayRunAnimation() { animator.Play("run"); } after private readonly int AnimationHashRun = Animator.StringToHash("run"); private void PlayRunAnimation() { animator.Play(AnimationHashRun); } さいごに コードの可読性、保守性、安全性や柔軟性を上げるための手法、設計の原則は今回記事に取り上げたことの他にも様々あると思います。 業務でのプルリクエストでのコメントで得た学びの他、「リーダブルコード」と「プリンシプルオブプログラミング」からも執筆にあたって参考にしました。これらの書籍は1年目で読んだのですが、とても勉強になりました。 今回は局所的なコードの改善事例を取り上げましたが、今後インタフェースを使った実装の分離や、デザインパターンを用いた実践なども体系的にまとめてアウトプットしてみたいです。 アドベントカレンダーの明日の記事は @kai_yamamoto です。お楽しみに! 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向けテスト】コマンドバトル【解説・チュートリアル】

はじめに これは、 の解答解説です。チュートリアルとしても使えます。 ☆敵オブジェクトとぶつかったらバトルシーンに移動する まず、プロジェクトを開いて再生して、今の状況を確認しましょう。 ・ペンギンがPlayerで、ライオンがEnemyという名前になっている? ・矢印キーでプレイヤーが動きますか? ・ライオンにぶつかる?(すり抜けない=コライダーがちゃんとついている) HitBoxというオブジェクトが既に入っています。ここにPlayerが入ったらシーン移動が起きるようにしましょう。 IsTriggerにチェックを入れて Playerスクリプトを開きます。 シーン移動のコードを書きましょう! コードは、覚えなくて大丈夫!!「Unity シーン移動」とかで検索するとすぐ出てくるので、調べて書くクセをつけましょう。 最後に、Unity側の設定でやるべきことがあったのを覚えてますか? 設定で、二つのシーンを登録しましょう。 できたら保存を忘れずに!次はBattleシーンを開いてね ☆バトルシーンが開いたらすぐ「PlayerHP:100」と「EnemyHp:100」とコンソールに表示する コンソールに数値を表示する、つまり「デバッグ」のやり方がわからない人は 「Unity デバッグ やり方」などで検索しましょう。 まずはPlayerとEnemyそれぞれのHP100を宣言します。 今回扱うHPは整数なので、int型で宣言します。 さらに、ゲーム始まって最初にだけHPは100から始まるよということを表示したいので、 デバッグを使って、それぞれ表示します。 ☆☆こうげきボタンを押したら...   →相手のHP-10(さらに、EnemyHP:〇〇とコンソールに表示する)   →自分のHP-5(さらに、PlayerHP:〇〇とコンソールに表示する) どんなコードを書くかわかりましたか? 全くわからなかったら、「Unity ボタン 押したら」とかで検索して、できるところまで書いてみましょう。 BattleScriptを開きます。 Update()の外に書きます。 AttackButton用の関数を用意して、その中に書きます。 プログラミングの引き算のやり方を覚えてましたか? さらに、それぞれ結果がわかるようにデバッグで表示しましょう。 できたら保存して、Unityで設定します。 BattleHpManager というオブジェクトが入っているので、そこにBattleスクリプトをアタッチしましょう。 次に、Canvasの下にあるAttackButtonをクリックしてInspectorウインドウを開き、BattleHpManagerを矢印のようにアタッチしましょう。 次に、赤枠のようにして、さっき作ったAttackButton用の変数を設定します。 再生して確認しましょう! 同じ手順で、 ・かいふくボタンを押したら→自分のHP+20(さらに、PlayerHP:〇〇とコンソールに表示する) も作れます。できなかった人は、上の手順を見ながらやってみましょう。 コードはこんな感じにかけてればOK(関数名は自分でわかりやすく決められていればOK) ☆☆プレイヤーのHPが0になったら「負け」敵のHPが0になったら「勝ち」とコンソールに表示する ヒントはif文!全く書けなかったらこれも一度調べてみてね。 if文を使った基本的なコードなので書けるように頑張ろう! こんなコードになりましたか?Updateの中に書きましょう。 ※追記:こうげきボタンの関数の中に入れるのがベストです!(毎フレーム判定する必要はないので) 一応どちらでもOK ちなみに、UpdateとStartの違いはわかりますか?わからなかったらメンターに質問してね〜 ☆☆☆アイテムボタンを押したら→プレイヤーの攻撃力が1.5倍になる 全くやり方が思い浮かばない人は、一緒に考えてみましょう。 まず、プレイヤーの攻撃力はどうやって書いていましたか?コードを振り返りましょう。 こうげきボタンを押した時、enemyHPから-10しましたね。 つまりこれがプレイヤーの攻撃力になります。 なので、PlayerPower(攻撃力)を10と宣言して ここに変数で置き換えます。 そして、アイテムボタン用の関数を用意して、アイテムボタンが押されたらplayerPowerが1.5倍になるようにすれば完成ですね!プログラミングでの掛け算の書き方を覚えてますか?わからなかったら「C# 掛け算」などで検索しましょう! あれ?このままだとエラーが出ますね。何がおかしいかわかりますか?これがわかれば100点! 「1.5」と、上の方の変数を宣言した行に注目! playerPowerは、int型で宣言しましたが、int型は整数を扱う型ですよね? なのに、ここで「1.5」という小数を扱ってしまっています。 なので、型が一致しないのでエラーが出ていることがわかりますね。 では、小数の時は何を使って宣言するか覚えていますか?わからなければ検索! floatですね!書き換えましょう まだエラーは出ます。小数には、fをつけるんでしたね まだエラーが出るのですが、なぜかわかりますか? enemyHPとplayerPowerをつかって計算しています。異なる型の変数同士で一つの計算をしようとしているからです。なので、 enemyHPの方もfloatにしておきましょう。 最後に、Unity側でボタンの関連付けを忘れずに行いましょう。 おわり 以上で完成です! ただ、このままだと、アイテムの使用制限がないのでこのままだと攻撃力を無限に1.5倍にできちゃったりと、ゲームとしては良くなかったり、HPがコンソールでしか見れなかったりするので、余裕のある人はより良いゲームに改変してみてね。 さらに、 こんな感じでPlayerHPから引かれる数(=敵の攻撃力)を書き換えると、敵の攻撃力がランダムになってよりゲーム性が増したりします! このワークで復習できた技術 重要順 ・変数、四則演算、デバッグ ・if文 ・衝突判定 OnTrigger ・シーン切り替え ・Start() Update() ・Button
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初級者向けUnityテスト】コマンドバトル

問題 ポケモンみたいなターン制バトルを作ろう! 準備 UnityPackageをダウンロードしてください。 ダウンロードの場所↓ プロジェクトを新規作成して、インポートしましょう。 ※推奨バージョン:2020.2.7(多少違っても問題ないとは思います) まず、Mainシーンを開いて再生し、プレイヤーが十字キーで動かせるか確認しましょう。 作るもの ☆ ・Hitbox(ライオンについてる)にぶつかったらBattleシーンに移動する(Playerスクリプトに書く) ・HPはPlayerもEnemyも100からスタート(ここからBattleScriptに書く) ・バトルシーンが開いたらすぐ「PlayerHP:100」と「EnemyHp:100」とコンソールに表示する ☆☆ ・こうげきボタンを押したら   →相手のHP-10(さらに、EnemyHP:〇〇とコンソールに表示する)   →自分のHP-5(さらに、PlayerHP:〇〇とコンソールに表示する) ・かいふくボタンを押したら→自分のHP+20 (さらに、PlayerHP:〇〇とコンソールに表示する) ☆☆☆ ・プレイヤーのHPが0になったら「負け」敵のHPが0になったら「勝ち」とコンソールに表示する ☆☆☆☆ ・アイテムボタンを押したら→プレイヤーの攻撃力が1.5倍になる 発展問題 ・どうすればもっといいゲームになるか考えてみよう ・HPの数値をゲーム画面にテキストとして反映する ・敵の攻撃力(相手のHP-10)の数値を、-5 から -30の間でランダムにする ・かいふくボタンを押して回復した時も、相手のターンとして自分のHPをマイナスする 解説↓
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】iOSでウィンドウからSafeAreaを取得するプラグインを作る

iOS 15でデバイスを回転させるとScreen.safeAreaが正しい値を返さないことがフォーラムで話題になっています。 Unity SafeArea is inconsistent between different starting rotations Screen.safeAreaが正しい値を返さないのは何年も前からある現象で、UnityとしてはもうFixしたと言ってみたり、また再発したりを繰り返しています。 原因はUIViewのsafeAreaInsetsが正しい値を返さないことにあり、UIWindowのsafeAreaInsetsだと正しかったりします。 どうもウィンドウからビューにイベントの伝達がうまくいってないっぽく、これはUnityの責任というより、Appleの責任ではないかとも思われます。 UnityでiOSビルドしたプロジェクトを探すと、UnityView.mmというファイルのComputeSafeArea(UIView* view)という関数でsafeAreaを取得しているようですが、毎回ここを書き換えるのも面倒です。 なのでフォーラムの投稿にもありますが、UIWindowからsafeAreaを取得するネイティブプラグインを作って対処することを考えます。 iOSプラグイン extern "C" { char* convertNSStringToCString(const NSString* nsString); char* GetWindowSafeArea(); } char* convertNSStringToCString(const NSString* nsString) { if (nsString == NULL) return NULL; const char* nsStringUtf8 = [nsString UTF8String]; char* cString = (char*)malloc(strlen(nsStringUtf8) + 1); strcpy(cString, nsStringUtf8); return cString; } char* GetWindowSafeArea() { UIEdgeInsets insets = UIEdgeInsetsZero; UIWindow *window = UnityGetMainWindow(); CGRect bounds = window.bounds; CGFloat scale = window.screen.scale; if (@available(iOS 11.0, *)) { insets = window.safeAreaInsets; } // 上下逆の座標系 CGFloat x = insets.left * scale; CGFloat y = insets.bottom * scale; CGFloat w = (bounds.size.width - insets.left - insets.right) * scale; CGFloat h = (bounds.size.height - insets.top - insets.bottom) * scale; CGRect safeArea = CGRectMake(x, y, w, h); NSString *stringData = [NSString stringWithFormat:@"%f/%f/%f/%f", safeArea.origin.x, safeArea.origin.y, safeArea.size.width, safeArea.size.height]; char* data = convertNSStringToCString(stringData); return data; } Unity側の読み込み using UnityEngine; using System.Runtime.InteropServices; public class UtilityPlugin : MonoBehaviour { #if UNITY_EDITOR #elif UNITY_IPHONE [DllImport("__Internal")] private static extern string GetWindowSafeArea(); #elif UNITY_ANDROID #else #endif public static Rect GetSafeArea() { Rect rect = Screen.safeArea; #if UNITY_EDITOR #elif UNITY_IPHONE string data = GetWindowSafeArea(); if (data != null) { string[] rectArray = data.Split('/'); if (rectArray.Length >= 4) { float x = float.Parse(rectArray[0]); float y = float.Parse(rectArray[1]); float w = float.Parse(rectArray[2]); float h = float.Parse(rectArray[3]); rect = new Rect(x, y, w, h); } } #elif UNITY_ANDROID #endif return rect; } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む