- 投稿日:2019-06-22T22:52:22+09:00
【Unity】360度全天球をSkyBoxに設定する(Skybox/Cubemap)
環境メモ
⭐️Mac OS Mojave バージョン10.14
⭐️Unity 2018.2.15f1
⭐️Blender v2.79.6
⭐️Inkscape 0.92Qiitaの容量オーバーで画像が貼り付けられなくなりました。(errorとなる)
続きは、「はてなブログ」に記載してます↓
かぴばらさんの覚書ブログ nonkapibara
類似内容?Unity?
— non (@nonnonkapibara) June 22, 2019
?360度全天球?をUnityに取り込んでみたよぉ?
「福岡市文学館(赤煉瓦文化館)」の建物を
「カシャっ!!」ってスマホで撮って?GIMPで軽く加工してみたよぉ?(思い付きで色をつけてみた。)
Unityでマウスぐりぐりして360度みれるのね〜#unity #unity3d pic.twitter.com/3jdZhmbi0P?WebVR ?A-Frame
— non (@nonnonkapibara) June 20, 2019
?360度?全天球を試してみたよぉ。
InkscapeとGIMPを使って絵を描いて?A-FrameのWebブラウザでVR表示できるのね
?\( ^o^ )/?
デフォルトで2眼表示にも切り替わるのでVRゴーグルで試してみたよぉ。?めっちゃ楽しい??#VR #WebVR pic.twitter.com/iANmvyVGMf
- 投稿日:2019-06-22T19:24:53+09:00
UnityのSlerpで回転終了を検知する
Slerpの回転終了を検知したい!
Unityで滑らかな回転を実現してくれるQuaternion.Slerpを使う機会があったのですが
その回転終了を検知するのに手間取ったお話です
回転している間は入力を受け付けたくない時などに使えると思いますいろいろ試してみた
ド直球にやってみる
if (transform.eulerAngles == rotAngle) { Debug.Log("回転終了"); }transform.eulerAngles : Vecotr3(オイラー角)で角度を取得
rotAngle : Vector3型の変数名、回転後の角度なぜか反応なし…
そこで回転している軸の成分(Z軸ならtransform.eulerAngles.z)だけ見てみると…
※90°まで回そうとした際の値です
あと少しのところで止まっていることが判明、道理でできないわけだ……少し変えてみる
if (Mathf.Round(transform.eulerAngles.x) == rotAngle.x && Mathf.Round(transform.eulerAngles.y) == rotAngle.y && Mathf.Round(transform.eulerAngles.z) == rotAngle.z) { Debug.Log("回転終了"); }Mathf.Round : 最も近い整数を偶数丸めで返す、四捨五入に近いやつ
一応できましたが0°に戻るときだけ検知が遅くなっているのが気になりますね…
結局
以下のようにしました
if (Mathf.DeltaAngle(Mathf.Round(transform.eulerAngles.x), rotAngle.x) == 0 && Mathf.DeltaAngle(Mathf.Round(transform.eulerAngles.y), rotAngle.y) == 0 && Mathf.DeltaAngle(Mathf.Round(transform.eulerAngles.z), rotAngle.z) == 0) { Debug.Log("回転終了"); }Mathf.DeltaAngle : 与えられた角度の差を求める
分かりにくいですが0°と360°なら0が返り、0°と390°なら30が返ってきますサンプルコード
改造等ご自由にどうぞ
Rotation.csusing System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// アタッチされたオブジェクトを回転させるスクリプト /// </summary> public class Rotation : MonoBehaviour { private float rotSpeed; //回転の速度 private Vector3 rotAngle; //回転後の角度 private bool isRotation; //回転中かどうか private const int ROT_MAX = 360; //最大回転角 private void Start() { //回転速度を設定 rotSpeed = 5.0f; } private void Update() { if (isRotation == true) { //Slerpで回転 transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(rotAngle), rotSpeed * Time.deltaTime); //終了検知 if (Mathf.DeltaAngle(Mathf.Round(transform.eulerAngles.x), rotAngle.x) == 0 && Mathf.DeltaAngle(Mathf.Round(transform.eulerAngles.y), rotAngle.y) == 0 && Mathf.DeltaAngle(Mathf.Round(transform.eulerAngles.z), rotAngle.z) == 0) { transform.rotation = Quaternion.Euler(rotAngle); isRotation = false; } } } /// <summary> /// 回転を開始するメソッド /// </summary> public void StartSlerpRotate(Vector3 newAngle) { //回転中は入力を受け付けない if (isRotation == false) { //回転角度を0°~360°間でループさせる rotAngle = new Vector3( Mathf.Repeat(rotAngle.x + newAngle.x, ROT_MAX), Mathf.Repeat(rotAngle.y + newAngle.y, ROT_MAX), Mathf.Repeat(rotAngle.z + newAngle.z, ROT_MAX)); //回転開始 isRotation = true; } } }
あとはStartSlerpRotateメソッドを呼んで回したい分の角度(Vector3型)を渡せばOKです
ちなみにですが渡ってきた値が360°を超えないようにMathf.Repeatでループさせています
今回初めての投稿だったのですがいかがだったでしょうか。
わかりやすく出来ているといいのですが…
少しでもお役に立っていただければ幸いです
- 投稿日:2019-06-22T17:41:10+09:00
【Unity】360度全天球をUnityに取り込んでみた
環境メモ
⭐️Mac OS Mojave バージョン10.14
⭐️Unity 2018.2.15f1↓↓↓↓↓↓類似内容
https://twitter.com/nonnonkapibara/status/1142279959416696833
?Unity?
— non (@nonnonkapibara) June 22, 2019
?360度全天球?をUnityに取り込んでみたよぉ?
「福岡市文学館(赤煉瓦文化館)」の建物を
「カシャっ!!」ってスマホで撮って?GIMPで軽く加工してみたよぉ?(思い付きで色をつけてみた。)
Unityでマウスぐりぐりして360度みれるのね〜#unity #unity3d pic.twitter.com/3jdZhmbi0P1.「360度写真」を下記からダウンロードする
http://www.hdrlabs.com/sibl/archive.html360度写真は複数枚の写真を繋ぎ合わせた1枚のJPG写真エクイレクタングラー(equirectangular)という。
3.「Sphere100.fbx」のダウンロード。
「Sphere100.fbx」360動画用全天球用FBXモデルを下記のサイトからダウンロードする
https://t.co/kJSLDQuMWh4.AssetsにFBXを取り込み、HierarchyにDrag&Dropで配置する
5.Sphere100のマテリアルに、jpgをDrag&Dropでセットする
完成!!
↓↓↓類似内容
【WebVR】360度全天球を試してみた。A-FrameでWebVR 表示してみたよぉ。
https://qiita.com/nonkapibara/items/1419e55bae2c4c611523?WebVR ?A-Frame
— non (@nonnonkapibara) June 20, 2019
?360度?全天球を試してみたよぉ。
InkscapeとGIMPを使って絵を描いて?A-FrameのWebブラウザでVR表示できるのね
?\( ^o^ )/?
デフォルトで2眼表示にも切り替わるのでVRゴーグルで試してみたよぉ。?めっちゃ楽しい??#VR #WebVR pic.twitter.com/iANmvyVGMf?Unity?
— non (@nonnonkapibara) June 22, 2019
?360度全天球?をUnityに取り込んでみたよぉ?
「福岡市文学館(赤煉瓦文化館)」の建物を
「カシャっ!!」ってスマホで撮って?GIMPで軽く加工してみたよぉ?(思い付きで色をつけてみた。)
Unityでマウスぐりぐりして360度みれるのね〜#unity #unity3d pic.twitter.com/3jdZhmbi0P【Unity】360度全天球をSkyBoxに設定する(Skybox/Cubemap)
https://qiita.com/nonkapibara/items/27aac80da1fb4ff03d0f
- 投稿日:2019-06-22T17:03:30+09:00
[Unity]無理矢理OpenCVを使う
0.はじめに
UnityでいざOpenCVを使おうとするとOpenCV for Unityなどのアセットが必要になってくる
しかしこれ$95もするので、安易に手を出しにくい(OpenCV plus Unityという無料のアセットもあるが)
だったら必要なOpenCVのライブラリだけラップすればいいんじゃね?というのが今回の記事
ただ七面倒臭いことを色々しなくちゃいけないのでそいうのが嫌な方は素直にアセットを使うのがオススメ最終目標はUnity側で撮ったスクリーンショットをOpenCV側に渡してグレースケール化し出力すること
環境はWindows10とVisualStudioを使用1.ラップする
まず、OpenCVからビルド済みのOpenCVを入手(Releases->windows)後
DLL化するプロジェクトにDLLとLIBとheaderが入ってるopencv2フォルダをぶっこむ
詳しいC++のDLL化はUnityでC++を使う方法リンカー->入力でlibを設定後、opencv2フォルダがあるパスを構成プロパティ->インクルートディレクトリに加える
ちなみにこれがないとコンパイルエラーになる
(OpenCV側のincludeが一部<>で括られているheaderが存在するため)C++のコード
DLLAPI unsigned char* __stdcall opencv_sample(unsigned char* img_data, unsigned int img_width, unsigned int img_heingt, unsigned char channel) { cv::Mat m_img(img_heingt, img_width, CV_8UC3, img_data); cv::Mat tmp_gray_img; cv::Mat color_gray_img; cv::cvtColor(m_img, tmp_gray_img, cv::COLOR_RGB2GRAY); cv::cvtColor(tmp_gray_img, color_gray_img, cv::COLOR_GRAY2RGB); int color_gray_img_len = color_gray_img.rows * color_gray_img.cols * color_gray_img.channels(); unsigned char* dst = (unsigned char*)::CoTaskMemAlloc(color_gray_img_len); memcpy_s(dst, color_gray_img_len, color_gray_img.data, color_gray_img_len); return dst; }配列をグレースケール化して返す関数
Unity側から送られてくるのはbyte配列なのでC++側はunsigned charのポインターとなる
1チャンネルのグレースケールだと扱いにくいので3チャンネルのグレースケールとして返す
CoTaskMemAllocでメモリを確保すると.Net側でのGC対象とすることができるReturning Strings from a C++ API to C#
Release x64でビルドして実際にDLLを作成する2.Unityでスクリーンショットを撮る
詳しいやり方はHow to save a picture (take screenshot) from a camera in game?
Unityのコード
private void LateUpdate() { bool takeScreenShot = Input.GetKeyDown(KeyCode.F1); if (takeScreenShot) { RenderTexture rt = new RenderTexture(resWidth, resHeight, 24, RenderTextureFormat.ARGB32); camera.targetTexture = rt; Texture2D screenShot = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false); camera.Render(); RenderTexture.active = rt; screenShot.ReadPixels(new Rect(0, 0, resWidth, resHeight), 0, 0); camera.targetTexture = null; RenderTexture.active = null; Destroy(rt); byte[] bytes = screenShot.EncodeToPNG(); string filename = "./Assets/img/original.png"; System.IO.File.WriteAllBytes(filename, bytes); byte[] gray_bytes = opencv_toGRAYscale(screenShot); save_bytes(gray_bytes, "./Assets/img/gray.png"); } }opencv_toGRAYscaleとsave_bytesは後程説明
スクリーンショット
実際に撮ったものがこちら
この画像をOpenCVに渡してグレースケール化していく3.UnityにOpenCVを導入する
AssetsにPlugins/x86_64フォルダを作成
先ほどビルドしたDLLとopencv_world410、opencv_world410dをコピーする
後の設定はUnityでC++を使う方法を参照宣言
static class OPENCV { [DllImport("dll_opencv", CallingConvention = CallingConvention.StdCall)] public static extern byte[] opencv_sample(byte[] img_data, uint img_width, uint img_height, byte channel); }先ほど作った関数をUnity側で使えるように宣言します、unsigned charのポインターだった仮引数はbyteの配列とし
戻り値も同様にbyteの配列ですTexture2Dをグレースケールのバイト配列として返す関数
private byte[] opencv_toGRAYscale(Texture2D screenShot) { byte[] r_bytes = screenShot.GetRawTextureData(); byte[] gray_bytes = OPENCV.opencv_sample(r_bytes, (uint)resWidth, (uint)resHeight, 3); DestroyTexture2D(screenShot); return gray_bytes; } private void DestroyTexture2D(Texture2D texture) { Destroy(texture); }気を付けて欲しいのは、先ほども説明した通り
グレースケール化したと言っても戻り値の配列は1チャンネルじゃくて3チャンネルということ
動的に確保したTexture2Dは明示的に破棄しないとメモリを圧迫する原因になります。配列を出力する関数
private void save_bytes(byte[] bytes, string file_path) { Texture2D s_texture = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false); byte[] tmp_bytes = new byte[resWidth * resHeight * 3]; for(int i = 0; i < resWidth*resHeight*3; i++) { tmp_bytes[i] = bytes[i]; } Debug.Log(tmp_bytes.Length); s_texture.LoadRawTextureData(tmp_bytes); byte[] png_bytes = s_texture.EncodeToPNG(); System.IO.File.WriteAllBytes(file_path, png_bytes); DestroyTexture2D(s_texture); }わざわざ配列をコピーしているのは、CoTaskMemAllocで確保した配列の長さがUnity側で
参照できずLoadRawTextureData内部でエラーを吐いてしまうそのための策
ただ例外処理はされているのでやらなくても出力は普通にできる。結果
まとめ
実際作ってみるとわかるが、やはり結構な手間とある程度のC++の知識が必要となってくるので
ネイティブなOpenCVにはあるのにAssetにはない機能だったり、まだ対応していない機能が欲しかったりする場合
いち早く実装できる可能性はあるかもしれない。
それ以外は安全性から見ても普通にAssetを使った方が無難に思える。
- 投稿日:2019-06-22T17:03:30+09:00
[Unity]アセットなしで無理矢理OpenCVを使う
0.はじめに
UnityでいざOpenCVを使おうとするとOpenCV for Unityなどのアセットが必要になってくる
しかしこれ$95もするので、安易に手を出しにくい(OpenCV plus Unityという無料のアセットもあるが)
だったら必要なOpenCVのライブラリだけラップすればいいんじゃね?というのが今回の記事
ただ七面倒臭いことを色々しなくちゃいけないのでそいうのが嫌な方は素直にアセットを使うのがオススメ最終目標はUnity側で撮ったスクリーンショットをOpenCV側に渡してグレースケール化し出力すること
環境はWindows10とVisualStudioを使用1.ラップする
まず、OpenCVからビルド済みのOpenCVを入手(Releases->windows)後
DLL化するプロジェクトにDLLとLIBとheaderが入ってるopencv2フォルダをぶっこむ
詳しいC++のDLL化はUnityでC++を使う方法リンカー->入力でlibを設定後、opencv2フォルダがあるパスを構成プロパティ->インクルートディレクトリに加える
ちなみにこれがないとコンパイルエラーになる
(OpenCV側のincludeが一部<>で括られているheaderが存在するため)C++のコード
DLLAPI unsigned char* __stdcall opencv_sample(unsigned char* img_data, unsigned int img_width, unsigned int img_heingt, unsigned char channel) { cv::Mat m_img(img_heingt, img_width, CV_8UC3, img_data); cv::Mat tmp_gray_img; cv::Mat color_gray_img; cv::cvtColor(m_img, tmp_gray_img, cv::COLOR_RGB2GRAY); cv::cvtColor(tmp_gray_img, color_gray_img, cv::COLOR_GRAY2RGB); int color_gray_img_len = color_gray_img.rows * color_gray_img.cols * color_gray_img.channels(); unsigned char* dst = (unsigned char*)::CoTaskMemAlloc(color_gray_img_len); memcpy_s(dst, color_gray_img_len, color_gray_img.data, color_gray_img_len); return dst; }配列をグレースケール化して返す関数
Unity側から送られてくるのはbyte配列なのでC++側はunsigned charのポインターとなる
1チャンネルのグレースケールだと扱いにくいので3チャンネルのグレースケールとして返す
CoTaskMemAllocでメモリを確保すると.Net側でのGC対象とすることができるReturning Strings from a C++ API to C#
Release x64でビルドして実際にDLLを作成する2.Unityでスクリーンショットを撮る
詳しいやり方はHow to save a picture (take screenshot) from a camera in game?
Unityのコード
private void LateUpdate() { bool takeScreenShot = Input.GetKeyDown(KeyCode.F1); if (takeScreenShot) { RenderTexture rt = new RenderTexture(resWidth, resHeight, 24, RenderTextureFormat.ARGB32); camera.targetTexture = rt; Texture2D screenShot = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false); camera.Render(); RenderTexture.active = rt; screenShot.ReadPixels(new Rect(0, 0, resWidth, resHeight), 0, 0); camera.targetTexture = null; RenderTexture.active = null; Destroy(rt); byte[] bytes = screenShot.EncodeToPNG(); string filename = "./Assets/img/original.png"; System.IO.File.WriteAllBytes(filename, bytes); byte[] gray_bytes = opencv_toGRAYscale(screenShot); save_bytes(gray_bytes, "./Assets/img/gray.png"); } }opencv_toGRAYscaleとsave_bytesは後程説明
スクリーンショット
実際に撮ったものがこちら
この画像をOpenCVに渡してグレースケール化していく3.UnityにOpenCVを導入する
AssetsにPlugins/x86_64フォルダを作成
先ほどビルドしたDLLとopencv_world410、opencv_world410dをコピーする
後の設定はUnityでC++を使う方法を参照宣言
static class OPENCV { [DllImport("dll_opencv", CallingConvention = CallingConvention.StdCall)] public static extern byte[] opencv_sample(byte[] img_data, uint img_width, uint img_height, byte channel); }先ほど作った関数をUnity側で使えるように宣言します、unsigned charのポインターだった仮引数はbyteの配列とし
戻り値も同様にbyteの配列ですTexture2Dをグレースケールのバイト配列として返す関数
private byte[] opencv_toGRAYscale(Texture2D screenShot) { byte[] r_bytes = screenShot.GetRawTextureData(); byte[] gray_bytes = OPENCV.opencv_sample(r_bytes, (uint)resWidth, (uint)resHeight, 3); DestroyTexture2D(screenShot); return gray_bytes; } private void DestroyTexture2D(Texture2D texture) { Destroy(texture); }気を付けて欲しいのは、先ほども説明した通り
グレースケール化したと言っても戻り値の配列は1チャンネルじゃくて3チャンネルということ
動的に確保したTexture2Dは明示的に破棄しないとメモリを圧迫する原因になります。配列を出力する関数
private void save_bytes(byte[] bytes, string file_path) { Texture2D s_texture = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false); byte[] tmp_bytes = new byte[resWidth * resHeight * 3]; for(int i = 0; i < resWidth*resHeight*3; i++) { tmp_bytes[i] = bytes[i]; } Debug.Log(tmp_bytes.Length); s_texture.LoadRawTextureData(tmp_bytes); byte[] png_bytes = s_texture.EncodeToPNG(); System.IO.File.WriteAllBytes(file_path, png_bytes); DestroyTexture2D(s_texture); }わざわざ配列をコピーしているのは、CoTaskMemAllocで確保した配列の長さがUnity側で
参照できずLoadRawTextureData内部でエラーを吐いてしまうそのための策
ただ例外処理はされているのでやらなくても出力は普通にできる。結果
まとめ
実際作ってみるとわかるが、やはり結構な手間とある程度のC++の知識が必要となってくるので
ネイティブなOpenCVにはあるのにAssetにはない機能だったり、まだ対応していない機能が欲しかったりする場合
いち早く実装できる可能性はあるかもしれない。
それ以外は安全性から見ても普通にAssetを使った方が無難に思える。
- 投稿日:2019-06-22T16:01:33+09:00
Photon使ってUnityでカーゲームが作りたい
Unityでオンラインゲーム作れるようになりたい!!!
UnityではPhoton Realtime SDKを使えば無料でできると聞いたので、制作過程を記します。使用したUnityのバージョンは2018.3.6です。1. photonアカウントとアプリの準備
まずはphotonでアカウントを作成します。
新しいアプリを作成するを選んでください。
こんな感じで今回作るゲームの情報を入れておきます。今回使うのはPhoton Realtimeです。
作成するを押して、以下の画面になります。詳細を確認しましょう
このアプリケーションIDはあとで使うので、コピーしておきましょう!
2. Unity側の準備
Unityでオンライン化したいプロジェクトに対し、AssetStoreからPhoton Unity Networking Classic-FreeをImportします。Photonは似たようなAseetが沢山あるので注意してください。
Unityのプロジェクトをphotonと繋げるところからやっていきましょう。
Importするとすぐ、AppIDを打ち込む画面が現れます。先ほどコピーしたアプリケーションIDをペーストしてください。
入れ損ねた場合や間違えた場合はWindow > Photon Unity NetWorking > HighLight Server Setting から、PhotonServerSettingのInspectorから編集できます。
さっきPhotonのマイページで作ったアプリケーションIDをAppidに入力してください。下の画像の青いところです。日本なのでRegionをJpにしておきます。Auto-join Lobbyにチェックをつけて、ゲーム開始時に自動でロビーに入るようにします。
Photonに接続するスクリプトを書きます。
Hierarchy Viewに空のオブジェクト(PhotonManager)を作って、下のスクリプト(PhotonManager.cs)をアタッチしてください。PhotonManager.csusing UnityEngine; using System.Collections; public class PhotonManager : MonoBehaviour { void Start() { // Photonに接続する(引数でゲームのバージョンを指定できる) PhotonNetwork.ConnectUsingSettings(null); } // ロビーに入ると自動的に呼ばれる void OnJoinedLobby() { Debug.Log("ロビーに入りました"); // ルームに入室する PhotonNetwork.JoinRandomRoom(); } // ルームに入室すると自動的に呼ばれる void OnJoinedRoom() { Debug.Log("ルームへ入室しました"); } // ルームの入室に失敗すると自動的に呼ばれる void OnPhotonRandomJoinFailed() { Debug.Log("ルームの入室に失敗しました"); // ルームがないと入室に失敗するため、その時は自分で作る // 引数でルーム名を指定できる PhotonNetwork.CreateRoom("myRoomName"); } }実行してConsoleを確認してみましょう。こんな感じになっていたら成功です!
ちなみにphotonに繋げないWi-fiもあるみたいです。ポート番号が解放されていないとそうなるみたいです。Wi-fiを変えたらうまくいくということがありました。
3. プレイヤーを生成する
今のままでは1人プレイしかできないので、各プレイヤーが操作できる車を生成できるようにします。とりあえず2人プレイできるようにしましょう。オブジェクトの位置や角度を同期させるには、PhotonNetworkを用います。
接続中の人数
int PhotonNetwork.countOfPlayers
現在このアプリケーションをプレイしているユーザー数(マスターサーバーで5秒間隔で取得可能)を取得できます。(by Photon Network Class Reference)
ネットワーク越しのPrefabの生成
static GameObject PhotonNetwork.Instantiate(string prefabName, Vector3 position, Quaternion rotation, int group)
ネットワーク越しにプレハブからインスタンス作成します。このプレハブは"Resources"フォルダー直下に配置されている必要があります。Resourcesフォルダーのプレハブを使う代わりに、手動でInstantiateしてPhotonViewを割り当てることもできます。(by Photon Network Class Reference)
PhotonManagerのOnJoinedRoomの中を書き加えていきます。
PhotonManager.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class PhotonManager : MonoBehaviour { void Start() { // Photonに接続する(引数でゲームのバージョンを指定できる) PhotonNetwork.ConnectUsingSettings(null); } // ロビーに入ると呼ばれる void OnJoinedLobby() { Debug.Log("ロビーに入りました。"); // ルームに入室する PhotonNetwork.JoinRandomRoom(); } // ルームに入室すると呼ばれる void OnJoinedRoom() { Debug.Log("ルームへ"+ PhotonNetwork.countOfPlayers +"人入室しました。"); //CarPrefabを生成する transform.rotation = Quaternion.Euler(0, 180, 0); //Carの向きが逆になってになってしまう問題を解決 if (PhotonNetwork.countOfPlayers < 2) { transform.position = new Vector3(66, 0, 0); GameObject car1 = PhotonNetwork.Instantiate("CarPrefab", transform.position, transform.rotation, 0); car1.name = "car1"; } if (PhotonNetwork.countOfPlayers == 2) { transform.position = new Vector3(60, 0, 0); Debug.Log("2台目生成できたよ"); GameObject car2 = PhotonNetwork.Instantiate("CarPrefab", transform.position, transform.rotation, 0); } } // ルームの入室に失敗すると呼ばれる void OnPhotonRandomJoinFailed() { Debug.Log("ルームの入室に失敗しました。"); // ルームがないと入室に失敗するため、その時は自分で作る // 引数でルーム名を指定できる PhotonNetwork.CreateRoom("myRoomName"); } }Resourcesフォルダ
PhotonはResourcesというフォルダからPrefabを生成するというシステムらしいです。
Carの名前をCarPrefab、
Prefabsフォルダの名前をResourcesに変えるか、新たにResourcesというフォルダを作るかしましょう。Photon Viewコンポーネント
同期するオブジェクトにつけるコンポーネントです。
CarPrefabのInspectorを開き、Photon ViewとPhoton Transform Viewをつけます。PositionとRotationにチェックを入れましょう。Photon Transform ViewをPhoton ViewのObserved Componentsにドラッグ&ドロップします。このようにクライアント間で通信を行いたい際にオブジェクトにPhotonViewコンポーネントを追加し、Obaserved(監視対象)を指定すると、そのオブジェクトが同期されるようになります。
CarPrefabのInspevtorはこんなかんじになります。
逆に、同期したくないオブジェクトはPhotonNetwork越しに生成すると厄介なことになります。
カメラとか、、、、、、、、、むやみに同期させない方が良いということを学びました。ネットワーク負荷の軽減のためにも、同期するオブジェクトは必要最低限にすることを心がけましょう。CarPrefabの子オブジェクトのMainCameraを消してください。カメラはあとでプレイヤー別になるようにしますが、今ついていると2人以上になったとき同期しておかしなことになります。適当な位置にに固定カメラを設置しておきましょう。
CarPrefabの準備が済んだら、CarPrefabをResourcesにいれてPrefab化してください。Hierarchyに残ったCarPrefabは消しちゃって大丈夫です。実行して、Scene Viewにcar1が生成されたのを確認できたら成功です!
ちゃんと2台生成したCarが同期しているか、ビルドして確認しましょう。☆. 同期の確認方法
Photonは沢山Buildします。Buildしないと1つのパソコンで2人分の動作確認ができないからです。
手始めに今どんな挙動を見せるのか確認してみます。PC向けAppをBuildしてください。
動作確認をする際は、まずAppを起動し、次にUnityの再生ボタンを押すようにしてください。
Windowedにチェックを入れておくと楽です。チェックを入れないと全画面で起動します。
2台出たら成功です。
しかしこのままでは片方が移動するともう片方も移動してしまっています。次の章で直していきましょう。4. プレイヤーをユーザーが各自で操作できるようにする
bool PhotonView.isMine
PhotonViewの所有者が「自分」で、このクライアントからコントロール可能の場合trueです。
PUNは所有権という概念があります。それぞれのPhotonViewをコントロールしたり破棄できる主体のことです。 所有者がローカルPhotonPlayerの場合trueです。 シーンのPhotonViewで、Master client上で動作しているならtrueです。(by Photon Network Class Reference)プレイヤーを操作するスクリプトの部分に、isMineの条件を足します。プレイヤーを操作しているCarUserControl.csを書き換えていきましょう。
CarUserControl.csusing System; using UnityEngine; using UnityStandardAssets.CrossPlatformInput; using UnityStandardAssets.Vehicles.Car; public class CarUserControl : MonoBehaviour { private PhotonView myPhotonView; private CarController m_Car; float h; //horizontal float v; //Vertical private void Start() { this.myPhotonView = GetComponent<PhotonView>(); m_Car = GetComponent<CarController>(); } private void FixedUpdate() { if (myPhotonView.isMine) //自分のPhotonViewだったら { if (RaceManager.instance.isRacing == true) { h = CrossPlatformInputManager.GetAxis("Horizontal"); v = CrossPlatformInputManager.GetAxis("Vertical"); } m_Car.Move(h, v, v, 0); } } }Builidして、それぞれのCarを動かすことができていれば成功です!
5. 各プレイヤーを追従するカメラを作る
失敗例
その①
PhotonViewのついたCameraを子オブジェクトに持つCarPrefabをPhotonNetworkから生成した場合
→ カメラが同期してちゃんと自分の車につかないその②
PhotonViewのないCameraを子オブジェクトに持つCarPrefabをPhotonNetworkから生成した場合
→ 操作とカメラが逆転する謎現象が起きたその③
PhotonViewのついたCameraをPhotonNetworkから生成し、CarPrefabを追従するようにした場合
→ これもカメラが同期して自分の車につかない全部Buildしないと気づかない失敗です。同期したくないオブジェクトはPhotonNetwork越しに生成すると厄介なことになります。ネットワーク負荷の軽減のためにも、同期するオブジェクトは必要最低限にすることを心がけましょう。
成功例
PhotonViewのついてないCameraをPhotonNetworkを介さずに生成した場合
まずはCameraをCameraPrefabに改名し、CameraScriptをつけてください。CarUserControl.csとCameraScript.csに書き加えていきます。
CarUserControlのAwakeでCameraをCarの子オブジェクトとしてPhotonNetworkを介さずに生成します。CameraPrefabをResourcesに入れてください。CarUserControl.csusing System; using UnityEngine; using UnityStandardAssets.CrossPlatformInput; using UnityStandardAssets.Vehicles.Car; public class CarUserControl : Photon.PunBehaviour { private PhotonView myPhotonView; private CarController m_Car; float h; //horizontal float v; //Vertical private void Awake() { this.myPhotonView = GetComponent<PhotonView>(); m_Car = GetComponent<CarController>(); if (myPhotonView.isMine) //Carが自分の車である場合 { GameObject camPrefab = (GameObject)Resources.Load("CameraPrefab"); //ResourcesフォルダからCameraPrefabをとってくる GameObject cam = Instantiate(camPrefab, transform.position, transform.rotation); //CameraPrefabを生成する cam.transform.parent = gameObject.transform; //Cameraの親オブジェクトはCarとする //この時点でCameraの相対座標は(0,0,0)になっているので、CameraScriptで位置を調整する } } private void FixedUpdate() { if (myPhotonView.isMine) { if (RaceManager.instance.isRacing == true) { h = CrossPlatformInputManager.GetAxis("Horizontal"); v = CrossPlatformInputManager.GetAxis("Vertical"); } m_Car.Move(h, v, v, 0f); } } }この段階ではCarの中にCameraが生成されてしまいます。
Camera.csのStart内に一行足してください。ここで相対位置を調整します。上記の生成した時に代入しようとしても指定した値が代入されなかったためです。CameraScript.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraScript : MonoBehaviour { // Start is called before the first frame update void Start() { transform.position += new Vector3(0, 3, 5); } // Update is called once per frame void Update() { } }再生してカメラがついてくることを確認したら、Buildして動作確認しましょう。
完成品の動画順位つけたりもしたいですね。
お疲れ様でした!
参考資料
基本説明 - Photon Unity Networking
UnityでPUNを使ったオンラインマルチプレイの実装-準備編-
Unity猫本のサンプルゲームをPhotonでオンライン対戦ゲーム化してみた
Unityカーレース
- 投稿日:2019-06-22T15:45:11+09:00
Unity (2018.3)でAndroidのapkをコマンドラインビルドする for mac
Unity で Android の apk ファイルを
コマンドラインからビルドする手続きです。ビルドはコマンドラインから行いますが、
詳細なビルド手続きは Unityプロジェクト内で記述しておく必要があります。環境
mac : 10.14.5 (Mojave)
unity : 2018.3.0f2unity側でのビルド手続き
UnityでBuilder.csというファイルを作成します。
(一応Asset/Editor
直下に作成したけど、場所はどこでも良いのかな??)ここで定義したクラス名と関数名は
Builder.Build
という形で
シェルスクリプトのUNITY_BUILDE_NAME
で参照されます。
outputPath
で指定したパスにapkファイルが出力されます。
今回の状態では、Unityのプロジェクトフォルダ直下に生成されます。Builder.csusing UnityEditor; using UnityEngine; using System.Collections.Generic; using System.Linq; public static class Builder { public static void Build() { var paths = GetBuildScenePaths(); var fileName = "app.apk"; var outputPath = $"./{fileName}"; var buildTarget = BuildTarget.Android; var buildOptions = BuildOptions.Development; var buildReport = BuildPipeline.BuildPlayer( paths.ToArray(), outputPath, buildTarget, buildOptions ); var summary = buildReport.summary; if (summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded) { Debug.Log("Success"); } else { Debug.LogError("Error"); } } private static IEnumerable<string> GetBuildScenePaths() { var scenes = new List<EditorBuildSettingsScene>(EditorBuildSettings.scenes); return scenes .Where((arg) => arg.enabled) .Select((arg) => arg.path); } }シェルスクリプト
コマンドラインからのビルド手続きをシェルスクリプトで書きます。
ビルドログは
UNITY_LOG_PATH
で指定したパスに出力されるので、
うまくビルドができない場合は、logファイルで原因を確認しましょう。unityBuild.sh#!/bin/bash UNITY_APP_PATH=/Applications/Unity/Unity.app/Contents/MacOS/Unity UNITY_PROJECT_PATH=Unityプロジェクトのパスを入力 UNITY_BUILDE_NAME=Builder.Build UNITY_LOG_PATH=./build.log $UNITY_APP_PATH -batchmode \ -quit \ -projectPath $UNITY_PROJECT_PATH \ -executeMethod $UNITY_BUILDE_NAME \ -logfile $UNITY_LOG_PATH \ -platform Android \ -isRelease false if [ $? -eq 1 ]; then echo "error!! check logfile: ${UNITY_LOG_PATH}" exit 1 fi echo "success!!" exit 0ターミナル
以下コマンドをターミナルから実行します。
Unityが起動した状態で行うとうまくビルドができないので、
Unityは終了させた状態で実行します。ターミナルから実行sh unitybuild.sh実行後正常に完了したら、apkファイルが
outputPath
に生成されますので、
実機にインストールして動作を確認しましょう。github
プロジェクトはgithubにアップロードしてます
https://github.com/becky3/unity_command_build/tree/commandline_android
- 投稿日:2019-06-22T15:45:11+09:00
UnityのAndroidビルドをコマンドラインから行う (mac向け)
Unity で Android の apk ファイルを
コマンドラインからビルドする手続きです。ビルドはコマンドラインから行いますが、
詳細なビルド手続きは Unityプロジェクト内で記述しておく必要があります。環境
mac : 10.14.5 (Mojave)
unity : 2018.3.0f2unity側でのビルド手続き
UnityでBuilder.csというファイルを作成します。
(一応Asset/Editor
直下に作成したけど、場所はどこでも良いのかな??)ここで定義したクラス名と関数名は
Builder.Build
という形で
シェルスクリプトのUNITY_BUILDE_NAME
で参照されます。
outputPath
で指定したパスにapkファイルが出力されます。
今回の状態では、Unityのプロジェクトフォルダ直下に生成されます。Builder.csusing UnityEditor; using UnityEngine; using System.Collections.Generic; using System.Linq; public static class Builder { public static void Build() { var paths = GetBuildScenePaths(); var fileName = "app.apk"; var outputPath = $"./{fileName}"; var buildTarget = BuildTarget.Android; var buildOptions = BuildOptions.Development; var buildReport = BuildPipeline.BuildPlayer( paths.ToArray(), outputPath, buildTarget, buildOptions ); var summary = buildReport.summary; if (summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded) { Debug.Log("Success"); } else { Debug.LogError("Error"); } } private static IEnumerable<string> GetBuildScenePaths() { var scenes = new List<EditorBuildSettingsScene>(EditorBuildSettings.scenes); return scenes .Where((arg) => arg.enabled) .Select((arg) => arg.path); } }シェルスクリプト
コマンドラインからのビルド手続きをシェルスクリプトで書きます。
ビルドログは
UNITY_LOG_PATH
で指定したパスに出力されるので、
うまくビルドができない場合は、logファイルで原因を確認しましょう。unityBuild.sh#!/bin/bash UNITY_APP_PATH=/Applications/Unity/Unity.app/Contents/MacOS/Unity UNITY_PROJECT_PATH=Unityプロジェクトのパスを入力 UNITY_BUILDE_NAME=Builder.Build UNITY_LOG_PATH=./build.log $UNITY_APP_PATH -batchmode \ -quit \ -projectPath $UNITY_PROJECT_PATH \ -executeMethod $UNITY_BUILDE_NAME \ -logfile $UNITY_LOG_PATH \ -platform Android \ -isRelease false if [ $? -eq 1 ]; then echo "error!! check logfile: ${UNITY_LOG_PATH}" exit 1 fi echo "success!!" exit 0ターミナル
以下コマンドをターミナルから実行します。
Unityが起動した状態で行うとうまくビルドができないので、
Unityは終了させた状態で実行します。ターミナルから実行sh unitybuild.sh実行後正常に完了したら、apkファイルが
outputPath
に生成されますので、
実機にインストールして動作を確認しましょう。github
プロジェクトはgithubにアップロードしてます
https://github.com/becky3/unity_command_build/tree/commandline_android
- 投稿日:2019-06-22T15:45:11+09:00
UnityのAndroidビルドをコマンドラインから行う (Mac用)
Unity で Android の apk ファイルを
コマンドラインからビルドする手続きです。ビルドはコマンドラインから行いますが、
詳細なビルド手続きは Unityプロジェクト内で記述しておく必要があります。iOSビルドの手続きはこちらの記事に記載しています。
環境
Mac : 10.14.5 (Mojave)
Unity : 2018.3.0f2unity側でのビルド手続き
UnityでBuilder.csというファイルを作成します。
(一応Asset/Editor
直下に作成したけど、場所はどこでも良いのかな??)ここで定義したクラス名と関数名は
Builder.Build
という形で
シェルスクリプトのUNITY_BUILDE_NAME
で参照されます。
outputPath
で指定したパスにapkファイルが出力されます。
今回の状態では、Unityのプロジェクトフォルダ直下に生成されます。Builder.csusing UnityEditor; using UnityEngine; using System.Collections.Generic; using System.Linq; public static class Builder { public static void Build() { var paths = GetBuildScenePaths(); var fileName = "app.apk"; var outputPath = $"./{fileName}"; var buildTarget = BuildTarget.Android; var buildOptions = BuildOptions.Development; var buildReport = BuildPipeline.BuildPlayer( paths.ToArray(), outputPath, buildTarget, buildOptions ); var summary = buildReport.summary; if (summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded) { Debug.Log("Success"); } else { Debug.LogError("Error"); } } private static IEnumerable<string> GetBuildScenePaths() { var scenes = new List<EditorBuildSettingsScene>(EditorBuildSettings.scenes); return scenes .Where((arg) => arg.enabled) .Select((arg) => arg.path); } }シェルスクリプト
コマンドラインからのビルド手続きをシェルスクリプトで書きます。
ビルドログは
UNITY_LOG_PATH
で指定したパスに出力されるので、
うまくビルドができない場合は、logファイルで原因を確認しましょう。unityBuild.sh#!/bin/bash UNITY_APP_PATH=/Applications/Unity/Unity.app/Contents/MacOS/Unity UNITY_PROJECT_PATH=Unityプロジェクトのパスを入力 UNITY_BUILDE_NAME=Builder.Build UNITY_LOG_PATH=./build.log $UNITY_APP_PATH -batchmode \ -quit \ -projectPath $UNITY_PROJECT_PATH \ -executeMethod $UNITY_BUILDE_NAME \ -logfile $UNITY_LOG_PATH if [ $? -eq 1 ]; then echo "error!! check logfile: ${UNITY_LOG_PATH}" exit 1 fi echo "success!!" exit 0ターミナルから実行
以下コマンドをターミナルから実行します。
Unityが起動した状態で行うとうまくビルドができないので、
Unityは終了させた状態で実行します。ターミナルから実行sh unitybuild.sh実行後正常に完了したら、apkファイルが
outputPath
に生成されますので、
実機にインストールして動作を確認しましょう。github
プロジェクトはgithubにアップロードしてます
https://github.com/becky3/unity_command_build/tree/commandline_android
- 投稿日:2019-06-22T13:53:35+09:00
【Unity】Zenject使用時にPlayModeテストに失敗する
問題
Zenject使用時に、PlayModeテストを行うと次のようにテストに失敗してしまうことがあります。
Zenject.ZenjectException : Assert hit! Cannot load scene 'TestSceneContextEvents' for test 'TestSceneContextEvents'. The scenes used by SceneTestFixture derived classes must be added to the build settings for the test to work対処法
Assets > Plugins > Zenject > OptionalExtras > IntegrationTests > SceneTests > TestSceneContextEvents
で、
TestSceneContextEvents
シーンをScenes In Buildに追加します。これで無事にテストが通ります。
- 投稿日:2019-06-22T13:07:21+09:00
PlayerPrefsにクラスを丸ごと保存/セーブしたい!
PlayerPrefsにクラスを丸ごと保存/セーブしたい!
というわけで作りました。
ジェネリック<T>で型を指定して保存できるクラス、「PlayerPrefsUtil」ダウンロードはこちら(.unitypackage)←
デモが置いてあるgithubのリポジトリはこちら← (特に見る意味ない)それにしても、なんで保存できないんでしょうね。
使い方
①保存のメソッド
PlayerPrefsUtil.SetObj<T>(string key,T obj,[bool compression = false]);
・Tでクラスの型を指定して
・引数keyに文字列を入れて
・T objに指定した型のデータを入れます
・オプション引数のcompressionにtrueを代入することで、内部で文字列の圧縮をかけます②ロードのメソッド
T PlayerPrefsUtil.GetObj<T>(string key);
・キーに保存されているデータをTでクラスの型を指定して返します注意ですが、PlayerPrefsのStringに保存しているので、HasKeyなどPlayerPrefsの機能がそのまま使えますが
PlayerPrefsと同じようにPlayerPrefs.Save();を走らせないと保存されません(レジストリに書き込まれない)。使用例
まず、下のような名前と体力を持ったクラスがあるとします
public class Enemy { //名前 public string Name; //体力 public int HP; public Enemy(string name, int hp) { Name = name; HP = hp; } }これを保存/ロードする場合
Enemy goblin = new Enemy("ゴブリン",10); //保存 PlayerPrefsUitl.SetObj<Enemy>("GOBLIN_SAVE_KEY",goblin); //ロード goblin = PlayerPrefsUtil.GetObj<Enemy>("GOBLIN_SAVE_KEY");これで変数goblinの保存とロードができます。
内部の処理など
圧縮(compression)はどういう処理をしているか
保存するときに圧縮するかしないかを選べますが、何をするかというと
①XMLSerializerでデータをXML形式のStringに変換
②文字列をbyte[]に変換しSystem.IO.Compressionの機能を使用して圧縮 (ここが圧縮)
③圧縮された文字列をBase64形式に変換して保存という感じで圧縮をかけています。
なので、外部から簡単に読まれなくなるという多少の利点もあります。なぜXMLSerializerを使用したのか
先ほどの工程でXMLSerializerが登場しましたが、なぜJsonUtilityではないのかというと
結論から言うと 複雑なList構造やDictionaryを保存でき、かつUnityEngineのクラスを保存できる という点です。クラスを丸ごと保存できる機能だと、候補が3つあり
今回使った「XMLSerializer」、Unityでよく使われる「JsonUtility」、バイナリに変換できる「BinaryFormatter」。
それぞれ特徴があるのですが、今回いろいろ試して分かったことがありまして、
JsonUtilityは複雑なList構造に弱く、BinaryFormatterはUnityEngineのクラス、例えばVec2、Vec3クラスなどを保存できなかったんです (シリアライザがない?らしい) 。
その両方が可能だったのが、XMLSerializerだった。という訳です。なので、外部ツールとの互換性やデータの容量が気にならなければXMLSerializerを使用して保存するのがいいと思います。
- 投稿日:2019-06-22T11:05:38+09:00
UnityでExcel Importerを使って、シナリオの管理とノベルゲームの基礎を作ってみた。RPGの会話にも使えるよ。
現在RPGを製作中で、ひとまずシナリオのメッセージを表示する箇所を作ることになりました。
シナリオのデータはスプレッドシートで作成して管理するのがよいと思い試してみました。
また、キャラクターやアイテムなど様々なデータも管理する必要があるので応用が利くと考えました。以下URLは作成した成果物をExpotPackegeしたファイルのリンクです。
適当に新しく作ったプロジェクトや、あるいは現在制作中のゲームで使ってみてください。
ダウンロードしてインポートすればすぐに使えます。
https://1drv.ms/u/s!AqdG-lHUsHuI2gl-QYk7ZC_pQeKr?e=YUZg7Bたいていの場面や状況でメッセージを映すことができると思います。
例えば、Timelineを使ったムービーの字幕を映したり、戦闘中に必殺技を使ったときに技名を叫んだりなどです。なぜできるかというとOnEnableで最初の文章を映すようにしています。
なのでスクリプトをアタッチしているオブジェクトのアクティブ状態を有効化するだけでよいからです。
それにあわせて、対話が終了したときにアクティブ状態を無効にするよう作りました。NPCとの会話であれば、NPCごとにUIのプレハブを子オブジェクトとして持たせればOKです。
ただ、話しかける部分は無いので作る必要があります。
NPCオブジェクトのUIを有効化する関数を呼び出せばよいでしょう。Unity Excel Importerのメリット
Excelのデータをスクリプタブルオブジェクトに変換してくれる超便利なやつです。
しかも、ただそれだけではありません。以下のメリットがあります。1.簡単にExcelファイルを変換できる
2.Enumに対応している
3.#を行の先頭のセルに最初の1文字として書いておくだけで、その行は読み込まさせないことができる。
4.Excelファイルに空の行や列を挿入するだけで、それ以降のデータは読み込まないように作られている
5.自動更新機能が付いている便利な機能がそろっていますから使うほかはありえませんでした。
もはや頭が上がりません。RPGでは様々な箇所に使用しますから常に感謝しています。作成の手順
手順は以下の通りです。
1.最低限必要な機能を考える
2.エクセルにシナリオを書く
3.Entityクラスを書く
4.エクセルデータをインポートする
5.Excel Assetを半自動で作成する
6.UIを構築する
7.シナリオを表示するプログラムを書く
8.1に戻って機能を増やす私は、以上を繰り返して仕組みを作りました。
仕様変更を繰り返したともいえます。
ですが、エクセルのインポートが簡単なので問題は発生しませんでした。シナリオを読むのに必要な処理
1.NPCに話しかけるなどの会話のきっかけ
2.メッセージなど映すUIの表示
3.最初の文章を映す
4.次の文章を読む
5.選択肢を映す
6.選択肢で分岐する
7.分岐したけど、結局は同じ結末になって収束する
8.対話が終了するときにUIを非表示にする
以上が最低限必要な処理だと思います。
今回は、話しかけるなどのきっかけの部分はありません。ご注意ください。UIを構成するオブジェクト
シーンの全体像です。撮影のために選択肢のオブジェクトを有効化しています。
CanvasにはスクリプトのDialougeクラスをアタッチしています。
メッセージウインドウの背景部分はボタンで作っています。Eventには次のメッセージを映す関数を割り当てています。
選択肢もボタンです。同じように、それぞれ選択肢ごとに関数を割り当てています。選択肢はChoicesObjectという名前の空のオブジェクトの子にしています。
ふたつの選択肢を同時に非アクティブにするためです。Excelファイル
作成したプログラムをバグがないか試すのに必要なデータのみ書いています。
1行目と2行目が同じメッセージなのは、選択肢が表示されたときメッセージウインドウが暗くなるようにしてあるからです。
まず、本文を明るく映し、つぎに本文を暗くして選択肢を明るく映す仕組みです。
そのため、2行目は、あくまでも選択肢がメインの文章になります。また、9行目は本文がありません。J列のendOfTalkがTRUEになっているところが他の行とは違います。
この行の役割は対話が終了するかのフラグだけです。8行目の本文を映すために必要です。
フラグがTRUEだったらとき、すぐにUIを非アクティブにする仕組みだからです。Entityクラス
Excel Importerで最初に作成する必要のあるスクリプトです。
エクセルで各列に記述する要素をパブリックな変数で宣言しておきます。注意点が3点あります。
1.Serializable属性を記述すること
2.何も継承しないこと
2.変数名をExcelファイルの各列の項目名と一致させること
以上が注意点になります。それぞれの変数の意味についてはコメントを読んでみてください。
[System.Serializable] public class Sentence //センテンスと読みます。文章という意味です。シナリオは文章で構成されているので、この名前にしました。 { public int id; //検索するための一意な数値です。 public string message; //メッセージの本文です。 public bool branch; //選択肢があるかを判定するために使います。 public string yesMessage; //肯定的な選択肢です。 public string noMessage; //否定的な選択肢です。 public int choseYes; //肯定的な選択肢を選んだとき、どの文章に分岐するかのIDです。 public int choseNo; //否定的な選択肢を選んだとき、どの文章に分岐するかのIDです。 public bool doConnect; //分岐した後に収束するときなどに使います。 public int skipId; //収束するとき、どの文章から読むのかのIDです。 public bool endOfTalk; //対話が終了する最後の文章なのか判定するのに使います。 }ExcelAssetクラス
ScriptableObjectクラスを継承していて、エクセルの行ごとのデータを持った配列を持っているクラスです。
インポートしたエクセルデータをもとにExcel Importerが半自動で作成してくれるスクリプトです。
手動で作成してもよいですが注意点があります。1.[ExcelAsset]属性を書いておくこと
2.クラス名はエクセルのファイル名と同じにすること
3.変数名はエクセルのシートの名前と同じにすること
4.ScriptableObjectクラスを継承すること
5.リストの型を間違えないようにすること
以上が注意点です。それでは、作成してみましょう。
まず、右クリックと左クリックで作成します。
そのあと一部だけ書き換える必要があります。
そして、エクセルデータは再インポートするとスクリプタブルオブジェクトに変換されます。
コメントアウトを消してから
リストの型を自分で作ったものに書き換えます。//書き換える前 using System; using System.Collections; using System.Collections.Generic; using UnityEngine; [ExcelAsset] public class Chapter : ScriptableObject { //public List<EntityType> sentences; // Replace 'EntityType' to an actual type that is serializable. }//書き換えた後 using System.Collections.Generic; using UnityEngine; [ExcelAsset] public class Chapter : ScriptableObject { public List<Sentence> sentences; // Replace 'EntityType' to an actual type that is serializable. }Dialogueクラス
UIの制御をするクラスです。
処理の流れは以下の通りになります。
1.UIが有効化する
2.一意な行番号でデータを検索して取得する
3.Textコンポーネントにメッセージを代入する
4.選択肢があれば、選択肢のアクティブ状態を有効化する
5.選択肢を選んだあとは無効化する
6.対話が終了フラグがあったらUIを無効化するusing System.Linq; using UnityEngine; using UnityEngine.UI; public class Dialogue : MonoBehaviour { [SerializeField] GameObject dialogue = null; [SerializeField] GameObject choices = null; [SerializeField] Button readMore = null; [SerializeField] Text massage = null; [SerializeField] Text yBranchMassage = null; [SerializeField] Text nBranchMassage = null; [SerializeField] Chapter chapter = null; //文章を検索するときに使うIDです。 int CurrentSentenceID = 0; //オブジェクトが有効になったとき呼び出すように、Unityが用意してくれているイベント関数です。 private void OnEnable() { //最初の1行目を表示します。 Sentence result = SerchSentence(CurrentSentenceID); ShowingMassage(result); } private void Update() { Debug.Log(CurrentSentenceID); } //文章の続きを表示します。 public void ReadmoreMessage() { //文章番号をひとつだけ次に進めます。 CurrentSentenceID++; //文章番号をもとに文を検索します。 Sentence result = SerchSentence(CurrentSentenceID); //メッセージがこれ以上ない場合はダイアログUIを非アクティブにする。 EndOfTalk(result); //得られたメッセージを表示します。 ShowingMassage(result); ShowingMassageIsBranch(result); Connect(result); } //メッセージを表示させるだけです。 void ShowingMassage(Sentence sentence) { massage.text = sentence.message; } Sentence SerchSentence(int Id) { //シナリオのメッセージ配列の中から文章をIDで検索して取得します。 Sentence result = chapter.sentences.First( (Sentence line) => { return line.id == Id; } ); return result; } //選択肢のメッセージを表示させるだけです。 //選択肢のUIはボタンとテキストで構成されています。 void ShowingMassageIsBranch(Sentence sentence) { if (sentence.branch) { SwichReadmoreInteractable(); SwichCoicesActivate(); yBranchMassage.text = sentence.yesMessage; nBranchMassage.text = sentence.noMessage; } } //肯定を選択したとき、ボタンコンポーネントのEventから呼び出されます。 public void ChooseYes() { //選択肢を押下したときReadmoreボタンを有効にします。 SwichReadmoreInteractable(); //選択肢を非表示にします。 SwichCoicesActivate(); Sentence result = SerchSentence(CurrentSentenceID); Skip(result.choseYes); } //否定を選択したとき、ボタンコンポーネントのEventから呼び出されます。 public void ChooseNo() { //選択肢を押下したときReadmoreボタンを有効にします。 SwichReadmoreInteractable(); //選択肢を非表示にします。 SwichCoicesActivate(); Sentence result = SerchSentence(CurrentSentenceID); Skip(result.choseNo); } //文章を読み飛ばします。 //選択の結果、シナリオが分岐したのち収束するときなどに使用します。 void Connect(Sentence sentence) { if (sentence.doConnect) { Skip(sentence.skipId); } } //別の文章に読み飛ばします。 void Skip(int id) { Sentence result = SerchSentence(id); CurrentSentenceID = result.id; ShowingMassage(result); } //最後の文章になったらUIを非表示にします。 //会話が終わったのにもかかわらず、UIが表示されていたらバグです。 //だから非表示する必要があります。 void EndOfTalk(Sentence sentence) { if (sentence.endOfTalk) { //このゼロは現在読んでいる文章をリセットするための数値です。 CurrentSentenceID = 0; SwichDialougeActivate(); } } //Readmoreボタンを押下の可不可を切り替えます。 //選択肢があるとき、選択肢以外のボタンを押せなくするために使用します。 void SwichReadmoreInteractable() { if (readMore.interactable) { readMore.interactable = false; } else { readMore.interactable = true; } } //アクティブ状態を切り替えます。 void SwichActiveState(GameObject target) { if (target.activeSelf) { target.SetActive(false); } else { target.SetActive(true); } } //選択肢のアクティブ状態を切り替えます。 void SwichCoicesActivate() { if (choices.activeSelf) { choices.SetActive(false); } else { choices.SetActive(true); } } //対話UIのアクティブ状態を切り替えます。 void SwichDialougeActivate() { if (dialogue.activeSelf) { dialogue.SetActive(false); } else { dialogue.SetActive(true); } } }最後に
Excel Importerを公開してくださっている@mikitoさん、ありがとうございます!
コメントで質問したときコードの一部が正しく表示されていなかったにもかかわらず
問題のある個所を丁寧に指摘していただいたことを改めて心から感謝いたします。追記
話しかけるを実装するにはどうしたらいいか気になって人もいると思いますのでメモ書き程度に記しておきます。
1.レイキャストや当たり判定を使ってNPCに近づいたことを判定したときに
2.NPCオブジェクトがUIを有効化する以上です。
@mikitoさんによるExcel Importerの紹介と説明の記事
https://qiita.com/mikito/items/2ad911f69180c15102a1
- 投稿日:2019-06-22T09:07:10+09:00
F#初心者がUnity + F#でゲーム制作している話
はじめに
プライベートでゲーム制作をしていること、その開発にUnity + F#を使っていることについて書きました。本稿では、
- どんな人が
- なぜ
- どんなゲームを
- どのように
作っているか、そして、
- なぜF#を使ったのか
- F#をどの程度使えればメリットが得られるか
について書きました。
どんな人
30代男性、ゲームが好きな職業プログラマです。
友人や妻と一緒にゲームをやることが多いです。1人プレイのゲームもやりますが、多人数でやることのほうが多いです。ゲームは頭を使ってやらないとすぐ飽きてしまうので、難しめのゲームか対戦ゲームばかりやっています。
仕事では業務システムの受託開発をしています。地味なBtoBのシステムばかりで、ゲーム開発とは無縁です(今回初めてゲーム開発をしています)。プログラミング言語もC#とPHPがほとんど、仕事で関数型言語を使うことはありません。趣味でHaskellを触ることはありますが、F#は使ったことがありません。なぜ自分でゲームを作っているか
友人とやるゲームは、協力ゲームもやりますが対戦ゲームもやります。主に4人でゲームをすることが多いです。協力ゲームには困っていませんが、2対2の対戦ゲームで満足できるものが少ないと感じています。
2対2の対戦ゲームには、
- 2対2の対戦用に設計されたもの
- 2対2以外の対戦用に設計されているが、2対2も出来るもの
- 対戦用に設計されていないが対戦も出来るもの
などがあります。このうち「2対2の対戦用に設計されたもの」は非常に少なく、それ以外は(2対2の対戦をしようとすると)ゲームシステムやゲームバランスが悪い場合がほとんどです。最初から2対2の対戦用に設計されたゲームでなければ、なかなか満足できません。
また、(市販の)対戦ゲームのゲームバランスは、ベテラン向けに調整されているという点も問題です。私たちは若いころこそヘビーゲーマーだったものの今ではおっさん週末ゲーマーです。家庭や仕事があり、ゲームの練習に時間を割きたくありません。そうすると、たまに集まって対戦するときには、それぞれの習熟度が全然違います。「ストーリーモードは一通りクリアした」から「ボタン配置教えて」まで。こんな状態の4人で、ある程度白熱した対戦を実現したいのです。
まとめますと
- 2対2の対戦ゲームは新しいゲームを待っていても期待できない
- 市販の対戦ゲームのゲームバランスは、私たちに適していない
という理由から自分でゲームを作ることにしました。
どんなゲームを作っているか
ガ〇ダムバーサスシリーズのパクりです。
- 3D対戦ゲーム
- 2対2のみ。それ以外の人数は考慮しない
- 敵は自動ロックオン。エイム操作、視点移動は不要
- オンライン対戦可
動画
主な設計
このゲームの設計で解決しなければならない課題は、
- オンライン対戦でも遅延を感じさせない
- 全てのプレイヤーの間で状態を同期すること
です。これらの課題を解決するため、以下のような戦略を取りました。
- 全てのプレイヤーの間で直接通信する
- 通信ではプレイヤーの操作情報のみを送る
- 操作をしてからゲームに反映するまでに一定の遅延(現在は100ms)を設ける
この戦略で実装し、4人対戦しても十分に遅延が隠せることを確認しました。
しかし、これだけでは状態の同期が取れない問題が見つかりました。同期が取れないということは、各々の画面で別の状態が表示されていて、ある人は負けそうだから頑張ろうと考えていたり、ある人は既に勝利を収めて喜んでいたり…ゲームが成り立ちません。調べてみると、Unityの物理演算は毎回結果が同じにはならないようです。Unityでリプレイ機能(同じ結果を再現する機能)を実装するのに苦労されている方がたくさんいらっしゃることも分かりました。今回はUnityの物理演算は使わずに、独自の実装でオブジェクトを動かすことにしました。これでかなり改善されましたが、まだ完全に同期を取れていません。実はオブジェクトの移動こそ独自に実装したのですが、衝突判定だけはUnityの機能をまだ使っています。これが原因であるか特定していませんが、衝突判定まで実装するのは結構大変そうです…。開発体制
開発は私1人で行っています。週末土日のうち、どちらかの午前に開発を進めることが多いですが、全く開発の時間を取れない週もあります。また、スキルと優先順位の問題で
- プログラム→作る
- 3Dモデル→全てアセットストア
- 2D画像→フリー素材
- 音楽→フリー素材
としています。
プライベートの空き時間で開発をしているため、
- 開発時間を短くしたい
- 自分がいらない機能は後回し
- UIは汚くてよい
- 影響の小さいバグは無視
- モチベーションを保ちたい
- 機能追加を優先
- 楽しくなるものから実装
- 機能追加の単位を小さくしたい
- まとまった作業時間が取れない
- 実装の途中で時間が経つと忘れる
- 仕様書、設計書を作りたくない
- 楽しくない
- 「今月は設計書を書いただけ」とかは嫌
といった私自身からの要望があります。
Unity + C#で作り始めた
始めはUnityとC#で作り始めました。F#に興味はあったものの、以下の理由でC#を選択しました。
- UnityではC#を使うのが普通
- F#は構文すら知らない。学習コストがかかる
- ゲーム制作すら初めてなのに、使ったことのない言語を選んだら開発が進まないかも
- すぐに開発を始めることを重視
難しいことをあれこれ考えるよりも開発を進めることを優先しました。機能をどんどん追加して行き、必要になってから設計を見直してリファクタリングすることにしました。
しばらく開発を進めると、予想通り品質の悪いコードが増えてきました。特に問題になったのが状態の管理です。ゲームではかなり多くの状態(キャラの位置、向き、ライフ、残弾、硬直…)を持たなければなりません。例えばライフだけを取っても、キャラ生成時、弾が当たったとき、格闘が当たったとき、回復アイテムを使ったとき…など様々な場合にライフという状態が変更されます。これらを正しく管理しなければ、バグの検出も難しくなりゲームを正しく動作させられません。こんな複雑なことを考えていたらバグも増えますし、何より機能追加のスピードが遅くなります。機能追加はゲーム制作で一番楽しいところで、これが気持ちよく出来ることはとても大事です。F#に移植した
状態を正しく管理するのは、「状態を変更するコード」と「状態を変更しないコード」を分離出来れば簡単になります。これってF#が得意なことじゃありませんか?Immutableな変数と純粋な関数で「状態を変更しないコード」つまり副作用のないコードを書き、限られたコードだけで状態を変更するようにします。
移植にはかなりの時間がかかりました。3か月間の週末プログラミングで書いたC#のコードを移植するのにゴールデンウィーク中5日間ぐらいかかりました。移植の手順は以下の通りです。
- C#のクラスやロジックをそのままF#に移す
- while、for、foreachをmapやiterに変える
- 変数をimmutableに、クラスをrecord、algebric dataに変える
- Unityオブジェクトに変更を加えるコードを限定するようにリファクタリング
これだけでコードの管理がとても楽になりました。移植作業後の機能追加がとても速くなり驚きました。
Unityでゲーム制作するならF#?
今回C#をやめてF#を使うことにした目的はコードを管理しやすくすることで、その目的は達せられたと考えています。
一方でF#を使わなくても達成する方法はいくらでもあります。そもそもコードの構成をしっかり考えてから実装すればC#でも品質の良いコードは書けます。F#でも設計方針がなければ同じようにコードの品質が悪くなったでしょう。今回はF#を使うことで改善する見込みがあったからF#を使いました。
私はF#に移植したことで良かったと信じていますが、F#の恩恵を受けるための最低基準
既にC#でゲーム開発をしている人がF#に興味を持った場合、どれくらいF#について学べば恩恵を受けられるでしょうか?今回、私はF#の構文すら事前に知らずに移植を始めました。構文は調べながら、だいたいこのページのDos and Don’tsに従ってF#を書けば十分に恩恵を受けられると思います。F#初心者や関数型言語初心者は、これ以上の難しい話はまずは無視しても良いのではないでしょうか。ただし、Unityを使う以上、mutableな変数やclassを使うことは避けられないと思います。避けられないとはいえmutableな変数とclassをほとんどなくすことは出来ます。完璧にしなくとも十分メリットを感じられるはずです。
- 投稿日:2019-06-22T03:07:13+09:00
[Unity]SteamVR Plugin v2.2でviveのタッチパッドを使ってみる
steamVR Pluginを触り始めたとき何が何だか分からなかったので書きます。
拙い点もあると思いますがよろしくお願いします。
参考
SteamVR Unity Plugin v2.2.0でのインプット入力について
steamVR Plugin 2.2では元から設定されている入力があったり、なかったりします。
例としては、
InteractUI : グリップの部分
Teleport : タッチパッドのボタン
GrabPinch : グリップの部分
GrabGrip : 側面のボタン
などがすでに設定されています。
(SteamVR_Input_Actions.csの中身)設定の追加
タッチパッドの位置の入力は設定されていないので追加する必要があります。
追加するには2工程ほどあり、
①SteamVR Inputに追加
②入力バインドで追加
をしなければいけません。SteamVR Input
UnityのWindow → SteamVR Input で開けます。
下の画像のように ActionsのInで+を押して新たな入力を追加します。
名前はTrackPadとしています。
この入力はx軸とy軸で与えられるためTypeをvector2にする必要があります。
追加出来たら左下のSave and generate を押します。入力バインド
SteamVR Inputで作成できたので入力バインドで設定をします。
SteamVR Inputウィンドウの右下にあるOpen binding UI を押します。
下の画像のようなウィンドウが表示されるので 編集 を押します。
下の画像のように表示されたらトラックパッドのところで+を押します。
以下のように設定していきます。
最後に 個人用バインドを保存 を押して追加完了です。入力テスト
pad_test1.csというスクリプトを作成してテストしてみます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Valve.VR; public class pad_test1 : MonoBehaviour { private SteamVR_Action_Vector2 TrackPad = SteamVR_Actions.default_TrackPad; private Vector2 pos; // Use this for initialization void Start () { } // Update is called once per frame void Update () { pos = TrackPad.GetLastAxis(SteamVR_Input_Sources.RightHand); Debug.Log(pos.x +" " + pos.y); } }次に適当にキューブを動かしてみます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Valve.VR; public class pad_test1 : MonoBehaviour { private SteamVR_Action_Vector2 TrackPad = SteamVR_Actions.default_TrackPad; private Vector2 pos; // Use this for initialization void Start () { } // Update is called once per frame void Update () { pos = TrackPad.GetLastAxis(SteamVR_Input_Sources.RightHand); transform.localPosition = new Vector3( pos.x * 0.1f,0, pos.y * 0.1f); } }ヒエラルキーで右手にキューブを追加してスクリプトを付けます。
パッドをぐりぐり動かしてみるとキューブも動きました。
極座標表示にする
極座標についての簡単な図
まず極座標にして何が嬉しいか を説明します。
今回の場合で言うと、
・パッドを3分割してそれぞれの処理をしたい場合、θを使えば簡単になる
if分の条件で0<θ<120°のようにすることができます。
x,yで表した場合は少し難しいことになります。
・rを使うことで中心からどれだけ離れているかがわかりやすい
x,yで表した場合は縦か横の場合にしかわかりませんね。
中心から0.7以上離れた場合などを考えたい場合などに便利です。コードは以下のようになります。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Valve.VR; public class pad_test1 : MonoBehaviour { private SteamVR_Action_Vector2 TrackPad = SteamVR_Actions.default_TrackPad; private Vector2 pos; float r,sita; // Use this for initialization void Start () { } // Update is called once per frame void Update () { pos = TrackPad.GetLastAxis(SteamVR_Input_Sources.RightHand); transform.localPosition = new Vector3( pos.x * 0.1f,0, pos.y * 0.1f); r = Mathf.Sqrt(pos.x* pos.x + pos.y* pos.y); sita = Mathf.Atan2(pos.y, pos.x) / Mathf.PI * 180; Debug.Log(r +" "+ sita ); } }ちゃんと表示されました。
トラックパッドの下側だと角度がマイナスになってしまいます。
気になる人は初期値として+180をしましょう。式は以下の通りです。
プログラムではAtanが2種類あるので気をつけましょう。
角度は式のまんまだとRad表示になるのでDegに変換するために、3.14で割って180で掛けています。
- 投稿日:2019-06-22T00:08:55+09:00
UnityでPlayFabのタイトルデータを取得して表示する
今回は Unity で PlayFab のタイトルデータを取得して表示してみます。
※PlayFabとは?という方は こちら をご覧ください。まえがき
1. タイトルデータとは
タイトルデータ
とはゲームの設定データのことです。
PlayFab にはゲームの設定データを保存しておける KVS としての機能があります。
タイトルデータ
という名前が少し分かりづらいような気もしますが、PlayFab は1つのゲームのことをタイトル
と表現しますので、それを覚えておけば混乱しないかなと思います。2. タイトルデータの注意点
PlayFab のタイトルデータは値を設定してから反映されるまで、最大で15分のタイムラグがあります。
そのため、タイトルデータは動的に更新されるようなデータの管理には適しません。
どのようなデータをタイトルデータとして保存するかは十分に検討が必要です。ちなみに私は個人開発のゲームのマスタデータの管理に使用しています。
マスタデータになにか問題があった場合にデータの修正結果が最大15分間反映されない可能性はありますが、個人開発のゲームですし許容できると判断しました。タイトルデータを保存する
今回は GameManager から保存します。
左メニューのコンテンツを開くとタイトルデータが表示されます。タイトルデータには2つの種類があります。
- タイトルデータ(Clientから読めるデータ)
- タイトル内部データ(サーバーサイドでしか読めないデータ)
データの性質を考慮して、適切な方を使用してください。値は JSON で入力することが可能です。
値を入力したら下部のボタンでタイトルデータを保存します。
サンプルの GachaMaster の値のコピペはこちらからどうぞ
[ { "ID": 1, "Name": "レアキャラ1", "Rank": 2, "Rate": 16 }, { "ID": 2, "Name": "レアキャラ2", "Rank": 2, "Rate": 16 }, { "ID": 3, "Name": "レアキャラ3", "Rank": 2, "Rate": 16 }, { "ID": 4, "Name": "レアキャラ4", "Rank": 2, "Rate": 16 } ]Unity でタイトルデータを取得して表示する
1. 下準備(Unity から PlayFab SDK を使う準備と PlayFab へのログインの実装)
@ume67026265 さんが PlayFab のアカウントの作り方と、Unity へ SDK を導入する記事を書かれています。
不慣れな方はこちらを参考に進めるとスムーズに準備ができると思います。
- PlayFab始めました ※アカウントの作り方
- PlayFabでUnityを動かしてみる -その1下準備編- ※Unity へ PlayFab SDK を導入する手順
- PlayFabでUnityを動かしてみる -その2 APIコールを作ってみる- ※ユーザーを PlayFab へログインさせるための手順
2. Unity でタイトルデータを取得して表示する
2.1 シリアライザの用意
PlayFab と直接関係がある話ではありませんが、マスタデータなどを JSON で管理する場合、Unity標準のシリアライザ(JsonUtility)では上手く扱えないケースが多くあります。
その場合は Utf8Json などを利用しましょう。
Utf8Json を使う準備はこんな感じです。
1. Release から最新の unipackage を落としてインポート
2. ProjectSettings で 以下のように設定する
2.2 PlayFabLogin クラスを編集
下準備で作った PlayFabLogin という Script を編集します。
PlayFab から TitleData を取得して表示するメソッドと、GachaMaster 用のクラスを PlayFabLogin クラスに追加します。public static void GetTitleData() { var request = new GetTitleDataRequest(); PlayFabClientAPI.GetTitleData(request, OnSuccess, OnError); void OnSuccess(GetTitleDataResult result) { Debug.Log("GetTitleData: Success!"); var loginMessage = result.Data["LoginMessage"]; Debug.Log(loginMessage); var gachaMaster = Utf8Json.JsonSerializer.Deserialize<GachaMaster[]>(result.Data["GachaMaster"]); foreach (var master in gachaMaster) { Debug.Log(master.Name); } } void OnError(PlayFabError error) { Debug.Log("GetTitleData: Fail..."); Debug.Log(error.GenerateErrorReport()); } } public class GachaMaster { public int ID { get; set; } public string Name { get; set; } public int Rank { get; set; } public int Rate { get; set; } }2.3 OnLoginSuccess メソッドの中で GetTitleData を呼ぶ処理を追加
private void OnLoginSuccess(LoginResult result) { Debug.Log("Congratulations, you made your first successful API call!"); GetTitleData(); // ここを追加 }これを Unity で再生してみましょう。
取得したタイトルデータが表示できました。参考/関連