- 投稿日:2020-12-07T23:10:52+09:00
M5Stack + Unity 『草刈りゲーム』の作り方
この記事は プロトアウトスタジオのアドベントカレンダー2020 の7日目の記事です!
草刈りゲーム
実は、今日2020年12月7日はプロトアウトスタジオの卒業制作のクラウドファンディングのスタート日です。
私のは、これです。
ハッカソンから生まれたアイデア『草刈りゲーム』のeスポーツイベントを開催したい
必要なもの
ハード
- 草刈り機
- M5Stack Gray
- 3G拡張ボード
- ソラコムのSIMカード (ナノサイズ)
※プランは plan01s , plan-D事前準備
・SORACOMのアカウントをとること
・Unityをインストールすることシステム構成図
- 草刈り機にM5StackGrayをつけて、ジャイロセンサーの値をSORACOMの3G回線でクラウド(SORACOM HARVEST)に上げます。
(詳細記事)M5StackGRAYのジャイロセンサの値を3G拡張ボードを使ってSORACOM Harvestに送ってみた
- 定期的にUnity側からクラウドを参照し、ジャイロセンサーの値によってゲーム内キャラクターの挙動を決める
(詳細記事)UnityからSORACOM Harvestにあるデータをゲットする方法コード
M5Stack
M5StackGrayのジャイロセンサーの値をSORACOMの3G回線でクラウド(SORACOM HARVEST)に上げるコードです。
(詳細記事)M5StackGRAYのジャイロセンサの値を3G拡張ボードを使ってSORACOM Harvestに送ってみたmain.cpp//<M5Stack.h>をincludeする前に、IMUモジュールを#defineしておく #define M5STACK_MPU6886 #include <M5Stack.h> //3G通信に必要 #define TINY_GSM_MODEM_UBLOX #include <TinyGsmClient.h> float gyroX, gyroY, gyroZ; TinyGsm modem(Serial2); /* Serial2 is Modem of 3G Module */ TinyGsmClient ctx(modem); void setup() { Serial.begin(115200); M5.begin(); M5.Power.begin(); M5.IMU.Init(); M5.Lcd.clear(BLACK); M5.Lcd.setTextColor(WHITE); M5.Lcd.println(F("M5Stack + 3G Module")); M5.Lcd.print(F("modem.restart()")); Serial2.begin(115200, SERIAL_8N1, 16, 17); modem.restart(); M5.Lcd.println(F("done")); M5.Lcd.print(F("getModemInfo:")); String modemInfo = modem.getModemInfo(); M5.Lcd.println(modemInfo); M5.Lcd.print(F("waitForNetwork()")); while (!modem.waitForNetwork()) M5.Lcd.print("."); M5.Lcd.println(F("Ok")); M5.Lcd.print(F("gprsConnect(soracom.io)")); modem.gprsConnect("soracom.io", "sora", "sora"); M5.Lcd.println(F("done")); M5.Lcd.print(F("isNetworkConnected()")); while (!modem.isNetworkConnected()) M5.Lcd.print("."); M5.Lcd.println(F("Ok")); M5.Lcd.print(F("My IP addr: ")); IPAddress ipaddr = modem.localIP(); M5.Lcd.print(ipaddr); delay(2000); } void loop() { M5.update(); M5.Lcd.clear(BLACK); M5.Lcd.setCursor(0, 0); M5.Lcd.println(F("Uptime post to SORACOM Harvest")); /* HTTP GET example */ if (!ctx.connect("uni.soracom.io", 80)) { Serial.println(F("Connect failed.")); return; } Serial.println(F("connected.")); /* build payload */ M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ); M5.Lcd.setCursor(0, 10); M5.Lcd.printf("gyro=(%5.1f, %5.1f, %5.1f)", gyroX, gyroY, gyroZ); char payload[1024]; M5.Lcd.setCursor(0, 15); /* send request */ ctx.println("POST / HTTP/1.1"); ctx.println("Host: uni.soracom.io"); ctx.println("Content-Type: application/json"); char content_length_hdr[32]; sprintf(content_length_hdr, "Content-Length: %lu", strlen(payload)); ctx.println(content_length_hdr); ctx.println(); ctx.println(payload); Serial.println("sent."); /* receive response */ while (ctx.connected()) { String line = ctx.readStringUntil('\n'); Serial.println(line); if (line == "\r") { Serial.println("headers received."); break; } } char buf[1 * 1024] = {0}; ctx.readBytes(buf, sizeof(buf)); /* body */ ctx.stop(); M5.Lcd.println(buf); delay(1000 * 10); }Unity
定期的にUnity側からクラウドを参照し、ジャイロセンサーの値によってゲーム内キャラクターの挙動を決めます。
(詳細記事)UnityからSORACOM Harvestにあるデータをゲットする方法kusakari.csusing UnityEngine; using System.Collections; using UnityEngine.Networking; public class UnityChanGame : MonoBehaviour { //Soracom Harvest 設定 private string APIKEY = "APIKEY"; private string APITOKEN = "APITOKEN" public float gyroX, gyroY, gyroZ; private Animator animator; void Start() { animator = GetComponent<Animator>(); } // Update is called once per frame void Update() { //ソラコムHarvest StartCoroutine(GetText("https://g.api.soracom.io/v1/subscribers/#####/data?sort=desc")); //各ジャイロセンサーの閾値。ここの数値を実際の草刈機の振動で調整する。 if ( Mathf.Abs(gyroX) > 30 || Mathf.Abs(gyroY) > 30 || Mathf.Abs(gyroZ) > 30) { // 座標を取得 Vector3 unitychanPos = transform.position; unitychanPos.z -= 0.005f; transform.position = unitychanPos; //歩くアニメーション(アニメーションの設定が必要です) animator.SetBool("is_walking", true); } else { animator.SetBool("is_walking", false); } } IEnumerator GetText(string url) { using (UnityWebRequest www = UnityWebRequest.Get(url)) { www.SetRequestHeader("Accept", "application/json"); www.SetRequestHeader("X-Soracom-API-Key", APIKEY); www.SetRequestHeader("X-Soracom-Token", APITOKEN); yield return www.SendWebRequest(); if (www.isNetworkError || www.isHttpError) { Debug.Log(www.error); } else { //JSON Objectというアセットのインポート要 JSONObject json = new JSONObject(www.downloadHandler.text); JSONObject content = json[0].GetField("content"); string content2 = content.str; string str = System.Text.RegularExpressions.Regex.Unescape(content2); JSONObject jsonObj = new JSONObject(str); gyroX = jsonObj.GetField("gyroX").n; gyroY = jsonObj.GetField("gyroY").n; gyroZ = jsonObj.GetField("gyroZ").n; Debug.Log("gyroX:" + gyroX + "\t gyroY:" + gyroY + "\t gyroZ:" + gyroZ) } } }(参考記事)
- ユニティちゃんを動かす
- リアルアバター(FBXファイル)をUnityで動かす今後の課題
- クラウドの負荷
今は、都度ジャイロセンサーの値をあげ、Unity側から取得にいっていますが、すごく非効率です。クラウドの利用制限にひっかかる懸念がありますし、貴重なIoTのクラウドのリソースをそんな使い方するのはあまりよろしくないと思います。
クラウドを介せず、ジャイロセンサーの閾値が変化したときだけ、webhookでPCに接続したマイコンに伝え、そこから制御する方法とか考えられそうです。
- 実際に草刈りをしているかの判定
今は、ジャイロセンサーで取得したエンジンの振動によりゲーム内キャラクターの動きを決めていますが、実際に草を刈らなくても、ゲーム内キャラクターは動くという欠点があります。
草刈りをしているか、していないかを音から判定する方法ができないか検討中です。最後に
2020年12月12日にオンラインピッチイベントで草刈りゲームのことをピッチします。
ぜひ応援にきてください!
ヒーローズリーグ オンライン 決勝・BINGO大会 & ProtoOut DEMO DAY
- 投稿日:2020-12-07T22:47:52+09:00
カンニング万歳!JetBrains Rider逆引きチートシート
最初に
この記事は JetBrains2020.3 と、その拡張機能であるVimなどのよく使うショートカットキーに関する逆引きチートシートとなっております。vim拡張機能でのショートカットも記載しております。当方の環境としては、Windowsを使用しておりますので、Macの方には分かりにくい部分も多いかもしれません。ご了承ください。
逆引きチートシートの推奨する見方
このチートシートを効率良く確認する推奨環境の紹介です。まずディスプレイが二枚以上あると、常にチートシートを表示させながらエディタを操作することができるのでオススメです。また、スクロールを行わずに効率的に作業を進めながらチートシートを確認するために、チートシートのページを普段使いのブラウザ以外で文字を小さくして複数ウィンドウに表示させることをオススメします。「Spectacle」というアプリをPCに入れるとショートカットキーの押下でウィンドウを簡単に上寄せ半分表示、斜め右上1/4表示などの表示が可能なので、是非使用してみて下さい。
共通操作
行の編集
キー 動作 備考 [ctrl]+[X] 行の切り取り 未選択状態の行全体を切り取り [ctrl]+[C] 行のコピー 未選択状態の行全体をコピー [Ctrl]+[↩︎] 現在のカーソル行の下に空行を追加 カーソルは行頭に移動する [ctrl]+[shift]+[↩︎] 現在のカーソル行の上に空行を追加 カーソルは行頭に移動する [shift]+[alt]+[クリック] マルチカーソルをクリック場所に作成 [shift]+[alt]+[Down] マルチカーソル下方向に作成 複数行を選択中はその行数分 Tab 行をインデント 行のスペース部分にカーソルがある状態で有効 [shift]+[Tab] 行をアンインデント [ctrl]+[W] 選択範囲を段階ごとに広げていく [ctrl]+[shift]+[s] 全てを保存 [alt]+[↩︎] 現在の選択箇所でのクイックアクション一覧を表示 行の移動
キー 動作 備考 [alt]+[Down] メソッド単位で下に移動する [alt]+[Up] メソッド単位で上に移動する 行の選択
キー 動作 備考 [alt]+ドラッグ 矩形選択 コメントの編集
キー 動作 備考 [ctrl]+[k]+[c] 行コメント記号をトグル 複数行を選択中はその行数分 [ctrl]+[shift]+[/] ブロックコメント記号をトグル 選択中は選択部分 ファイル/フォルダ自体の表示
キー 動作 備考 [ctrl]+[Tab] 開いているファイルの切り替え表示 開いた後は^を押下したままTabで切り替えられる [ctrl]+[F4] 現在開いているファイルを閉じる ファイルやコードのパスコピー
キー 動作 備考 [ctrl]+[shift]+[c] 現在アクティブなファイルのパスをコピー [ctrl]+[alt]+[shift]+[c] 現在アクティブなファイルのパスをコピー 検索/置換(シンボルに関しては後述)
キー 動作 備考 [ctrl]+[F] 上部に検索ポップアップ表示 カーソル位置のワードがマッチされていればその単語を使用する [ctrl]+[shift]+[F] 全ファイルにまたがってキーワード検索をする [ctrl]+[H] 置換ポップアップ表示 同上 [F3] 次を検索 マッチワードがあればその単語で検索 [shift]+[F3] 前を検索 同上 ブックマーク操作操作
キー 動作 備考 [ctrl]+[k],[k] 現在の位置にブックマークを追加・削除 [ctrl]+[`] ブックマーク一覧を表示 [ctrl]+[k],[n] 次のブックマークへ移動 [ctrl]+[k],[p] 前のブックマークへ移動 [ctrl]+[k],[p] 前のブックマークへ移動 [ctrl]+[shift]+[1~9] 数字に対応したブックマークを追加・削除 [ctrl]+[1~9] 数字に対応したブックマークへ移動 IDE操作
キー 動作 備考 [shift]+[F4] 現在開いているコードファイルを新しいウィンドウで開く [ctrl]+[shift]+[E] 最近変更した箇所群の表示 [alt]+[1] Explorer画面の表示 [alt]+[3] Find画面の表示 [alt]+[5] デバッグ画面の表示 デバッグ時に有効 [alt]+[7] NuGet画面の表示 [alt]+[8] UnitTests画面の表示 [alt]+[9] Git画面の表示 [alt]+[左右キー] 各ツールウィンドウ内でのタブ移動 [ctrl]+[Tab] 画面のSwitcherウィンドウ表示 タブに対応した文字を打ち込むと即座に切り替わる [ctrl]+[G] 指定した行番号に移動 [ctrl]+[-] 前に表示していた箇所に戻る [ctrl]+[shift]+[-] 次に表示していた箇所に戻る [↩︎] エディタにフォーカスを戻す [shift]+[esc] ツールウィンドウを閉じる [ctrl]+[alt]+[s] Setting画面の表示 エラー表示時移動
キー 動作 備考 [ctrl]+[alt]+[2] ソリューション内でのエラー一覧表示 [alt]+[shift]+[Page Up] 次のエラーと警告に移動 [alt]+[shift]+[Page Down] 前のエラーと警告に移動 [shift]+[esc] ツールウィンドウを閉じる Git操作
キー 動作 備考 [alt]+[9] Git画面の表示 以下はGitツールがアクティブ状態の時に有効 [ctrl]+[alt]+[k] 選択ファイルをコミットする [ctrl]+[alt]+[z] 選択ファイルをロールバックする [ctrl]+[shift]+[h] 選択ファイルをスタッシュする [ctrl]+[alt]+[z] 選択ファイルをロールバックする [ctrl]+[alt]+[u] 選択ファイルをスタッシュから取り出す [↩︎] エディタにフォーカスを戻す 以下はエディタがアクティブ状態の時に有効 [ctrl]+[alt]+[w] pull用のウィンドウ表示 MergeするかRebaseするか選べる [ctrl]+[alt]+[k] コミット用のウィンドウ表示 [ctrl]+[shift]+[k] push Merge操作
キー 動作 備考 [shift]+[alt],[right] リモートの変更を優先する [F7] 次の差分に移動する コーディング時の操作
シンボル検索/表示
キー 動作 備考 [F12] シンボルの定義を表示 基本はこっちの方が使い勝手が良い [shift]+[F12] 全ファイルにまたがって参照検索しファイルを開く [ctrl]+[R],[R] シンボルのリネーム デバッグ操作
キー 動作 備考 [F9] ブレークポイントの切り替え [F5] デバッグスタート又はコンティニュー [alt]+[F5] アタッチする [shift]+[F5] アタッチを外す [F10] ステップオーバー [alt]+[shift]+[F8] フォースステップオーバー [F11] ステップイン [shift]+[F7] スマートステップイン [shift]+[F11] ステップアウト Vim操作
モード切り替え
キー 動作 備考 [i] 挿入モード カーソル位置から [a] 挿入モード カーソルの後から [:] コマンドラインモード [v] ビジュアルモード [esc] ノーマルモード ファイル操作
キー 動作 備考 [:]+[e] ファイルをパス指定で開く [:]+[q] 開いているファイルを閉じる エディタ操作(ノーマルモード)
キー 動作 備考 [dd] カーソルがある行を切り取り [dd]の前に数値を入れて切り取る行数の指定が可能 [yy] カーソルがある行のコピー [yy]の前に数値を入れてコピーする行数の指定が可能 [P] カーソルがある行にペースト [p] カーソルの下の行にペースト [u] Undo [^]+[r] Redo [U] 行に対して行った変更の全てを取り消す [v] 選択開始 [V] 行選択 [^]+[v] 矩形選択 [gv] 直前の選択範囲を再選択 検索/置換(ノーマルモード)
キー 動作 備考 [/]+[文字列] 前方検索 [?]+[文字列] 後方検索 [#] カーソル位置の単語を前方検索 [*] カーソル位置の単語を後方検索 [n] 次の候補 [shift]+[n] 前の候補 [gd] カーソル位置のローカル宣言を検索 [gD] カーソル位置のグローバル宣言を検索 [:]+[%s/from/to/g] ページ全体で置換 fromが検索語句,toが置換語句 [:]+[32,50s/from/to/g] 32〜50行目まで置換 gは繰り返し、cなら一回毎に確認 テキスト操作(ノーマルモード)
キー 動作 備考 [x] 1文字削除 Deleteキーと同じ [X] 1文字削除 BSキーと同じ [D] カーソル位置から行末まで削除 [s] 1文字削除して挿入モードへ切り替え [S] 現在の行を削除して挿入モードへ切り替え [r] カーソル位置の一文字だけ置換 [R] 置換モードに切り替え [J] 現在の行と下の行をスペースありで連結 [gJ] 現在の行と下の行をスペースなしで連結 [~] 大文字/小文字に変換 [^]+[a] カーソル位置の数字をインクリメント [^]+[x] カーソル位置の数字をデクリメント [^]+[p] (挿入モード時)単語を後方向に検索し、補完する [^]+[n] (挿入モード時)単語を前方向に検索し、補完する インデント操作(ノーマルモード)
キー 動作 備考 [>] 現在の行をインデント [<] 現在の行を逆インデント 画面分割(ノーマルモード)
キー 動作 備考 [:]+[sp]or[vsp] エディタウィンドウを縦に分割 [:]+[q] エディタウィンドウを閉じる [:]+[qall] 全てのエディタウィンドウを閉じる 移動操作(ノーマルモード)
キー 動作 備考 [gg] 最初の行へ [G] 最後の行へ [数値]+[G] 指定した行へ [0] 行の最初へ インデント無視 [^(Ctrlではない)] 現在の行の最初へ テキストの最初 [$] 行の末尾へ [-] 前の行の最初へ [+] 次の行の最初へ [%] カーソル位置にある括弧に対応する括弧へ [H] エディタの上端へ移動 Hの前に数値で上から数えた行へ [M] エディタの中央行へ移動 [L] エディタの下端へ移動 Lの前に数値で上から数えた行へ [ctrl]+[o] 前回のジャンプ位置へ戻る [ctrl]+[i] 次回のジャンプ位置へ戻る [z]+[↩︎] 現在の行をエディタの一番上に位置なるようにスクロールする テキストの最初 [w] 次の単語に移動 [b] 前の単語に移動 [e] 単語の末尾に移動 [W] 次の単語に移動 記号は無視 [B] 前の単語に移動 記号は無視 [E] 単語の末尾に移動 記号は無視 [f]+[一文字] カーソルを指定した文字まで移動 3fiで3つ目のiまで移動 [t]+[一文字] カーソルを指定した文字の左端まで移動 3tiで3つ目のiの左側まで移動 スクロール操作(ノーマルモード)
キー 動作 備考 [ctrl]+[b] 1画面上に移動 [ctrl]+[y] 1行上に移動 [ctrl]+[f] 1画面下に移動
- 投稿日:2020-12-07T18:50:05+09:00
UnityのWebGLビルドでFirestoreからリアルタイムにデータを取得して反映する
概要
UnityのWebGLビルドしたものからfirestoreにアクセスしたい
firebaseのSDKでWebGLで動作するものがないのでちょっと工夫する必要がある
流れとしては
- C#からjslibの関数を呼び出し
- jslib内でデータの取得をしたらSendMessage関数でC#の更新用の関数を呼び出し
jslibの作成
Assets配下にPluginsというディレクトリを作成して.jslibという拡張子のファイルを作成
ES2015以降の記法は使えないので注意firestore.jslibmergeInto(LibraryManager.library, { // 関数呼び出し Hello: function () { window.alert("Hello, world!"); }, Firestore: function() { // firebaseのconfig var firebaseConfig = { apiKey: "xxxxxxxxxxxxxxxxx", authDomain: "xxxxxxxxxxxxxxxxxxxxxxx", databaseURL: "xxxxxxxxxxxxxxxxxxx", projectId: "xxxxxxxxxxxxxxxxx", storageBucket: "xxxxxxxxxxxxxxxxxxxx", messagingSenderId: "xxxxxxxxxxxxxxxxxx", appId: "xxxxxxxxxxxxxxxxxxxxxxx" } // firebaseの初期化 firebase.initializeApp(firebaseConfig); var db = firebase.firestore(); db.collection("unity").doc("IHzXZcKpKwOiVFEVPbYT") .onSnapshot(function(doc) { console.log("Current data: ", doc.data()); SendMessage('Text', 'UpdateText', doc.data().text); }); } });C#側のスクリプト
以下のスクリプトを空オブジェクトなんかにつけておく
TextManager.csusing UnityEngine; using System.Runtime.InteropServices; using UnityEngine.UI; public class TextManager : MonoBehaviour { [DllImport("__Internal")] private static extern void Firestore(); // js側から更新が合った時に呼び出される関数 public void UpdateText(string newText) { // テキストコンポーネントの取得 Text text = GameObject.Find("Text").GetComponent<Text>(); text.text = newText; } void Start() { //js側の関数を呼び出してデータの監視開始 Firestore(); } }ビルドしてできたindex.htmlでfirebaseSDKを読み込むように修正
WebGLようにビルドするとindex.htmlが表示されるので微修正
<!DOCTYPE html> <html lang="en-us"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Unity WebGL Player | firestore-test</title> <link rel="shortcut icon" href="TemplateData/favicon.ico"> <link rel="stylesheet" href="TemplateData/style.css"> <script src="TemplateData/UnityProgress.js"></script> <script src="Build/UnityLoader.js"></script> <script> var unityInstance = UnityLoader.instantiate("unityContainer", "Build/Build.json", {onProgress: UnityProgress}); </script> </head> <body> <div class="webgl-content"> <div id="unityContainer" style="width: 960px; height: 600px"></div> <div class="footer"> <div class="webgl-logo"></div> <div class="fullscreen" onclick="unityInstance.SetFullscreen(1)"></div> <div class="title">firestore-test</div> </div> </div> // ここでfirebaseを読み込む <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-firestore.js"></script> </body> </html>おまけ
同じ様なやり方でデータの追加や更新もできるのでセーブデータとかの管理に使えるかも
参考
https://docs.unity3d.com/ja/2020.2/Manual/webgl-interactingwithbrowserscripting.html
- 投稿日:2020-12-07T18:24:54+09:00
Unityで「積み上げ式棒グラフのリザルト画面」を作る&いい感じに音をつける
積み上げ棒グラフのリザルト画面とは?
対戦ゲームなどで複数ラウンドの試合を繰り返して合計点数を競うタイプのゲームを考えます。1ラウンドごとの得点合計をグラフで積み上げ、基本点数とボーナス点数が横方向に積みあがってく見せ方を「積み上げ棒グラフのリザルト画面」と呼びます。
正式な名称が分からないのですが、たとえば「Ultimate Chicken Horse」というタイトルではそのような見せ方になっています。Ultimate Chicken Horse - Launch Trailer
https://youtu.be/FYaE_xw4krw?t=25これをUnityで作る方法を考えてみましょう。完成形の動きは次の通りです。
Layout Groupの活用
Unityの標準コンポーネントのひとつに、「Layout Group」というものがあります。
これは、アタッチされているゲームオブジェクトの子となるUI要素の位置を調整・変更するコンポーネントです。ボタンなどのUI要素を、指定の空間内にバランスよく並べることができます。
親となるコンポーネントにアタッチすると子のRect Transformが従属し自動で変更されます。横方向用は「Horizontal Layout Group」、縦は「Vertical Layout Group」です。このコンポーネントの本来の使い方は、「UIの整列」です。スクリーンサイズが異なる端末へのマルチ展開や、プレイヤーがUIのサイズを自由に変更できるシステムへの対応の時に使います。
このコンポーネントは、ゲームが実行中であってもリアルタイムに反映されます。これを転用して、積み上げ式の棒グラフを楽に作ることができます。
長さ0のImageをあらかじめ用意しておき、順番にWidthを変更していきます。
それぞれのImageが横並びに整頓されるため、棒グラフに使うと、1段目の棒が伸びきった位置が2段目の起点にできます。長さの変更は、DoTweenを使ってなめらかに変更します。(描画負荷はたぶんよろしくないのですが、リザルト画面では他の描画処理はあんまりいそがしくないだろう、という想定であまり考えません)
実装
まずは、親となるゲームオブジェクトにHorizontal Layout Groupコンポーネントをアタッチします。「左端から伸びる」動きのために、Child AlignmentをMiddle Leftに設定します。また、隙間は作らないのでチェックボタンは全部外して、Padding, Spacingも0に設定します。
一緒に新規のコードを作ってアタッチします。(ここではStacked Bar Resultsという名前のスクリプトファイルにしています)
コードでは、まずこのコンポーネントを親として、高さが一定で幅が0のImageを生成して参照をリストで持ちます。いわゆるオブジェクトプーリングです。
StackedBarResults.cspublic int barHeight = 80; List<Tuple<RectTransform, Image>> barDataList = new List<Tuple<RectTransform, Image>>(); private void Start() { for (var i = 0; i < 20; i++) { barDataList.Add(GenerateBarParts()); } } private Tuple<RectTransform, Image> GenerateBarParts() { GameObject go = new GameObject("BarParts"); go.transform.parent = this.transform; Image image = go.AddComponent<Image>(); RectTransform rect = go.GetComponent<RectTransform>(); rect.sizeDelta = new Vector2(0, barHeight); return new Tuple<RectTransform, Image>(rect,image); }ゲームを実行状態にしたら、プールしたImageを使って順番にバー伸びアニメを表示します。リストbarDataListにImageとRectの参照セットが入っているので、ひとつずつに色と大きさを指定します。
StackedBarResults.cspublic int counter = 0; public void ShowBarParts(Color color, float size) { if(counter >= barCount) return; barDataList[counter].Item2.color = color; barDataList[counter].Item1.DOSizeDelta(new Vector3 (size, barHeight),1.0f) .SetEase(Ease.InCubic)); counter++; }ShowBarPartsメソッドにランダムな値を与えてみると、次のアニメーションでバーが伸びていきます。
これで「積み上げ棒グラフのリザルト画面」を作ることができました。
このバーのアニメーションがうまく再生されない場合は、HorizontalLayoutGroupのメソッド「CalculateLayoutInputHorizontal」や「SetLayoutHorizontal」を読んであげることで正常に反映されるようになるそうです。
いい感じの音をつける
さて、このバーのアニメーションに音をつけることもやっておきましょう。
バーが伸びているときに「ニニニニニ...」という効果音をつけたいと考えてみます。バーの長さは不定なので伸びきるタイミングは不定です。伸び初め~伸び加速時に音のピッチを変えて演出をつけながら、伸びきった時に停止音を再生したい場合はどのように実装すればよいでしょうか。
完成形は次の通りです。
https://youtu.be/JKEJXy76VCAUnity AudioSourceで実装しようとしたときは、
- バー伸びスタート:ループ音再生開始
- バー伸び中:音量、ピッチ(音の高さ)を変更
- バー伸びエンド:ループ音再生停止、停止音再生、ピッチ変更が途中だったらそれも停止
といった形で、処理や条件のチェック等が意外と面倒です。そこで本記事では、サウンドミドルウェアの「CRI ADX2」を使ってこのあたりの処理を簡略化します。
ADX2は、サウンドの各種演出を内包するライブラリと、音に演出を埋め込む専用ツールがセットになったSDKです。については、次の記事をご確認ください。Unityのサウンド機能をADX2で強化する
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377バー伸び開始~伸びアニメション中の音を作る
バーが伸びているときの音は、ADX2のツール「Atom Craft」で次の設定を行います。
この音には、次の演出が埋め込まれています。
- 音をループさせる
- 時系列に沿ってボリュームを上げる(黄色線)
- 時系列に沿ってピッチを上げる(青線)
プログラムからは、この「MeterStart」を再生とやると、短い音をループ再生しつつ、序盤はピッチとボリュームが上昇する演出で音が再生されます。
バー伸び終了時の音を作る
バーが伸びきった時は、終了時の音の再生と共に、バー伸び中の音を停止する処理が必要です。
この処理のために、ADX2の「アクション機能」を使います。
アクション機能は、音の中に「ほかの音の停止」や「パラメータの変更」などの指定を埋め込むことができるものです。「アクション機能」を使うことによって、連続した効果音の演出を作る時、1つのトリガーで複数の音の操作ができます。
今回は「ループ音の再生停止」と「バーが伸び終了時の効果音再生」を同時に行います。これによって、プログラム側が「何の音を止めなくてはならないか」を保持しておかなくてよくなります。
アクション機能については、詳しくは以下の記事をご確認ください。
CRI ADX2で「アクション」ベースの実装を行う
https://qiita.com/Takaaki_Ichijo/items/f9c0e67cd64d7e976ae8停止音「MeterStop」は、停止したときの音の通常再生と共に、「アクショントラック」と呼ばれるアクションを実行するトラックが追加されています。
この中で操作対象の音を指定し、アクションで何をするかはインスペクターの中で指定します。今回は、0.2秒をかけて音がフェードアウトする処理を「MeterStart」に対して行います。
コードの修正
音側にピッチ変更やループ、停止命令などがすべて埋め込まれているので、コード側は「MeterStart」「MeterStop」の2つの音を再生するだけになります。
StackedBarResults.cspublic AtomSource atomSource; public void ShowBarParts(Color color, float size) { if(counter >= barCount) return; barDataList[counter].Item2.color = color; atomSource.Play("MeterStart"); barDataList[counter].Item1.DOSizeDelta (new Vector3 (size, 100),1.0f) .SetEase(Ease.InCubic) .OnComplete(() => { atomSource.Play("MeterStop"); }); counter++; }ADX2を組み合わせることによって、アニメーションと連動する音の操作がかなり簡潔に記述できます。
まとめ
Horizontal Layout Groupを使うと、「積み上げ棒グラフのリザルト画面」がすぐ作れます。
また、一緒にADX2を使うことで、棒グラフのアニメーションの音を簡単に作ることができます。ループ再生やピッチ変更、再生停止などの処理をデータ側に持たせることで、実装量を減らしつつ、リッチな音の演出が表現可能です。
- 投稿日:2020-12-07T18:23:04+09:00
ScriptableObjectでシステム系クラスを実装する
はじめに
サムザップ #2 AdventCalendar 2020 の12/9の記事です。
株式会社サムザップ Unityエンジニアの尾崎です。
内容
UnityでScriptableObjectを使ってシステム系クラスを実装する手法を紹介します。
ScriptableObjectは一般的にはデータを効率よく格納するために使われます。
今回はシステム系クラスの実装に使ってみます。※ システムはいろんなクラスから呼び出される共通的なプログラムのことを表しています。サブシステムや基盤と呼ばれることもあります。
例えばゲームでは外部リソースやログ、サウンドなどを扱うクラスなどが該当します。この手法のメリットとデメリット
メリット
- Playモードでなくても動作する
- MonoBehaviourと違いシーンに依存せずプロジェクトのどこからでも使える
- アセットとして存在するので、他GameObjectのpublic変数(SerializeField)から参照できる
- staticクラスやシングルトンいらず
- インスペクタにツールを作ることができる
- デバッグがしやすくなる
- 開発効率化ツールを作りやすい
- システムをインターフェース化(抽象クラス化)することで柔軟なシステム構成にできる
- 実装を切り替えられる
- クラス内の分岐を減らせる
- Play中に変更した値がPlay終了後も残る
デメリット
- UpdateなどMonoBehaviourのイベントを使った処理ができない
- Play中に変更した値をPlay終了時に残さないためには工夫が必要
実装例 (シンプル版)
まずはシンプルにScriptableObjectを継承してシステム系クラスを実装するコード例を紹介します。
後半、ScriptableObjectとSerializeFieldを活用して複数の実装を簡単に切り替えるコード例を紹介します。システム
[CreateAssetMenu] public class SomeSystem : ScriptableObject { /// <summary> /// 何らかのパラメーター /// </summary> [SerializeField] private string _someString; /// <summary> /// 何らかのメソッド /// </summary> public void SomeMethod() { Debug.Log("SomeSystem"); } /// <summary> /// インスペクタのボタンからも実行できる何らかのメソッド /// </summary> private void Test() { Debug.Log("Test"); } }ScriptableObjectのアセットを生成すると以下のようになります。
Projectにアセットとして存在するのでプレイしなくてもインスペクタで操作できます。
パラメーター設定したり、Editor拡張でツールを作るのに便利です。※ インスペクタにボタンを表示するEditor拡張については省略しています
システムを利用
public class SomeScene : MonoBehaviour { [SerializeField] private SomeSystem _system; void Start() { _system.SomeMethod(); } }
アセットなのでシステムを使いたいクラスのSerializeFieldで参照にセットできます。代替手段との比較
MonoBehaviour
MonoBehaviourはシーンが変わると破棄されます。シーンをまたいでデータ保持したり処理するには不向きです。
ScriptableObjectはシーンに関わらず常に存在します。staticクラス
UnityのEditorから見えません。
アセットとして存在するのでインスペクタに表示でき、ビジュアル的に設定やツール提供を行えます。
1つのクラス(実装)に依存することになります。MonoBehviour + DontDestroyOnLoad (シングルトン)
プレイしてはじめてHierarchyに表示されインスペクタで操作できます。
そのため編集中にインスペクタで操作することはできません。
1つのクラス(実装)に依存することになります。Prefab
PrefabはInstantiateして使う前提なのでインスタンス化するたびにデータのコピーが発生します。
またシステム系クラスとしてPrefabを利用するには、利用側からのアクセス方法に工夫が必要です。多くは上のシングルトンになるでしょう。Instantiateしない場合は今回の手法と近しい性質を持っています。しかし、Unity標準ではPrefabはInstantiateしてHierarchyに配置して使うものなのでInstantiateしないというのは避けた方が良いでしょう。
Hierarchyに配置(画面に出す)のが前提なので、TransformやTag、Layerを標準要素として持っています。MonoBehaviourのライフサイクルイベントも扱います。ScriptableObjectはHierarchyに配置しない前提なのでTransformなど余分な要素はありません。
Prefabに利点があるとすると、複数のコンポーネントを持てることです。
依存性注入との比較
DIコンテナの導入が必要で大掛かりになります。
ScriptableObjectはUntyの標準的な仕組みで簡単に使えます。実装例 (切り替え版)
1つのシステムに対して複数の実装を行い、切り替えるコード例を紹介します。
システムのインターフェースを抽象クラスとして定義し、いくつかの具象クラスを作ります。
利用するクラスのSerializeFieldで使用する具象クラスのScriptableObjectアセットを選択します。システム (抽象クラス)
public abstract class SomeSystem : ScriptableObject { public abstract void SomeMethod(); }各実装が持つべきメソッドやプロパティを定義した抽象クラスです。
interfaceにしたいところですが、インスペクタに表示するために抽象クラスを選択します。システム (実装A)
[CreateAssetMenu] public class SomeSystemA : SomeSystem { public override void SomeMethod() { Debug.Log("SomeSystemA"); } }SomeSystemの1つ目の実装
何かの機能の正式な実装を行います。システム (実装B)
[CreateAssetMenu] public class DebugSomeSystem : SomeSystem { public override void SomeMethod() { Debug.Log("DebugSomeSystem"); } }SomeSystemの2つ目の実装
SomeSystemAに対する別実装を行います。
例. デバッグコードに変更する、実行プラットフォームごとに使用ライブラリ変更する、チュートリアル用などシステムを利用
public class SomeScene : MonoBehaviour { [SerializeField] private SomeSystem _system; void Start() { _system.SomeMethod(); } }SerializeFieldで参照するアセットを切り替えて、使用するシステム実装を選択します。
SomeSystem
型で宣言しているのでSomeSystemのサブクラスのみが選択候補にリストアップされます。大規模プロジェクトでは
上記で紹介したコード例は、小規模なプロジェクトで使いやすいものになっています。
しかし、大規模なプロジェクトになってくるとGameObjectの数が膨大になります。
そうすると各GameObject(コンポーネント)のSerializeField(public変数)に使用するシステムの参照をセットするというのは数が多すぎて現実的でなくなります。
大規模なプロジェクトでは使用する各システムの参照を保持するシステムを1つ作るのがおすすめです。具体的にはこちらの記事で紹介しているサービスロケーターが使えます。
Unityでサービスロケーター(ServiceLocator)を活用するサムザップの運用中タイトルではこれらを組み合わせて、デバッグしやすい大規模プログラムを構築しています。
- 投稿日:2020-12-07T18:16:48+09:00
【Unity】変数を他のスクリプトでも使う方法と簡単なゲームのチュートリアル【初心者向け】
初めに
あるスクリプトの変数を、他のスクリプトでも使えるようにするやり方です。関数についても説明してるよ
以下の簡単なゲームを例に説明します。よく使うので、どういう意味なのか・どんな場面で使える技術なのかわからない人も一緒にやっていきましょう!
メインの内容のやり方だけ知りたい人は他のスクリプトでscore変数が使えるように書いていくから読んでね動画
スペースキーを押すとプレイヤーの白いブロックがジャンプします。
青いブロックは1点、赤いブロックは100点で、コンソールに合計スコアが表示されます。作り方
準備
土台作り
まずゲームのステージを作りましょう!この段階は、何も見ずにできると良いです!
簡単に書くと、
・床を作る
・ブロックを3つ用意する
・それぞれわかりやすいように色をつける。ここでつまづいてしまった人は、さらにヒント!
・床は、1枚の板を置いて作る。Hierarchy
のCreate
タブから、3DオブジェクトのPlaneを選ぶよ
・ブロックはCubeという3Dオブジェクト
・色は、Project
のCreate
から、Materialを選択。Inspector
タブの上にある白い四角をクリックして、色を選択したら、そのMaterialをSceneビュー
にドラッグアンドドロップしてブロックに色をつける!要素を加えていく
さらに、完成動画を見てみると、プレイヤーが動く、つまり重力が必要です。また、青ブロックはすり抜け、赤ブロックにはぶつかります。この動きを作っていきましょう。何も見ずにできるともっとすごい!
やり方
・Playerのブロックに、RigidBody
をつける。
PlayerブロックをHierarchy
タブで選択した状態でInspector
タブを見ます。一番下のAddComponent
をクリックしてRigidBodyを検索して取り付けます。・青ブロックの
isTrigger
にチェックを入れる
青ブロックを選択した状態でInspector
タブを見て、BoxCollider
のisTriggerにチェックを入れます。こうすることで、このブロックはすり抜けてぶつからないけど当たった判定だけはできる状態になります。最後に、ProjectタブのCreateからC#スクリプトを選び、redScript、blueScript、playerScriptを作成して、それぞれのオブジェクトに取り付けましょう。後はコードを書くだけ!
スクリプトを書く
スペースキーを押すとプレイヤーがジャンプするスクリプト
・7行目、ジャンプ力を入れる変数を宣言。powerと名付けました。publicと付けるとUnityの画面でも数値が調整できます。
・8行目、AddComponentした
Rigidbody
を使いたいので、rbと名付けて、宣言します。・13行目、Start関数の中に書きます。中身は、
RigidBody
をゲットしてねという命令です。・19行目、Update関数の中に書きます。
Input.GetKeyDown(KeyCode.〇〇)は、キーを押すときの命令の定型文です。ここではSpaceキーを使います。「もしスペースキーを押したら、このスクリプトが付いているオブジェクトの、RigidBodyに力を加えてね(上向きで、最初に宣言したpowerの速さで)」
という意味になります。ここの説明はこれくらいにするので気になる人はAddForceとか調べてみてね
青ブロックを通るとスコアが+1になり、コンソールに表示されるスクリプト
・まず7行目で、scoreという名前の変数を用意するよと宣言します。自分のわかりやすい名前をつけよう。
・その次に、20行目のように、
OnTriggerEnter
というUnityで用意してくれている変数を使います。これは、簡単に説明するとトリガーがオンになったら{ }の中を実行するよ という意味です。さっきUnityでisTriggerにチェックを入れたけど、そのチェックがオンになっているとこの関数が使えます。・{}の中では、初めに設定したscoreに、+1をする(++は+1と同じ意味)
そして、「スコアは1」とデバッグで表示しろ。という意味になっています。他のスクリプトでscore変数が使えるように書いていく
いよいよ本題です。
さっき書いたスクリプトに赤枠の中を書き足しましょう。・7行目、static publicと付け足すのが一番のポイントです。これで他のスクリプトからもこの変数にアクセスできます。
今は、こういう機能のあるおまじないだと思っていいです。・25行目も同様にstatic publicをつけて、AddScoreという関数を作ります。これは自分でわかりやすく名前をつけて作るオリジナルの関数です。redscoreは、自分で名前をつけた引数です。
この中で、「redscoreもscore(全体のスコア)に足してね」と命令しています。これで赤と青ブロックの合計得点を計算できるはず!次にredScriptを書いていくので、それと合わせて説明します。
赤ブロックにぶつかるとスコアが+100になり、コンソールに表示されるスクリプト
赤いブロックはすり抜けずぶつかったら加点ですが、その場合はTriggerではなく、OnCollisionEnter
という関数を使います。Colliisionは衝突という意味の英語です。
なのでもしぶつかったら{}の中を実行してね、という感じの機能になってます。
実はいくつか種類があり、ぶつかり始めた時、ぶつかっている時、ぶつかり終わる時と分かれていますが今回は説明は省きます。23行目、この{}で、blueScriptの中にある、AddScoreを(100)の数値で実行してね、
blueScriptの中にあるscoreをデバッグで表示してね
という内容が書かれています。
この、blueScriptの中にあるAddScoreが今回のポイント!さっき、blueScript、つまり他のスクリプトで書いたことをこっちで命令しています。
そして、AddScoreの次の(100)とは、blueスクリプトの中で書いたredscoreのことです。redscoreという引数に100という値を入れることができます。static publicをつけておくと、他のスクリプトで利用できるんです。
ちょっとややこしいですが、全て書いた後で、2つのスクリプトを見てみると、青ブロックのトリガーに触れたら+1点、赤ブロックにぶつかったら+100点、という機能がちゃんと書かれていることが分かると思います。
ちなみに、staticをつけるのは1つのスクリプトだけで大丈夫です。
終わりに
簡単なゲームでも得点があるだけで一気に面白く、ゲーム性が増すのでぜひやってみてね。
長く&くどい説明になってしまいましたが、プログラミング自体が初めてだった私にとって、「staticとは」「関数、引数の概念」は理解するのにとても苦労したので、その時の自分に向けて説明するつもりで書きました!初めは、何かのサイトを見ても、関数名や変数名が自分で命名するものなのか、元々ある機能の決められた名前なのかもわからず混乱していたのでそこらへんも詳しく書いたつもりです。理解力高い人はコード見ただけで理解できそう!いいなー!参考になったら嬉しいです!
- 投稿日:2020-12-07T17:28:30+09:00
最高の振動をデザインするための下準備〜UnityからCore Haptic FrameWorkを使ってみた〜
はじめに
この記事は「サムザップ #1 Advent Calendar 2020」の12月7日の記事です。
昨日の記事は@KoniroIrisさんの「【Unity】アップデートされたTerrainでトンネルを掘ろう!」でした。今回は、iOS13で新しく追加されたCore Haptic FrameWorkをUnityを使って、iPhoneの振動生成を試行錯誤するツールを作ったので、その紹介をします。
振動作成を試行錯誤ためのツール
振動作成するためのUnityで下記のようなツールを用意しました。
このツールでできること
①強さの調整
②鋭さの調整
③始まりと終わりの強さと鋭さを設定し、徐々に振動を変化させる
④SOSを表現したサンプルの実行
パラメータを調整しながら、自分の表現したい振動を見つけることを目的としています。背景
私はよく通勤時間や隙間時間を使って、よくカジュアルゲームで遊びます。
カジュアルゲームをやっていて良く感じることは、ゲーム中の振動が心地いいということです。
ゲーム内容ではなく、またあの振動に触れたいと思い、ゲームをプレイしてしまうことがしばしばあります。
通勤中など、マナー音をゲームする人が多く、振動は重要なゲーム演出の一つだと言えます。
自分でこの振動をデザインしてみたいなと思い、Core Haptic FrameWorkを使って、上記のようなツールを用意しました。Core Haptic FrameWork
Core Haptic FrameWorkはiOS13で新しく追加されたフレームワークです。
詳しくは公式のドキュメントをご覧ください。
このFrameWorkをUnityで呼び出し、振動生成のツールの実装しています。以前までは下記のようにAppleがあらかじめ用意されたものから好みのものを選択し、振動を表現していました。
以前までの振動の呼び出しextern"C"voidplaySystemSound (int n) { AudioServicesPlaySystemSound(n); }Core Haptic FrameWorkではユーザーが独自に振動を作れるようになりました。
それぞれ強さ、鋭さ、長さ、遅延時間などが設定できます。強さ、鋭さ、遅延時間を指定して、振動を呼び出し(ツールの強さの調整、鋭さの調整に使用)func createHapticEvents() -> [CHHapticEvent] { let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1) let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1) let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0) return [event] }最初と最後の振動の強さ、鋭さを指定し、どれぐらいずつ振動を変化させるかを指定し、徐々に変化する振動を作ることができます。
fromからtoへと徐々に強さと鋭さを変更していく(ツールの【始まりと終わりの強さと鋭さを設定し、徐々に振動を変化させる】で使用)func createHapticEvents() -> [CHHapticEvent] { var events: [CHHapticEvent] = [] for i in stride(from: 0, to: 1, by: 0.1) { let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(1 - i)) let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(1 - i)) let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: i) events.append(event) } return events }最後により細かく設定する例になります。イベントタイプ、発生タイミングを設定することができます。
eventTypeと振動の継続時間を細かく設定(ツールのSOSサンプルで使用)func createHapticEvents() -> [CHHapticEvent] { let short1 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0) let short2 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.2) let short3 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.4) let long1 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 0.6, duration: 0.5) let long2 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.2, duration: 0.5) let long3 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.8, duration: 0.5) let short4 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 2.4) let short5 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 2.6) let short6 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 2.8) return [short1, short2, short3, long1, long2, long3, short4, short5, short6] }まとめ
今回はUnityからCore Haptic FrameWorkを呼び出し、振動を試行錯誤するためのツールの紹介しました。
ただツールを使って、振動を調整してきましたが、まだまだ自分の思った通りの振動を表現することはできていません。
デザイナーさんが色を見て、その色をだいたいの構成するRGB値を予想できるように、振動をデザインできるようになれたらと考えています。
明日はアドベントカレンダーの担当は@kurosawa_tomokazuさんです。
- 投稿日:2020-12-07T15:30:59+09:00
OpenCVplusUnity 画像を回転させる(アフィン変換処理)
はじめに
この記事ではOpenCVplusUnity(無料)を使用しています。
Unityで生画像を保存する際に回転させたいなんて時があると思います。RendereTextreでこねくり回したり、ピクセル取り出して処理することもできるかと思いますが面倒です。大人しくそんな時はOpenCVを使いましょう。楽です。今回はこの結果を目指します。Texture2Dを右回転、左回転に処理します。
といっても、することはPythonで使用する時と変わりません。
Texture2DをMatへ変換して回転変換を加えて再びTexture2Dへ返しているだけです。実装方法
OpenCVManager.csusing UnityEngine; using OpenCvSharp; public class OpenCVManager { /// <summary> /// Texture2Dを90度アフィン変換処理 true:右回転 false:左回転 /// </summary> public static Texture2D RotateAndResize(Texture2D tex, bool isRight) { Mat origin = OpenCvSharp.Unity.TextureToMat(tex); Mat dest = new Mat(); //cpu生画像を想定。横長画像前提 var center = new Point2f(origin.Cols / 2, origin.Cols / 2); Mat rMat = Cv2.GetRotationMatrix2D(center, 90, 1); Cv2.WarpAffine(origin, dest, rMat, new Size(origin.Rows, origin.Cols)); if (isRight) { Cv2.Flip(dest, dest, FlipMode.XY); } return OpenCvSharp.Unity.MatToTexture(dest); } }まとめ
余談ですが、iOSアーカイブの予定のある方は最初からOpenCVforUnityを購入をお勧めします。bitcodeエラーから抜けれず、アーカイブできなくて一度詰みました。全移行する苦行が待ってます。
もし、OpenCVplusUnityでアーカイブできた人は教えてください。外部ライブラリを使用した際は、アーカイブまで確認しろと教えられた良い教訓です。
- 投稿日:2020-12-07T13:18:59+09:00
MagicLeapようのプロジェクトをUnityEditorで動かす
MagicLeapようのプロジェクトをUnityEditorで動かす
この記事は、Magic Leap Advent Calendar 2020の8日目です
動作環境
Unity 2019.3.13f1
MagicLeapSDK 0.24.0.20200310rはじめに
初めまして。やまだと申します。お仕事で2案件ほどMagicLeap開発に携わり、その中でUnityEditorでのコンテンツ確認などのデバッグ用のモードの担当を行いました
今回はその時のTipsの話をしますMagicLeapのプロジェクトを作っていると、ちょっとした変更などは実機ではなく、UnityEditorで確認したい。と言うことが多々あると思います
しかし、MLHandTrackingBehaviour
などを使っていると、UnityEditorで実行してもエラーが出て動作の確認ができません。Zero Iterationを使えばシミューレーションできますが、PC上では動作が重たくなったりするので、
「単純なエラーチェック」であるような、精度の高さが必要でないような環境では必要とまではなりません
(設定もエンジニア以外にはまだ難しい部分もあるかと思います)なので今回は簡単に
MLHandTrackingBehaviour
などが入っていてもUnityEditorで実行する方法を紹介いたしますやること
例えば画像の一番上のコードを見てみると、以下のように書かれています
public static MLResult Start(bool initializeValues = false) { #if PLATFORM_LUMIN MLResult _result = MLHandTracking.Start(); if (!_result.IsOk) { Debug.LogErrorFormat("Error: MLHandTrackingStarterKit failed starting MLHandTracking. Reason: {0}", _result); } ... }エラーが出ていることからも分かるとおり、UnityEditorではHand系の処理がエラーで全く動きません
なので、ここの
#if PLATFORM_LUMIN
を以下のように書き換えますpublic static MLResult Start(bool initializeValues = false) { #if PLATFORM_LUMIN && !UNITY_EDITOR MLResult _result = MLHandTracking.Start(); if (!_result.IsOk) { Debug.LogErrorFormat("Error: MLHandTrackingStarterKit failed starting MLHandTracking. Reason: {0}", _result); } ... }
&& !UNITY_EDITOR
を入れることで、UnityEditorでのHand系の処理に入らないようにできます他のところも同じようにすればエラーは出なくなるのですが、
MLAPISingleton
やMLHandMeshing
を使っている場合は少しだけ工夫が必要です結論から書いてしまうと、以下のようにする必要があります
public static MLResult RequestHandMesh(RequestHandMeshCallback callback) { #if !UNITY_EDITOR if (callback == null) { MLPluginLog.ErrorFormat("MLHandMeshing.RequestHandMesh failed. Reason: Passed input callback is null"); return MLResult.Create(MLResult.Code.InvalidParam); } ulong requestHandle = MagicLeapNativeBindings.InvalidHandle; MLResult.Code resultCode = NativeBindings.MLHandMeshingRequestMesh(Instance.nativeTracker, ref requestHandle); if (resultCode != MLResult.Code.Ok) { MLPluginLog.ErrorFormat("MLHandMeshing.RequestHandMesh failed to request hand mesh. Reason: {0}", resultCode); return MLResult.Create(resultCode); } Instance.pendingQueries.Add(new Query(requestHandle, callback)); return MLResult.Create(resultCode); #else return MLResult.Create(MLResult.Code.Ok); #endif }ここでは通常の処理を
#if !UNITY_EDITOR
で括って、else
としてreturn MLResult.Create(MLResult.Code.Ok);
を入れています。単純に処理が正常に終了した。と判断させているだけですねこのように、UnityEditorのための処理を書いていけばEditorで動かすことが可能になります
カメラの設定
Editorで動かせる様になって、確認していると、カメラの画角がMagicLeapでみるのと少しずれていることに気づくと思います。
MagicLeapは実行時にカメラのFieldOfViewをいじっているのですが、Editorではその処理が走らないためです。
ここはカメラにのFieldOfViewに44.57381
の値を設定するとある程度同じ様に見えますやっておくと良いこと
MagicLeapはMR機器なので、UnityEditorでシミュレートするには無理があります
ただ最初に書いた通り、簡単な確認はEditorでしたいです
なので、UnityEditorでデバッグできるようにしておくと便利です
弊プロジェクトでは以下の機能を入れていました
・カメラの移動
・コンテンツの倍速機能
・タッチ処理をクリックで行えるように最後に
今回の変更は簡単に行えますが、1点注意があります
それはMagicLeapSDKに手を加えているため、もしアップデートがあった場合は同じ変更を加える必要がある
と言う点です。しかしEditorで確認できるようになれば、実機がなくてもコンテンツの作成・確認が容易になるので、
これらの変更とデバッグモードを作るのをお勧めします。
- 投稿日:2020-12-07T02:46:58+09:00
Odin Validatorのススメ
Unityでのゲーム開発では、多くのGameObjectやPrefabそしてAssetが必要で、それらは他のGameObjectやAssetへの参照を持っています。その参照関係を正しく管理・運用するのは大規模なゲーム・長期間のプロジェクトではとても難しいです。「間違って使っているAssetを削除してしまい、それを参照している部分が原因で不具合が発生した」という経験はありませんか。このような不具合を、どうやったら防げるでしょうか?
自分のおすすめは、
Odin Validator
を用いたプロジェクト全体の自動検証です。Odi Validator
を用いれば「ここは必ず何かしらのAssetを参照しないといけないと設定する。もし参照していない場合は、エラーを表示する」という設定が実現できます。「間違って使っているAssetを削除してしまい、それを参照している部分が原因で不具合が発生した」という不具合を検知し、事前に防ぐことができます。この投稿では、
Odin Validator
を用いた、プロジェクトの自動検証を紹介します。参照しているやつがなくなってしまった!
Unityでのゲーム開発では、多くのGameObjectやPrefabそしてAssetが必要で、それらは他のGameObjectやAssetへの参照を持っています。その参照関係を正しく管理・運用するのはとても難しいです。特に、大規模なゲームで長期間のプロジェクトとなれば、なおさらでしょう。
たとえば、次のようなScriptableObjectのクラスがあるとします。
using UnityEngine; [CreateAssetMenu] public class Enemy : ScriptableObject { [SerializeField] private string enemyName; public string EnemyName => enemyName; [SerializeField] private Sprite sprite; public Sprite Sprite => sprite; [SerializeField] private GameObject model; public GameObject Model => model; // 略 }次のようにEnemy000というScriptableObjectを作成し、Inspectorから参照を設定したとしましょう。
さて、プロジェクトがすすみ、Assetも多くなってきて管理が難しくなってきたとします。本当は必要だったAsset(EnemySprite0)を誰かがうっかりプロジェクトから消してしまいました。その場合、次のようにSpriteの参照が「None」という状態になってしまいます。
もしこの状態で、ビルド・実行したらおそらく表示すべきSpriteが表示されない不具合が発生してしまいます。
このような「間違って使っているAssetを削除してしまい、それを参照している部分が原因で不具合が発生した」という不具合を経験した方も多いのではないでしょうか?ここで紹介した例は「Spriteが表示されない」でしたが、ゲームが進行不可能になったり、課金関連の不具合が発生したりと、もっと大きは不具合が発生してしまうことも考えられます。
このような不具合に、対応するために自分は、
Odin Validator
を用いたプロジェクト全体の自動検証を導入することをおすすめします。Odin Validatorとは?
Odin Validator
の前に、まずOdin
を紹介します。
Odin
(Odin - Inspector and Serializer
)はUnity向けのツール・ユーティリティカテゴリのライブラリです。非常に強力で豊富な機能を持っている人気のライブラリです。主な機能は次のとおりです。Inspectorの強化をはじめ、エディター拡張のユーティリティの提供します。Odin - Inspector and Serializer
は、Asset Store・もしくはOdinの公式ページから購入が可能です。自分の、
Odin
のおすすめ機能は「Required属性」です。Required属性がついたSerialize対象のフィールドは、「必ず参照や値を設定する必要がある」という条件が追加されます。もし参照や値を設定していない場合、次のようにInspectorに設定されていないことをわかりやすく表示してくれます。Odinを使って制約や条件を追加してみよう
Odin - Inspector and Serializer
では、多くの属性とそれに対応するValidatorを提供しています。これらの属性が付与されたフィールドは、ValidatorによりInspectorからの入力に条件や制約を加えることができます。
Odin Project Validator
では、これらの属性を用いて付与された条件や制約を検証できます。この節では、Odin - Inspector and Serializer
で提供されている属性とValidatorの使い方の一部を紹介します。Required
Required属性をつけることで、参照の設定・値の入力を必須にできます。
次のようなScriptableObjectのクラスがあります。
using UnityEngine; using Sirenix.OdinInspector; [CreateAssetMenu] public class ExampleRequire : ScriptableObject { // 本当はプロパティを準備したほうがいいが、サンプルのためpublicフィールド [Required] public string stringValue; [Required] public GameObject gameObjectReference; [Required] public Transform transformReference; [Required] public int intValue; [Required] public double doubleValue; }このScriptableObjectのInspectorは次のようになります。stringValue、gameObjectReference、transformReferenceの上部に「stringValue is Required」などのエラーメッセージが表示されていることに注目してください。Required属性を付与することで、そのフィールドの参照・値を設定していない箇所をエラー表示できます。
stringValueなどに参照・値を設定すると次のようにエラー表示が解消されます。
intやdoubleなどのフィールドには、Requiredをつけてもエラーメッセージが表示されないことに注意してください。
ValidateInput
ValidateInput属性をつけることで、指定したstaticメソッドを用いて検証できます。たとえば、次のコードは「ListなtargetAssetsが要素を1つ以上持っているか」を検証します。
using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; [CreateAssetMenu] public class ExampleValidateInput : ScriptableObject { [Required] [ValidateInput(nameof(ValidateNotEmpty))] public List<GameObject> targetAssets; private static bool ValidateNotEmpty( List<GameObject> targetAssets, ref string message ) { if (targetAssets.Count == 0) { message = "List is empty."; return false; } return true; } }このScriptableObjectのInspectorは次のようになります。targetAssetsは要素を1つも持っていないので、上部に「List is empty.」というエラーメッセージが表示されています。
targetAssetsが要素を1つ以上もつようになると、エラーメッセージが消えます。
このようにValidateInputを使うと、自分で指定したstaticメソッドにより、独自の検証を行うことができます。
AssetsOnly と SceneObjectsOnly と ChildGameObjectsOnly
AssetsOnly、SceneObjectsOnly、ChildGameObjectsOnly属性をつけることで、参照の設定を次のように条件づけできます。
- AssetsOnly・・・プロジェクトのAsset限定
- SceneObjectsOnly・・・シーン上のGameObjectやComponent限定
- ChildGameObjectsOnly・・・シーン上の自分の子GameObject限定
AssetsOnlyを指定しているフィールドに、シーン上のGameObjectやComponentは参照設定できません。SceneObjectsOnlyを指定しているフィールドに、プロジェクト上のAssetは参照設定できません。ChildGameObjectsOnlyはIncludeSelfという引数があり、自身も許可するかどうかを指定できます。
次のコードのtargetは、プロジェクト中のPrefabもシーン上のGameObjectも、どちらも参照設定できてしまいます。本来はどちらかしか設定できるべきではありません。
using UnityEngine; public class Example : MonoBehaviour { // プロジェクト中のPrefabもシーン上のGameObjectも // どちらも参照設定できてしまう [SerializeField] GameObject target; // プロジェクト中のAsset(Prefab)を指定すべき場合は、AssetsOnlyを // [SerializeField, AssetsOnly] GameObject target; // シーン上のGameObjectを指定すべき場合はSceneObjectsOnlyを // [SerializeField, AssetsOnly] GameObject target; }プロジェクト中のAsset(Prefab)を指定すべき場合はAssetsOnlyを、シーン上のGameObjectを指定すべき場合はSceneObjectsOnlyをつけることをおすすめします。
Odin Validatorを使ってみよう
RequiredやValidateInput、AssetsOnlyなど
Odin - Inspector and Serializer
の属性を使うことで、入力に条件や制約を加えそれを満たさなかった場合、Inspectorにエラー表示できます。しかしそのエラー表示に気がつかなかったとしたら、結局不具合が発生してしまいます。しかし、せっかくのわかりやすいエラー表示も、みのがしてしまったら意味がありませんね。そこでOdin Validator
の出番です。
Odin Validator
は、Odin - Inspector and Serializer
のAddonです。Odin Validator
を用いると、Required属性などの条件を満たしていないAsestやPrefab、GameObjectが存在しないか検証し、その結果を次のようにわかりやすく確認できます。Odin Validator
は、公式サイトから購入することができます。検証する範囲も、「今開いてるシーン」や「プロジェクト全体」など指定できます。また、その検証のタイミングをゲームプレイ実行時、ビルド実行時など指定できます。
Odin Validator
を用いることで、「間違って使っているAssetを削除してしまい、それを参照している部分がきっかけでシーンが壊れてしまった」という不具合を回避できます。
Odin Validator
はOdin公式ページから購入が可能です。この投稿の執筆時点では、2019年5月28日より前にOdinを購入したユーザーはOdi Validator
を無料でダウンロードできるようです。価格・購入については、最新の公式情報を参照してください。変更を加えるたびに、いちいちプロジェクトにあるSceneやPrefab、ScriptableObjectすべてにエラー表示がないかを確認することは、現実的ではありません。Requiredなどを使っていたとしても、「使っていないと思ったAssetを削除したら、最近まったく更新していないシーンで使っていて、それが原因で不具合が発生してしまった」ということは起きてしまいます。
先に説明した通り、このような不具合は、
Odin Validator
使うことで回避できます。
@{Odin Project Validator}を使えば、RequiredやValidateInput、AssetsOnlyなどの属性がついている部分を自動で検証し、満たしていない箇所をわかりやすく表示してくれます。
いちいちプロジェクト全体を目視で確認する必要はありません。
Odin
とOdin Validator
をプロジェクトに導入し、メニューの「Tools > Odin Project Validator」を選択すると次のようなウィンドウが開きます。一番上の「Scan Entire Project」を押すと次のような状態に遷移します。
このウィンドウの右上にある「Run Scan Entire Project」というボタンを押下します。もしプロジェクトにRequiredやValidateInput、AssetsOnlyを満たさないものがあった場合、次のようにそれを一覧表示できます。
検証した内容を一覧できます。また、条件を満たしていなかったAssetやGameObjectをこのウィンドウから直接編集できます。
もしプロジェクトにRequiredやValidateInput、AssetsOnlyを満たさないものがなければ、次のようになります。
「Scan Entire Project」はプロジェクト全体を検証します。
Odin Project Validator
では、これ以外にも次のような設定も提供しています。
名称 対象 Scan Entire Project プロジェクトにあるすべてのScene・Assetを検証する Scan All Assets プロジェクトにあるすべてのAssetを検証する Scan All Scenes プロジェクトにあるすべてのSceneを検証する Scan Open Scenes 現在開いているSceneを検証する Scan Scenes From Build Options ビルド対象に設定されているSceneを検証する 自分で設定を作ることも、既存の設定を変更することもできます。必要に応じて自分のプロジェクト運用にあった設定を試してみてください。
Automate Validation
Odin Validator
でプロジェクトの検証を行えば、参照や値の設定の間違いに気が付くことができます。しかし、ビルドする前にOdin Validator
での検証を忘れてしまったら、結局不具合が発生してしまう可能性があります。Odin Validator
による検証を忘れないために、Automate Validationを設定することをお勧めします。メニューの「Tools > Odin Project Validator」からウィンドウを開き、右上部にある「Automate Validator」ボタンを押すと次のようなAutomate Validationウィンドウが開きます。
ウィンドウ内のチェックボックスにチェックをつけて、Automate Validationを有効にするとそれぞれのタイミングで、指定した検証を実行できます。
- On Play ・・・ デバック実行
- On Build ・・・ ビルド実行
- On Project Startup ・・・ プロジェクトの起動
また、ビルド実行時に検証を行うように設定し、もし設定した条件を満たさない箇所をみつけたら、ビルドを失敗させる、ということも可能です。こうすることで、ゲーム出荷・リリースする前に必ず条件を満たすことを保証することができます。
まとめ
この章では、
Odin Validator
を紹介しました。間違って使っているAssetを削除してしまい、それを参照している部分が原因で不具合が発生した」という不具合の経験がある人は多いのではないでしょうか。
こういう不具合を防ぐために、ぜひOdin Validator
を導入してみてください!補足
各種バージョン
- Unity 2019.4.8f1
- Odin Inspector 3.0.2
- Odin Validator 3.0.2
- 投稿日:2020-12-07T01:36:46+09:00
【Unity】UnityからVRC用のアバターモデルをVRM形式に出力したけど失敗したからVRoidStudioで対応した話
最初に
どうも、ろっさむです。
今回はUnityからアバターモデルをVRMに出力した結果、失敗した話をまとめていこうと思います。VRMとは
アバターモデルのデータフォーマットの定義のことを指します。このVRMではアバター特有の情報を扱いやすい状態になっています。例えばテクスチャやマテリアルなどのデータがVRM形式のファイル1つにまとまっているため、アプリケーション内でのアバターロードが実行時に可能になったりします。
VRMに対応したアプリケーションやサービスもいくつかあります。
- Vroid Studio
- バーチャルキャスト
- cluster.
- オープンワールドゲーム「クラフトピア」
etc...
他にもVRM4Uを使用することでUE4へ持ち込むことも可能です。
今回はcluster.で勉強会を開いた際に、VRCで使用しようとしていたアバターをcluster.に持ち込もうとして失敗した過程を残しておこうと思います。ちなみに失敗の理由は、アバターモデルのサイズの問題です…。
更に詳しい情報が知りたい方は以下の公式リファレンスや記事がおすすめです。
出力手順と注意点
こちらに関しては以下の記事が非常に参考になるため、こちらを確認しながら進めれば基本的に問題ないと思います。
ただしcluster.でのVRM形式ファイルにはいくつか制限があるため、ここを注意点する必要があります。
cluster.でのVRM形式ファイルをアップロードする際に、VRCで使用していたアバターをそのまま使おうとすると…
上記のように制限に引っ掛かりアップロードに引っ掛かってしまいがちになるかと思います。
cluster.では以下の制限があります。マテリアル数はまだしもblendshape数やポリゴン数などはモデリングツールを使わないとどうにも削減は難しそうです。
ここから選択肢は4つに分かれます。
- VReducerを使用する
- VRoid製のアバターを作成してVroidStudioからアバターの軽量化を行う
- boothでcluster.利用可能なアバターを購入する
- REALITYなどの別サービスからアバターを作成してcluster.側へ連携して持っていく
「VReducerを使用する」方法について
こちらに関しては、実はcluster.でのイベント主催が終わったあとに知った方法なので、まだ試していません。
次回開催時に向けて試そうと思っているので、その際には以下の記事を参考にしてみようと思います。
VReducerを使ってVRoid アバターをclusterの制限に対応させる(ついでに表情も編集する)
「VRoid製のアバターを作成してVroidStudioからアバターの軽量化を行う」方法について
今回はこちらの方法を選択しました。
元々作成していたVroid製のモデルがあったので、こちらのVRMを開いて以下のようにVroidStuidioを用いて
撮影・エクスポート > エクスポート
を選択した状態にしておきます。まずはポリゴン数を32000に収める必要があるため、
髪の断面形状を変更する
と透明メッシュを削除する
にチェックを入れました。次にマテリアルを8以下にするため設定を
8
に変更します。テクスチャに関しても解像度を
2048*2048
に変更しておきます。次にボーン数を128に…と思ったらボーン数はクリアしていました。
これで「エクスポート」を押下してタイトルと作者とバージョンだけ記入して完了です。
無事cluster.のマイページにてアップロードができました。
「boothでcluster.利用可能なアバターを購入する」方法について
こちらが一番シンプルに金で解決してます。
「booth」というアバター等の非常に多ジャンルな創作物が販売されている総合マーケットです。このサイトの検索欄に「cluster vrm」などと記入して検索をかけると…たくさん出てきます。
ちなみに私はVroidStudioによる削減を試す前に以下のモデルを購入してしまいました。可愛いからOK。
「REALITYなどの別サービスからアバターを作成してcluster.側へ連携して持っていく」方法について
これに関しては本当に何も試していないので何とも言えないのですが、非常にお手軽に作成できそうなのが以下の記事を見てわかります。
clusterでREALITYのアバターを使おう!着せ替えゲーム感覚で簡単に作ってアップロード!
REALITYについては以下の公式サイトへGO
最後に
VRMでの出力方法を記載している記事は多くあるので、こちらに迷うことはあまりないかと思いますが、VRMに対応したサービスやアプリケーション毎に制限などが存在するため、初心者の方がつまずく原因になるのではないかと、今回対応方法をまとめてみました。
ただVRMで出力ができると先ほど紹介したCluster.だけでなくUE4でのゲーム制作などにも使用ができたりするので、試して色々遊んでみるのは非常に面白いと思います。
- 投稿日:2020-12-07T00:00:11+09:00
【cluster】実に簡単な弾幕系敵弾の実装方法について【Advent Calendar 2020】
この記事について
- この記事は、Cluster Creator Kit Advent Calendar 2020 7日目の記事です。
この記事を書いている人
- うのっちです。【clusterのうのっちはコチラから】
この記事でできること
- clusterのワールド作成において、通常のunity開発の感覚で、なおかつCCKならではのノンコーディングによる「敵弾発射→着弾したらdestroy、適当なタイミングでまた発射」を実装することができます。
- (unityの開発者としては、playgroundやmicrogamesのように、ノンコーディングでビジュアライズに近い環境で開発できるプラットフォームの存在というのは実に重要なことだと思っています)
- うのっちのclusterゲームワールド杯2020エントリー作品の機能を事例として紹介します。
事例
概要図
- 下記gif画像のようになります。
つくりかた
「弾幕ギヤ」
- 下図のように、適当なギヤを作ります。ギヤの数と回転速度が、弾幕に反映されます。
- コライダーについては、isTriggerにチェックを入れましょう。
- animationについては、公式の記事でも解説があるので、そちらを参考にしてみてください。
「弾生成トリガーブレード」
- 下図のように、弾幕ギヤに接触するオブジェクトを作ります。自分の場合は、なんの弾幕かわかるよう、対象をテクスチャとして設定しています。
コンポーネントの設定は、下記および下図を参考にしてみてください。(On Collide Item Trigger)
Rigidbodyでは、FreezeポジションとFreezeローテーションすべてにチェックを入れている(チェックいれないと吹っ飛ぶから)
key名は開発当初のアルファベットの連番だったので、その名残です。
「弾spawnCube」
- 弾のスポーンについては、下図のように、クリエイターキットドキュメント(Create Item Gimmick)に沿って設定しています。
トリガーkeyをロジックkeyに設定して、
生成する敵弾のプレファブを設定して、
そのプレファブをどこにスポーンさせるかのオブジェクトを設定して完成です。
「弾幕の仕上げ」
- 敵弾spawnを回転させなければ、タレットのように発射されます。
- この場合、発射間隔、つまりギヤの回転速度とギヤ数に注意してください。あまりにもたくさん発射されるようだと、設定によっては、敵弾同士でdestroyしたり、そもそも敵弾オブジェクト同士の衝突で予期できない放線を描くと思います。
- 回転させれば、当ワールドのように、弾幕を展開することもできます。
総括
- clusterはバーチャルSNSでもあるので、他者(アバターとの交流)が大切でもあります。
- しかしながら、CCKを利用した、よりゲーミングなワールドがもっともっと増えてもいいと、そう思います。(今後のclusterアプデのセーブ機能も、よりゲーミングに活用できそうですね!)
- 特にシューティングゲームに関しては、アイデアなど頭打ち感があり、家庭用もアーケードも「革新的な」ものはなかなかありませんが、だからこそ、今まで出し尽くされている様々なパターンをここで表現するのも大いにアリなのではと思います。
自分の実装ワールドの紹介
ワールド紹介より一部抜粋
- 敵弾生成ギヤシステムのご見学は、入室後、左へまっすぐ進んでください。
- 『敵の弾幕をかいくぐってレイドボス討伐なるか!?』
- 『弾幕を発生させるギヤシステムとトリガーギミックブレードを君は見つけることができるか!?』
- UnityroomでリリースしたFieldWalkingの新作をclusterバージョンとしてシステムを変えて制作しました。
- 今回はアクマがレイドボスとして登場します。
- 弾幕と残留弾に注意してくださいね。
- 散策したり、エンドレスな戦いを楽しんでください。
あしたの記事のご紹介
- あしたは、柏葉くるみさんの記事となります。ゲームジャムかな、W杯の話かな、ギミックかな、とにかく気になりますね!
- 上記、8(火)を迎えましたら、リンクを貼ります。
最後に、あらためて簡単な自己紹介
- もともとCOBOLやVBなど、いわゆる手続き型プログラミングが好きで活動していましたが、ビジュアルプログラミングに触れてからは、現在は主にunityと3Dプリンタやtoioなどを中心にインプットとアウトプットの活動をしています。
- ぜひ【うのっちポータルサイト】もよろしくお願いします。
- clusterユーザーと加速する非公式 Advent Calendar 2020 の8日も記事を書くのでよろしくお願いします。
- toio Advent Calendar 2020 の21日目にも記事を書くので、ロボット好きな方はこちらもどうぞ!