前回までで、キャラクターを表示させるところまではできました。細かいところを調整して仕上げをしていきます。
チュートリアルはこちらのページを元に進めています。
目次
キャラクターコントロールの調整
今の状態では、キーボードで操作をすると画面に映っているキャラクター全部が反応してしまいます。
これを修正する方法として、チュートリアルではprefabのCharacterControlとCharacterCameraコンポーネントを無効化して、自キャラクターをInstance化したときに自キャラクターのみ有効化するという方法を取ります。
まずmonsterprefabのInspectorから、CharacterControlとCharacterCameraのコンポーネントをチェックを外して無効化しておきます。(Character Controllerとは別なので注意してください)
次に、これをインスタンス化するときに有効にすればいいのですが…このコンポーネントはUnityScriptで書かれているのでC#のほうで操作できません。なので、操作できるようにスクリプトをPluginsの下に移動します。
チュートリアルでは「ScriptsをPluginsにリネームしてルートディレクトリの下に移動」とありますが、そこにはすでにPhotonのPluginsがあるので、別のディレクトリ名にします。
Monstergame/Resources/Scriptsのディレクトリ名をMonstergameに変更して、Plugins/Monstergameに移動します。
これでC#のほうから操作できるようになったので、OnJoinedRoomでインスタンスを作成しているところで、自キャラクターのみ有効化します。
/// <summary> /// ルームへ入室した /// </summary> public override void OnJoinedRoom() { GameObject monster = PhotonNetwork.Instantiate("monsterprefab", Vector3.zero, Quaternion.identity, 0); CharacterControl controller = monster.GetComponent<CharacterControl>(); controller.enabled = true; CharacterCamera camera = monster.GetComponent<CharacterCamera>(); camera.enabled = true; }
これで、自分のキャラクターのみがコントロールできるようになりました。
スムーズに移動する
クライアントを二つ立ち上げて見比べてみていると、リモートで操作されているほうの動きがぎこちないというか瞬間移動しているように見えます。これは、位置の同期がポイントごとに行われているためで、つまり実際に瞬間移動している。
こちらを改善していくにはまず、別のスクリプトを作ります。
Marco Poloのディレクトリに「NetworkCharacter」スクリプトを作り、monsterprefabに追加します。
NetworkCharacterクラスはひとまず以下のコードです。
public class NetworkCharacter : MonoBehaviour { public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.isWriting) { // We own this player: send the others our data stream.SendNext(transform.position); stream.SendNext(transform.rotation); } else { // Network player, receive data this.transform.position = (Vector3)stream.ReceiveNext(); this.transform.rotation = (Quaternion)stream.ReceiveNext(); } } }
OnPhotonSerializeViewが一定間隔で呼び出され…るのはいいのですが、if文で分岐しているのがちょっと直感的にわかりづらいですね…要は、同期しているインスタンスごと、それぞれの端末でこのOnPhotonSerializeViewは呼び出されてるわけですが、isWritingがtrueの場合は自キャラ、falseの場合はリモートのキャラとして認識して、自キャラの場合は情報を書き込み、リモートの場合は情報を読み出して反映する、というふうにしているよう。
上のコードではまず、位置の同期だけを行うようにしています。
そして、PhotonViewのObserved ComponentsにNetwork Characterを登録します。
ここに登録することでOnPhotonSerializeViewが一定間隔で呼び出されることになります。
このときに元々登録してあったtransformを上書きしてしまい、こちらで今後は移動を制御することにします。
実行してみると、動きは変わらないのですがちゃんと同期は取れていることがわかります。
で、このNetworkCharacterを改造します。
public class NetworkCharacter : Photon.MonoBehaviour { private Vector3 correctPlayerPos; private Quaternion correctPlayerRot; // Update is called once per frame void Update() { if (!photonView.isMine) { transform.position = Vector3.Lerp(transform.position, this.correctPlayerPos, Time.deltaTime * 5); transform.rotation = Quaternion.Lerp(transform.rotation, this.correctPlayerRot, Time.deltaTime * 5); } } void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.isWriting) { // We own this player: send the others our data stream.SendNext(transform.position); stream.SendNext(transform.rotation); } else { // Network player, receive data this.correctPlayerPos = (Vector3)stream.ReceiveNext(); this.correctPlayerRot = (Quaternion)stream.ReceiveNext(); } } }
OnPhotonSerializeViewで受け取った値をすぐに適用せずに保存し、Updateで少しづつ近づけるようにしてあります。
photonViewにアクセスするために、Photon.MonoBehaviourを継承するようにしているところも注意が必要です。これによって、自キャラの場合のみUpdateで更新するようにしています。
これを実行すると、だいぶスムーズに移動するようになりました。
アニメーションを加える
このモンスター、止まってるときもずっと歩いているアニメーションをしていたりとなんかおかしいところがあります。そのようなアニメーション状態を直して、同期していきます。
myThirdPersonControllerの追加
使用するのはmyThirdPersonControllerコンポーネントになります。これをmonsterprefabに追加して、以降のアニメーションはこちらで行います。
CharacterControlとCharacterCameraはもう使わないのでRemoveしておきます。
myThirdPersonControllerを追加したら、アニメーションをそれぞれ設定し、Buikにmonsterprefabを設定します。
あと、monsterprefab内のAnimationで、最後のidleが「None」になってしまっていたのでそれも登録しておきます。
isControllableを操作できるようにする
で、myThirdPersonControllerの修正に入っていきます。UnityScript(JavaScript)ですけどC#と似たようなもので。修正点は以下の部分です。
isControllableをpublicに、falseにしておきます。
public var isControllable = false;
で、Inspectorのほうを見ると「Is Controllable」がチェックされてしまっているので、こちらもチェックを外します。そうしないとこの値のほうが優先されてしまい、trueが初期値となってしまいます。
で、RandonMatchMaker#OnJoinedRoomでは、CharacterControlを有効にしていた場所で、このisControllableを有効にします。
/// <summary> /// ルームへ入室した /// </summary> public override void OnJoinedRoom() { GameObject monster = PhotonNetwork.Instantiate("monsterprefab", Vector3.zero, Quaternion.identity, 0); monster.GetComponent<myThirdPersonController>().isControllable = true; }
あとはこのisControllableに沿って処理を分けていきます。
入力の制御
145行付近、
var v = Input.GetAxisRaw("Vertical"); var h = Input.GetAxisRaw("Horizontal");
を
var v = 0; var h = 0; if (isControllable) { v = Input.GetAxisRaw("Vertical"); h = Input.GetAxisRaw("Horizontal"); }
に変更。自キャラの場合のみ、移動を受け付けるようにします。
365行の
if (Input.GetButton("Fire1")) { GetComponent.<Animation>()[attackPoseAnimation.name].AddMixingTransform(buik); GetComponent.<Animation>().CrossFade(attackPoseAnimation.name, 0.2); GetComponent.<Animation>().CrossFadeQueued(idleAnimation.name, 1.0); }
も、
if (isControllable && Input.GetButton("Fire1")) { _animation[attackPoseAnimation.name].AddMixingTransform(buik); _animation.CrossFade(attackPoseAnimation.name, 0.2); _animation.CrossFadeQueued(idleAnimation.name, 1.0); }
にします。チュートリアルのコードではエラーになるので、Input.GetbuttonはGetButtonに、animationは_animationにしてください。
306行
if (Input.GetButtonDown ("Jump")) { lastJumpButtonTime = Time.time; }
も
if (isControllable && Input.GetButtonDown("Jump")) { lastJumpButtonTime = Time.time; }
に変更して、Input操作を受け付けないようにします。
また、その上にある
if (!isControllable) { // kill all inputs if not controllable. Input.ResetInputAxes(); }
を削除しておきます。
ここまでで実行してみると、自キャラはちゃんと動くようになってると思いますが、リモートキャラは全然動かないようになっているので、リモートのほうでもキャラの状態を適用させます。
339行、Updateの中にある
if(controller.velocity.sqrMagnitude < 0.5) { _animation.CrossFade(idleAnimation.name); } else { if(_characterState == CharacterState.Running) { _animation[runAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, runMaxAnimationSpeed); _animation.CrossFade(runAnimation.name); } else if(_characterState == CharacterState.Trotting) { _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, trotMaxAnimationSpeed); _animation.CrossFade(walkAnimation.name); } else if(_characterState == CharacterState.Walking) { _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, walkMaxAnimationSpeed); _animation.CrossFade(walkAnimation.name); } }
を、
if (this.isControllable && controller.velocity.sqrMagnitude < 0.5) { _animation.CrossFade(idleAnimation.name); this._characterState = CharacterState.Idle; } else { if (_characterState == CharacterState.Idle) { _animation.CrossFade(idleAnimation.name); } else if (_characterState == CharacterState.Running) { _animation[runAnimation.name].speed = runMaxAnimationSpeed; if (isControllable) _animation[runAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, runMaxAnimationSpeed); _animation.CrossFade(runAnimation.name); } else if (_characterState == CharacterState.Trotting) { _animation[runAnimation.name].speed = trotMaxAnimationSpeed; if (isControllable) _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, trotMaxAnimationSpeed); _animation.CrossFade(walkAnimation.name); } else if (_characterState == CharacterState.Walking) { _animation[runAnimation.name].speed = walkMaxAnimationSpeed; if (isControllable) _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0, walkMaxAnimationSpeed); _animation.CrossFade(walkAnimation.name); } }
というように、isControllableではない場合にもステータスを見て変更するようにします。
またこの_characterStateは外部から変更するようになるので、30行の定義をpublicにしておきます。
public var _characterState : CharacterState;
そしてNetworkCharacter#OnPhotonSerializeViewで
void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.isWriting) { // We own this player: send the others our data stream.SendNext(transform.position); stream.SendNext(transform.rotation); myThirdPersonController myC = GetComponent<myThirdPersonController>(); stream.SendNext((int)myC._characterState); } else { // Network player, receive data this.correctPlayerPos = (Vector3)stream.ReceiveNext(); this.correctPlayerRot = (Quaternion)stream.ReceiveNext(); myThirdPersonController myC = GetComponent<myThirdPersonController>(); myC._characterState = (CharacterState)stream.ReceiveNext(); } }
として、_characterStateを同期するようにします。
これで、リモートのほうにもアニメーションが反映されていると思います。(ただ、攻撃のモーションは同期が用意されてないようで、反映されませんでした)