20211205のUnityに関する記事は12件です。

Unity 2021.2で強化されたインスペクターの数式機能や、複数要素選択時の線形配置機能について

はじめに Unity 2021.2で、インスペクターの機能が強化されました。多くの数式機能が利用できるようになりました。また、複数要素選択時に線形配置機能やランダム配置、インデックスを用いた配置を利用できるようになりました。 Unityステーション 「Unityをもっと使いやすく!Unityカイゼン委員会!」の本編や告知ツイートでも紹介されていましたね。 リリースノートはこちら。 Editor: Inspector number fields support more math expressions now: functions (sqrt, sin, cos, tan, floor, ceil, round), distribution over multi-selection (L, R) and can refer to current value to change it across multi-selection (+=3, *=2). さて、Unity Stationでも言及されていましたが、どうやらソースコードを読むとリリースノートや公式ドキュメントにない機能が存在するようです。 この投稿では、Unity Stationで言及されていた機能を、該当のソースコードに触れながらテキストとしてまとめます。 ※「Unityをもっと使いやすく!Unityカイゼン委員会!」の動画の該当部分は、15分あたりから! 一様分布機能とランダム分布機能 次のGIFは、複数のCubeを選択中インスペクターにL(-5, 5)と入力すると、各Cubeのx座標が「-5.0f, -2.5f, 0.0f, 2.5f, 5.0f」と配置される様子を示しています。 このように、Unity 2021.2から、複数のゲームオブジェクトを選択中にインスペクターに「L(a, b)」と入力すると、aからbまで線形に均等な値をとるような機能が追加されました。ソースコードに、ここにコメントがあります。 また似た機能として、「R(a, b)」と入力すると、aからbまでのランダムな値を設定できる機能が追加されました。ソースコードには、ここコメントがあります。 数式機能 ソースコードのここによると、「pi」と入力すると、円周率として扱われるようです。 ソースコードのここやここを読む限り、次の数式機能がサポートれているようです。 -で単項演算子の- +で二項演算子+ -で二項演算子- で二項演算子 /で二項演算子/ %で二項演算子% ^でMathf.Pow sqrtでMathf.Sqrt floorでMathf.Floor ceilでMathf.Ceiling roundでMathf.Round cosでMathf.Cos sinでMathf.Sin また、複数選択時に次の演算子をインスペクターに入力すると、すでに入力済みの値に対して、処理した結果で上書き可能です。 += -= *= /= インデックス機能 複数オブジェクト選択中に「#」を押すとそのオブジェクトの選択中のインデックスを利用できます。 これと一様分布と三角関数を使えば、こんなこともできます。12個のオブジェクトを選択中に、次の式をx・yにぞれぞれ入力し、円形にオブジェクトを配置します。 xに「10*sin(#/12*2*pi)」 yに「10*cos(#/12*2*pi)」 ソースコードはここ辺りだと思います。 変数機能 次はxが-10から10まで一様に並んだcubeです。変数機能を使って、 xに「v*1.5」 と入力し、既に入力された値に対して一様な処理(ここでは、x座標を1.5倍)をかけています。 ソースコードはここ辺りだと思います。 まとめ 複数のオブジェクトの座標を、まとめていい感じにしたいときに、活躍しそうですね! (Unityさん、ここら辺もドキュメント化してもらえると、嬉しいです(小声))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

音楽に目覚めたので、楽しくオリジナル曲が作れるゲームを作ってみた

音楽に目覚める 以前から音楽に興味はがあり、ピアノなども触ってはみたのですが長続きせず、途中で挫折することを繰り返している北城です。 皆さんもそのような経験ありますよね?? そんな中であったLogic Pro。 もうね、音楽の知識とか必要なくすごい簡単に曲が作れちゃうんですよ。 楽しくなってしまって、曲作りについて学ぼうと『作りながら覚える 3日で作曲入門』。 この本の通りにやると簡単に1曲作れます。 多くの人に音楽作りを体験して欲しくて、ゲームを作成しました。   お時間のある方は、ぜひやってみてください。 今回は、ゲームを作成する方法の肝についてまとめました。 音楽に必要な3大要素 音楽が、リズム・ハーモニー・メロディーの3大要素によって構成されているそうです。 それぞれが3つあれば、3の3乗通り、つまり、27通りの音楽が作成されます。 上記のゲームは、この27通りから自分の好きな曲を自ら作り、楽しめるゲームになっております。 音を出すための設定 unityで音を出すためには、 1)音を出すスピーカーにあたるAudio Source 2)音の元となるAudio Clip を設置する必要があります。 Clipを設定するためのcodeは以下です。 DrumSoundManager.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class DrumSoundManager : MonoBehaviour { public static DrumSoundManager instance; private void Awake() { if (instance == null) { instance = this; DontDestroyOnLoad(this.gameObject); } else { Destroy(this.gameObject); } } public AudioSource drumAudioSource; // BGMのスピーカー public AudioClip[] drumAudioClips; // BGMの素材( public Slider dslider; //音量調節用のスライダー private void Update() { drumAudioSource.volume = dslider.GetComponent<Slider>().normalizedValue; } public void PlayDrum(string clipName) { drumAudioSource.Stop(); switch (clipName) { default: case "Beach": drumAudioSource.clip = drumAudioClips[0]; break; case "Cafe": drumAudioSource.clip = drumAudioClips[1]; break; case "Night": drumAudioSource.clip = drumAudioClips[2]; break; } drumAudioSource.Play(); } public void DrumStop() { drumAudioSource.Stop(); } } 続いて、以下のように設定を行います。 ① ヒエラルキー上にDrumSoundManagerという空のオブジェクトを作成し、このオブジェクトにAudio Sourceコンポーネントをアタッチします。 ② DrumSoundManagerという空のオブジェクトにDrumSoundManager.CSもアタッチします。 ③ Audio SourceコンポーネントをDrumSoundManager(Script)のDrum Audio Sourceにアタッチします。 ④ Audio Clipsに音源となるMP3データを貼り付けます。 上記の工程をハーモニーとメロディーにも行います。 あとは、ボタンに音楽を開始する関数を貼り付ければ、完成です。 完成したゲームを再掲します。 ぜひ一度遊んでみてください!!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Oculus QuestのPassthrough APIでブラックアウトしてしまう

はじめに Oculus Questで透過型ARを体験できるPassthrough APIにおいて,外の映像がブラックアウトしてしまうという問題がありましたので現時点(2021年12月5日)における原因と解決法について記録しておきます. ためになる記事というよりは,出た障害に困っている人がいればと思い書かせてもらいます. 環境 Unity(2019.4.2f1) Oculus Integration(ver34.0) Oculus Quest(1,2両方で同様の現象が発生) 実装自体は,海外Youtuberのこの動画を参照しながら実装しました. 原因 この原因は,僕がUnity AssetstoreからインストールしたOculus Integration(ver34.0)を利用していたからでした.このOculus IntegrationのOVRPluginのバージョンが古いことが悪さをしているのではないかと思います. 解決方法 ここまで書けばわかると思いますが,Oculus Integration SDK(ver34.0)をOculusのデベロッパーサイトからダウンロードするとうまく動くようになりました.同じ沼にハマった人がいればこれで解決するのではと思います.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JobParallelForの改良案

はじめに FixedUpdateのタイミングに合わせてJobSystemのIJobParallelForで並列処理をする書き方を説明しようと思います。 処理速度を追求した内容ではなく使いやすさを優先で実装した例となります。 実装方法については下記を工夫しましたので最後まで読んで頂ければと思います。 Jobのnewは最初の1回で良さそう。 JobのNativeArrayへの参照も1回設定すれば良さそう。 なので、NativeArrayのメモリはAllocator.Persistentを使って毎回newで確保しない。 更新しないデータはNativeArrayに入れたまま使用する。 NativeArray、Arrayの配列へ要素を追加する場合は末尾に追加する。削除する場合は末尾と入れ替えてから削除する。 外からの要素の追加と削除はbufferを設けてJob実行中にも受けとれるようにする。 データの受け渡しはinterfaceを使用して疎結合にする。 ついでにinterfaceがnullか生存確認をして削除する。 前提知識 この記事はJobsystemのIJobParallelForについての詳細は説明しないので使ったことがある方を対象とします。 JobParallelForについてしか扱わないのでJobと省略して書いています。 バージョンはUnity2021.2.4f1とBurst 1.6.3で動作確認をしています。 全体のおおまかな流れ GameObjetからinterfaceをシングルトンのJobManagerに渡します。 JobManagerはinterfaceを受け取ったら一旦バッファに追加します。 バッファはJobの実行前のタイミングにメモリへ追加または削除します。 メモリの構成は列ごとに1つのオブジェクトのパラメータとして管理します。 データ追加時は最後の列に追加し、データ削除時は使用メモリの最後の列と入れ替えてから削除します。 Jobに必要な値をinterface経由で受け取り、更新してからJobを実行します。 JobはNativeArrayを参照していて列ごとに並列に処理します。 Jobの実行後の結果はinterface経由で返します。 図にしてみましたので参考にしてください。 最初にJobの実装内容を決める まずは下記の2つを決めます。 Jobで処理する内容と変数を決めます。 Jobの実行と完了のタイミングを決めます。 この記事では一番簡単そうなtransformをJobで更新する場合を実装していきます。 こんな感じにオブジェクトはなんでもいいですが、大量に回りながら落下していくのを想定してます。 1. Jobの実行内容と変数 Transformは非Blittable型なのでNativeArrayで直接扱えません。 そこでVector3とQuaternionはNativeArrayで扱えるので、位置をNativeArray<Vector3>で回転をNativeArray<Quaternion>で受け取ってJobで更新して結果を返すようにします。 他には移動速度をNativeArray<Vector3>に回転速度をNativeArray<Quaternion>にします。 今回のようなTransformを更新するだけの場合はIJobParallelForTransformを使ったほうが良いですが、ここではあえてIJobParallelForで実装します。 JobSystemParallelForTemplate.cs //Jobを定義 [BurstCompile] struct MyParallelForJob : IJobParallelFor { public NativeArray<Vector3> positions; public NativeArray<Quaternion> rotations; [Unity.Collections.ReadOnly] public NativeArray<Vector3> moveSpeeds; [Unity.Collections.ReadOnly] public NativeArray<Quaternion> rotationSpeeds; void IJobParallelFor.Execute(int i) { positions[i] += moveSpeeds[i]; rotations[i] *= rotationSpeeds[i]; //高さが-50m以下であれば100m上に移動させる if (positions[i].y < -100.0f / 2.0f) { positions[i] = new Vector3(positions[i].x, positions[i].y + 100.0f, positions[i].z); } } } 2. Jobの実行と完了のタイミング Jobの実行タイミングはなるべく空いている時間に実行して後で結果を受け取るのが理想です。 適切なタイミングをUnity公式のスクリプトライフサイクル等で確認します。 今回は物理演算の後のタイミングにJobを実行し、結果をFixedUpdateの最初のタイミングで受け取ることにします。 Jobの実行タイミング UnityにLateFixedUpdateのイベントが用意されていれば良かったのですが無いのでコルーチンのWaitForFixedUpdateのタイミングを使用します。 new WaitForFixedUpdate()は毎回newせずにキャッシュして再利用します。 Jobの完了タイミング FixedUpdateのタイミングにします。 ただし、他のFixedUpdateが実行されるよりも先に計算結果を得たいのでクラス定義の前に[DefaultExecutionOrder(-20)]を追加して実行タイミングを先になるように調整します。 JobSystemParallelForTemplate.cs [DefaultExecutionOrder(-20)]//実行タイミングを調整する public class JobSystemParallelForTemplate : MonoBehaviour { WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate();//キャッシュ //実行開始 void OnEnable() { //コルーチン開始 StartCoroutine(LateFixedUpdate()); } void OnDisable() { //コルーチン停止 StopCoroutine(LateFixedUpdate()); //jobが実行中だった場合を考慮 jobHandle.Complete(); isJobRunning = false; } //LateFixedUpdateが無いので代わり IEnumerator LateFixedUpdate() { while (true) { yield return waitForFixedUpdate; ArrayUpdate();//追加依頼と削除依頼のバッファを処理する if (ArrayUseLength == 0) continue;//要素がないから終了 DataUpdate();//データを更新する。 JobRun();//Jobの実行を開始する } } void FixedUpdate() { if (ArrayUseLength == 0) return; if (isJobRunning == false) return; jobHandle.Complete(); isJobRunning = false; ResultReturn();//Jobの結果を返す } } あとは順番に実装していきます Jobの実装内容が決まれば残りはほぼ似たような書き方になります。 データを一か所に集めて処理します 一か所に集める方法はなんでもいいですが、jobの実行と結果を受け取るタイミングも利用するのでここではMonoBehaviourをシングルトンで実装します。 このシングルトンの書き方は呼び出したシーンと一緒に削除されます。 マルチシーンで運用する場合はDontDestroyOnLoadを追加したり、先に削除されないようにしてください。 JobSystemParallelForTemplate.cs //シングルトン private static JobSystemTemplate instance; private static bool isDestroy = false; public static JobSystemParallelForTemplate Instance { get { if (instance == null) { if (isDestroy == false) { instance = FindObjectOfType<JobSystemParallelForTemplate>(); if (instance == null) { instance = new GameObject(typeof(JobSystemParallelForTemplate).Name).AddComponent<JobSystemParallelForTemplate>(); } } } return instance; } } 外部オブジェクトから登録と削除の依頼をするためのinterfaceを定義します Jobを実行する側にinterfaceを渡して、渡されたinterface経由で必要なデータを受け渡します。 Jobからみれば外部の実装を考慮しなくてよくなり疎結合にできます。 Jobの結果で直接値を更新しないので後の処理を実装側が柔軟に変更できます。 ここでのinterfaceが持つ役目は下記になります。 外部とのデータの受け渡しの疎結合化 外部のインスタンスの生存確認 追加、削除タイミングの調整 Job実行直前の更新データの取得 Jobの結果を返す IJobConnector //インターフェイスを定義 public interface IJobConnector { Vector3 GetPosition(); Quaternion GetRotation(); Vector3 GetMoveSpeed(); Quaternion GetRotationSpeed(); void SetResult(Vector3 position, Quaternion rotation); int MyArrayIndex { get; set; } } メンバ変数を定義します 確保するメモリの配列は毎回更新するデータと更新しないデータで分けて、無駄な更新をしないように扱いを分けます。 毎回更新するデータはArrayに入れてからCopyFromを使用してNativeArrayに渡します。 NativeArrayは各要素へのアクセスが遅いので少し手間をかけてCopyFromやCopyToでArray経由で渡した方が速いためです。 更新しないデータはNativeArrayに直接入れてそのまま利用します。 interfaceのIJobConnectorはArrayで保持します。 位置と回転は更新するのでArrayとNativeArrayの2つを定義します。 回転速度は今回は変更しないのでNativeArrayに保持します。 追加と削除用のBufferをListで定義します。 他にもメモリ管理やJobの実行に必要なものを定義します。 myParallelForJobもwaitForFixedUpdateもここでnewでインスタンス化します。 JobSystemParallelForTemplate.cs //メモリ管理 const int InitMemoryLength = 256;//初期化と追加でメモリを確保する時の配列サイズ const int JobBatchCount = 0;//Jobのバッチ実行時の分割数(0はコア数に自動設定) int memoryLength = 0; int ArrayUseLength = 0;//配列を使用中のlength //Jobの実行 JobHandle jobHandle; bool isJobRunning = false; //WaitForFixedUpdateのキャッシュ WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate(); //追加と削除のBuffer List<IJobConnector> addBuffer = new List<IJobConnector>(); List<IJobConnector> removeBuffer = new List<IJobConnector>(); //Interfaceの参照の保持 IJobConnector[] connectorArray = new IJobConnector[InitMemoryLength]; //NativeArrayにコピーするときに使う普通の配列 Vector3[] positionArray = new Vector3[InitMemoryLength]; Quaternion[] rotationArray = new Quaternion[InitMemoryLength]; Quaternion[] rotationSpeedArray = new Quaternion[InitMemoryLength]; //Jobの計算に使うNativeArrayの配列 NativeArray<Vector3> positions; NativeArray<Quaternion> rotations; NativeArray<Vector3> moveSpeeds; NativeArray<Quaternion> rotationSpeeds; //Jobを作成 MyParallelForJob myParallelForJob = new MyParallelForJob(); 必要な関数を実装します 関数をリストにしてみたら結構多かったですが順番に説明していきます。 InitMemory() Dispose() Resize() Add(IPredictionJobConnector item) Remove(IPredictionJobConnector item) Remove(int index) ArrayUpdate() DataUpdate() ResultReturn() InitMemory Awakeのタイミングでメモリを確保します。 確保時のAllocator.Persistentを使用してこのオブジェクトが存在する限り確保し続けます。 Allocator.Persistentはメモリ割り当てと解放が遅いと書かれていますが、毎回Jobを実行する度にメモリを確保して開放をするよりは速いです。 ずっと確保したままなのでJobへの参照もここで設定しておきます。 JobSystemParallelForTemplate.cs private void Awake() { //メモリを確保 InitMemory(); } //Jobで使用するメモリを初期化 void InitMemory() { this.memoryLength = InitMemoryLength; //初期設定でメモリを確保 positions = new NativeArray<Vector3>(this.memoryLength, Allocator.Persistent); rotations = new NativeArray<Quaternion>(this.memoryLength, Allocator.Persistent); moveSpeeds = new NativeArray<Vector3>(this.memoryLength, Allocator.Persistent); rotationSpeeds = new NativeArray<Quaternion>(this.memoryLength, Allocator.Persistent); //Jobの参照を設定 myParallelForJob.positions = positions; myParallelForJob.rotations = rotations; myParallelForJob.moveSpeeds = moveSpeeds; myParallelForJob.rotationSpeeds = rotationSpeeds; } Dispose OnDestroyのタイミングでメモリを解放します。 参照型を入れているArrayもnullにします。 JobSystemParallelForTemplate.cs void OnDestroy() { //Jobの完了を待つ jobHandle.Complete(); isJobRunning = false; //メモリを解放 DisposeMemory(); instance = null; } //使用メモリの解放 void DisposeMemory() { //NativeArrayの開放 positions.Dispose(); rotations.Dispose(); moveSpeeds.Dispose(); rotationSpeeds.Dispose(); //参照を持つArrayをnullにする connectorArray = null; } Resize 要素を追加するときにサイズが足りなかった場合拡張します。 本来はなるべくリサイズを行わないように設計するのが理想だと思いつつとりあえず実装します。 ArrayはResize()で簡単にリサイズできるのですがNativeArrayにResize関数は無いです。 なので同様に一行で書きたかったので関数を自作しました。 リサイズ後は関数内でNativeArrayをDispose()しているので忘れずにJobの参照も設定しておきます。 JobSystemParallelForTemplate.cs //使用メモリのサイズ変更 void Resize(int memoryLength) { this.memoryLength = memoryLength; //要素のコピーが必要ない場合はfalseを追加 ResizeNativeArray(ref positions, memoryLength, false); ResizeNativeArray(ref rotations, memoryLength, false); ResizeNativeArray(ref moveSpeeds, memoryLength, true); ResizeNativeArray(ref rotationSpeeds, memoryLength, false); //Jobの参照を設定 myParallelForJob.positions = positions; myParallelForJob.rotations = rotations; myParallelForJob.moveSpeeds = moveSpeeds; myParallelForJob.rotationSpeeds = rotationSpeeds; //配列のサイズを変更 Array.Resize(ref connectorArray, memoryLength); Array.Resize(ref positionArray, memoryLength); Array.Resize(ref rotationArray, memoryLength); Array.Resize(ref rotationSpeedArray, memoryLength); } //NativeArrayのリサイズ用関数 void ResizeNativeArray<T>(ref NativeArray<T> nativeArray, int length, bool dataCopy = true) where T : struct { if (dataCopy == true) { //データをコピーしてリサイズ var temp = new T[nativeArray.Length]; nativeArray.CopyTo(temp); nativeArray.Dispose(); nativeArray = new NativeArray<T>(length, Allocator.Persistent); Array.Resize(ref temp, length);//tempのサイズを変更 nativeArray.CopyFrom(temp); } else { //データをコピーしないでリサイズ nativeArray.Dispose(); nativeArray = new NativeArray<T>(length, Allocator.Persistent); } } AddRequest、RemoveRequest、Add、Remove AddRequest、RemoveRequestを外部から呼び出してBufferに追加します。 Bufferに追加された要素はWaitForFixedUpdateのタイミングに次で説明するArrayUpdate内でAdd、Removeを呼び出して実際にメモリを更新します。 Bufferを用意する理由はJobの実行中にNativeArrayを変更しないようにするためです。 JobSystemParallelForTemplate.cs //Job利用の追加依頼をバッファに貯めておく public void AddRequest(IJobConnector item) { addBuffer.Add(item); } //Job利用の削除依頼をバッファに貯めておく public void RemoveRequest(IJobConnector item) { removeBuffer.Add(item); } void Add(IJobConnector item) { //足りなければメモリサイズを拡張 if (memoryLength < ArrayUseLength + 1) { //メモリサイズを+1では無くInitMemoryLengthずつ加算で増やす。 memoryLength += InitMemoryLength; Resize(memoryLength); } //結果を返すために相手のInterfaceを保持する connectorArray[ArrayUseLength] = item; //毎フレーム更新しないデータはここで代入しておく。 moveSpeeds[ArrayUseLength] = item.GetMoveSpeed(); item.MyArrayIndex = ArrayUseLength; ++ArrayUseLength; } void Remove(int index) { if (index != -1) { --ArrayUseLength; //参照するデータは配列の最後と入れ替えてからnullにする。 connectorArray[index] = connectorArray[ArrayUseLength]; connectorArray[ArrayUseLength] = null; //入れ替えた側のIndexを更新 connectorArray[index].MyArrayIndex = index; //更新しないデータはindexがずれるので配列の最後と入れ替える。 moveSpeeds[index] = moveSpeeds[ArrayUseLength]; } } ArrayUpdate、DataUpdate、JobRun WaitForFixedUpdateから順番にArrayUpdate、DataUpdate、JobRunを呼びます。 呼び出し元のLateFixedUpdateも再掲載しておきます。 ArrayUpdate 外から登録されたBufferでAdd、Removeを呼び出してArrayを更新します。 interfaceがnullになっている場合もここで削除します。 DataUpdate 毎回更新が必要なデータだけを更新します。 NativeArray.CopyFrom()を使用した方が速いのでこういう実装になってます。 更新が必要ないデータはAddの時に代入しておくのでここでは更新しません。 JobRun Jobを実行します。 JobのScheduleで実行する配列のサイズにArrayUseLengthの値を指定しています。 Jobを実行したらisJobRunningをtrueにします。 JobSystemParallelForTemplate.cs //LateFixedUpdateが無いので代わり IEnumerator LateFixedUpdate() { while (true) { yield return waitForFixedUpdate; ArrayUpdate();//追加依頼と削除依頼のバッファを処理する if (ArrayUseLength == 0) continue;//要素がないから終了 DataUpdate();//データを更新する。 JobRun();//Jobの実行を開始する } } //配列を更新 void ArrayUpdate() { //connectorArrayがnullになっていた場合は削除(forで逆順処理) for (int i = ArrayUseLength - 1; 0 <= i; i--) { if (connectorArray[i].Equals(null)) { Remove(i); } } //追加バッファを処理 foreach (IJobConnector item in addBuffer) { if (item.Equals(null)) continue; Add(item); } addBuffer.Clear(); //削除バッファを処理 foreach (IJobConnector item in removeBuffer) { if (item.Equals(null)) continue; //indexと中身が違っていたらエラー if (connectorArray[item.MyArrayIndex].Equals(item) == false) { Debug.LogError("No target in the index"); continue; } Remove(item.MyArrayIndex); item.MyArrayIndex = -1; } removeBuffer.Clear(); } //データを更新 void DataUpdate() { for (int i = 0; i < ArrayUseLength; i++) { //毎フレーム更新するデータをArrayに代入 positionArray[i] = connectorArray[i].GetPosition(); rotationArray[i] = connectorArray[i].GetRotation(); rotationSpeedArray[i] = connectorArray[i].GetRotationSpeed(); } //NativeArrayの各要素へのアクセスは遅い //Arrayを経由してCopyFromでコピーした方が速い positions.CopyFrom(positionArray); rotations.CopyFrom(rotationArray); rotationSpeeds.CopyFrom(rotationSpeedArray); } void JobRun() { //Jobの実行順番を整理 jobHandle = myParallelForJob.Schedule(ArrayUseLength, JobBatchCount); //Jobを実行 JobHandle.ScheduleBatchedJobs(); isJobRunning = true; } ResultReturn FixedUpdateのタイミングにJobの完了を待って、Jobの結果をinterfaceで返します。 Jobが完了したらisJobRunningをfalseにします。 isJobRunningの判定でJobの実行前に結果を返さないようにしています。 呼び出し元のFixedUpdateも再掲載しておきます。 JobSystemParallelForTemplate.cs void FixedUpdate() { if (ArrayUseLength == 0) return; if (isJobRunning == false) return; jobHandle.Complete(); isJobRunning = false; ResultReturn();//Jobの結果を返す } //結果を返す void ResultReturn() { //結果の反映 positions.CopyTo(positionArray); rotations.CopyTo(rotationArray); for (int i = 0; i < ArrayUseLength; i++) { if (connectorArray[i].Equals(null)) continue; connectorArray[i].SetResult(positionArray[i], rotationArray[i]); } } Jobを使う側のスクリプト interfaceを継承してパラメータを渡す関数と結果を受け取る関数を実装します。 OnEnableにJobSystemTemplate.Instance?.AddRequest(this)で追加して OnDisableにJobSystemTemplate.Instance?.RemoveRequest(this)で削除します。 このスクリプトをGameObjectに追加して使います。 もし弾などに使う場合はJobの結果を実装側の都合でRigidbody.MovePositon(position)などに書き換えたりします。 UseJobTemplate.cs using UnityEngine; using A_rosuko.JobSystemParallelForTemplate; public class UseJobTemplate : MonoBehaviour, IJobConnector { Transform myTransform; [SerializeField] float moveSpeedRange = -0.03f; [SerializeField] float rotationSpeedRange = 10f; Vector3 moveSpeed; Quaternion rotationSpeed; void Awake() { myTransform = transform; rotationSpeed = Quaternion.Euler(0, Random.value * rotationSpeedRange, 0); moveSpeed = new Vector3(0, moveSpeedRange, 0); } void OnEnable() { JobSystemParallelForTemplate.Instance?.AddRequest(this); } void OnDisable() { JobSystemParallelForTemplate.Instance?.RemoveRequest(this); } public Vector3 GetPosition() { return myTransform.position; } public Quaternion GetRotation() { return myTransform.rotation; } public Quaternion GetRotationSpeed() { return rotationSpeed; } public Vector3 GetMoveSpeed() { return moveSpeed; } public void SetResult(Vector3 position, Quaternion rotation) { //Jobの結果を反映 myTransform.SetPositionAndRotation(position, rotation); } public int MyArrayIndex { get; set; } = -1; } 全スクリプト JobSystemParallelForTemplate.cs using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Collections; using Unity.Jobs; using Unity.Burst; namespace A_rosuko.JobSystemParallelForTemplate { //インターフェイスを定義 public interface IJobConnector { Vector3 GetPosition(); Quaternion GetRotation(); Vector3 GetMoveSpeed(); Quaternion GetRotationSpeed(); void SetResult(Vector3 position, Quaternion rotation); int MyArrayIndex { get; set; } } [DefaultExecutionOrder(-20)]//実行タイミングを調整する public class JobSystemParallelForTemplate : MonoBehaviour { //シングルトン private static JobSystemParallelForTemplate instance; private static bool isDestroy = false; //メモリ管理 const int InitMemoryLength = 256;//初期化と追加でメモリを確保する時の配列サイズ const int JobBatchCount = 0;//Jobのバッチ実行時の分割数(0はコア数に自動設定) int memoryLength = 0; int ArrayUseLength = 0;//配列を使用中のlength //Jobの実行 JobHandle jobHandle; bool isJobRunning = false; //WaitForFixedUpdateのキャッシュ WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate(); //追加と削除のBuffer List<IJobConnector> addBuffer = new List<IJobConnector>(); List<IJobConnector> removeBuffer = new List<IJobConnector>(); //Interfaceの参照の保持 IJobConnector[] connectorArray = new IJobConnector[InitMemoryLength]; //NativeArrayにコピーするときに使う普通の配列 Vector3[] positionArray = new Vector3[InitMemoryLength]; Quaternion[] rotationArray = new Quaternion[InitMemoryLength]; Quaternion[] rotationSpeedArray = new Quaternion[InitMemoryLength]; //Jobの計算に使うNativeArrayの配列 NativeArray<Vector3> positions; NativeArray<Quaternion> rotations; NativeArray<Vector3> moveSpeeds; NativeArray<Quaternion> rotationSpeeds; //Jobを作成 MyParallelForJob myParallelForJob = new MyParallelForJob(); //Jobを定義 [BurstCompile] struct MyParallelForJob : IJobParallelFor { public NativeArray<Vector3> positions; public NativeArray<Quaternion> rotations; [Unity.Collections.ReadOnly] public NativeArray<Vector3> moveSpeeds; [Unity.Collections.ReadOnly] public NativeArray<Quaternion> rotationSpeeds; void IJobParallelFor.Execute(int i) { positions[i] += moveSpeeds[i]; rotations[i] *= rotationSpeeds[i]; //高さが-50m以下であれば100m上に移動させる if (positions[i].y < -100.0f / 2.0f) { positions[i] = new Vector3(positions[i].x, positions[i].y + 100.0f, positions[i].z); } } } private void Awake() { //メモリを確保 InitMemory(); } //Jobで使用するメモリを初期化 void InitMemory() { this.memoryLength = InitMemoryLength; //初期設定でメモリを確保 positions = new NativeArray<Vector3>(this.memoryLength, Allocator.Persistent); rotations = new NativeArray<Quaternion>(this.memoryLength, Allocator.Persistent); moveSpeeds = new NativeArray<Vector3>(this.memoryLength, Allocator.Persistent); rotationSpeeds = new NativeArray<Quaternion>(this.memoryLength, Allocator.Persistent); //Jobの参照を設定 myParallelForJob.positions = positions; myParallelForJob.rotations = rotations; myParallelForJob.moveSpeeds = moveSpeeds; myParallelForJob.rotationSpeeds = rotationSpeeds; } void OnDestroy() { //Jobの完了を待つ jobHandle.Complete(); isJobRunning = false; //メモリを解放 DisposeMemory(); instance = null; } //使用メモリの解放 void DisposeMemory() { //NativeArrayの開放 positions.Dispose(); rotations.Dispose(); moveSpeeds.Dispose(); rotationSpeeds.Dispose(); //参照を持つArrayをnullにする connectorArray = null; } //使用メモリのサイズ変更 void Resize(int memoryLength) { this.memoryLength = memoryLength; //要素のコピーが必要ない場合はfalseを追加 ResizeNativeArray(ref positions, memoryLength, false); ResizeNativeArray(ref rotations, memoryLength, false); ResizeNativeArray(ref moveSpeeds, memoryLength, true); ResizeNativeArray(ref rotationSpeeds, memoryLength, false); //Jobの参照を設定 myParallelForJob.positions = positions; myParallelForJob.rotations = rotations; myParallelForJob.moveSpeeds = moveSpeeds; myParallelForJob.rotationSpeeds = rotationSpeeds; //配列のサイズを変更 Array.Resize(ref connectorArray, memoryLength); Array.Resize(ref positionArray, memoryLength); Array.Resize(ref rotationArray, memoryLength); Array.Resize(ref rotationSpeedArray, memoryLength); } //実行開始 void OnEnable() { //コルーチン開始 StartCoroutine(LateFixedUpdate()); } void OnDisable() { //コルーチン停止 StopCoroutine(LateFixedUpdate()); //jobが実行中だった場合を考慮 jobHandle.Complete(); isJobRunning = false; } //LateFixedUpdateが無いので代わり IEnumerator LateFixedUpdate() { while (true) { yield return waitForFixedUpdate; ArrayUpdate();//追加依頼と削除依頼のバッファを処理する if (ArrayUseLength == 0) continue;//要素がないから終了 DataUpdate();//データを更新する。 JobRun();//Jobの実行を開始する } } void FixedUpdate() { if (ArrayUseLength == 0) return; if (isJobRunning == false) return; jobHandle.Complete(); isJobRunning = false; ResultReturn();//Jobの結果を返す } //Job利用の追加依頼をバッファに貯めておく public void AddRequest(IJobConnector item) { addBuffer.Add(item); } //Job利用の削除依頼をバッファに貯めておく public void RemoveRequest(IJobConnector item) { removeBuffer.Add(item); } //配列を更新 void ArrayUpdate() { //connectorArrayがnullになっていた場合は削除(forで逆順処理) for (int i = ArrayUseLength - 1; 0 <= i; i--) { if (connectorArray[i].Equals(null)) { Remove(i); } } //追加バッファを処理 foreach (IJobConnector item in addBuffer) { if (item.Equals(null)) continue; Add(item); } addBuffer.Clear(); //削除バッファを処理 foreach (IJobConnector item in removeBuffer) { if (item.Equals(null)) continue; //indexと中身が違っていたらエラー if (connectorArray[item.MyArrayIndex].Equals(item) == false) { Debug.LogError("No target in the index"); continue; } Remove(item.MyArrayIndex); item.MyArrayIndex = -1; } removeBuffer.Clear(); } void Add(IJobConnector item) { //足りなければメモリサイズを拡張 if (memoryLength < ArrayUseLength + 1) { //メモリサイズを+1では無くInitMemoryLengthずつ加算で増やす。 memoryLength += InitMemoryLength; Resize(memoryLength); } //結果を返すために相手のInterfaceを保持する connectorArray[ArrayUseLength] = item; //毎フレーム更新しないデータはここで代入しておく。 moveSpeeds[ArrayUseLength] = item.GetMoveSpeed(); item.MyArrayIndex = ArrayUseLength; ++ArrayUseLength; } void Remove(int index) { if (index != -1) { --ArrayUseLength; //参照するデータは配列の最後と入れ替えてからnullにする。 connectorArray[index] = connectorArray[ArrayUseLength]; connectorArray[ArrayUseLength] = null; //入れ替えた側のIndexを更新 connectorArray[index].MyArrayIndex = index; //更新しないデータはindexがずれるので配列の最後と入れ替える。 moveSpeeds[index] = moveSpeeds[ArrayUseLength]; } } //データを更新 void DataUpdate() { for (int i = 0; i < ArrayUseLength; i++) { //毎フレーム更新するデータをArrayに代入 positionArray[i] = connectorArray[i].GetPosition(); rotationArray[i] = connectorArray[i].GetRotation(); rotationSpeedArray[i] = connectorArray[i].GetRotationSpeed(); } //NativeArrayの各要素へのアクセスは遅い //Arrayを経由してCopyFromでコピーした方が速い positions.CopyFrom(positionArray); rotations.CopyFrom(rotationArray); rotationSpeeds.CopyFrom(rotationSpeedArray); } void JobRun() { //Jobの実行順番を整理 jobHandle = myParallelForJob.Schedule(ArrayUseLength, JobBatchCount); //Jobを実行 JobHandle.ScheduleBatchedJobs(); isJobRunning = true; } //結果を返す void ResultReturn() { //結果の反映 positions.CopyTo(positionArray); rotations.CopyTo(rotationArray); for (int i = 0; i < ArrayUseLength; i++) { if (connectorArray[i].Equals(null)) continue; connectorArray[i].SetResult(positionArray[i], rotationArray[i]); } } public static JobSystemParallelForTemplate Instance { get { if (instance == null) { if (isDestroy == false) { instance = FindObjectOfType<JobSystemParallelForTemplate>(); if (instance == null) { instance = new GameObject(typeof(JobSystemParallelForTemplate).Name).AddComponent<JobSystemParallelForTemplate>(); } } } return instance; } } void OnApplicationQuit() { isDestroy = true; instance = null; } //NativeArrayのリサイズ用関数 void ResizeNativeArray<T>(ref NativeArray<T> nativeArray, int length, bool dataCopy = true) where T : struct { if (dataCopy == true) { //データをコピーしてリサイズ var temp = new T[nativeArray.Length]; nativeArray.CopyTo(temp); nativeArray.Dispose(); nativeArray = new NativeArray<T>(length, Allocator.Persistent); Array.Resize(ref temp, length);//tempのサイズを変更 nativeArray.CopyFrom(temp); } else { //データをコピーしないでリサイズ nativeArray.Dispose(); nativeArray = new NativeArray<T>(length, Allocator.Persistent); } } } } Gistにもおいておきます。 リンクはこちら 補足 intreface == nullは正常に判定されないようなのでintreface.Equals(null)と書きました。 この記事を書いてる途中にRemoveの高速化案を思いついたので入れてみました。 interfaceの実装側に依存する書き方になるのであまり良くありませんが、頻繁に追加と削除を繰り返す場合に有効だと思います。 最初の実装ではArray内を検索して削除していたのですが、下記を変更しています。 メモリへAdd時にオブジェクト側にindexを渡してをキャッシュしておきます。 Remove時にindexも一緒に渡せば対象をArrayから探さずに削除できます。 別のオブジェクトのRemoveで順番の入れ替えが発生した場合はindexを更新します。 まとめ Jobの内容と実行タイミングを決めれば、あとはメモリ確保の違いだけでほぼ一本道で完成できると思います。 メモリの管理については愚直に1行づつ書いているので、数が多くなるとミスが発生しやすくなります。 構造体でまとめて扱えば楽そうですがNativeArrayのCopyFrom、CopyToのコピーが思ったより速かったので今回の実装にしました。 下記の参考のしゅみぷろ様のサイトではunsafeでポインタでNativeArrayにアクセスする方法も書かれていて参考になります。 さいごに このスクリプトは実行タイミング、実行条件、実行内容、データ、それらの接続を分けることを意識して使いやすさを優先で書きました。 Transformを更新するだけの場合はIJobParallelForTransformを使った方が倍以上速いですし、データの渡し方も工夫の余地もまだありそうです。 もしかしたらECSを使うとすっきり書けるのかもしれませんが、難しかったのとpreviewだったのでよく分かってません。 良い書き方や改善案があれば教えてください。 ここに書かれている私が書いたスクリプトはCC0としますので好きに使って頂いてかまいません。 JobSystemは本当に高速に実行できるのでもっと気軽に簡単に扱えるようになるといいですね。 少し長くなってしまいましたが、最後まで読んでいただいてありがとうございました。 この記事が皆様のお役に立てればうれしいです。 参考 JobSystemの理解をするのにとても助かりました。 いつもわかりやすい記事をありがとうございます。 しゅみぷろ SpringBoneのJobSystem化でわかったこと ECSとJobSystem 基礎 テラシュールブログ 【Unity】C# Job Systemを自分なりに解説してみる かめくめ UnityのC# Job SystemとBurst Compilerを使ってみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(Unity LFS)UnityでLFSを使用した時の拡張子が無名ファイルへの対処法

UnityでLFS(Large File Storage)を使用すると、100MB以上のファイルを扱えます。 今回は、 Library/Artifacts/以下の拡張子が無名のファイルへの対処法です。 LFSは、.gitattributesにファイルの種類を指定して、使用します。 Library/Artifacts/以下に拡張子が無名のファイルがかなり存在します。 これらのファイルには、半角の空白を指定することで対応できました。 .gitattributes * filter=lfs diff=lfs merge=lfs -text インターネットの検索に書いていなかったので、困っている方がいれば一助になれれば嬉しいです。 ※この設定をすると、LFSのデータが結構溜まります。 VidelLinkでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同人プロジェクトマネジメント入門:計画編その1

同人プロジェクトマネジメント入門の目次はこちら はじめに 計画編では、計画をどう立てるかについてだけでなく、どう実行するかまで包括的に解説をします。 今回は、計画の基本的なポイントについて列挙していきます。 仕事の割り当て方法 いつ割り当てるのか 小出しに割り当てること 計画を立てる時は長期的に考える逆算方式と、今週は何をするかなどを決める積み上げ方式の2つのやり方があると思いますが、ゴールが自由な同人プロジェクトでは、積み上げ方式をお勧めします。その理由は以下の2つです 未来の予測はできないから:同人プロジェクトでは、そもそもちゃんと工数の予測などをせずに、しかもメンバーの力もあまり分からない状況で話が進む場合が多く、未来の進捗状況を予測することが極めて難しいです。比較的確実にわかる来週再来週あたりの内容だけを決めるのが最も効率がいいです。 仕事の巻取りリスクを減らすため:計画がずれたときに一番手軽な解決方法が「進んでない奴の仕事を進んでる奴に渡す」です。しかし、基本的に人間の優秀さはばらつきます。自分の仕事がどんどん巻き取られていくのを見てうれしいメンバーは居ません。参加する意欲すら失ってしまいますし、仕事を巻き取る側も「私の仕事ばっかり増えて!」と損した気持ちになります。だれも幸せにならない手法です。この状況を回避するために小出し割り当てを使います。もともと割り当てられていないプールから仕事を割り当てたなら、心理的なダメージを回避できます。 待ちを生まないこと 「Xさんの仕事が終わらなかったのでできませんでした」という発言が出る状況を作らないことが重要です。 仕事はなくなった瞬間に新規に割り当てましょう。 このときに重要なのが、以前に解説したタスクの炎上しやすさです。 依存関係の少ない仕事を多めに用意している場合には、手が空いている人に即座に仕事を割り当てられます。また、単調な仕事であれば能力が足りないメンバーでも速度が遅いだけでマイナス進捗を生むことはありません。 もしも、Xさんの仕事が終わらないと着手できない仕事ばかりで、それでもなおXさんの仕事が遅いことが原因でプロジェクトが遅れているならば、マネージャーとしては、Xさんと話し合ってきちんと納得させた状態でXさんから仕事を巻き上げることが必要です。 失踪判定・モチベーション消滅判定はマネージャーがする 基本的にはプロジェクトメンバーは、「出来るか分からないけどやります」とは言っても「出来ませんでした」が言えません。しっかり報告できるタイプのメンバーは大体熱意があって有能な人のでさらっと熟してしまいます。 したがって、定期的にコミュニケーションをとることを徹底したうえで、一定時間返信がなければ無言で仕事を巻き取って良いです。待ってもあまりいいことはありません。もともと頑張ってくれているメンバーのモチベーションを奪うような人間関係上の行動は止めた方がいいですが、プロジェクトのボトルネックになっているメンバーのモチベーションは基本的に無いので、どれだけ下がろうがプロジェクトに影響しません。 再割り当ての判断は早い方がいいです。この判断を簡単に出来るように、あまり割り当て量を増やさないということが重要です。 誰に割り当てるのか 強いメンバーにクリティカルなタスクを割振り、弱いメンバーに単純労働を割り振る クリティカルなタスクとは、依存関係やタスクの大きさから考えて、完成しないとプロジェクトが炎上しかねないタスクです。 プロジェクト開発初期段階でも、メンバーごとの能力は分かってくると思うので、その時点で割り当ての方針をマネージャーが決めて下さい。マネージャーが決めた割振りならあまり不満感は出ません。「しゃーなしやる」感を減らす唯一の方法は、事前に決めたルールに従って粛々と仕事を割り振ることです。 もしも、クリティカルな仕事をそれをやるだけの能力がないメンバーに割り振った場合、そのメンバーのモチベーションを犠牲にして強制的に仕事を取り上げるか、事なかれ主義を貫いてじりじりと納期を遅らせるしかなくなります。 したがって、出来るだけ早期に能力を見極めることが重要です。もしも、だれにも能力がない場合には、外部の人間を捕まえて何とかしてもらうというのもアリです。 これをうまく見極めるための方法として、プロジェクトの規模が半年以上でメンバーも4,5人以上になる場合には、最初に超小型プロジェクトを作ってみる方法もおすすめです。一度プロジェクトをやらせてみれば簡単に能力を推測できます。 学習コストの考え方 特定の技術分野においてはどうしてもプロジェクトメンバーに学習してもらうという手続きも必要です。しかしながら、学習というのは以下の理由から基本的にリスキーな選択肢です。 学習期間は学び終わるまでに分からない 慣れていない技術を使うことになる 不慣れが原因で設計がぐちゃぐちゃになった場合、後に響く これらのリスクはできるだけ負わないことが賢明です。プロジェクトの成功だけを目指すなら、枯れた技術を愚直に使う方がいいと思います。 どうしても学習する必要がある分野がある場合には、次の項で解説する「先に金で何とかしてから置き換える」を応用した「先にライブラリで出来るだけ楽に実装してから、自作実装に徐々に置き換える」という方法をとることを検討してみてください。 なにを割り当てるか 「もう先にアセット使ってしまった方がいい」という発想 仕様書作成編では、「金を使って時間を買え!」と書きましたが、金を使わないと決めた素材にさえ金を使うことが出来ます。 具体的には、「UI画像が完成するまでレイアウトが分からんよ」とか「3Dモデルが来るまでアクションパートはきついな」とかの状況は、仮にUI画像を完成品のゲームで使わなくても、仮のUI画像を入手しておけば、待ちを減らせます。 この待ちとは要するに仕事同士の依存関係です。すなわち、タスクの炎上しやすさに直結する事項であり、きわめて重要な要素です。 「先にアセットを買って、それを使ってくみ上げて、後からアセットを自作素材で置き換える」という方法をとると、一応の完成が早くなりますから、計画を立てる上でも非常に効果的な方法です。 また、仕様変更にはなるものの、最悪仮の素材でゲームを完成と言い張ることもできます。 「細かい仕様決定」を実装者の仕事にするのもよい 同人プロジェクトでは、仕様が細かく決まり切らず「いい感じによろしく」という指示が発生したり、「全部俺の言うことを聞け」という人が発生したりします。これは仕様の認識のずれや、プロジェクトの人間関係の歪み、特定の人間への依存を生みやすくあまり良くない状況です。そこで、「大まかなやりたいことをマネージャーから伝えて、それを実装する人が細かい仕様を決める。仕様決めの結果は進捗報告会で報告する」という方法をおすすめします。実装者が実装の視点をもって細かい仕様を決定し、仕様決めの負担を分散して、進捗として報告することで認識のずれを防ぐことが出来ます。また、仕様決定は比較的楽しいので、この段階で失踪することは少ないですし、大まかな仕様が決まっているので暴走して多くの仕様を詰め込むこともできません。細かいことの予測なんてどうせつかないので、マネージャー側で全部対応しようとすること自体が間違っているでしょう。 テスト実装の罠 仕様が複雑だと、とりあえずTestXXXというシーンを作ったり、TestScriptという名前のスクリプトを作ったりしたくなります。しかし、この手のスクリプトは、同人プロジェクトでは特にですが、かなりの確率で結局流用されて本番のコードにも混ざります。つまり、全然テストじゃないということです。テストというのはテストという品質保証の全然楽しくない作業をするという意味であり、雑に設計をする言い訳ではないのです。したがって、テストは後に書かせましょう。テスト駆動開発とかの考え方もありますが、あれは最初にテストという実行可能な仕様書を用意しようという、TestXXXとかの雑さとは対極にあるものであり、テストという名前の付いたナニカを最初に書いている、という理由で同一視していいものではありません。 これを阻止するためには、仕様書時点でタスクを小さくしましょう。そして、その範囲で確実にしっかりと設計を考えさせましょう。素人の考えた設計だろうと、雑に考えたものをアドホックに拡張するよりマシです。 おわりに 計画の設定と実行は、マネージャーの基本的な作業になります。この作業をしている時間が一番長いので、計画のうまさにマネージャーの実力が現れると思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでルービックキューブ

はじめに 思い付きでルービックキューブを作ってみたのでその記録として残す。 実際に動く画面がこちら ルービックキューブ作ってみた 改善点は多い pic.twitter.com/qhP8xPnuFj— momantai (@momantai_jp) December 4, 2021 めんどくさくて平面での操作にしたがちゃんと3Dのモデルは作っている。 どんな流れで思考したかの記録。 プロジェクトファイルはここ スワイプ機能 まずスマホでタッチして操作できるようにしようと思い実装。 こちらを参考にした 【Unity】ドラッグ(スワイプ)でオブジェクトを移動させる ルービックキューブの作成 blenderを使ってCubeを26個(見える部分)、それぞれの色をPlaneで張り付けた。 Unityにimportする際に中央にCubeを用意し、rubic1~27の名前を付けている。 今回のやり方では別にUnityの方でCubeとPlaneで用意してもいいと思う。 回転の実装 回転させたいCubeを選択し、縦横を選択して動かせるように考えたのが以下の方法。 1. 3×3×3のstring型の3次元配列を用意し、rubic1~27をそれぞれ入れる 2. タッチしたCubeの名前を取得し、配列内を検索し、添字を求める。 3. 回転するCubeを取得し、その内1つのCubeを親としそれ以外を子にする。 4. 回転中心を回転面の真ん中のCubeにして90度回転させる。 5. 親子関係を解消させる 6. 3次元配列の中身を回転に応じて書き換える。 回転させるCubeの取得はRayを飛ばしてやっている。 長くなるので左右回転のみ記述 using System.Collections; using System.Collections.Generic; using UnityEngine; public class ScreenInput : MonoBehaviour { GameObject target; [SerializeField] float mouse_sensitivity = 0.2f; [SerializeField] float touch_sensitivity = 0.001f; GameObject[] childobjects= new GameObject[9]; //回転させるCubeを格納する配列 string[,,] rubic= new string[3,3,3]; //ルービックキューブを表す配列 string[,,] crubic= new string[3,3,3]; //回転させた後書き換える用の配列 int i,j,k,ti,tj,tk,n=1; void Start() { //1. 3×3×3のstring型の3次元配列を用意し、rubic1~27をそれぞれ入れる for(i=0;i<3;i++){ for(j=0;j<3;j++){ for(k=0;k<3;k++){ rubic[i,j,k] = "rubic"+n; crubic[i,j,k] = "rubic"+n; n++; } } } } void Update() { //2.タッチしたオブジェクトを取得する if(Input.GetMouseButtonDown(0)){ target = null; Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit = new RaycastHit(); if(Physics.Raycast(ray, out hit)){ target = hit.collider.gameObject; //2.オブジェクトがどこにあるか探索 for(i=0;i<3;i++){ for(j=0;j<3;j++){ for(k=0;k<3;k++){ if(target.name == rubic[i,j,k]){ ti=i; tj=j; tk=k; } } } } } } //ドラッグ中もしくはスワイプ中 if (Input.GetMouseButtonUp(0)) { float dx,dy; Vector3 point; //マウスの場合 dx = Input.GetAxis("Mouse X") * mouse_sensitivity; dy = Input.GetAxis("Mouse Y") * mouse_sensitivity; //タッチの場合 if (Input.touchCount > 0) { dx = Input.touches[0].deltaPosition.x * touch_sensitivity; dy = Input.touches[0].deltaPosition.y * touch_sensitivity; } if(dx > 0.001f || dx < -0.001f){//左or右回転 //3.回転させるrubic[i,j,k]を取得する iはrayで取得したもので固定 i=0; for(j=0;j<3;j++){ for(k=0;k<3;k++){ childobjects[i] = GameObject.Find(rubic[ti,j,k]); i++; } } //4.実際に回転させる point = childobjects[4].transform.position; for(i=1;i<9;i++){ childobjects[i].transform.SetParent(childobjects[0].transform); } if(dx < -0.001f){//右回転 childobjects[0].transform.RotateAround(point,Vector3.up,90); for(j=0;j<3;j++){ for(k=0;k<3;k++){ rubic[ti,j,k] =crubic[ti,3-k-1,j]; } } } else if(dx > 0.001f){//左回転 childobjects[0].transform.RotateAround(point,Vector3.down,90); for(j=0;j<3;j++){ for(k=0;k<3;k++){ rubic[ti,j,k] =crubic[ti,k,3-1-j]; } } } //5. 親子関係を解消させる for(i=1;i<9;i++){ childobjects[i].transform.parent= null; } //6. 3次元配列を書き換える for(i=0;i<3;i++){ for(j=0;j<3;j++){ for(k=0;k<3;k++){ crubic[i,j,k] = rubic[i,j,k]; } } } } } } } 現在ある問題点 縦横の操作の区別が難しい。→xy方向にベクトル量で詳しく設定するとかしないとダメかな~という所。3Dに見えるような視点で操作させるならさらに色々必要? 最初の動画のように白面を正面にしている所からのスタートになる。 本来のルービックキューブを考えるとランダムに回してからのスタートや別の面を正面にした時の操作も可能にした方が理想。回転の実装方法から変えた方が良いかも。思いついたのはカメラをそれぞれの面で切り替えらえるようにして、その都度3次元配列を書き変える方法。 回転するとCubeがずれて見える。これはモデリングが下手なのが原因。 あと継ぎ足しで長くなったのでソースコードちゃんと分けたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【UnityC#】MonoBehaviourでもインターフェイスを利用して疎結合を実現する2つの方法

はじめに C#をはじめとするオブジェクト指向言語では、クラス同士が直接アクセスするのを避け、インターフェイスを介してアクセスさせることで疎結合が実現されることは周知の事実です。そのため、UnityC#でもMonoBehaviour同士をインターフェイスを介して依存させたい!でもMonoBehaviourの制約のせいでうまくいかない!って方は割と多いのではないでしょうか。 例えば、以下のようにFugaComponentがHogeComponentのインターフェイスIHogeComponentに依存したいとき。 public interface IHogeComponent { void SayHoge(); } public class HogeComponent : MonoBehaviour, IHogeComponent { public void SayHoge() { print("Hoge"); } } public class FugaComponent : MonoBehaviour { // 疎結合のためインターフェイスを通じてアクセスさせたい。 private IHogeComponent m_hoge; //←このm_hoge、どこから持ってくる!? void Start() { m_hoge.SayHoge(); } } FugaComponentはインターフェイスIHogeComponentをフィールドに持つことでHogeComponentとFugaComponentとの結合度を弱めているのですが、m_hogeの中身はどうやって代入するのかはなかなか難しい問題です。というのも、 SerializeField属性をつけてインスペクタから代入しては?→インターフェイスはシリアライズできません! コンストラクタから渡す?→MonoBehaviourなクラスの場合はコンストラクタ作れません! GetComponent<T>からインターフェイス取得しては?→同じGameObjectにアタッチされているコンポーネント同士ならOKだけど、違う場合はGameObjectへの参照が必要になって美しくないし安全性も落ちる! このような問題が発生するからです。 そこで本記事では、上記のような問題を解決するべく、MonoBehaviourなクラスにインターフェイスを引き渡す方法を2つ紹介します。 方法1. Odin Inspectorを使ってインターフェイスをシリアライズする 通常、次のようにインターフェイスをSerializeFieldしてもインスペクタ上には表示されません。 public class FugaComponent : MonoBehaviour { [SerializeField] private IHogeComponent m_hoge; } ▲インターフェイスをSerializeFieldしても… ▲インスペクタ上には表示されない ところが、Odin Inspectorというアセットに含まれているSerializedMonoBehaviourクラスを使うと、インスペクタ上でインターフェイスを引き渡すことができるようになります。 public class FugaComponent : SerialiedMonoBehaviour { [SerializeField] private IHogeComponent m_hoge; } ▲MonoBehaviourの代わりにSerializedMonoBehaviourを継承させると… ▲インターフェイスもインスペクタ上に現れる! この方法であれば、インスペクタ上でもIHogeComponentを実装したコンポーネントがアタッチされたGameObjectしかセットできないですし、ソースコード上にも余計なメンバが現れないため、安全かつ美しいです。 ただし、Odin Inspectorは60.50ドルの有料アセットです(たまに半額セールもある)。 お金をかけたくない場合は、次に紹介するまったく異なるアプローチを取ることもできます。 方法2. Zenjectを使ってインターフェイスを注入する Zenject(Extenject)は、UnityC#でDependency Injection(DI、依存性の注入)1を実現する無料アセットです。 DI(依存性の注入)は簡単に言うと、クラス同士の依存関係を設定しておくと、必要な時に必要なオブジェクトを引き渡してくれる(=注入してくれる)仕組みです。 Zenjectの使い方は複雑なので、順を追って説明します。 注入してほしいインターフェイスにInject属性をつける インスペクタからオブジェクトを引き渡す際はSerializeField属性を付けますが、Zenjectによってオブジェクトを注入する際はInject属性を付けます。 public class FugaComponent : MonoBehaviour { [Inject] private IHogeComponent m_hoge; } ちょうど、SerializeFieldの代わりにInjectを付けるイメージです。こうすることで、Zenjectに対してIHogeComponentを注入して欲しいです!という意思表示ができます。 依存関係定義ファイルMonoInstallerを作成する インターフェイスを注入すると言っても、実際に注入されるのは具体的なオブジェクトのインスタンスです。とあるインターフェイスの注入を要求されたとき、具体的にはどんなオブジェクトを注入すれば良いのかをZenjectに対して設定する必要があります。この設定ファイルをInstallerと言います。 Projectビューのプラスボタンをクリックして、Zenject→Mono Installerをクリックします。任意の名前をつけてファイルを保存します。 作成されたファイルには、次のようなコードが記載されています。 using UnityEngine; using Zenject; public class UntitledInstaller : MonoInstaller { public override void InstallBindings() { } } このInstallBindingsメソッド内に、「どのインターフェイスが要求されたらどのオブジェクトを注入するか」を定義していきます。 例えば、IHogeComponentインターフェイスが要求されたらHogeComponentを注入するように設定する場合は、次のようにします。※あくまで一例です。 using UnityEngine; using Zenject; public class UntitledInstaller : MonoInstaller { public override void InstallBindings() { Container .Bind<IHogeComponent>() .To<HogeComponent>() .FromComponentInHierarchy() .AsCached(); } } まずメソッド内1行目のContainerですが、これはDiContainerクラスのインスタンスです。DiContainerは設定通りにオブジェクトを注入してくれる核となる存在です。これからこのDiContainerに対してさまざまな依存関係の設定を行っていきます。 2行目のBind<T>メソッドは「このインターフェイスが要求されたら」という意味です。DiContainerに対して、IHogeComponentが要求されたら(Injectされたら)、なんらかのオブジェクトを注入してね、と設定します。 3行目のTo<T>メソッドはBind<T>で設定されたインターフェイスが要求されたときに、実際に注入するオブジェクトを設定します。この例では、IHogeComponentが要求されたときにHogeComponentを注入するように設定しています。 4行目のFromComponentInHierarchyメソッドは、「注入するオブジェクトはヒエラルキー上から持ってきてね」と設定しています。 もちろんこれは一例であり、注入するオブジェクトはさまざまな場所から持ってくることができます。詳しくはこちらのサイトが参考になります。 5行目のAsCachedメソッドは、注入オブジェクトをキャッシュして再利用するように設定しています。例えば、IHogeComponentが複数のクラスから要求された場合、2回目以降はヒエラルキー上から持ってくるのではなく、キャッシュされたオブジェクトをそのまま注入します。 他にもAsSingleやAsTransientなどがあります。詳しくはこちらのサイトが参考になります。 以上で、ひとまず依存関係の設定は完了しました。 Contextを用意してInstallerをセットする 最後に、作成したInstallerの影響範囲を設定します。これには、Contextというものを使用します。 Contextにも様々ありますが、ここではひとつのシーン全体に影響するSceneContextを使用します。 もし、Contextに対して詳しく知りたい場合は、以下のようなサイトが参考になります。 ヒエラルキービューのプラスボタンをクリックして、Zenject→Scene Contextをクリックします。 すると、シーン上にSceneContextコンポーネントがアタッチされたGameObjectが作成されると思います。 SceneContextコンポーネントのMonoInstallersの部分に、先ほど作成したInstallerをセットします。 Add Componentボタンを押して先ほどのInstallerをアタッチし、それをMonoInstallersにドラッグしてセットできます。 以上で、SceneContextに対してInstallerのセットが完了しました。 これで晴れて、このSceneContextの影響範囲に対して、セットしたInstallerの設定内容に従って、オブジェクトの注入が実行されるようになります。 Scene上に注入を要求するコンポーネントを配置する SceneContextの影響範囲内(同一Scene内)に、オブジェクト注入を要求するコンポーネントを配置します。また、今回の例ではFromComponentInHierarchyに設定しているため、ヒエラルキ上に注入されるオブジェクト(コンポーネント)も配置されている必要があります。 次のように、Scene上に注入されるHogeComponent、注入を要求するFugaComponentをそれぞれアタッチしたGameObjectを配置します。 動作確認用に、HogeComponentとIHogeComponentを以下のように実装しました。 public interface IHogeComponent { void SayHoge(); } public class HogeComponent : MonoBehaviour, IHogeComponent { public void SayHoge() { print("Hoge"); } } FugaComponentはIHogeComponentの注入を要求し、SayHoge()を実行します。 public class FugaComponent : SerializedMonoBehaviour { [Inject] private IHogeComponent m_hoge; void Start() { m_hoge.SayHoge(); } } この状態でPlayすると、コード上ではm_hogeに対してオブジェクトを代入していないのに、きちんとSayHoge()が呼べていることが分かります。Zenjectによるオブジェクトの注入が効いている証拠です。 OdinとZenjectの使い分け 以上、UnityC#においてインターフェイスを引き渡す方法を2つ紹介しました。 OdinとZenjectは、片方だけでもインターフェイスを引き渡すという目的は達成できるのですが、どちらにも得意な場面・不得意な場面があると考えています。 例えば、Odinの場合はインスペクタ上で視覚的にオブジェクトの注入ができるため、同じインターフェイスを実装していても実体が異なる複数のインスタンスを注入したい場合に向いています。 ▲異なる複数のインスタンスを視覚的に注入できる これをZenjectで行うには、Installerの設定時にIDを使った注入設定を細かく行う必要があるため、設定と管理が煩雑になります。 反対にゲームのManager的なクラスなど、ただひとつのインスタンスしか持たないオブジェクトを注入したい場合、インスペクタ上で手動で注入するよりも、依存関係を設定してZenjectにやってもらったほうが楽です。 このように、OdinとZenjectは得意不得意があると思いますので、結局は両方とも導入して、適材適所で使い分けるのが一番良いのではないかと考えています。 Odinはインターフェイス注入以外にもインスペクタを使いやすくする機能が揃ってますし、DIはUnityだけではなく様々な場面で使われるデザインパターンです。どちらも勉強しておいて損はないかと思います(自分も勉強中)。 番外編. どうしてもOdinを買わずにインスペクタでインターフェイスを渡したい場合 昔の自分がOdinを使わずに無理矢理インスペクタでインターフェイスを(擬似的に)渡す方法を考えてました。 ご興味ある方は下記参照ください。 この記事はZenject, DI(依存性の注入)について詳しく解説する記事ではありません!正確性に欠ける表現がある可能性があることをご了承ください。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでGitのリビジョンを表示したい!

0.前提 本記事はhcscode Advent Calendar 2021 の11日目の記事です。 この記事ではGit管理しているUnityプロジェクトで現在のリビジョン等を表示する方法について書いていきたいと思います。 ※Gitに関しての説明は省きますので、分からない方は別途調べてください。 1.この記事を読んで出来るようになること Unity内で、現在のリビジョンを確認できる! こんな感じで表示したりできるようになります。 2.やり方 1.環境構築 まずはGitを入れます。(Gitコマンドを使用するため) ここから最新版をダウンロードします。 ダウンロードしたexeを実行し、インストールを行います。(保存先のパスをこの後で使いますので、覚えておいてください) 下記の設定は「Use Windows' default console window」を選択してください。 PATHを通します。 「path」と検索して、「システム環境変数の編集」を開き画像の手順を行います。 先ほどインストールしたGitフォルダ内のbinフォルダを選択してください。 プロジェクトの作成、リポジトリの作成等をおこなってください。 2.スクリプト作成 1.作成したスクリプト using System.Diagnostics; /// <summary> /// Git情報を取得するクラス /// </summary> public class GitInfoGetter { /// <summary> /// プロセス /// </summary> private Process _process; /// <summary> /// コンストラクタ /// </summary> public GitInfoGetter() { _process = new Process(); //起動するアプリケーションを指定する _process.StartInfo.FileName = System.Environment.GetEnvironmentVariable("ComSpec"); //シェルを使用するか _process.StartInfo.UseShellExecute = false; //出力をStandardOutPutに書き込むか _process.StartInfo.RedirectStandardOutput = true; } /// <summary> /// デストラクタ /// </summary> ~GitInfoGetter() { _process = null; } /// <summary> /// リビジョンを取得 /// </summary> /// <returns>リビジョン</returns> public int GetRevision() { //実行するコマンドを設定 _process.StartInfo.Arguments = "/k git log --date=iso --pretty=format:\"% h\" | find /c \" \""; //実行 _process.Start(); //結果を取得 string result = _process.StandardOutput.ReadLine(); //終了 _process.Close(); //文字列を数値に変換 if (!int.TryParse(result, out int revision)) { UnityEngine.Debug.LogErrorFormat("Error : GetRevision, result ={0}", result); revision = -1; } return revision; } /// <summary> /// 現在のコミットIDを取得 /// </summary> /// <returns>コミットID</returns> public string GetCurrentCommitID() { //実行するコマンドを設定 _process.StartInfo.Arguments = "/k git rev-parse --short HEAD"; //実行 _process.Start(); //結果を取得 string result = _process.StandardOutput.ReadLine(); //終了 _process.Close(); return result; } } 後は必要に応じて、処理を呼んで結果を受け取ってください。 2.説明 //起動するアプリケーションを指定する _process.StartInfo.FileName = System.Environment.GetEnvironmentVariable("ComSpec"); ここで、コマンドプロンプトを起動することを設定しています。 //実行するコマンドを設定 _process.StartInfo.Arguments = "/k git log --date=iso --pretty=format:\"% h\" | find /c \" \""; このコマンドは、「git log --date=iso --pretty=format:\"% h\"」で下記のような出力が行われます。 7e70e6a ///最新のコミットID(短縮版)※このコメントは出力には含まれません。 87ad678 e4883fd 33fa433 6868ba7 5bcfb3b  ///最初のコミットID(短縮版)※ そして、「| find /c \" \""」でこの出力から、行数を数えて出力しています。(正確には、空白が含まれている行数) こうやって現在のリビジョン数を取得しています。 //実行するコマンドを設定 _process.StartInfo.Arguments = "/k git rev-parse --short HEAD"; このコマンドでは最新のコミットID(短縮版)を出力しています。 上でいう「7e70e6a」が出力されます。 3.最後に いかがでしたでしょうか。 コマンドを実行することで、色々なこともできるので、是非試してみて下さい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VR空間で球面上を自由に歩き回りたい(Unity)

はじめに (この記事はUT-virtual Advent Calendar 2021に参加しています。) 昨年末から人生初のゲーム制作を始め、「オープンワールド化された地球の球面上を歩き回る」ゲームを友人と開発していました。 その際に球面上を自由に歩き回る仕組みの実装で行き詰まり、日本語でわかりやすく説明した記事も見つからなかったので実際に書いたコードをまとめておきます。 Unity初心者&Qiita初記事なので色々変な箇所があるかと思いますが悪しからず。 使用環境 Oculus Quest/Quest2 Unity 2020.3.7f1 Oculus Integration 25.0 Oculus XR Plugin 1.9.1 XR Plugin Management 4.0.6 実装に必要な要素 大きく分けると以下の2つになります。 重力(球面上のどこでも中心方向に力が働き、常に地面と垂直に立っていられるようにするもの) 球面上での滑らかな移動 ①重力 重力を発生させるもの(地球など)にGravityAttractorを、重力の影響を受けるもの(プレイヤー、建造物など)にGravityBodyをアタッチします。 GravityAttractor using System.Collections; using System.Collections.Generic; using UnityEngine; public class GravityAttractor : MonoBehaviour { //プレイヤーにかかる重力 float playergravity = -10f; //プレイヤー以外にかかる重力 float gravity = -10f; public void Attract(Transform body, GameObject gameObject) { Vector3 targetDir = (body.position - transform.position).normalized; Vector3 bodyUp = body.up; body.rotation = Quaternion.FromToRotation(bodyUp, targetDir) * body.rotation; if(gameObject.tag == "Player") { gameObject.GetComponent<Rigidbody>().AddForce(targetDir * playergravity); } else { gameObject.GetComponent<Rigidbody>().AddForce(targetDir * gravity); } } } 上の例ではプレイヤーとそれ以外の物体で働く重力を変えられるようになっています。 GravityBody using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent (typeof (Rigidbody))] public class GravityBody : MonoBehaviour { GravityAttractor planet; void Awake() { planet = GameObject.FindGameObjectWithTag("Planet").GetComponent<GravityAttractor>(); GetComponent<Rigidbody>().useGravity = false; //Rigidbody内にあるGravityは使わないので無効にする GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezeRotation; } void FixedUpdate() { planet.Attract(this.gameObject.transform, this.gameObject); } } ②球面上での滑らかな移動 プレイヤーの移動を制御するスクリプトを作り、以下のコードを加えてプレイヤーにアタッチします。 PlayerController using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { Rigidbody rigid; float cameraSensitivityX; //カメラの回転速度 float walkSpeed; //歩行速度 private Vector3 moveAmount; private Vector3 smoothMoveVelocity; void Start () { rigid = this.GetComponent<Rigidbody>(); cameraSensitivityX = 150f; walkSpeed = 3f; } void Update () { //左スティックでカメラの操作 this.transform.Rotate(Vector3.up * OVRInput.Get(OVRInput.RawAxis2D.LThumbstick).x * Time.deltaTime * cameraSensitivityX); //右スティックで移動 Vector3 moveDir = new Vector3(OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).x, 0, OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).y).normalized; Vector3 targetMoveAmount = moveDir * walkSpeed; moveAmount = Vector3.SmoothDamp(moveAmount, targetMoveAmount, ref smoothMoveVelocity, .15f); } void FixedUpdate() { GetComponent<Rigidbody>().MovePosition(GetComponent<Rigidbody>().position + transform.TransformDirection(moveAmount) * Time.fixedDeltaTime); } } カメラの改善 上のコードではUnityシーン上のプレイヤー(GameObject)の向きと現実のHMDの向きが同期していません。よってこのままだと、左スティックを使わずに現実の自分が体の向きを変えた際に、右スティックの入力方向とプレイヤーの動く向きが一致しなくなります。体を反転させて進行方向を180度変えたらスティックを前に倒してもプレイヤーは後ろに移動してしまう、といった問題が発生してしまうわけです。 これでは体験者にとって非常に不便なので、HMDの向きを取得してGameObjectのRotationに反映させるようにします。 PlayerController using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { Rigidbody rigid; float cameraSensitivityX; //カメラの回転速度 float walkSpeed; //歩行速度 private Vector3 moveAmount; private Vector3 smoothMoveVelocity; void Start () { rigid = this.GetComponent<Rigidbody>(); cameraSensitivityX = 150f; walkSpeed = 3f; } void Update () { //左スティックでカメラの操作 this.transform.Rotate(Vector3.up * OVRInput.Get(OVRInput.RawAxis2D.LThumbstick).x * Time.deltaTime * cameraSensitivityX); //HMDのY軸の角度を取得 InputTracking.GetNodeStates(nodeStates); foreach(var node in nodeStates) { if(node.nodeType == XRNode.Head) { headState = node; break; } } headState.TryGetRotation(out headRotation); Vector3 changeRotation = new Vector3(0, headRotation.eulerAngles.y, 0); //右スティックで移動 Vector3 moveDir = new Vector3(OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).x, 0, OVRInput.Get(OVRInput.RawAxis2D.RThumbstick).y).normalized; Vector3 targetMoveAmount = Quaternion.Euler(changeRotation) * moveDir * walkSpeed; moveAmount = Vector3.SmoothDamp(moveAmount, targetMoveAmount, ref smoothMoveVelocity, .15f); } これで現実の自分がいくら回転しても、体が向いている方向にスティックを倒せばまっすぐ進むことができるようになります。 おわりに Unityで作られる3Dゲームの多くは平地が舞台なので、球面上の移動に実装に関しては先行事例が非常に少なく苦労しました。おまけに球面となると数学の力も必要になるのでまあ大変です。 それでも一度完成させてしまえば、「球面上を自由に歩き回れる」という要素それだけでVR体験として楽しめるものになりました。 球面にどうやってオブジェクトを配置するのかなど難しい点は他にも色々あるのですが、球面オープンワールドは普通のVRゲームでは味わえない面白さが詰まっていそうです。 参考文献 コードはここから引用したものを一部改変しています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オープンワールド系のゲーム開発を経験したので色々話す#0

まずは挨拶 お久しぶりです。てんぺんです。 いつの間にか就活も終わり気づいたら12月になろうとしています。 この期間、何もしていなかったかというと、そういうわけでもなく... タイトルの通りオープンワールドのゲームの開発をし始めたので、そのお話を何回かに分けて話そうと思います。(毎週投稿予定) 下記リンクからそれぞれの記事に飛べるので、是非読んで行ってください。 #1 TerrainとDitail編 https://qiita.com/hemmiyuya/items/b9bc17d65cc629f07500 #2 街づくり編 12/12日投稿予定 #3 NPC編 12/19投稿予定 #4 クエスト編 12/26投稿予定 まとめ 読んでもらったらわかると思うのですが、バチバチにコードを書いていく時間よりも、Unityの機能を使って何かする。という時間が結構長いので、プログラムを書くのが好きな人だと結構やっていて苦痛な時間が多いのかなと思います。 それでも、自分が作りたいものがどんどんできていくというのはとても嬉しいしワクワクするものなのでこの記事を読んだ方も是非挑戦してみてください。(オープンワールドゲームに限った話じゃないけどね!) あと、自分も初心者だったりするので、もっとこーゆーことしたら良くなるよとか、他にはどんなことしたんですか?とか、アドバイス質問等あったらどんどんください! それではノシ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オープンワールド系のゲーム開発を経験したので色々話す#1 TerrainとDetail編

UnityのTerrainを使って地形を作る オープンワールドゲームということで、まずは広大な世界を作らなければいけないと思います。(ゼル伝しかり原神しかり Unityには、Terrainという便利で優秀()な機能があるので全力で使っていきます。 Terrainの基本的な使い方は他で調べて頂いて、今回はオープンワールドの地形を作っていく上で大切なことを話して行こうと思います。 ↑ 一応Terrainの学習に用いたサイトを貼っておきます。 まず最初にやること UnityでTerrainを使っていると、そのうち「処理が重すぎる」という問題に直面すると思います。 実はデフォルトの設定だとかなり高品質なTerrainを作るようになっていて、ステージ制の1場面の規模があまり大きくないゲームだと問題はないと思いますが、オープンワールドとなるとかなり広大な地形をたくさん作ることになるので、重くてゲームどころじゃないといったことになってきます。 というわけで設定を見直していきましょう。 ヒエラルキーのCreate/3D Object/TerrainからTerrainを作ります。 インスペクターのTerrainコンポーネントの歯車アイコンをクリックして設定を開きます。 すると、色々と項目が出てきて大変だとは思いますが、今回見ていくのは Pixel Error Base Map Dist Ditail Distance Ditail Density Tree Distance BillBoard Start Ditail Resolution Per Patch ◯◯ Resolution 系統 になります。そこそこ多いですが、かなり軽量化に繋がるので少しずつ説明していきます。 Pixel Error 簡単に言うと、「遠くの地形を正確に描画するかどうか」の設定になります。 数値を大きくすると、遠くの描画が雑になっていきますが、処理は早くなっていきます。 僕はこのプロジェクトでは30に設定しています。 Base Map Dist Pixel Errorのテクスチャバージョンになります。Terrainに設定しているテクスチャが指定した数値より遠くになると雑に描画されるようになります。 ゲームの規模に合わせて、数値を大きくしていくといいかなと思います。遠くを狙撃するようなゲームの場合はひと工夫必要かも? Ditail Distance 後述するDitail(草とかの小物)の描画範囲です。指定した数値までの範囲を描画します。 Ditail Density Ditailの密度設定です。ここをいじりながら設定したことはないですが、Ditailの設定からも配置する密度は変えられるので、ここの値はあまりいじらなくてもいいかも?一応小さくなればなるほど処理は軽くなります。 Tree Distance Ditail Distanceの木バージョンです。Ditailよりはおおきな値を設定してあげるといいと思います。 BillBoard Start 木をメッシュではなく、ハリボテ(2D)で表示するようになる距離です。木の量によっては、一気にメッシュ数が減るので軽量化に繋がると思います。Tree Distanceと合わせていい感じに設定していきましょう。 Ditail Resolution Per Patch 描画処理の勉強が足りなくて、よくわからなかったので引用させてもらうと 1回のドローコールで描画される範囲のサイズです。値は8~128の間で設定可能で、大きいほど描画コストが下がります。 ・・・らしいです。とりあえず見た目に影響が出ない程度におおきな値を設定してあげるといいと思います。 ◯◯ Resolution 系統 いわゆる解像度設定です。見た目が大きく変わるのでクオリティも大きく変動しますが、その分描画処理も大きく変わります。 また、途中で変えると見た目がおかしくなったりすることもあるので、この項目は一番最初に設定しておきましょう。 これらの項目を見直すことで、劇的にTerrainを軽量化することができると思います。 中には、設定を変えると見た目がおかしくなったり、地形データが吹き飛ぶものもあるので、できるだけ一番最初に設定して途中で変えるようなことがないようにしましょう。 まっ平らな地形を作らない 地形を作っていく上で、結構見落としがちですが簡単に工夫できるのがこれだと思います。 ↑まっ平らな地形 ↑軽く起伏をつけた地形 他にも道っぽい砂利があったり風景があったりで単純に比べるのはちょっとずるな感じですが、こんな感じで起伏をつけただけでそれっぽい感じが作れます。 これはクリエイターだからこそ感じてしまうことなのかもしれませんが、Unityでゲーム作ってみた的な初心者向けのサイトを漁っているとまっ平らな地形で作っているものが結構あるので、すごい「Unityじゃん」感が出てしまうんですよね。 それを解消するために、まずは起伏をつけて地形を作ってみるといいと思います。 ただし!! プレイヤーがいける場所で大きな起伏をつけるのはやめましょう!! せっかくの広いマップが見えにくくなる他、急な斜面はUnityの物理演算を使っていると壁のような判定になって登るのが困難になります。 独自で物理を実装していて、周りが見えにくくても良いという状況であれば問題無いとは思いますが...色々と手間になるので、地形を作るのに慣れるまでは避けたほうがいいと思います。 Ditailを設定する 風景を作っていく上で欠かせないのがDitailの配置です。 先程の比較画像の通り、草花を生やしたり、小石を並べていったりするだけでそれっぽい雰囲気を作り出すことができます。 感覚的なお話になってしまうのですが、このような感じで一種類の草だけではなく、黄色い花、赤い花、緑の草、木の枝、などなど、多くの種類のDitailを置いていくといいと思います。 ↑荒れ地のDitail例。荒れ地の瓦礫はテクスチャだけでも表現できますが、Ditailとしても配置すると立体感がでてより良く見えます。他にも木の枝や小石、枯れ草なども配置してもともとは自然があった感を出して行くといいのかなと思います。 こんな感じでたくさんのDitailを登録しておくと、簡単に置けるので楽になります。(背景がないやつは2Dテクスチャ、背景が黒いやつはメッシュ) 今回のまとめ というわけで、第一回TerrainとDetail編でした。 時間がかかる世界観構築部分ですが、やればやるほど見た目が良くなって遊ぶ側も探索したい!という気持ちが出てくるので、時間の許す限りこだわって作っていきましょう。 特にTerrainは本当にできることの多い機能です。アセットなんかを漁ってみると軽量化だったり、より複雑な地形を作れるようになったり、なんなら自動生成してくれるものまであります。 もしお金に余裕があるのであればそれらを使ってみるとまた違う世界が見えてくるのかなと思います。 おまけ 実際に使ってみたアセット ほんとに最後のおまけとして、こちらのNature Rendererアセットを紹介しておきます。 名前の通り、自然関係のレンダリングを見直してくれる(Terrainにスクリプトをつけると勝手にやってくれる)のですが.... まじでさいきょーwwwってなるぐらい優秀なアセットです。 ↑アセットなし ↓アセットあり 影の描画や、Ditailの描画処理を大きく変更するので、この同じ場面でも大きく雰囲気が変わってきます。 特に、メッシュを使ったDitailの描画はありえないぐらい自然になるので、是非とも使ってほしいアセットとなっています。 あと、これを入れたらびっくりするぐらいTerrainが軽くなります。上でやった軽量化は何だったんだって思うぐらい軽くなるので初めて入れたときはびっくりしました。 というわけで今度こそ本当に終わろうと思います。 ではノシ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む