【Unity】UNET(PC-Android) をしてみたお話
はじめに
UNETの基本的な使い方は我らが凹みTipsにまとまっています。
凹みTips 味わい深い(調べてるうちに何度もたどり着いて、そのたびに読み返している
— てんちょー (@shop_0761) 2017年4月13日
UNETのよくわからなさ加減はizmさんの記事の太字だけでもさらっと目を通すと、つらさがわかると思います…
https://t.co/ZDj1sduP9T
— 絵麻さんを養って幸せな家庭を築く (@izm) 2017年4月10日
とりあえずゲラゲラ笑いながら一通り訳しましたんで…みなさまの…UNETハマりが…減ることを…祈っております…
いくつか抜粋します
対応策:スクリプトの名前を動くようになるまで適当に変えます。(つらい)
とか
NetworkSimulationは期待した通りに動きません
かなしみ
今回は個人的にハマったりしたポイントや理解しにくいとこをまとめてみます。 Android6.0.1(Galaxy S6 Edge)-PC(Windows 7)間の通信がメインです。
PCをHost、AndroidをClientとして考えます。
Unityは5.6.0f3です。
ちなみにUnityをまともに初めて2-3ヶ月くらいなので、間違いとかは多めに見て欲しいです…(間違いがあれば教えてください)
ハマリポイントたち
Androidでの実機テスト
いちいち実機に移してテストするのは面倒だったのでPCでテストして、区切りのいいとこで実機テストしてました。
怠惰ゆえにUnity-Android間でテストしたくなりますが、出来ないっぽい…?
一応NetworkManagerのnetworkAddressにPC側のIPAdressを設定していても駄目でした。 どうやら、Client側がHost側を見つけられない → TimeOut のようなので 同一LAN内からUnityEditorが見えてないのかなと思ったり。
そのため、毎回PC版ビルドとAndroidに転送しなければテストできない… (方法があれば教えてください…)
ちらっとようてんさんに聞いたところ、Android側が悪さをしている可能性があるらしく、カーネルをゴニョゴニョするといけるかもとのこと。(やりたくない
悩める民たち(解決してなさそう)
https://forum.unity3d.com/threads/wifi-issues-on-lan-or-android-client-windows-server.336183/#n10
この辺ググっても全然でなくてみんなPhoton使ってるのかな…
この記述をみてもしや…と思って出来たくらい情報がない…
ネットワーク上の別PCと通信するには、ビルドしたアプリを配布して行う。LAN Client にホスト側のIPを設定するのを忘れずに。
---------------- 2018/02/16 追記 ---------------------
そういえば @shop_0761 さんが嘆いてたUNETのAndroidとUnityエディタの通信は、ウィンドウズのファイアウォール切って試すと通信成功したので、横着するならこれで良い気はしました。
— 絵麻さんを養って幸せな家庭を築く (@izm) 2018年2月13日
Firewall 切ると行けるらしいので イメージ図を描きました pic.twitter.com/0pCrlUQe7U
— てんちょー (@shop_0761) 2018年2月14日
おまけ
実行時に勝手にServer、Clientを判断してStartするのはこんな感じで書きました。
こいつはNetworkConnector.csとして作成して、NetworkMangerがAttachされてるGameObjectに 一緒にAttachしてます。
using UnityEngine; using UnityEngine.Networking; public class NetworkConnector : NetworkBehaviour { NetworkManager manager; //こいつを用意しておくとInspectorから変えれてPC上でのデバッグに割りと便利 お好みで public bool isStartAsHost = true; public string serverIPAdress = "(対象のPCのIPAdressをいれる 指定しないとlocalhostっぽい)"; // Use this for initialization void Start() { manager = GetComponent<NetworkManager>(); if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer) { if (isStartAsHost) { manager.StartHost(); Debug.Log("Start as Host"); } else { manager.networkAddress = serverIPAdress; manager.StartClient(); Debug.Log("Start as Client"); } } else if (Application.platform == RuntimePlatform.Android) { manager.networkAddress = serverIPAdress; manager.StartClient(); Debug.Log("Start as Client"); } } } }
参考
simplestar-tech.hatenablog.com
Attribute
UNETはAttributeをつけることで、いろいろ挙動を制御できます。 Serverだけで実行したい関数、ClientからServerにデータを送りたいなどなど。
あちこちみても、結局この図に集約されている気がします。
まず、冒頭の凹みTipsの記事より
まず大前提として、UNET では同じゲームのコードでクライアント・サーバ共に動かしています。サーバ専用の言語を覚えたりする必要はありません。
これを把握していないと混乱して死にます。(死んだ)
ちょっと言い換えると、"一つのソースでServer、Clientが共存している" といったイメージです。
なので、同じソース内でServerとClientのやり取りをすることになります。 (ココらへんは混乱ポイントな気がする)
ので、"この変数は今Clientで更新したんだな"とかを把握しておかなきゃいけないわけで…
イメージ図を書いてみました。こんな感じで共存しています。緑のやつが実際に呼べる関数です。
Attributeの中で一番ハマったのはCommand / ClientRpc Attributeです。
またまた凹みTipsより
クライアントからサーバへのコマンドの送信 Cmd から始まる関数に Command アトリビュートをつけるとサーバで実行されるクライアントから呼び出せる関数になる
と
サーバからクライアントの RPC(リモートプロシージャコール) Rpc から始まる関数に ClientRpc アトリビュートをつけるとクライアントで実行されるサーバから呼び出せる関数になる
ポイントは "○○で実行される△△から呼び出せる関数" の部分です。
ちょっと自己流に言い換えるなら "実行するのは○○にある関数だけど、呼べるのは△△からだけだよ" って感じだと思ってます。
この"△△からだけ"ってのが重要で、
凹みTipsにもある
ただし、サーバかクライアントかといったことや、参照しているオブジェクトが各クライアントから見てローカルなのかリモートなのかは強く意識する必要があります。
ここに繋がるのではないかと思ってます。
もちろん呼び出すところを間違えるとエラーが出ます。
ただし、izmさんの記事より
[ServerCallback] クライアントから呼ばない時に付けますがエラーや警告が出ません。
同様に
[ClientCallback] サーバーから呼ばない時に付けますがエラーや警告が出ません。
ので注意しましょう。
--- 2017/04/24追記
また、NetworkManagerからPlayerとしてSpawnしたPrefabには権限(LocalPlayerAuthority)が付与されます。
権限がない他のPrefabからCommand Attribute のついた関数を呼び出そうとするとWarningが出ます。
Trying to send command for object without authority.
ので、基本的にはCommandはPlayerで使うものになりますが、Unity5.2から他のオブジェクトにも 権限を持たせられるようになったそうです。
詳しくはこちら
NetworkBehaviour を継承しておいて、 isServer、isClient、isLocalPlayerなどを使って分岐しましょう。
ただこれらも注意しないと同時にフラグが立ったりする可能性があるので、一応InspectorをDebug表示にするなどしてEditor上で確認するのがよいかと。
例) Hostとして起動した時のPlayer
参考
SyncVar
変数をServerとClientで同期できます。 が、
izmさんの記事より
変数をサーバーからすべてのクライアントに自動的に同期するために使用されます。クライアントからそれらに割り当てないでください。クライアント側での割り当ては無意味です
はい。 Clientで更新した変数はSyncVar Attributeで同期できません。
ので、こんな感じで書きました。
[SyncVar (hook = "OnUpdatePlayerID")] int playerID = 0; [Client] void setPlayerID(int id){ playerID = id; CmdUpdatePlayerID(id); } [Command] void CmdUpdatePlayerID(int id) { playerID = id; } [Client] void OnUpdatePlayerID(int id) { //処理 }
流れ
- ClientのどこかでsetPlayerID() -> ClientのplayerIDを更新 Syncされない
- CmdUpdatePlayerID() -> ServerのCmdUpdatePlayerID()を実行
- Server側のplayerIDが更新されるのでここでSyncする
- ついでにhookしておいたOnUpdatePlayerID()が呼べる(ただし、SyncしたのはClient側なのでClientで呼ばれる)
と、これを同一コード内でやるので、ごっちゃごちゃする可能性が…
hookのところはあってもなくてもおっけーです。(hook あまり自信ない…
hookに関してはこちらを
Network Message
Attributeよりわかりやすい気がします。(ただ、こいつも同じコード内でやり取りするとこを書くけど)
参考
適当に書いてみたやつを貼ります。Network Messageを使いたいコード内に書いちゃえば使えます。(やっつけ) まあFindしてるのは重たいのでホントはやらないほうがいいですが。
(手直ししてる部分があるので、あやしいとこがあるかも)
public NetworkClient client; int playerID; //MessageTypeを判別するのに使う public class MyMsgTypes { public static short MY_MES = 1001; }; //送りたいMessageのクラス MessageBaseを継承しておくこと public class MyMesssge : MessageBase { public byte[] bytes; public Vector3 pos; public bool isHit; public int playerID; } public void SetupClient() { NetworkManager manager = GameObject.Find("NetworkManager").GetComponent<NetworkManager>(); Debug.Log("Find NetworkManager: " + manager); client = manager.client; client.RegisterHandler(MsgType.Connect, OnConnected); Debug.Log("Setup Client"); } //OnHogehogeを登録しておく public void SetupServer() { NetworkServer.RegisterHandler(MyMsgTypes.MY_MES, OnHogehoge); Debug.Log("Setup Server"); } //繋がったときに呼ばれる public void OnConnected(NetworkMessage netMsg) { Debug.Log("Connected to server"); } //送る時用 適当に作った //MyMessageをnewして、値をセットして、client.Send()すればなんでもいいはず //Client Attributeをつけるといいかも public void SendMes(){ MyMesssge msg = new MyMesssge(); msg.bytes = ... msg.pos = ... msg.isHit = ... msg.playerID = ... client.Send(MyMsgTypes.MY_MES, msg); Debug.Log("Send Msg"); } //受け取ったときに呼ばれる 今回はServer側に登録してあるので、Serverで呼ばれる public void OnHogehoge(NetworkMessage netMsg){ MyMesssge msg = netMsg.ReadMessage<MyMesssge>(); byte[] bytes = msg.bytes; Vector3 pos = msg.pos; bool isHit = msg.isHit; int playerID = msg.playerID; Debug.Log("recieved Msg"); }
RegisterHandlerでデリゲートを追加する感じで使えると思います。
SetupClientはあってもなくてもいいですが、client.Send()は使えるようにどこかでclientを格納しておく必要があります。
これはClient→Serverですが、RegisterHandlerを同じように使えば逆もできるはず…(やってないけど)
サンプルがpublicになってたので、なんとなくpublicにしてあります(変な地雷を踏みたくないので)
-- 2017/04/25追記
playerにIDを割り振りたい
参考
だいたいどこを見ても、
playerNetID = GetComponent
().netId;
で持ってきています。確かにユニークです。
ただ、playerに0始まりでIDを振りたいとなると話は別です。
ここから想像のお話なので、あってるかはわかりません。詳しくはソースを読んで下さい。
この netId 恐らくScene上のNetworkIdentity Component の数に依存している気がします。
というのも、netIdで取ってきた数値がなぜか 4 始まりだったので、
playerNetID = GetComponent<NetworkIdentity>().netId - 4;
とかやってました。
ですが、他のprefabでNetworkIdentityを使うことになったので、追加するとErrorが吐かれてしまいました。
つまり、今後SceneにUNET周りを使いたいPrefabなりなんなりが増えるたびにIDがズレます。
しかも恐らくplayerはSceneの最後でSpawnされるのかどんどん後ろにずれていく気がします。
public override void OnStartLocalPlayer() { if (isClient) { playerID = GameObject.FindGameObjectsWithTag("MainCamera").Length - 1; } }
とかで取ってくるほうがよい気がしました…
想像おわり。
--
おまけ -AndroidでScreenShot-
普通はApplication.CaptureScreenshotを使うらしいのですが、 これは真っ黒になるので使えませんでした。
こちらを参考にRenderTexture経由のほうがよいです。
ついでに、保存先はAndroidの場合 Application.dataPath ではなく、 Application.persistentDataPathのほうがいいそうです。(というかこっちでないと保存できない)
Application.dataPathはapk内の領域になるので、ビルド時はAndroidではファイル生成とかできないですよ。Application.persistentDataPathに生成しましょー
— 城黒白 (@taku_nishimu) 2017年4月14日
PCに送りたいときはできたbyte配列をNetworkMessageで送ってやればよいです。 ただし、サイズが大きいと怒られるので分割して送って受信側でくっつけてやればよいです。 (正しく送れるとは言っていない)
おまけ2 同一LAN内のはずなのに繋がらない
Network初心者あるある(だと勝手に思ってる)
テスト環境で複数Wifiが飛んでいて、ついつい全て端末に登録しておくと 勝手にWifiを繋ぎ変えられてデバッグで死ぬ可能性があります。
そのため、1つしか登録しないとか、繋がらないときは端末のIPAdressに pingを飛ばして確認したほうがよいです。(1時間くらい悩んだ)
あとはTimeOutの時間を伸ばしておくと良いかもです。 NetworkManager → timeouts → Connect Timeout で設定できます。
まとめ
さいごに
デバッグがつらい!! 毎回ビルドしなきゃいけない!!
聞いた話によるとPhotonのほうが楽らしいです。 そっちは見てないのでわかりません。
以上ここ1週間くらい格闘している(進行形)お話でした。 (つづくかも
なにかあれば@shop_0761まで
あなたにUNETのご加護があらんことを