UNET NetworkAnimatorでアニメーションを同期させる
3Dの人型キャラクターが登場するネットワークゲームでは、キャラクターの位置が同期するのは当然として、アニメーションも同期している必要があります。
アニメーションの同期方法は色々と考えられますが、この記事では、UNETが標準で用意しているNetworkAnimatorという機能を使ってアニメーションを同期させる方法を解説します。
NetworkAnimator概要
NetworkAnimatorは、Unity標準のアニメーション機能(Mecanim)によるアニメーションを同期させる機能です。
「自分のスクリプトからAnimatorにパラメータを渡してアニメーションを行わせる」というのが、Unityの標準的なアニメーションの流れですが、NetworkAnimatorは、そのパラメーターの同期を自動で行なってくれます。
使い方 概要
- まずは普通にAnimatorを使って、普通にローカルでキャラクターがアニメーションするように作る
- 同オブジェクトにNetworkIdentityをアタッチする
- 同オブジェクトにNetworkAnimatorコンポーネントをアタッチする
- 同期対象のパラメーターを指定する
以上です。とても簡単です。ほぼ「NetworkAnimatorをアタッチするだけ」と言っても良いくらいです。
使ってみる
実際にプロジェクトを作成して試してみましょう。
準備
新規の3Dのプロジェクトを作成します。
キャラクターの3Dモデルをダウンロードし、解凍して、プロジェクトにインポートしてください。
今回、アニメーションデータは、StandardAssetsの中に入ってるものを使いたいと思います。
Unityのメニュー>Assets>Inport Package>Charactersをインポートしておいてください。
地面の作成
Planeを作成し、場所を(0, 0, 0)とします。
カメラは適当に見やすい位置・角度に調整してください。
プレイヤーオブジェクトの作成
インポートしたPacchi.fbxをシーンに配置します。
オブジェクト名を「Player」にします。
場所は(0, 0, 0)にしておいてください。
移動処理の作成
NetworkAnimatorを使うだけであれば、移動は本当は関係無いのですが、ネットワークゲームでアニメーションの同期を行いたいときは、大抵、移動処理もセットだと思うので、移動処理を作ります。
PlayerオブジェクトにCharacterControllerをアタッチします。
設定はこんな感じにするとちょうど良いと思います。
新規スクリプト「Player」を作成し、Playerオブジェクトにアタッチします。
まずは、ネットワーク関係無しに、普通にアニメーションするように作ります。
using UnityEngine; public class Player : MonoBehaviour { // 歩行速度(メートル/秒) [SerializeField] float m_WalkSpeed = 1.5f; // 各種コンポーネントの参照 CharacterController m_CharacterController; void Start() { // 各種コンポーネントの参照を取得する m_CharacterController = GetComponent<CharacterController>(); } void Update() { // 移動量(メートル/秒) Vector3 motion = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * m_WalkSpeed; // 左シフトキーでダッシュ(2倍速) if (Input.GetKey(KeyCode.LeftShift)) { motion *= 2f; } // CharacterControllerで移動する m_CharacterController.Move(motion * Time.deltaTime); // 移動先を向く transform.LookAt(transform.position + motion); } }
ただ歩くだけでは、アニメーションさせた際に面白くないので、シフトキーでダッシュするようにしてみました。
アニメーションさせる
次は、ネットワーク関係無しに、普通にアニメーションさせます。
Playerオブジェクトには、たぶん最初からAnimatorコンポーネントが付いていると思いますが、確認してください(付いてなければ追加してください)。
Projectビューから、AnimatorControllerを新規作成します。名前は「Player」とでもしましょう。
PlayerオブジェクトのAnimatorのController欄に、今作ったPlayer AnimatorControllerを設定します。
あと、移動量はスクリプトで制御するため、Apply Root Motionは外します。
Player AnimatorControllerを開きます。
スクリプトから移動速度を受け取るため、float型のパラメーター「Speed」を追加します。
空いてる部分を右クリックし、新規Blend Treeを追加します。
作成したBlend Treeをダブルクリックで開きます。
次のように設定します。
これで、移動速度が0のときはIdleアニメーション、移動速度が1.5のときはWalkアニメーション、移動速度が3のときはRunアニメーションとなります(移動速度が中途半端な値のときは、良い感じにブレンドされて再生されます)。
次に、Playerスクリプトを改修します。
using UnityEngine; public class Player : MonoBehaviour { // 歩行速度(メートル/秒) [SerializeField] float m_WalkSpeed = 1.5f; // 各種コンポーネントの参照 CharacterController m_CharacterController; Animator m_Animator; void Start() { // 各種コンポーネントの参照を取得する m_CharacterController = GetComponent<CharacterController>(); m_Animator = GetComponent<Animator>(); } void Update() { // 移動量(メートル/秒) Vector3 motion = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * m_WalkSpeed; // 左シフトキーでダッシュ(2倍速) if (Input.GetKey(KeyCode.LeftShift)) { motion *= 2f; } // CharacterControllerで移動する m_CharacterController.Move(motion * Time.deltaTime); // 移動先を向く transform.LookAt(transform.position + motion); // Animatorに「Speed」というパラメーター名で移動速度を渡す m_Animator.SetFloat("Speed", motion.magnitude); } }
良い感じにアニメーションするようになりました。
ネットワーク対応させる
いよいよNetworkAnimatorなどを使って、ネットワーク対応させていきたいと思います。
プレイヤーオブジェクトにNetworkIdentityをアタッチします。
それぞれのローカルでプレイヤーオブジェクトを制御するために、LocalPlayerAuthorityはオンにしましょう。
続いて、NetworkTransformをアタッチします。
設定はとりあえずそのままで良いです(TransformSyncModeがSyncCharacterControllerになっていることを確認してください)
Playerスクリプトを少し改修します。
using UnityEngine; using UnityEngine.Networking; public class Player : NetworkBehaviour { // 歩行速度(メートル/秒) [SerializeField] float m_WalkSpeed = 1.5f; // 各種コンポーネントの参照 CharacterController m_CharacterController; Animator m_Animator; void Start() { // 各種コンポーネントの参照を取得する m_CharacterController = GetComponent<CharacterController>(); m_Animator = GetComponent<Animator>(); } void Update() { // ローカルプレイヤー以外では何もしない if (!isLocalPlayer) return; // 移動量(メートル/秒) Vector3 motion = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * m_WalkSpeed; // 左シフトキーでダッシュ(2倍速) if (Input.GetKey(KeyCode.LeftShift)) { motion *= 2f; } // CharacterControllerで移動する m_CharacterController.Move(motion * Time.deltaTime); // 移動先を向く transform.LookAt(transform.position + motion); // Animatorに「Speed」というパラメーター名で移動速度を渡す m_Animator.SetFloat("Speed", motion.magnitude); } }
そしていよいよ、NetworkAnimatorをアタッチします。
Animator欄には、自分(Playerオブジェクト)を登録してください。
すると、AnimatorControllerが持つパラメーターが一覧表示されます(今回はSpeedというパラメーターひとつだけ)。Speedというパラメーターを同期したいので、チェックをオンにします。
以上でPlayerオブジェクトは完成です。
プレハブ化し、シーンからPlayerオブジェクトは削除してください。
空のオブジェクトを新規作成し、「NetworkManager」という名前にします。
NetworkManagerコンポーネントとNetworkManagerHUDコンポーネントをアタッチしてください。
NetworkManagerのPlayer Prefab欄にPlayerプレハブを登録します。
以上で完成です。
実行ファイルを作成してゲームを2つ以上起動し、動作確認してみてください。
NetworkAnimatorの注意点
以上のように、NetworkAnimatorはとても簡単に使うことができますが、いくつか注意点があります。
NetworkAnimatorはAnimatorのパラメーターを同期する機能なので、パラメーターを使わずにアニメーションさせるような場合は使えません(例:Animator.Play()やAnimator.CrossFade()で直接アニメーションさせる場合)。
あと、Triggerを使ったアニメーションも注意が必要です。
パンチ、キック、銃撃や被ダメージなどの瞬発的なアニメーションは数値型のパラメーターではなく、トリガーで行う場合が多いと思いますが、NetworkAnimatorはバグっていてトリガーによるアニメーションが正しく動きません(※少なくともUnity2017.3ではまだ直ってない)。
まず、トリガーの使い方ですが、トリガー以外のパラメーター(float, int, bool)と違い、自動的な同期の対象外です。例えば、「Punch」という名前のTrigger型のパラメーターがあったとして、下図の箇所をオンにしても、期待する動作はしません。
トリガーを使いたい場合、NetworkAnimatorにSetTriggerというメソッドが用意されているので、それを使います。次のような感じです。
// スペースキーでトリガーによるアクションを行う if (Input.GetKeyDown(KeyCode.Space)) { m_NetworkAnimator.SetTrigger("Punch"); }
スペースキーでPunchという名のトリガーを発火し、アニメーションをさせる例です。
ただし、これだけだと、前述の通り、バグのせいで、こんなことになってしまいます。
ホスト側(左画面)で、アニメーションが二重に再生されてしまいます。トリガーが二回発火されてしまうのです。
NetworkAnimatorのソースを読んでみましたが、まあバグだろうなあ、という感じでした。残念ながらこのバグはずーっと放置されています。
このバグ、回避できないこともないのですが、結構めんどくさいし、しかもこの問題を回避したとしても、NetworkAnimatorのSetTriggerって、リモートクライアントにとって、すごくラグいので、もう使わないほうが良いかなって思います。
なぜ、リモートクライアントにとってラグいのかというと、その実装方法のためです。NetworkAnimatorのSetTriggerは、呼ばれると、その情報をサーバーに送信し、それを受信したサーバーは、それを全クライアントに配信します。クライアントがサーバーから情報を受信して初めてアニメーションが実施されます。自分のローカルプレイヤーオブジェクトでさえもです。なので、ボタンを押した瞬間にアニメーションが再生されることはなく、一度ネットワーク経由でサーバーに行って、返ってきて、初めてアニメーションされるのです。だからとてもラグいです。
どうしてこんな実装方法になっているのか、理解できない・・・
とにかく、NetworkAnimatorでトリガー使い物にならないです。
NetworkAnimatorに頼らず、CommandとClientRPCなどを使って自前実装したほうが良いでしょう。どうせ、パンチとかキックとか実装するときは、CommandとClientRPC使うでしょうし、そのついでにアニメーションさせてしまえば良いでしょう。以下、サンプルコードです。
using UnityEngine; using UnityEngine.Networking; public class Player : NetworkBehaviour { Animator m_Animator; void Start() { m_Animator = GetComponent<Animator>(); } void Update() { // ローカルプレイヤー以外では何もしない if (!isLocalPlayer) return; // スペースキーでパンチ if (Input.GetKeyDown(KeyCode.Space)) { m_Animator.SetTrigger("Punch"); CmdPunch(); } } [Command] void CmdPunch() { RpcPunch(); } [ClientRpc] void RpcPunch() { // 他人のプレイヤーの場合のみパンチを再生する。 // 自分のプレイヤーは既にしているはずだからしない。 if (!isLocalPlayer) { m_Animator.SetTrigger("Punch"); } } }
これで、ラグも最小限となり、ホストで二重にトリガーが発火されることもなくなります。