Netcode for GameObjectsの機能紹介 NetworkTransformコンポーネントについて

f:id:fb8r5jymw6fd:20211202232056p:plain

 

はじめに

エンジニアの松原です。この記事はSynamon Advent Calendar 2021の4日目の記事になります。

前回の記事では簡単にNetcode for GameObjectsの内容について触れましたが、これら以外にもUNetやMLAPIで用意されていた機能を引き継いで、Netcode for GameObjectsの機能として利用できるものがあります。

この記事ではその機能の一つである、NetworkTransformコンポーネントについて紹介したいと思います。

今回も前回に引き続き、今回の内容について個人のGitHubリポジトリにアップロードしておりますので、もしよろしければ触ってみてください!

 

今回の機能を反映したサンプルシーン名はSampleScene_NetworkTransform.unityになります

f:id:fb8r5jymw6fd:20211202232951p:plain

 

NetworkTransformについて

f:id:fb8r5jymw6fd:20211202084250p:plain

NetworkTransformは前回の記事中に登場したNetworkBehaviourを継承したクラスで、Player(Player Prefab)の位置情報に関する更新を処理してくれる補助系のコンポーネントに位置づけされています。補助系とはいえ、マルチプレイを実現するためには必須としての機能が詰まっています。以下にその機能について解説します。

 

Transform(位置、回転、スケール)の同期処理

前回の記事では、公式のチュートリアル Binding on "Hello World" のコードを改造し、直接NetworkBehaviour継承クラスにPlayerの位置更新処理を行うコードを実装していました。通常のMonobehaviourではそれで問題ないのですが、NetworkBehaviourではサーバー側に更新処理を通知するRPCを送信する仕組みが必要なため、少し勝手が変わってきます。

 

前回の記事ではPlayerの位置情報を更新するためのRPCを呼び出す処理が更新がクライアント側の端末でUpdate()が呼び出されるたび発生しており、結果としてネットワークの送受信の処理が負荷がかかる実装になっていました。

NetworkTransformでは、処理負荷を減らすための設計が含まれています。以前に送信したTransform情報と現在のTransform情報の差分を取得、その差分がしきい値を超えている状態(ダーティな状態)であれば現在のTransform情報をRPCを通してサーバー側に送る仕組みになっています。

このしきい値はNetworkTransformのインスペクタから変更が可能になっています。位置情報、回転情報、スケール情報の各しきい値を変更できるようになっており、このしきい値を大きくすれば同期処理の間隔は短くなります。全くキャラクターを操作しない場合は位置情報の更新についての処理そのものが送信されなくなります。

f:id:fb8r5jymw6fd:20211202093612p:plain

 

また、必要ないと思われるパラメータを同期処理の対象から外すことができます。以下のように、通常のオンラインゲームではTransformのスケール情報はあまり扱わないと思うので、チェックを外しておくのが良さそうです。

f:id:fb8r5jymw6fd:20211202090152p:plain

 

Interpolate(位置補完)処理

Transform情報の更新間隔が荒いほど、動きのカクつきが目立つようになります。それを解決するための機能として、NetworkTranformではInterpolate(位置補完)機能が利用できます。これは同期毎のTransform情報間の差分情報を利用して、新しいデータを受け取るまでに位置の補完処理を自動で行うようになります。

f:id:fb8r5jymw6fd:20211202200703p:plain

 

Interpolateが動いている状態と動いていない状態の違いは想定しづらい思いますので、それぞれ以下の条件でInterpolateありなしの状態を比較しました。しきい値の設定は以下のようになっています。

f:id:fb8r5jymw6fd:20211202211054p:plain

 

実際にInterpolateありとなしでそれぞれ動いている動画をgifにしました。

Interpolateありでのアニメーションです。

f:id:fb8r5jymw6fd:20211202213530g:plain

Interpolateなしでのアニメーションです。

f:id:fb8r5jymw6fd:20211202213608g:plain

このようにInterpolateのありではしきい値が高めでもがっつり補完処理を掛けてくれます、その上通信負荷も減るので使用するメリットは大きいと思います。

ただ、補完処理そのものはCPU上で計算されているようなので、たくさんしないといけない場合はInterpolateの使い分けは必要になってくるかもしれません。

 

更新対象をローカル座標に変更する(In Local Space有効)

NetworkTransformはデフォルトではグローバル座標を対象とした更新処理がかかります。(コードとしては transform.position や transform.rotation など)

In Local Spaceのオプションを有効にすることで、更新対象をローカル座標に変更することができます。(コードとしては transform.localPosition や transform.localRotationなど)

f:id:fb8r5jymw6fd:20211202214808p:plain

 

以上でNetworkTransformについての機能紹介になります。便利な機能を持っていますが、実はNetcode for GameObjectsのバージョン1.0.0では、更新権限についてはNetworkBehaviourの特徴を引き継いでおり、ransformの書き換えはサーバーまたはホスト側の端末のみしか行えません。

クライアント側に相当する端末ではtransformを書き換えられないので、前回紹介したようなダミー表示を利用するなどの方法を間に挟む必要があります。

ただし、Unityの公式側でこの問題を解消するために、クライアント側端末でもtransformを更新できるようになるコンポーネントがあります。それが以下に紹介するClientNetworkTransformになります。

 

ClientNetworkTransform

ClientNetworkTransformはクライアント側端末でもtransformの更新を取り扱えるようになるコンポーネントです。このコンポーネントはNetworkTransformのクラスを継承して作られています。

厳密には他の端末への同期処理はサーバーへRPCを送信することで実現しており、実質的にはサーバー側で同期処理が行われ、サーバーから各クライアントへ位置情報の更新情報が送られているのには変わりません。

それであっても、書かないといけないコード量がだいぶ減るので、Netcode for GameObjectsを初めて触る人にはお勧めしたいコンポーネントです。

f:id:fb8r5jymw6fd:20211202220154p:plain

 

インストール方法

このコンポーネントは通常のNetcode for GameObjectsには含まれていません。サンプルとして含まれているので、サンプルをインポートすることでAssetに追加することができます。

f:id:fb8r5jymw6fd:20211202221320p:plain

 

前回の内容からClientNetworkTranformを利用できるように変更する

前回取り上げたNetcode for GameObjectsのサンプルリポジトリをClientNetworkTransformに対応したものに置き換えてみました。今回はPlayer_NetworkTransformというPrefabを用意しました。

PlayerSyncTransformBehaviourというコンポーネントは前回記事のPlayerSyncBehaviourをClientNetworkTransformに対応させたものになります。

f:id:fb8r5jymw6fd:20211202222203p:plain

 

以下はPlayerSyncTransformBehaviourのクラスのコードになります。前回紹介したPlayerSyncBehaviourのクラスより、大幅にコード量が減りました。表示用ダミーを用意しなくて済むようになったのは大きなメリットです。

#nullable enable

using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;

public class PlayerSyncTransformBehaviour : NetworkBehaviour
{
[SerializeField]
private float speed = 5f;

private PlayerInputHelper? playerInputHelper = null;

[SerializeField]
private UnityEvent<GameObject>? onTrackingObjectPresented;

public override void OnNetworkSpawn()
{
if (IsOwner)
{
playerInputHelper = FindObjectOfType<PlayerInputHelper>();

onTrackingObjectPresented?.Invoke(gameObject);

SetRandomPosition();
}
}

private void SetRandomPosition()
{
var position = new Vector3(Random.Range(-3f, 3f), 0f, Random.Range(-3f, 3f));
transform.position = position;
}

private void UpdatePlayerInputs(float delta)
{
if (playerInputHelper == null) return;

if (playerInputHelper!.HasMoveInput)
{
var move = playerInputHelper!.Move;
var direction = new Vector3(move.x, 0f, move.y);

var position = transform.position;
transform.LookAt(direction + position, Vector3.up);
transform.position += direction * speed * delta;
}
}

private void Update()
{
var delta = Time.deltaTime;

if (IsOwner)
{
UpdatePlayerInputs(delta);
}
}
}

 

ClientNetworkTransform(NetworkTransform)の課題

ClientNetworkTransformは便利な機能ですが、更新間隔がTransform情報の更新差分に対するしきい値で送信頻度を判断するため、高速で動く物体や高速で回転する物体等、Transformの数値が大きく変更されるユースケースにはあまりマッチしていないと思います。

また、Interpolationの実態はBufferedLinearInterpolatorというクラスで補完処理が行われています。このクラスの補完処理の想定外の動きをするTransformは挙動が安定しない可能性があります。競技性の高いゲームでは補完処理の実装に気を使う必要が出てくるため、あえて自前で実装していく必要があるかもしれません。

 

最後に

Unity公式のネットワークシステムは以前のUNetやMLAPIの機能を色濃く継承していますが、以前からの仕様の変更点も多いので、しばらくは自らソースコードを読むなどの努力が必要になるかと思います。引き続きNetcode for GameObjectsについて追っていきたいと思います!

 

謝辞

今回ClientNetworkTransformの情報を調べるにあたり、Denikさんの下記のブログ記事を参考にさせていただきました。ありがとうございました!

 

宣伝

今年のSynamonはアドベントカレンダーを実施しております、よろしければ他の記事もお読みいただけると嬉しいです!

qiita.com

 

また、弊社ではUnityエンジニアを募集しています。興味がある方は是非以下のページを覗いてみてください!

twitter.com

meety.net

herp.careers

Oculus GoのアンロックOSビルドを入れてみた

この記事はSynamon Advent Calendar 2021の3日目です。

この記事の結論:現状ではそんなに面白い事は出来ませんでした!


2018年5月1日に発売、その翌年である2019年5月21日に後続のOculusQuestが発売され、2020年末で販売を終了。流れ星のように役目を終え去っていった一体型・3DoFヘッドマウントディスプレイであるOculus Go。

その軽量さや限定された操作によるお手軽さから未だに根強い人気もあるこのデバイスですが、先日このようなニュースが報じられました。 www.moguravr.com

これにより、OculusGoのブートローダーのロックを解除し、誰でもOSを開発、インストールすることが可能になる様なので、軽く触ってみました。

アンロックされたOSビルド及び、導入方法のドキュメントにはこちらからアクセス可能です。 developer.oculus.com

インストールは上記ウェブサイトの案内に従えばすぐ終わります。ただし、デバイスに保存していたデータはすべて消えるので注意してください。

試してみた様子

デバイスがアンロック状態になると、起動時に以下のような警告画面が表示されるようになります。

その後セットアップ画面に入ります。アンロック後もOSを入れ替えなければ、Oculusアプリを使ってセットアップ→ストアやアカウントへのアクセスは引き続き出来るようです。

adbで確認してみると、root権限は取れていました。

f:id:Sokuhatiku:20211202175637p:plain

root権限があるので、色々見れたり消したり出来ます f:id:Sokuhatiku:20211202193344p:plain

adb reverseも打てます。

f:id:Sokuhatiku:20211202181804p:plain

adb reverseが出来るならlocalhostでWebRTCのデバッグとかも出来ます。今どきはngrokがあるのでちょっと便利になる程度ですが……

ついでにPlayストアのapkをインストールしてみたものの、残念ながら使えないようです。(これはアンロックしなくても試すことが出来ます)

f:id:Sokuhatiku:20211202175124p:plain

結局アンロックすると何ができるのか

root権限とブートローダーの署名チェック無効化により、Oculus Goの制限が解除されてAndroid(Linuxカーネル)で出来ることは全部出来るようになっている筈です。ただし、Oculus Goをかぶって見ることが出来るVR空間はOculusのメニューアプリがレンダリングしているものなので、そこに直接手を入れる、例えばポインタ形状を変化させたりメニュー項目を増やすといったような、いわゆるroot化したスマホのようなカスタマイズをするのは難しそうです。(そのアプリ自体を差し替えたりとかはできると思います。)

もうちょっとドキュメントが充実すれば色々出来そうな感じはあるのですが、現状だとちょっと情報不足で、面白いことをするのは難しそうです。自分もAndroidの低レイヤーに詳しいわけではないので、ちまちま調べながら引き続き探ってみます。

Unityでのマルチプレイアプリ開発を便利にしてくれるEditor拡張-ParrelSyncを深ぼってみる

はじめに

こんにちはエンジニアの吉田です。
Synamon Advent Calendar 20212日目は、UnityのPhotonなどを使ったマルチプレイアプリ開発で必須のEditor拡張「ParrelSync」について、その実装を深ぼってみる中で知れた内容をまとめました。ParrelSyncは使っているけど、その仕組みについてもう少し理解しておきたいという人におすすめの記事となっております。

なお、ParrelSyncの使い方はここでは説明しませんので、使い方を知りたい方はREADMEや日本語の解説記事を検索して読んでください。


ParrelSync

github.com

通常、Unityでは一つのプロジェクトを複数のUnityEditorで開くことはできません。もちろんプロジェクトフォルダを丸ごとコピーして別プロジェクトとすれば開けますが、これでは片方のプロジェクトの変更がもう片方に反映されないため不便です。ParrelSyncはシンボリックリンク等(後述するが正確にはOSによってリンクの貼り方が異なる)を使うことによってこの問題を解決しており、オリジナルのプロジェクトの変更をコピー先にも自動で反映してくれます。

ちなみに、一つのプロジェクトを複数のUnityEditorで開くのがどんな場面で便利かというと、Photonなどを使ったマルチプレイアプリ開発の効率化にとても役立ちます。通常複数人のプレイを実際に再現するにはスタンドアロンビルドし、そのビルドを複数立ち上げる必要があります。しかし、同一プロジェクトで複数のUnityEdtiorを立ち上げることができれば、各Editorでゲームを再生すればそれが再現できてしまうのです。

複数人コラボレーションができるNEUTRANSを開発しているSynamonではそれなりにお世話になっているツールですが、GitHubのスター数が1,372個あるので世の中的にもそれなりにメジャーではあるようです。(2021/11/13日現在)

これは余談なのですが、自分はParrelSyncをPallalelSyncだとずっと勘違いしてました。 ググったところ"Parrel"というのは、こんな感じの玉がつながってるものを意味してるらしいです。


動作環境

  • UnityEditor:2020.3.16f1
  • ParrelSync:ver1.5.0(リポジトリのReleaseページよりunitypackageをダウンロードしてインポートした)
  • OS:Windows10


本題

それでは中身を調べてみてわかったことについて書いていきます。

ParrelSync自体はとても小さなプロジェクトでC#のファイルは12個しかありません。また、ClonesManager.csというクラスが中核でこのクラスを見れば大体の処理内容がわかります。

クローン方法について

まず、メインの機能であるプロジェクトのクローン方法について見ていきます。 クローン方法の概要はREADMEに書いてあります。

...Asset, Packages, ProjectSettingsのコピーとシンボリックリンクを使ったオリジナルプロジェクトへの参照をつくる。Library, Temp, objといった他のフォルダは各クローンごとに独立させる。... (筆者訳)

実際にクローンをつくってみます。 新しいUnityプロジェクトを「DigParrelSync」という名前つくり、そのクローンを1つ作成してみました。 クローンされたプロジェクトの名前は「DigParrelSync_clone_0」になっており、中身は以下の画像にあるとおりです。

f:id:kkkkkkssssss:20211115110709p:plain
クローン直後

〇クローンされたプロジェクトの中身

  • (リンク)Assets
  • Library
  • Packages
  • (リンク)ProjectSettings
  • .clone
  • .parrelsyncarg
  • collabignore.txt

順番に見ていきます。


リンクが貼られるもの:Assets, ProjectSettings, (AutoBuild, LocalPackages)

まず、リンクが貼られるものについて見ていきます。対象は、Assets, ProjectSettingsフォルダ、そしてもし存在する場合にはAutoBuild, LocalPackagesフォルダも対象となります。

これらはリンクによってオリジナルのプロジェクトと同期されるものです。リンク方法はOSによって異なっており以下のようになっています。

// メソッドの中身はそのままですが、メソッドの順番は本物と異なります。
// 日本語のコメントは適宜筆者が追加してます。

private static void CreateLinkWin(string sourcePath, string destinationPath)
{
    // /Jオプションはジャンクションなので、正確にはシンボリックリンクじゃない!
    string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath);
    Debug.Log("Windows junction: " + cmd);
    ClonesManager.StartHiddenConsoleProcess("cmd.exe", cmd);
}

private static void CreateLinkMac(string sourcePath, string destinationPath)
{
    sourcePath = sourcePath.Replace(" ", "\\ ");
    destinationPath = destinationPath.Replace(" ", "\\ ");
    var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);

    Debug.Log("Mac hard link " + command);

    ClonesManager.ExecuteBashCommand(command);
}

private static void CreateLinkLinux(string sourcePath, string destinationPath)
{
    sourcePath = sourcePath.Replace(" ", "\\ ");
    destinationPath = destinationPath.Replace(" ", "\\ ");
    var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);           

    Debug.Log("Linux Symlink " + command);

    ClonesManager.ExecuteBashCommand(command);
}

// 長いのでここでは中身を省略。どちらもSystem.Diagnostics.Processをつかってcmd.exeやbashを実行してます。
private static void StartHiddenConsoleProcess(string fileName, string args){/*省略*/}
private static void ExecuteBashCommand(string command){/*省略*/}

それぞれSystem.Diagnostics.Processを使い、cmd.exeやbashを別プロセスで起動させリンクをつくっています。MacとLinuxは同じコマンドで、Windowsは異なります。

コードを見るとわかるのですが、MacとLinuxは文字通りシンボリックリンクなのに対し、Windowsではジャンクションを使っています。Windowsでは"ハードリンク"、"シンボリックリンク"、"ジャンクション"という3種類のリンクが区別されるので、正確にいうならシンボリックリンクではないです。 コード内でもDebug.Log("Windows junction: " + cmd);と書いてるのでシンボリックリンクではなくジャンクションにしいるのは意図的なようです。

Windowsでジャンクションではなくシンボリックリンクを使った場合どうなるかを試してみたところ、リンクをつくることができませんでした(↑コードのCreateLinkWin()内のmklink /Jの部分をmklink /Dに置き換えて試した)。シンボリックリンクがつくれない正確な理由はわかりませんが、Windowsではシンボリックリンクをつくるには管理者権限が必要なのが原因なのではないかなと思いました。

ちなみに管理者権限でUnityを起動してどうなるか試したかったのですがやり方がわかりませんでした。Unity.exeやUnityHubを管理者権限で実行してもなぜか管理者権限にならない...

まぁ、ジャンクションだからといって問題があるわけではないので、これは放っておきます。

あと細かいところですが、Mac用のメソッド内にDebug.Log("Mac hard link " + command);と書かれており、実際はシンボリックリンク使ってるのに"hard link"と書かれていてちょっと紛らわしいです。ここは修正のプルリクを出すことにします。

// Link Folders
// それぞれの変数が表すパスは以下。
// assetPath = "/Asset", projectSettingPath ="/ProjectSettigns", autoBuildPath = "/AutoBuild", locakPackages = "/LocalPackages"
ClonesManager.LinkFolders(sourceProject.assetPath, cloneProject.assetPath);
ClonesManager.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath);
ClonesManager.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath);
ClonesManager.LinkFolders(sourceProject.localPackages, cloneProject.localPackages);

また、READMEには書かれていませんが、プロジェクトフォルダ直下にAutoBuild, LocalPackagesという名前のフォルダがある場合はそれらについてもリンクが張られる仕様がコードでは見られました。AutoBuildって何に使うフォルダなんですかね?もし知ってる人いたら教えてください。


最初だけコピーがつくられるもの:Library, Packages

次はリンクは貼られないが、クローン作製時にのみ一度だけオリジナルからコピーが作られるものを見ていきます。対象はLibrary, Packagesフォルダです。

READMEでは「クローン毎に独立させる」と書いてあったLibraryフォルダですが、リンクは貼られないものの、クローン作製時にはオリジナルのものがコピーされるようです。(コード該当箇所

また、READMEには書かれていませんでしたが、PackagesフォルダもLibraryフォルダと同様にリンクが貼られないもののクローン作製時にはオリジナルのものがコピーされています。そしてLibarryフォルダとの違いは、各クローンプロジェクトの起動時にその中身がオリジナルプロジェクトのものと一致しているかの確認と、異なっている場合はコピーし直しがされていました(コード該当箇所

Libraryフォルダが初回だけコピーされる理由やTempフォルダがコピーもリンクもつくられない理由については、以下の記事が参考になります。 (記事の中ではLibraryフォルダもシンボリックリンクする方法が紹介されている点がParrelSyncのやり方と異なるので注意です。)

tsubakit1.hateblo.jp

Unityが特定のプロジェクトを複数起動できない最大の理由はTempフォルダの存在です。このフォルダは「起動時・終了時に削除される」事が期待されており、この中のファイルはゲーム再生時やビルド時の一時フォルダとして使用されています。

Tempフォルダは、プロジェクトを起動する各EditorごとにそのEditorが起動中に管理しておきたい情報を一時保存する場所なので、クローンにはコピーされないようですね。

LibraryフォルダはTempとは異なり、長期的なキャッシュを行うフォルダです。このフォルダにも様々なファイルが格納されますが、概ねファイルと対になるので、Assetsが同一ならば共有してもたぶん問題にはなりません。

LibraryフォルダはTempフォルダとは異なり長期的なキャッシュを保持する場所のようですね。

また上記記事の中ではLibraryフォルダもシンボリックリンクで共有していますが、ParrelSyncでは行われていません。どちらが良いのか断定はできませんが、両方のやり方をしたことがある自分の経験ではLibraryフォルダはリンクしないParrelSyncの方が安定している気がしました。Libraryフォルダをリンクしていたときは時々原因不明のエラー(Libraryをリンクしているのが原因かも不明)がでてクローン側のプロジェクトを立ち上げ直す必要があったためです。


新たにつくられるもの:.clone, .parrelsyncarg, collabignore.txt

次はオリジナルプロジェクトには存在しておらず、クローン側にだけつくられるものたちを見ていきます。対象は、.clone, .parrelsyncarg, collabignore.txtファイルです。

.cloneファイルは、ParrelSyncがプロジェクトがオリジナルなのかクローンなのかを識別するために使われるファイルです。そのため絶対に消してはいけないファイルです。誤って消してしまわないように気をつけましょう。

.parrelsyncargファイルは、クローンプロジェクトに特定の文字列を受け渡すためのファイルです。中身はただのテキストです。Documentでも説明されている通り、ParrelSyncではクローンプロジェクト側に特定の文字列を受け渡しClonesManagerクラス経由でコードからそれを取得できる仕組みが用意されています。自分はまだ使ったことがありませんが、マルチプレイアプリを複数立ち上げる際に、ユーザー名などをここで受け渡すと便利かもしれませんね。

collabignore.txtファイルはUnity Collaborateを使っている場合に、クローンプロジェクトがクラウドにつながってしまうのを防ぐためのファイルです。

docs.unity3d.com


その他勉強になったこと

クローン側ではアセットの変更を不可能にする方法

ParrelSyncではおかしな挙動を防ぐためにクローン側ではAssetの変更が不可能になっています。クローン側でアセットを変更して保存しようとするとこんなダイアログが表示されて変更が保存されません。

f:id:kkkkkkssssss:20211115110919p:plain
クローン側のプロジェクトはAssetの変更を保存できない

これどうやって実現してるのか気になって調べたのですが、UnityEditor.AssetModificationProcessorを使っていました。このクラスを継承していれば、Assetの変更保存時にOnWillSaveAssets(string[] paths)が呼ばれます。ここで引き数のpathsには変更のあったAssetのパス一覧が渡され、それを加工して戻り値として返すことができます。クローンプロジェクトの場合は、↑のダイアログを出しつつ、ここでパス一覧を上書きして空にしているようです。こんなAPIがあるんですね、知らなかった。

public class ParrelSyncAssetModificationProcessor : UnityEditor.AssetModificationProcessor
{
    public static string[] OnWillSaveAssets(string[] paths)
    {
        if (ClonesManager.IsClone() && Preferences.AssetModPref.Value)
        {
            if (paths != null && paths.Length > 0 && !EditorQuit.IsQuiting)
            {
                EditorUtility.DisplayDialog(
                    // 長いのでここでは中身省略させてもらいます。
                );
                foreach (var path in paths)
                {
                    Debug.Log("Attempting to save " + path + " are blocked.");
                }
            }
            return new string[0] { };
        }
        return paths;
    }
}


ファイル操作

ParrelSyncではフォルダをコピーしたりリンクを貼ったりしていますが、OSに依らずそのやり方は次の2通りです。

どちらも自分はあまり使ったことはありませんが、存在をしっておくといつか便利そうだなと思いました。

また一つ気になった点として、クローンプロジェクトの削除の際、MacとLinuxではUnityEditor.FileUtil.DeleteFileOrDirectoryを使っているのに対し、Windowsではコマンドで削除しています。

// クローンプロジェクトの削除処理をプラットフォームによって変化させているところを抜粋。
// 見やすさのためコードコメントはここでは消去した。

switch (Application.platform)
{
    case (RuntimePlatform.WindowsEditor):
        Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");

        identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
        File.Delete(identifierFile);

        // ここがWindowsだけ異なる。FileUtil.DeleteFileOrDirectory()を使わずコマンドで削除している。
        args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath);
        StartHiddenConsoleProcess("cmd.exe", args);

        break;
    case (RuntimePlatform.OSXEditor):
        Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");

        identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
        File.Delete(identifierFile);

        FileUtil.DeleteFileOrDirectory(cloneProjectPath);

        break;
    case (RuntimePlatform.LinuxEditor):
        Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
        identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
        File.Delete(identifierFile);

        FileUtil.DeleteFileOrDirectory(cloneProjectPath);

        break;
    default:
        Debug.LogWarning("Not in a known editor. Where are you!?");
        break;
}

このようにWindowsだけ別の方法を使っている明確な理由は、コミット履歴などをたどってみても不明でした。

ただ試しに自分の手元のWindowsでUnityEditor.FileUtil.DeleteFileOrDirectoryを用いてジャンクションになっているフォルダを削除してみたところ、ジャンクションフォルダが削除されるだけでなく、オリジナルフォルダの中身も消えてしまいました!それが仕様なのかどうかは調べても情報がでてこず、もしかしたらUnityのAPI側のバグかもしれないなーと思いました。ここについては調べだすとParrelSyncというテーマから離れてしまうので一旦深追いはしませんが、いずれ何かわかったらブログに書きたいなと思います。


おわりに

今回は、Unityを使ったマルチプレイゲーム開発で必須のEditor拡張「ParrelSync」について、その中身を詳しく調査しました。

ただ使用するだけなら中身を詳しく知っていなくても特に問題はないのですが、少しでも中身を知っている方が安心して使えますね。また他の方がつくられたコードを読むことで、各種機能の実装方法を調べることでコードの書き方や知らなかったAPIを知れて、自分の勉強にもなりました。


宣伝

Synamon2021年アドベントカレンダーやります!
Unity/C#/XR関連の情報についてたくさん発信するので是非ウォッチしておいてください!

qiita.com

Synamonではエンジニア絶賛採用強化中です!少しでもご興味ある方はカジュアル面談や採用ページを覗いてみてください!

twitter.com

meety.net

herp.careers

テックブログ運営を回すための取り組み 〜黄金の回転編〜

回り始めたテックブログの舞台裏

こんにちは! エンジニアリングマネージャーの佐藤(@unsoluble_sugar)です!
Synamon Advent Calendar 2021、記念すべき1日目の記事を書かせていただきます。

qiita.com

8月から本格的に再起をはじめたテックブログについて、4ヶ月間運営を回して得られたデータを分析してみたので、本記事で公開いたします。

f:id:unsoluble_sugar:20211129172937j:plain

記事後半では、運営を回し続けるための具体的な取り組みについてもご紹介します。テックブログ(技術ブログ)運営で悩まれている方は、前回の記事と合わせてご覧いただければ幸いです。

synamon.hatenablog.com

データから読み解くテックブログ運営

本テックブログにはGoogle Analyticsが導入されていたため、こちらを使ってデータ分析をしてみました。

公開記事数

まずは直近4ヶ月間の公開記事数から見ていきましょう。

2021年8~11月末時点までに公開できた記事は、全21記事です。2021年で見ると、7月以前は5月の1記事公開のみでしたので、めちゃくちゃ更新頻度アップしてます。

f:id:unsoluble_sugar:20211129174400p:plain

エンジニアメンバー10名前後の組織かつ、決して更新が活発ではなかった状況からここまで改善できるとは、正直思っていませんでした。

ちなみに2020年以前の記事数すべてを合算すると15記事です。つまりこれまでの3年分の記事数以上を、この4ヶ月で公開したことになります。

f:id:unsoluble_sugar:20211129174657j:plain

めっちゃすごい。ひとえにエンジニアの皆さんの頑張りのおかげですね。

ページビュー数

ブログ運営で気になるのが、ページビュー数。いわゆるPV数です。

具体的な数値の公表は差し控えますが、2021年1~7月までのPV数と、8~11月までの期間内PV数を比較すると、約1.3倍ものアクセス向上が実績値として出ています。月別のグラフを見ても、なだらかに右肩上がりで底上げされていることがわかるかと思います。

f:id:unsoluble_sugar:20211129203029p:plain

このアクセス数増加に関しては、Synamon公式Twitterアカウントでの告知や、メンバー個人アカウントによる拡散の影響が大きかったです。

「最低でも週に1記事公開する!」というシンプルな目標を立てつつ、コーポレートチームとの連携プレーで着実に見てもらう人を増やしてきた結果が、直近のデータに現れました。

現状SNSでの露出はTwitterがメインですが、はてなブックマークやFacebook等での流入が得られると、また違ったターゲット層の取り込みも期待されるでしょう。

f:id:unsoluble_sugar:20211129220104j:plain

記事公開やSNS投稿の時間を一般的なITエンジニアの業務終了時刻に設定することで、少しでも目に触れる機会を増やすといった工夫もしています。

f:id:unsoluble_sugar:20211129220232j:plain

社内にインターネットマーケティングやソーシャルメディア展開に強いメンバーが居る組織であれば、より多くの打ち手が出せるはずです。そういった意味では、弊社のテックブログはまだまだ試行錯誤を続けている段階です。

約束された勝利の剣がほしいですね。

注目度の高かった記事

今回の集計期間内でアクセス数を多く集めたものとして、以下のような記事が挙げられました。

直近の更新記事にフォーカスすると、Unity最新バージョンのC#9機能紹介、Quest 2関連記事への注目度が非常に高かったですね。

また、xR標準仕様の調査まとめやgilotの記事は、それぞれ2019年・2020年に書かれた記事ですが、今もなお継続して読まれています。過去に書かれた記事でも、良い記事は長く読まれますね。

synamon.hatenablog.com

synamon.hatenablog.com

エンジニア界隈では「はてなブックマーク」のテクノロジーカテゴリで取り上げられると、記事を目にするユーザーも多く、第三者による拡散や高いSEO効果が見込めます。

f:id:unsoluble_sugar:20211129183611p:plain

2018年からのデータを追ってみると、Synamonテックブログも年に1度くらい大きなアクセスの波が来ていました。直近の投稿ではバズった記事こそないものの、それに近しいアクセス数が集められていたようです。

f:id:unsoluble_sugar:20211129181731j:plain

技術ブランディングという意味合いでは、Unity C#やPhoton、Goといった言語・フレームワークの話題以外にも、NrealやQuest 2、Varjo XR-3など注目度の高いデバイスと組み合わせた記事を増やすなど、いま以上にSynamonの技術力をアピールできる機会が創出できると理想的かと感じました。

synamon.hatenablog.com

その他のデータ

参考までに、その他のデータも軽く載せておきます。

  • 新規:84.4% / リピーター:15.6%
  • デスクトップ:67.2% / モバイル:31% / タブレット:1.8%
  • ブラウザ別ではChromeが約70%
  • モバイルアクセスの半数以上はiPhone
  • トップページへのアクセスからの離脱率は66.8%

Google Analyticsでは様々なデータを見ることができますが、すべて見ていくとキリがありません。

f:id:unsoluble_sugar:20211129181136j:plain

通常業務や日々の社内タスクもあるわけで、テックブログ運営に割けるリソースは限られています。まずは大枠のデータ変遷を抑えておけば、何をKPIの指標とするかもそのうち見えてくるのではないでしょうか。

とはいえ、テックブログ運営に関しては「数字を追うことだけが全てではない」点には注意したいところです。

続けるために実践していること

続いて前回の記事では書ききれなかった「継続性のある運営施策」について紹介していきます。

運営定例MTGと記事管理シート

テックブログ運営にあたり、Spreadsheetによる「記事管理シート」を用意しました。

f:id:unsoluble_sugar:20211129173931j:plain

当初の目標であった「週に1記事公開する」ためのスケジュール管理と、記事の投稿ステータス状況把握、記事ネタの確認・調整等がメインとなっています。

f:id:unsoluble_sugar:20211129204906j:plain

シートの入力項目は以下のとおりです。

  • 記事ステータス
  • 担当者
  • タイトル
  • 公開予定日
  • 下書きプレビューURL
  • SNSシェアテキスト
  • 公開日
  • 公開URL
  • 備考欄

簡易なシートではありますが、これひとつあるだけでだいぶ違う気がしています。あとから参加される方も、これまでの記事を俯瞰して見ることが可能なので、どのような記事が書かれていたかの変遷を追うことができます。

f:id:unsoluble_sugar:20211130101117j:plain

週に1度開催している「運営チーム定例MTG」では、記事管理シートを中心に、投稿スケジュールの確認や直近の記事ネタ相談などを進めています。

f:id:unsoluble_sugar:20211001162521p:plain

尻を叩きまくる「テックブログ書けおじさん」の存在

はい、僕です(自己紹介)。

ことある毎に「テックブログ書きませんか?」と、エンジニアメンバーにうるさいくらい声をかけ続けています。 個人目標との絡みや、プロジェクトでの成果、社内共有会や技術調査で得られた知見、日々の雑談シーンなどなど、隙あらば「テックブログ書けおじさん」と化しています。

f:id:unsoluble_sugar:20211129231849j:plain

これは一種のキャラ作りのようなもので、嫌われる勇気を持って結構頑張っています。実際「逐一うるせぇなぁ」と思われている方も居ることでしょう(面と向かって言われたら普通に泣くので心のなかにしまっておいてください)。

なぜわざわざこんなことをしているかと言えば、普段ブログ等で社外にアウトプットする習慣がない方は

  • そもそも記事を書くきっかけがない
  • 何が書けるかわからない
  • 書く時間なんて無ねぇよ

などの考えを抱いていることが多いからです(佐藤調べ)。

f:id:unsoluble_sugar:20211001162454p:plain

f:id:unsoluble_sugar:20211001162509p:plain

f:id:unsoluble_sugar:20210928173212p:plain

会社のテックブログで書く意義やメリットを誰かが伝え続けなければ、行動変容を促すことはできません。また、業務内の時間を使って良いかの境界が曖昧で躊躇していたメンバーも見受けられました。

f:id:unsoluble_sugar:20210928174653p:plain

通常業務以外の社内活動時間で、どの程度のリソースをかけるべきか。全社OKRや個人目標の兼ね合いなどを踏まえ、感覚値の認識合わせをしていくことが大切です。現段階でその役割を担うのが、エンジニアリングマネージャーである自分だろうと決意を抱いた次第です。

代表との1on1やリーダー&マネージャーMTGの場で提案・調整を重ね、現フェーズにおいては「あくまで業務に支障のない範囲で動く」のが妥当と判断しています。

そのための運営チーム

仮に尻を叩く「テックブログ書けおじさん」が居なくなった場合、継続性が損なわれる恐れがあります。だからこそ「運営チーム」という形態をとることにしました。

f:id:unsoluble_sugar:20211130101022j:plain

普段の僕の行動を見ることによって「こんな感じで動けば書いてくれる人が増えるのか」といったノウハウであったり、得られるメリットを伝えていく重要さについて少しでも理解してくださる方が増えれば、継続性もある程度担保されるのではないかと考えています。

このあたりはもう少し言語化し、より多くのメンバーに共感してもらえるような表現手法でテンプレ化できると、第2、第3の「テックブログ書けおじさん」が自然に生まれていくことでしょう(?)

個人の性格や話す相手によってアプローチは変えていく必要はあるので、決してひとつの正解があるわけではありません。純粋なコミュニケーションスキルであったりコーチング的な要素も絡んできますね。

自分もまだまだ未熟なので、運営チームやマネージャー層の皆さんの助けを借りながら精進していきます。

レビュー、社内Slack展開、SNSシェア

記事管理シートや運営MTGの補足資料として、記事投稿フローなども整備しています。

f:id:unsoluble_sugar:20211129214143j:plain

記事を書いたのち社内Slackのテックブログチャンネルに展開、内容をエンジニア間でレビューするという仕組みです。

f:id:unsoluble_sugar:20211130101048j:plain

始めの方こそ自分が積極的にチェックしていたものの、最近はメンバー各自がタスクの合間に見てくれることが増え、良いレビュー体制が整ってきました。

f:id:unsoluble_sugar:20211129214639j:plain

f:id:unsoluble_sugar:20211129214651j:plain

f:id:unsoluble_sugar:20211129214658j:plain

レビュー後は投稿予約を設定して、SNSシェア用のテキストをコーポレートへ共有。

f:id:unsoluble_sugar:20211129215800j:plain

社内全メンバーに周知されるという流れです。

f:id:unsoluble_sugar:20211129220509j:plain

カジュアル面談・面接の場でテックブログに対する言及をしていただいた際は、適宜社内にフィードバックもしています。書いた記事を実際に見てくれている人が居ることを可視化するのも、モチベーション維持に必要な要素ですね。

最も必要なのは「巻き込み力」

上記のような施策を打ち、社内Slackに展開したところで「最近エンジニア勢がテックブログ頑張ってるけど、実際どんな感じなん?」といった疑問を抱かれる場合も多々あるでしょう。

そう、エンジニアメンバー以外への伝え方や巻き込み方も大変重要なのです。

弊社の場合ですと「エンジニア採用に苦戦している」という共通認識が周知されていたため、採用面での有用性や会社としての技術ブランディング確立を柱に据えています。

f:id:unsoluble_sugar:20210928174702p:plain

扱っている技術の紹介や、組織・チームのカルチャーといった内部の取り組みを発信し続けることは、十分に会社の資産になりえます。Twitterのような一過性で流れていくフロー型ではなく、ストック型の情報形態である点がブログの強みです。

本記事でご紹介してきたとおり、長く読まれる良い記事が増えていくとPV数も底上げされ、認知度向上やファンの創出に繋がります。実際にカジュアル面談・面接の場面でも、テックブログの話をしてくれる採用候補者さんが少しずつ増えており、社内にもジワジワとその良さみが浸透している感があります。

ささいな変化を見逃さず、積極的に社内外へアピールしていくことも「テックブログ書けおじさん」の仕事と言えるでしょう。

今回のアドベントカレンダー参戦も、その取り組みの一環です。チームMTGや全社MTGなど公の場での告知・宣言に加え、個別の草の根活動の甲斐もあり、25日分ある枠もすべて埋まりました。デザイナー陣も丸め込み、より多くのメンバーに関わっていただくようお願いしています。

f:id:unsoluble_sugar:20211130124705j:plain

普段ほとんど社外向けに記事を書くことがないメンバーも居るため、実際全部書いてもらえるかはわかりません。「それでもチャレンジしてみよう!」というのが、Synamonが掲げる3つのバリューの体現であると思っています。

「Synamonの○○さん」「あの記事を書いていた○○さん」「あの技術領域に詳しい○○さん」といった個人のブランディングが確立できると、相乗効果でテックブログの存在もより強固なものになっていくことでしょう。

現状のテックブログ運営やアドベントカレンダーの企画も、決して僕ひとりでは進められませんでした。多くのメンバーの協力のもと成り立っています。

f:id:unsoluble_sugar:20211129231656j:plain

ひとりでは成し得ないことも、他者を巻き込んで実現していく。それが組織・チームで動く上で最も必要なスキルなのかもしれませんね。

note.synamon.jp

最後に

本テックブログやnote記事のお知らせは、Synamon公式Twitterで発信しています。アドベントカレンダーへの挑戦や、本記事でご紹介した運営施策を見て、弊社の取り組みに興味を持っていただけましたらぜひフォローお願いします!

twitter.com

カジュアル面談も実施中ですので「詳しく話を聞いてみたい!」という方はチェックいただけると嬉しいです。

▼カジュアル面談はMeetyから   meety.net  

▼エントリーはこちら   herp.careers

Synamonアドベントカレンダー、そしてテックブログの今後にご期待ください!

qiita.com

追記:完走しました!

synamon.hatenablog.com

Qiita Advent Calendar 2021に参加します!

アドベントカレンダー、やっていくぞ…!

こんにちは!
Synamonエンジニアリングマネージャーの佐藤(@unsoluble_sugar)です。

2021年もいよいよ年末に近付いてきましたね…!

今年の8月より再始動をかけたテックブログ運営も、当面の目標として掲げた「週に1記事くらい出せる体制づくり」ができてきた印象です。

synamon.hatenablog.com

記事を書いてくれる人が増えてきたこともあり「せっかくだしアドベントカレンダーやろうぜ!」ってことで、今年はQiita主催のAdvent Calendarへ初参戦することにしました!

f:id:unsoluble_sugar:20211124111159j:plain

Qiita Advent Calendarとは、クリスマスまでの日数をカウントダウンするアドベントカレンダーの慣習にもとづいて毎年12月1日から25日までの期間限定で開催される記事投稿イベントです。

毎年、クリスマスのQiitaを盛り上げる一大イベントとなっております。興味のあるトピックのカレンダーに参加し、1年の締めくくりを最高に盛り上げていきましょう🎉

Qiita Advent Calendar 2021 - Qiita

Synamonで扱っているXR(VR/AR/MR)関連技術を中心に、普段なかなか手が出せなかった領域へのチャレンジもあるかもです。これまでテックブログで記事を書いているエンジニアメンバーに加え、デザイナー陣も巻き込んで、2021年の終わりを盛り上げていければと思います!

f:id:unsoluble_sugar:20211129182910j:plain

自分はノリと勢いで複数枠埋めてますが、実際どこまで書けるかドキドキしてますw
空き枠はそのうち妖精さんが埋めてくれることでしょう…( ˘ω˘ )

というわけで、Synamon Advent Calendar 2021の応援よろしくお願いします!

qiita.com

追記:完走しました!

synamon.hatenablog.com

お知らせ  

株式会社Synamonでは、Unity / C#エンジニア、テックリードを中心に新しい仲間を募集中です。
「XRが当たり前の世界をつくる」というミッションに共感したメンバーが、切磋琢磨しながら日々挑戦しています!
カジュアル面談も実施中なので、ご興味ある方はお気軽にご連絡ください~!

▼カジュアル面談はMeetyから   meety.net  

▼エントリーはこちら   herp.careers  

また、本テックブログやnote記事のお知らせは、Synamon公式Twitterでも発信もしています。
弊社の取り組みに興味を持っていただけたら、ぜひフォローお願いいたします!   twitter.com  

Unity公式のNetcode for GameObjectsのHelloWorldチュートリアルを改造しつつ触ってみた

f:id:fb8r5jymw6fd:20211122060239p:plain

はじめに

エンジニアの松原です。最近の開発でネットワーク回り関連のフレームワークを調査していたところ、ちょうどNetcode for Gameobjects がリリースされました。

 

仕事上でこれまでUNetやMLAPIを使ったことが無かったため、せっかくなのでこの機会に触ってみようと思い、実際にコードを書きつつ試してみました。

今回自分がハマったポイントやコードの書き方の落としどころについて記事にしました。

また、今回コードに起こしたものを以下の個人用のGitHubにリポジトリに置いていますので、参考になれば幸いです。

 

実行環境について

下記のUnityバージョン、依存パッケージを使っています。

Unity Version: 2021.2.3f1

InputSystem: 1.1.1

Multiplayer Tools: 1.0.0-pre.2

Netcode for GameObjects: 1.0.0-pre.3

 

HelloWorldを読みつつ改造

Unityの公式ページにNetcode for GameObjectのチュートリアルであるYour First Networked Game "Hello World"をベースに展開していきたいと思います。

 

チュートリアルではGUI周りの構築をコードベースから行おうとしていたので、そこに依存しないように分離しました。例えば、Adding Scripts to Hello WorldHelloWorldManager のクラスの代わりに、以下のようなクラスを作って、GUIからイベントのシリアライズをしました。ボタンのInspectorのOnClick()にNetworkSelectorのメソッドを呼び出すようにしています。

using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;

public class NetworkSelector : MonoBehaviour
{
[SerializeField]
UnityEvent<string> onStartNetwork;

public void StartNetworkAsHost()
{
NetworkManager.Singleton.StartHost();
onStartNetwork.Invoke("Started as a host");
}

public void StartNetworkAsClient()
{
NetworkManager.Singleton.StartClient();
onStartNetwork.Invoke("Started as a client");
}

public void StartNetworkAsServer()
{
NetworkManager.Singleton.StartServer();
onStartNetwork.Invoke("Started as a server");
}
}

f:id:fb8r5jymw6fd:20211122041251p:plain

f:id:fb8r5jymw6fd:20211122041304p:plain

f:id:fb8r5jymw6fd:20211122041018p:plain

 

他、InputSystemを利用して、ゲームパッドの左アナログスティック入力とキーボード(WASDと矢印キー)の入力を扱うようにしています。

f:id:fb8r5jymw6fd:20211122041546p:plain

このバインドのInputActionAssetをPlayerInputのActionsに設定します。

f:id:fb8r5jymw6fd:20211122041724p:plain

兄弟コンポーネントにPlayerInputがあり、かつMonobehaviour継承のクラスにOn+[Action名]の関数を書いておくと、自動的にこの関数にInputActionAssetのActionに対応するInputActionのイベントがハンドルされ、入力を取得できるようになります。(型も上の画像に合わせて、Vector2として取得できるようにしています)

この性質を利用して、入力した値をプロパティとして参照できるよう、public属性のプロパティに設定して外部のスクリプトから読み込めるようにしています。

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerInputHelper : MonoBehaviour
{
[SerializeField]
private float stickDeadZone = 0.1f;

public Vector2 Move { get; private set; }

public bool HasMoveInput => Move.magnitude > stickDeadZone;

public void OnMove(InputValue value) => Move = value.Get<Vector2>();
}

 

NetworkObject、NetworkBehaviour、RPC、NetworkVariableについて

Building on "Hello World"で一旦はチュートリアルは終わるのですが、これだけだと把握しづらかったので、自分なりにまとめてみました。

 

Playerのオブジェクトはネットワーク接続時、クライアントのオブジェクトと自分のオブジェクトはインスタンス化される

チュートリアルの通り、Netcode for Gameobjectを使ってマルチプレイを行うにはプレイヤーとしてみなすGameObjectにNetworkObjectとNetworkBehaviour継承のクラスの両方をアタッチしPrefab化し、Player PrefabとしてNetworkManagerに登録しておく必要があります。

f:id:fb8r5jymw6fd:20211122105428p:plain

f:id:fb8r5jymw6fd:20211122105535p:plain

以下のリンクにもあるように、Player(便宜上、Player Prefabのことを指します)とみなすGameObjectにNetworkObjectとNetworkBehaviour継承のクラス(上の図の例ではPlayerSyncBehaviourという名前のクラス)両方を追加します。
NetworkObjectはNetworkIdを持っており、個々のクライアントを識別するためのオブジェクトとして動作します。NetworkBehaviourはMonobehaviourの拡張したクラスで、そのNetworkObjectがUnity上でどう振舞うかを定義します。

For an object to be replicated across the network, it needs to have a NetworkObject component. Each object which uses components networking functionality, like NetworkTransform or NetworkBehaviours with NetworkVariables or RPCs, needs a NetworkObject component on the same GameObject or in a parent.

 

ネットワークに接続すると、自分が所有するPlayerオブジェクトのほか、他のクライアントのPlayerオブジェクトもインスタンスとしてHierarchyに表示されます。

f:id:fb8r5jymw6fd:20211122105154p:plain

各クライアントはサーバーまたはホストから受け取った通信内容をもとに、自分のオブジェクトを含め、各クライアントのPlayerオブジェクトの更新処理を行っているようです。

 

RPCは引数付きで送れる、関数名の末尾に(ServerRpc)または(ClientRpc)を追加する必要がある

NetworkBehaviour継承のクラス内で指定の関数に[ServerRpc]や[ClientRpc]アトリビュートを付けることにより、その関数をRPCとして扱えるようになります。チュートリアルには特に言及がありませんでしたが、関数の末尾にそれぞれのRPCに対応するServerRpcまたはClientRpcを追加しないとコンパイルエラーが出るようです。

また、引数にプリミティブまたは構造体を設定することで、値を載せてRPCを送ることができます。

[ServerRpc]
private void SubmitMovingDirectionRequestServerRpc(Vector3 direction, ServerRpcParams rpcParams = default)
{
UpdateVariablesOnServer(direction, Vector3.zero);
}

GameObjectやNetworkObject、NetworkBehaviourはRPCで直接送ることはできませんが、NetworkObjectReferenceやNetworkBehaviourReferenceを引数に指定して渡すことでRPCに参照を渡すことができ、サーバー側で処理ができるようです。当たり判定の処理で他のプレイヤーの参照をRPCに渡すなどの使い方ができそうです。

 

NetworkVariableはサーバー側でのみ書き換え可(※2021/11/22時点)

(今後変更ありそう?)

NetworkVariableはクライアントサーバー間でプレイヤー情報のプロパティとして利用することができ、サーバー側で変更されたプロパティをRPCを介さずにやり取りできる、NetworkBehaviour継承のクラス内で利用できるクラスです。一見するとクライアントサーバー関係なしに扱えるように見えますが、値の変更はサーバー側のみという制限があります。(参考:コンストラクタ呼び出し時、NetworkVariableReadPermissionにOwnerOnlyを指定することで、オーナー以外のクライアントの読み取りを禁止する設定を追加できます)

NetworkVariable のページの説明では、パーミッションの設定を下記のように設定することで書き換えられるように書いていますが、Netcode for GameObject 1.0.0以降のバージョンではサーバーのみでしか書き換えられません。(※2021/11/22時点) 以前のバージョンではサーバー以外でもNetworkVariableの書き換えができていたようです。

f:id:fb8r5jymw6fd:20211122045101p:plain

 

Unityフォーラムに正式な回答がありました。1.0.0ではサーバー側のみ書き換え可とあります。

There are no NetworkVariableSettings anymore. Only the server can write to NetworkVariables in the 1.0.0 version.

 

これは自分が所有しているオブジェクトであっても、同期情報に関するであればすべてサーバー(またはホスト)側でのみでしか変更を受け付けないため、毎回サーバーにRPCで処理を実行し、サーバー側での実行をクライアント側に反映させる実装が必要になります。

f:id:fb8r5jymw6fd:20211122051026p:plain

 

もう一つの問題としては、NetworkVariableはNetworkBehaviour継承クラスのメンバとして扱っていますが、このクラスで実装したコードはサーバーでもクライアントでも同様に動作するため、サーバークライアントの2系統+所有権(Owner)ありなしでのオブジェクトの動作が1つのコードに混在するため、しっかりと考えて設計しないとバグの温床になりそうです。

 

ラグを感じさせないギミックが別途必要

自分に所有権があるオブジェクトであっても、RPC経由でサーバーへ書き換え要求をした後に反映されるため、自分のキャラクターまたはアバターの移動に若干のラグを感じます。

これを解消するため、私のサンプルでは自分のキャラクターを表示用とネットワーク同期用で分けて処理しています。

以下のコードはPlayerSyncBehaviourの OnNetworkSpawn()メソッドの実装です。自分がClientの時かつ、そのオブジェクトのオーナーである時は、このオブジェクトにアタッチされている子GameObject(キャラクターのグラフィックのコンポーネントがあるもの)をダミー表示用のGameObjectを親として移しています。

public override void OnNetworkSpawn()
{
if (IsOwner)
{
playerInputHelper = FindObjectOfType<PlayerInputHelper>();

if (IsServer)
{
onTrackingObjectPresented?.Invoke(gameObject);
}
else
{
playerLocalDummy = new GameObject("PlayerLocalDummy");
for (int i = 0; i < transform.childCount; i++)
{
transform.GetChild(i).SetParent(playerLocalDummy.transform, false);
}
onTrackingObjectPresented?.Invoke(playerLocalDummy!);
}

SetRandomPosition();
}
}

 

また、位置更新処理は以下のように行っています。

private void Update()
{
var delta = Time.deltaTime;

if (IsOwner)
{
UpdatePlayerInputs(delta);
}

var currentPosition = networkCurrentPosition.Value;
var movingDirection = networkMovingDirection.Value;

if (movingDirection.magnitude > 0.001f)
{
var position = transform.position;
var direction = movingDirection.normalized;
transform.LookAt(direction + position, Vector3.up);
transform.position += movingDirection * speed * delta;
}
else
{
transform.position = currentPosition;
}
}
private void UpdatePlayerInputs(float delta)
{
if (playerInputHelper == null) return;

if (playerInputHelper!.HasMoveInput)
{
var move = playerInputHelper!.Move;
var direction = new Vector3(move.x, 0f, move.y);

if (IsServer)
{
UpdateVariablesOnServer(direction, Vector3.zero);
}
else
{
if (playerLocalDummy != null)
{
var position = playerLocalDummy.transform.position;
playerLocalDummy.transform.LookAt(direction + position, Vector3.up);
playerLocalDummy.transform.position += direction * speed * delta;
}
SubmitMovingDirectionRequestServerRpc(direction);
}
}
else
{
...

 

NetworkBehaviourの位置情報はNetworkVariableそのままの値を反映し、playerLocalDummy(Gameobject)の位置情報はコントローラーのインプット値を反映しています。このようにすることで見かけ上でラグが無いように見せることができます。

f:id:fb8r5jymw6fd:20211122053345p:plain

 

実際に動作しているものは以下のgifアニメーションになります。

f:id:fb8r5jymw6fd:20211122065637g:plain

最後に

Unity公式のネットワークシステムは今回初めて触ったのですが、DOTS(またはECS)のようにまだまだ変更がありそうです。使い方の目星はついてきたので、今後は動向を注意深く追っていきたいと思います!

f:id:fb8r5jymw6fd:20211122060057p:plain

 

お知らせ

今年のSynamonは、アドベントカレンダーを実施する予定です!乞うご期待!

qiita.com

 

また、弊社ではUnityエンジニアを募集しています。興味がある方は是非以下のページを覗いてみてください!

twitter.com

meety.net

herp.careers

PMBOK 7th 勉強会 第4回【Section3: プロジェクトマネジメント原則】後半

こんにちは、エンジニアの吉田です。

Synamonでは有志のメンバーが集まり定期的にPMBOK 7thの勉強会を行っています。 今回で勉強会記事も4回目。 前回に引き続き「The Standard for Project Management」の第三章「Project Management Principles」について書いていきます。

ちなみに、今回でやっとPMBOKの前半パートが終了です。張り切って書いていきます!!


前回記事はこちら↓ synamon.hatenablog.com


[PMBOK 7thの章立て]

  • The Standard for Project Management

    1. Introduction

    2. System for Value Delivery

    3. Project Management Principles <-- 今回書くのはこの第三章の後半です!

  • A Guide to the Project Management Body of Knowledge

    1. Project Performance Domains

    2. Tailoring

    3. Models, Methods, and Artifacts


PMBOKとは

PMBOK(Project Management Body of Knowledge)とは、PMI(Project Management Institute)が出版している、プロジェクトマネジメントに関するノウハウや手法を体系化してまとめた本です。

1996年の初版出版以来、4年に1度くらいの頻度で改訂が繰り返されており、2021年8月に第7版(英語)が出版されました。


Section3: プロジェクトマネジメント原則

前回までのおさらい

本題に入る前に前回までの記事の内容を少しおさらいします。

PMBOKは2つの大きくパートで分かれており、前半の「The Standard for Project Management」では後半の「A Guide to The Project Management Body of Knowledge」をきちんと理解するための用語解説やプロジェクトマネジメントに関する各知識を組み立てていくための基本的な考え方が書かれています。

この記事で紹介する「The Standard for Project Management」の第三章「Project Management Principles」では、「プロジェクトマネジメントの原則 」というタイトルで、プロジェクトマネジメントに関する各知識のベースとなる考え方が紹介されています。

原則は12個ありそのうち最初の6つについて前回の記事で紹介しました。今回はその続きとして、残りの6つについて書いていきたいと思います。

7/12 文脈によって調整する

プロジェクトにはそれぞれ個性があるので適切なアプローチを選ぶのが大事であり、そしてアプローチ方法は一回決定して終わりではなく継続的に調整していくべき。(意訳)

この原則では、プロジェクトごとにその特性を捉え、それに合わせたアプローチをとること大切さが書かれていました。その中でも「PjMだけでなく、チームメンバー全員でアプローチ方法を考えることによって、それぞれのコミットメントがあがる」と書かれていた部分が個人的に印象にのこりました。

メンバー全員がアプローチ方法を考えるって短期的に見るとリソースの無駄づかいに思えるかもですが、中長期的に見ると各メンバーがアプローチ方法(戦略)を深く理解していることって色んなところに効いてくるよなと。また、この原則のテーマである"調整"を適切に行うということに対しても、全員がアプローチ方法を理解してれば、プロジェクトの途中でアプローチ方法のずれに気づきやすくなって上手くいくんじゃないかなーと個人的に考えたりしました。

8/12 プロセスと成果物に品質を組み込む

プロジェクトの目的を達成し、関連するステークホルダーが設定したニーズ、用途、受け入れ要件に沿った成果物を作成するために、品質を重視する。(意訳)

この原則では、品質(その意味は色々ありますが詳しくは本を読んでください)を担保するために、どうプロセスを設計していくかということが書かれていました。

個人的に、この原則のポイントは「"成果物"だけじゃなくて"プロセス"の品質についても書かれていること」かなと思います。もちろん成果物の品質が高いことが重要なんですが、プロジェクトとしてはそれだけじゃな駄目で、リソースの消費を適切に抑えられれているかどうかといったプロセスの品質も重要ですよね。

9/12 複雑さをナビゲートする

プロジェクトのライフサイクルを健全に回すための具体的な施策を取るために、プロジェクトの複雑さを継続的に評価、ナビゲートする。(意訳)

この原則では、プロジェクトの不確実な部分をプロジェクト中に定期的に評価して適切に対処していこうよということが書かれていました。

複雑さを生んでる原因についてよくあるパターンとかについて書かれていました。複雑性に対しては明確な対処方が存在しているわけではないので、複雑性による被害を予防するというよりは、複雑性を定期的に評価しておき被害を最小限にするというのが基本的な方針のようですね。

10/12 リスクの反応の最適化

リスクは不確実性から生まれる(意訳)

この原則では、リスクが不確実性から生まれること、そして生まれたリスクにどう対処するかが書かれていました。

印象的だったのは、Negative Risk, Positive Riskという言葉が使われていることで、リスクというと悪いイメージになりがちですが、いい影響のものもあるよというところでした。また、リスクはその対応コストよりも対策コストの方が安く済む場合が多いということも書かれていました。

11/12 適応性と回復力の追求

ほとんどのプロジェクトは課題や障害に遭遇する。そこで適応性と回復力があれば影響に上手く対応し成功につなげることができる。(意訳)

この原則では、プロジェクトにおいて対策をしていても不可避な課題や障害などが起きた場合にどう対処にうまく回復していくかが書かれていました。

「プロジェクトが当初の計画通りに進行することはほとんどない」というのは、自分も今まで参加してきてプロジェクトの経験から納得できます。個人的に印象に残ったは、障害への適応性と回復力をつくりだすための方法が書いてあった部分です。「短いフィードバックループをつくること」「多様なメンバーでチームを構成すること」といったようなアジャイルやスクラムにある考え方との共通点が多く見られました。

12/12 構想している将来の状態へ到達するための変化を可能にする

変化へのアプローチを構造化して用意しておくべき。(意訳)

この原則では、プロジェクトで想定される変化について、その準備をきちんとしておくべきということが書かれていました。

この節で記憶に残ってるのは、変化に対応できるようなチームの作り方について触れらていた箇所です。「変化できるチームをつくるには、説得より動機付けがおすすめだ」と書かれていました。リーダーの命令で動いているチームよりも、各メンバーが自分の意志で何故変化するのか理解できているチームが強いというのは納得ですね。


まとめ・感想

今回は三章後半として、プロジェクトマネジメントの大事な12個の原則のうち後半6個について書きました。

どの原則も大切なことが書かれているなーと思いつつも、抽象的な話が多くまだまだ内容を掴みづらいなというのが正直な印象でした。ただ、自分で理解してあまりよくわからなかった箇所も勉強会でみんなでディスカッションして納得できる部分もたくさんありました。

また、同僚と内容についてディスカッションしていくと、本の理解が深まるだけでなく、同僚がどういう考えや価値観を持っているのかもわかってくるのが楽しいなと思いました。

次回以降の記事ではいよいよPMBOK後半の「A Guide to The Project Management Body of Knowledge」に突入していきます。この記事を書いている時点でも、後半の勉強会が始まっているのですが、ここまでの前半よりも内容が具体的になってきていて個人的には読みやすいなと感じます。今からPMBOK読まれる方は前半パートはさらっと流し読みだけして、後半から入るのがおすすめかもしれません笑。