土鍋で雑多煮

UnityでXR・ゲーム開発をしています。学んだことや備忘録、趣味の記録などを書いていきます。

Unityと複数台のESP32間でUDP通信をする

どうも、土鍋です。

IVRCに向けた開発を行っている際に使用した「Unityと複数台のESP32間でのUDP通信」の知見をまとめました~。

Unityがサーバー側、ESP32がクライアント側で、Unityからデータを全部のESP32に向けて送信しています。また、今回のIVRC作品では使用しませんでしたが、一応ESP32側からのデータの受信もできます。
ルーターIPアドレスの固定を行ってください。

ちなみに、ESP32側のコードは@suzakutakumi3 にお願いしました。

github.com

サーバー側(Unity)

UniTaskで非同期処理を行っていて、一応送受信できる設計になっています。

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UDP
{
    public class UDPServer
    {
        private UdpClient udpClient;

        public UDPServer(int port)
        {
            udpClient = new UdpClient(port);
        }
        
        public async UniTaskVoid Send(string data, IPEndPoint endPoint, CancellationToken token = default)
        {
            byte[] message = Encoding.UTF8.GetBytes(data);
            await udpClient.SendAsync(message, message.Length, endPoint);
            Debug.Log("Send:\"" + data + "\" To:" + endPoint.Address + "," + endPoint.Port);
        }
        
        public async UniTask<string> Receive(CancellationToken token = default)
        {
            var result = await udpClient.ReceiveAsync();
            IPEndPoint endPoint = result.RemoteEndPoint;
            Debug.Log("Receive:" + endPoint.Address + "," + endPoint.Port);
            byte[] getByte = result.Buffer;
            string data = Encoding.UTF8.GetString(getByte);
            return data;
        }
    }
}

Sendメソッドはstring型で送信データ、IPEndPoint型で送信先を設定して送信します。 Receiveメソッドはデータを受け取るまで待機して、データが送られてきたらstringをリターンします。

クライアント側(ESP32)

UDPLibrary.h

#pragma once
#include <WiFi.h>
#include <WiFiUdp.h>

class UDPLib {
  private:
    WiFiUDP wifiUdp;
    String host;
    int send_port;
    int receive_port;
  public:
    void begin(String ip, int send_p, int rec_p);
    void send(String);
    String read();
};

UDPLibrary.ino

#include "UDPLibrary.h"

void UDPLib::begin(String ip, int send_p, int rec_p) {
  host = ip;
  send_port = send_p;
  receive_port = rec_p;
  wifiUdp.begin(receive_port);
}

void UDPLib::send(String text) {
  byte Data[256];
  char hostC[256];
  host.toCharArray(hostC,text.length());
  Serial.println(hostC);
  text.getBytes(Data, text.length() + 1);
  wifiUdp.beginPacket(hostC, send_port);
  wifiUdp.write(Data, text.length() + 1); //10進数のASCiiで送信される
  wifiUdp.endPacket();
}

String UDPLib::read() {
  char x[512] = "";
  if (wifiUdp.parsePacket()) {
    for (int i = 0; i < 512; i++) {
      int v = wifiUdp.read();
      if (v == -1) {
        x[i] = '\0';
        break;
      }
      x[i] = v;
    }
  }
  String r=x;
  return r;
}

テスト用コード

以下はテスト用に作成したコードです。
適当な空のオブジェクト作ってスクリプトを貼っつけてください。

サーバー側

using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.InputSystem;


namespace UDP
{
    public class UdpTest : MonoBehaviour
    {
        [SerializeField]
        private string _sendData = "test";
        [SerializeField]
        private List<string> address;
        [SerializeField]
        private int receivePort = 9000;
        [SerializeField]
        private int sendPort = 9001;
        
        private UDPServer _udp;
        private List<IPEndPoint> _ipEndPoints = new List<IPEndPoint>();

        private void Start()
        {
            _udp = new UDPServer(receivePort);
            
            var token = this.GetCancellationTokenOnDestroy();
            WaitReceive(token).Forget();

            for (int i = 0; i < address.Count; i++)
            {
                _ipEndPoints.Add(new IPEndPoint(IPAddress.Parse(address[i]),sendPort));
            }
        }

        private void Update()
        {
            if (Keyboard.current.digit0Key.wasPressedThisFrame)
            {
                _udp.Send(_sendData,_ipEndPoints[0]).Forget();
            }
            if (Keyboard.current.digit1Key.wasPressedThisFrame)
            {
                _udp.Send(_sendData,_ipEndPoints[1]).Forget();
            }
            if (Keyboard.current.digit2Key.wasPressedThisFrame)
            {
                _udp.Send(_sendData,_ipEndPoints[2]).Forget();
            }
        }
        
        async UniTaskVoid WaitReceive(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                string data = await _udp.Receive(token);
                Debug.Log(data);
                //await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: token);
            }
        }
    }
}

インスペクターで送信先のESP32のIPアドレスを設定します。 このテスト用コードでは個別にキーを押して送信していますが、実際使用した際はfor文回して全ESP32に同じデータを送信していました。

クライアント側

#include"UDPLibrary.h"

UDPLib _udp;

char* ssid="ssid";
char* password="pass";

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while ( WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println("connected "+String(ssid));
  Serial.println(WiFi.localIP());
  _udp.begin("192.168.0.32",9000,9001);
}

void loop() {
  if(Serial.available()>0){
    String text=Serial.readStringUntil('\n');
    _udp.send(text);
  }
  String r=_udp.read();
  if(r!=""){
    Serial.println(r);
  }
}

Arduino IDEのシリアルモニタでデータの確認できます。

参考記事

【Unity】UDP通信する|Hiko|note

C# UdpClientの使い方解説 - Qiita

サイバーエージェントさんのプロトスプリントリーグに参加した話

先日サイバーエージェントさんのプロトスプリントリーグに参加してきましたので、その経験をまとめていきたいと思います。

プロトスプリントリーグとは?

「3日間でお題に沿ったゲームをチームで開発し、ゲームとしての総合的な完成度の高さを競う」というものです。※公式HPより

www.cyberagent.co.jp

3人1組、計5チームで、各チームに2人メンターさんがサポートについてくれるといった形でした。

キックオフから事前準備期間(2/4~2/10)

キックオフの中でチームメンバーとテーマの発表がありました。 テーマは...

「遠隔×〇〇」

でした。

うーん...難しい...
〇〇の部分は自由に決められるので実質自由テーマですね。

キックオフの後にチームごとにdiscordに集まり顔合わせ&ミーティングを行いました。初めて会う人との開発は初めてだったので緊張していましたが、チームメンバーの人たちはかなり話しやすい人たちで最終日まで本当に楽しく開発ができました!! その日はアイデア出しを行いましたが、決めきらなかったのでそれぞれで一回考えようということになりました。

準備期間中はキックオフを含めると4回集まり、アイデアの決定、ゲームの詳細な内容や仕様の決定、タスク分担、素材選定、素材作成、クラス設計、UI設計などを行いました。

不幸なことに準備期間がもろに期末試験にかぶっており、ミーティングと試験勉強を同時進行で進めなければならないというかなり鬼畜スケジュールでした...

テーマですが、私のチームは

「遠隔×宇宙」

と決めました。
具体的には
「主人公の衛星が故障してしまって使える機能は電波の発射のみ」
「自分の付近にある探査機をハッキングしていき、ネットワークを構築していく。ネットワークを広げていきながら、地球に電波を発射できる巨大通信機までのネットワークを作り、地球に救難信号を送る」 というものになりました。

↓私が描いた雑なイメージ図

3Dゲームで一人称のプレイヤーの挙動を作ったことがあるのが私だけだったので、プレイヤーの挙動、UI等を中心に担当になりました。
事前準備ではクラス設計、画面設計↓やメーター等の素材作りを行いました。

1日目(2/11)

プレイヤーのInput周りと基本挙動を完成させました。まず、操作ですが宇宙ということで360°ぐるぐるどの方向も見れていまいます。これではプレイヤーは操作しづらいですし酔ってしまいます。そこで上下方向に回転制限をつけ、横回転はワールド座標、縦回転はローカル座標にすることで違和感のない挙動にしました。 その後、衛星に接続するためにRayを飛ばして接続先の衛星の情報を取得する部分を作りました。

2日目(2/12)

プレイヤーの移動部分とUIを大体完成させました。電波を発射して情報取得後 、接続、そしてカメラ移動という部分を作成しました。UIは画面左右のメーターや画面下部の文字列をプレイヤーステータスによって更新するようにしました。
この辺から他のメンバーの担当部分とのやり取りをする部分が増えてきますが、常に通話を繋ぎ仕様の確認をこまめに行い大きな問題が発生することはなかったです。

この日のお昼に中間発表があり、その際にほかチームのメンターさんからもアドバイスが頂けました。もらったアドバイスの中で「操作が複雑そう、違和感があるのは良くない」という点には常に意識しました。

3日目(2/13)

(3日目...といいつつ寝てないので明確に区切れない)

ブラッシュアップとデバッグをメインで行いました。PostProcessingを導入したり、ユーザーが遊びやすいようにUI/UXを調整しました。また、プレイに関わる大きいバグを修正しました。

しかし、

webGLでの提出だったのですが、webGLでのビルドがうまくいきませんでした...
(正確にはビルドはできるがブラウザ上で開くとポップアップが出て動かなくなる) おそらくAwake()などシーン再生開始時の処理が多すぎたのが問題だと思います。

最終発表会&懇親会

最終発表では他チームの完成度の高さに驚きました。アイデアがおもしろいのはさることながら、デザインや作り込みが素晴らしかったです。私達のチームはがっつりプログラマーばかりだったので、デザインが得意な人がいなかったのでビジュアル面が弱かったかなーと思います。

最終発表後、懇親会がありました。懇親会では「nonpi foodbox」というサービスでお酒とおつまみを提供していただきました。まさかオンライン懇親会で食事を用意していただけるとは思いませんでしたw

懇親会では社員さんの貴重な話を聞けたり、他チームの人との交流ができました。やはり、ゲームのインターンということでゲーム好きばかりでめちゃくちゃ楽しい懇親会でした。

まとめ

ここまで濃い3日間は大学でプログラミングを始めてから初めてでした。3日間でちゃんと遊べるゲームを作るという時点で大変ですがメンバーやメンターさんから様々な新しい知識を得られるというインプットとアウトプットが凝縮された3日間でした。

反省としては、クラス設計したのですが全くそのとおりに作れなかった点や他のメンバーに読みづらい、使いづらいコードになってしまった点です。 設計、アーキテクチャデザインパターンあたりの勉強を進めようと思います。

メンターさんによると優勝するチームの特徴は二日目の中間発表時点でほぼ完成しているチームだそうです。確かに今回優勝したチームも中間発表でほぼ完成していたなあ...残り時間ブラッシュアップに時間をかけられるのは確かに強いですもんね...

いやーそれにしてもやはりインターンハッカソンに参加するのはいいですね!
チームメンバーやメンターさんから様々な知識を学べますし、なんといっても技術に対するモチベーションがとてつもなく上がります。モチベは技術力向上の最大のきっかけになります。

この記事を読んでくださって、まだインターンハッカソンに参加したことがない人は是非参加してみてください。技術力ないからというのは気にしないでとりあえず申し込んでみましょう!必ず、技術力向上に繋がるはずです!!

大学で初心者向けUnity勉強会&ハッカソンの主催をした話

年明け早々1月にイベントを主催したので、その記録や反省を書きたいと思います。

イベントの概要

  • イベント名:UnityThon
  • 期間:2022年1月8日から26日
  • 内容:初心者向け勉強会&ハッカソン
  • 対象:プログラミングの基礎的な知識がある会津大生

開催に至った経緯

一番の経緯は大学でのUnity人口を増やしたいと思ったからです。Unityは簡単にゲームを作れるゲームエンジンです。プログラミング初心者はもちろん、オブジェクト指向やチーム開発などを経験するにはもってこいのツールだと考えています。
さらに会津大学はコンピュータ系の大学にも関わらず、大学の半分くらいの生徒はプログラミングを勉強と思っています(おそらく)。しかしもっとプログラミングを楽しんで欲しいという思いから今回開催したいと思うようになりました。

準備

2021年11月学園祭
会津大学にはゲーム系サークルがなんと3サークルもあります。正確にはそれぞれ個性があるのですが... その3サークルの部長が学園祭で駄弁っていた際にゲーム系3サークルで協力しようという話が持ち上がります。その第一弾として特に共通な知識であるUnityの勉強会を実施しようということになりました。

12月
まず、3サークルや技術好きが集まっている団体の人に協力を仰ぎ、サポートについてくれる人や運営に協力してくれる人を集めました。何度か会議を行いイベント内容を詰め、タスク分担、スケジュール等をまとめました。12月後半にはデザインが得意な先輩にお願いしロゴやポスターのデザインを作っていただきました。Googleフォーム等を整備し、12月25日にアナウンスを行いました。1月頭に交流や連絡用にDiscordを整備し、参加登録をしてくれた方にメールでその案内を送りました。

私は勉強会資料作りを担当し、年末から年明け、勉強会前日ぎりぎりまで作りました。(徹夜)

勉強会

日程:第一回 1月8日、第二回 1月12日
場所:大学

参加者16人ほどで一人講師、サポート3人でちょうどいい感じに回すことができました。第一回は6時間に及ぶ長丁場でしたが、作った資料(スライド100枚)をほぼ消化することができました。

ハッカソン

期間:1月19日~1月26日
場所:オンライン

ハッカソン期間開始直前で福島県にまん延防止重点措置が発令されてしまい、当初ハッカソンもオフラインの予定でしたが完全オンラインに移行しました。

1月19日Git勉強会&キックオフ
前半でチーム開発に向けて、Git勉強会を行いました。 その後、キックオフを行い、チームの顔合わせを行いました。 ハッカソンは参加者14人で4チームでした。チーム分けはwindowsmac間で問題が発生しがちなのでそこを分け、その後同じサークルで固まらないように分けました。各チームに一人メンターをつけ、いつでもサポートできるようにしました。

1月22,23日
この二日間はメンターが常にスタンバイして質問にすぐ答えられる環境を作りました。(ホントは対面の予定だったのでまる二日がっつり開発できる日を確保してた)

1月26日最終発表会、表彰式
WebGLでビルドを行い、unityroomにアップロードしてもらい、参加者同士で遊べるようにしました。 表彰はオーディエンス賞とSA(メンター)賞で順位付けしました。

反省

勉強会

第一回勉強会は多少抜けはあったが重要な部分はあらかた網羅できていて難易度もちょうどよかったと思う。 第二回勉強会はそもそも資料が完成していなかったうえに内容もいまいち適当な部分が目立った。

ハッカソン

オンラインということやチーム開発に慣れていない人が多くいたが、そのサポートが足りなかったと思います。
最大の反省は最終発表会&表彰式において私の司会下手で進行がもたついたり、足早になってしまったり、盛り上がりに欠けてしまった点です。

今後に向けて

勉強会資料は今回のイベントで作ったものをブラッシュアップすれば来年も使えるものを作れたと思います。
司会に関しては慣れという部分も大きいと思うが、事前にリハーサルをするなど準備をしっかりしておくべきだったと思います。また、運営陣の人たちにチャットで盛り上げてもらっても良かったと思います。
全体としてはイベントとしてしっかり完遂できたのは大きいと思います。また、このイベントがきっかけでUnityを初めて、イベント終了後も質問に来てくれる人もいてめちゃくちゃうれしいです。自分の至らない点が多く反省は多いですが、それら含めて良い経験になりました。

VR技術者認定試験アプリケーションコースの勉強した内容

この記事はAizu Advent Calendar 2021の22日目の記事です。

めちゃくちゃ久しぶりの更新になってしましました。もっとブログ活用しないとなあ...
ということで今回は12/18にオンラインで実施されたVR技術者認定試験アプリケーションコース受験に向けて勉強した内容をまとめてみたいと思います。 とはいえ、詳しくはこちらの方の記事が分かりやすくまとまっているので、私の記事では試験の頻出項目について口語的にまとめてみたいと思います。 量が多く間に合わないので、当日にある程度記事を書いて公開し、後日追記していく形にしようと思います。

リアルとバーチャルの融合ー複合現実感

複合現実感

概念

多くの方は「VRとARはわかるけどMRって何?」となりがちです。
人工現実感(Virtual Reality)はコンピュータ内や遠隔の環境に現実感を与える技術のことを指します。
そして拡張現実感(Augmented Reality)はいわば実環境とVR環境の中間に位置しています。
そして同じくその中間に位置している概念として拡張VR(Augumented Virtuality)があります。
前者は現実にVR環境の情報を重畳するのに対して、後者は実世界のものや人をモデル化しVR環境に統合することをいいます。
そして、複合現実感(Mixed Reality)はそれら全てにまたがる概念と言えます。

それらをスペクトルに表すとこのようになります。

|-----------MR-----------|
実環境---AR---AV---VR

レジストレーション

幾何学レジストレーション
ARにおいて、視覚的に実環境とVR環境を融合することで、それぞれの環境で定義された3次元座標系を一致させることをいいます。

撮像系・・・カメラ、人間の視覚

投影変換:内部カメラパラメータ・・・カメラキャリブレーション

位置姿勢:外部カメラパラメータ・・・刻々と変化するのでリアルタイムに計測する必要がある→ラッキングが必要

ビューイング変換・・・ワールド座標系とカメラ座標系間の座標変換

ラッキング方式

アウトサイドイン方式
環境に設置したセンサを利用し、撮像系の位置姿勢を計測
インサイドアウト方式
撮像系に取り付けたセンサなどを利用し位置姿勢を計測
ハイブリッド方式
両方を用いる

センサ方式
ARシステムの撮像系を使用しない
カメラ方式
ARシステムの撮像系を使用

  • マーカ方式
    マーカ画像内で2次元座標を検出する。
  • 自然特徴方式
    環境にもともとある特徴を利用。
    • ボトムアップ手法
      画像から特徴検出→抽出された特徴をモデル内の特徴と照合→誤照合を除去→位置姿勢計算
    • トップダウン手法
      ラッキング履歴から現在の位置姿勢を予測→予測情報をもとにモデル内の特徴を画像平面に投影する→投影位置近傍で対応特徴候補を探索する→位置姿勢計算
      ※急激な変化に弱い

実世界情報提示技術

合成方式

光学透過式
ハーフミラーなどでCGを重ね合わせる。
ビデオ透過式
カメラで撮影した映像内でCG合成

HMDの光学系

網膜投影型
水晶体の屈折力を用いないので視距離によらず鮮明な映像を提示可能
接眼光学型
標準的なHMD
各種の光学系が使われる

  • リレー光学系
  • 偏心光学系
  • 反射屈折光学系

ホログラフィック光学素子
透過度が高い光学透過式を実現可能

頭部搭載型プロジェクタ
プロジェクタの映像をハーフミラーを通して環境側に投影
再帰性反射
接眼光学系に起因する映像歪みがない

プロジェクションマッピング プロジェクタで実物体に直接映像を投影する

実世界モデリング技術

レンジセンサ

  • 三角測量
    • 受動型ステレオ
      2点から撮影した画像をもとに三角測量の原理で3次元座標を求める。
    • 能動型ステレオ
      光切断法 ・・・ラインレーザを投影しレーザーの投影面とカメラの各画素の視線の交点から座標を求める。
  • 光速の利用
    • 飛行時間方式
      パルスレーザーを発射し反射して戻ってくるまでの時間を測定
    • 位相差検出方式
      位相差を検知することで距離を測定

ウェアラブルコンピュータ

特徴

情報提示技術

入力インターフェイス技術

コンテキスト認識技術

ユビキタスコンピューティング

続きは後日追記します。

【備忘録】Prefabの動的生成

よく使うので備忘録として書きます。

prefabの動的生成

GameObject obj = (GameObject) Resources.Load("Prefabs/Prefab1");
GameObject GeneratedObj = Instantiate(obj, Vector3.zero, Quaternion.identity);

Resources.Load("パス")でResourcesフォルダ内のファイルをロードできるのですが、後ろのカッコ内はパスを入力できるので、
「Resourcesフォルダ内のPrefabsフォルダの中にあるPrefab1」と指定できます。
※Resourcesフォルダは自分で作る必要があります。
その後、Instantiate(生成するオブジェクト, 位置, 回転)で生成できます。

Resources.Loadについて

ファイルをResourceフォルダの正確な場所に配置しなければならないので使い勝手が悪い。
そこでインスペクター上からPrefabを設定できる方がいいかもしれない。

[SerializeField]
GameObject obj;
GameObject GeneratedObj = Instantiate(obj, Vector3.zero, Quaternion.identity);

ちなみに

子オブジェクトとしてPrefabを生成したい時がある

と思うのですが、
その場合は上記のスクリプト

obj.transform.parent = content.transform;

と足すことでできます。※contentは親オブジェクト

しかしUIを子オブジェクトとして生成する場合、上記のようにではなく

obj.transform.SetParent(content.transform, false);

のほうが良いらしいです。

Prefab生成時に実行されるもの

StartやAwakeなど、素の状態だとゲーム開始時に実行されるものが実行される。

展望

自己紹介

はじめまして!土鍋と申します。現在は会津大学のA-PxL(VR部)に所属しており、VRゲーム制作をメインに活動しています。現在までで学んだことは、Unity, Blender, C, C# などです。

経緯

さてさて、初めてのブログの初めての記事ということで何を書くか。 とまあ、悩んでいたんですがとりあえずブログ開設に至った経緯でも。

きっかけとしてはサークルで定期的に開催されるLT会で、とある先輩がアウトプットの重要性について解説されていたことが最初です。その後、あらゆる知見を得たときの備忘録や作品の発表、解説の場があると便利だと感じ、一つ学年が上がった今、開設に至りました。

このブログの方針や展望

基本的には本当に様々なことについて書いていくつもりです。興味関心に基づいて、どんどん挑戦してどんどん記事をかけたらと思います。

具体的には…

  • Unity
  • プログラミング(C, C#, C++, Java)
  • ハードウェア(ラズパイ, Arduino)
  • VR, AR, MR等のxR技術に関して
  • 趣味(旅行、アニメ、ゲーム)
  • その他知見や備忘録

リンク

自分に関係があるもののリンク

A-PxLのブログ

aizu-vr.hatenablog.com

A-PxLのTwitter

A-PxL (@aizu_PxL) | Twitter