MLAPI v0.1.0 Quick Start サンプルプロジェクトを作成しました
新 MLAPI 取り合えず触れてみよう!という方向けに,MLAPI v0.1.0 の Quick Start 作成しました. リポジトリはこちらです.
- 検証環境
- 元ネタ
- Part 1|インストール
- Part 2|NetworkManager の設定
- Part 3|Static Environment の作成
- Part 4|Player の作成
- Part 5|PlayerScript の作成
- Part 6|Playerの動作確認
- Part 7|ParrelSync でマルチプレイ確認
- Part 8|PlayerName の表示
- Part 9|NetworkVariable と ServerRpc の利用
- Part 10|PlayerName/PlayerColor の同期確認
- Part 11|簡単なテキストチャットの表示
- Part 12|武器のスイッチ実装
- Part 13|簡易的な武器オブジェクトの実装
- Part 14|武器オブジェクト変更処理の確認
- Part 15|SceneReference作成(微調整)
- Part 16 ~ 19|シーン遷移
- Part 20|武器の中身の作成
- Part 21|銃弾の作成
- Part 22|銃弾処理の確認
- 終わりに
検証環境
- Unity 2020.3.1f1
- MLAPI v0.1.0
- MLAPI Community Contribution v12
元ネタ
元ネタは Mirror Quick Start です.
深い事を考えずにこちらの Mirror のサンプルスクリプトを MLAPI v0.1.0 に対応させています. そのため密結合で無駄の多い部分やダーティコードな部分がありますが,そこは大目に見てください. 独自にカスタマイズしている部分もあります.Mirror Quick Start の Part16 ~ Part 19 はここでは省略しています.
今回はHosted (Host-Client) 接続方式のため,Server部分の実装はしていません. 黒河さんが先日新MLAPIに対応したUnityChanサンプルを公開しておりますので, Serverを介するDGS接続方式の場合はこちらを参考ください.
https://t.co/9eUDwtrmDI
— 黒河優介(YusukeKurokawa) (@wotakuro) 2021年3月26日
前にUnityステーションで紹介したMLAPIのサンプルを更新しました。
新MLAPIへの対応してます。
unity2020.3ブランチの方ですので,cloneの際はご注意ください.
Part 1|インストール
MLAPI v0.1.0 は現在主に git URL add でインストールするのがメインになっています.
以下で Add package from git URL しましょう. https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi.git?path=/com.unity.multiplayer.mlapi
また,NetworkManagerHudコンポーネントを利用するため, MLAPI Commnuty Contributions の Extensions も Add します.
加えて,ParrelSync もインストールしましょう. ParrelSync は Multiplayer 補助用のライブラリで,同じプロジェクトのエディタを複数広げることが出来, エディタ上でのテストプレイの効率を早めることができます.以下で Add package from git URL します.
https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync
ParrelSync は以下の記事で説明しています.
Part 2|NetworkManager の設定
適当なシーンを作成します. 空オブジェクトを作成し,NetworkManagerと命名します. その後,以下のコンポーネントを追加します.
NetworkTransport が None ということでワーニングが出ているので, ◎ボタンを押すと Transport 一覧がプルダウンで出ます. 今回はUNetTransportを利用します.UNetTransportを選択してください.
UNetTrasnport コンポーネントが自動的に追加されます.
Part 3|Static Environment の作成
Plane を作成し,Position (0, -1, 0),Scale (2, 2, 2) とします. Plane の色を変えたい場合は各自で Matarial を作成してください.
Part 4|Player の作成
Capsule を作成し,NetworkPlayer と名付けます. 以下のコンポーネントを追加します.
作成後 Prefab 化し,Hierarchy から消します.
NetworkManager コンポーネントの NetworkPrefabs が現在「List is Empty」なので, ここに先ほど作成したNetworkPlayerを追加します. その後,Default Player Prefab にチェックを入れます.
Part 5|PlayerScript の作成
以下のように PlayerScript を作成し,NetworkPlayerオブジェクトに追加します.
using UnityEngine; // NetworkBehaviour 継承に必要 using MLAPI; namespace MLAPIQuickStart { // MonoBehaviour ではなく NetworkBehaviour を継承する public class PlayerScript : NetworkBehaviour { private Transform _cameraTransform; private static readonly string HORIZONTAL = "Horizontal"; private static readonly string VERTICAL = "Vertical"; /// <summary> /// MLAPI の Setup が完了後に呼ばれるコールバック /// </summary> public override void NetworkStart() { _cameraTransform = Camera.main.transform; if (IsLocalPlayer) { transform.position = new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5)); _cameraTransform.transform.SetParent(transform); _cameraTransform.localPosition = new Vector3(0, 0, 0); } } private void Update() { // ローカルプレイヤーではない場合,以下の処理を走らせない if (!IsLocalPlayer) { return; } var moveX = Input.GetAxis(HORIZONTAL) * Time.deltaTime * 110.0f; var moveZ = Input.GetAxis(VERTICAL) * Time.deltaTime * 4f; transform.Rotate(0, moveX, 0); transform.Translate(0, 0, moveZ); } } }
Part 6|Playerの動作確認
実行します.実行後に左上にHUDが出てくるので,Hostボタンを押下します. 一人称視点で動作できるのが確認できるかと思います.
Part 7|ParrelSync でマルチプレイ確認
ParrelSync タブ > Clone Manager > Create new clone を押します.
実行完了後に,Clones Manager の画面は以下のように変わります.Open in New Editor を押します.
クローンプロジェクトを開いたら,オリジナルとクローン両方実行してみます. 片方がHostで入った後,片方でClientで入ります.二人現れれば成功です!
Part 8|PlayerName の表示
NetworkPlayerプレハブの子として空オブジェクトを作成し,FloatingInfoと命名します. Transform設定は以下のようにします.
FloatingInfoの子として3D Textを作成し,Text_PlayerNameと命名します. Transformや3D Textの設定は以下のようにします.
Part 9|NetworkVariable と ServerRpc の利用
プレイヤーの名前と色を同期するために,同期変数 NetworkVariable を使います. PlayerScript.cs にコードを追加します.
using UnityEngine; // NetworkBehaviour 継承に必要 using MLAPI; // RPCに必要 using MLAPI.Messaging; // NetworkVariable 利用に必要 using MLAPI.NetworkVariable; using Random = UnityEngine.Random; namespace MLAPIQuickStart { // MonoBehaviour ではなく NetworkBehaviour を継承する public class PlayerScript : NetworkBehaviour { [SerializeField, Tooltip("プレイヤー名のテキスト")] private TextMesh textPlayerName; [SerializeField,Tooltip("プレイヤー情報")] private GameObject floatingInfo; /// <summary> /// プレイヤーの色設定に利用するマテリアルのクローン /// </summary> private Material _playerMaterialClone; /// <summary> /// カメラのTransform /// </summary> private Transform _cameraTransform; #region Constants private static readonly string HORIZONTAL = "Horizontal"; private static readonly string VERTICAL = "Vertical"; private static readonly float MOVEX_COEFFICIENT = 110.0f; private static readonly float MOVEZ_COEFFICIENT = 4.0f; #endregion /// <summary> /// プレイヤー名の同期変数 /// </summary> private readonly NetworkVariable<string> _networkPlayerName = new NetworkVariable<string> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); /// <summary> /// プレイヤー色の同期変数 /// </summary> private readonly NetworkVariable<Color> _networkPlayerColor = new NetworkVariable<Color> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}, Color.white); private void Awake() { // フック関数の設定 _networkPlayerName.OnValueChanged += OnNameChanged; _networkPlayerColor.OnValueChanged += OnColorChanged; // カメラのTransformのキャッシュ _cameraTransform = Camera.main.transform; } private void Start() { // ローカルプレイヤーの場合,カメラを一人称視点に設定し,プレイヤー名のテキストを画面下に表示する if (IsOwner) { var thisTransform = transform; thisTransform.position = new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5)); _cameraTransform.transform.SetParent(thisTransform); _cameraTransform.localPosition = new Vector3(0, 0, 0); floatingInfo.transform.localPosition = new Vector3(0, -0.3f, 0.6f); floatingInfo.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); var playerName = "Player" + Random.Range(100, 999); var color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); SubmitPlayerNameServerRpc(playerName); SubmitPlayerColorServerRpc(color); } } private void Update() { // ローカルプレイヤーではない場合,プレイヤー名のテキストをカメラに向けさせる if (!IsLocalPlayer) { floatingInfo.transform.LookAt(_cameraTransform); return; } // ローカルプレイヤーの場合,移動・回転処理を実行する Move(); } private void OnDestroy() { _networkPlayerName.OnValueChanged -= OnNameChanged; _networkPlayerColor.OnValueChanged -= OnColorChanged; } /// <summary> /// プレイヤー名を設定する /// </summary> /// <param name="playerName">playerName</param> /// <param name="serverRpcParams"></param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerNameServerRpc(string playerName, ServerRpcParams serverRpcParams = default) => _networkPlayerName.Value = playerName; /// <summary> /// プレイヤー色を設定する /// </summary> /// <param name="playerColor">playerColor</param> /// <param name="serverRpcParams"></param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerColorServerRpc(Color playerColor, ServerRpcParams serverRpcParams = default) => _networkPlayerColor.Value = playerColor; /// <summary> /// プレイヤー名が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldName">前のプレイヤー名</param> /// <param name="newName">現在のプレイヤー名</param> private void OnNameChanged(string oldName, string newName) => textPlayerName.text = newName; /// <summary> /// プレイヤー色が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldColor">前のプレイヤー色</param> /// <param name="newColor">現在のプレイヤー色</param> private void OnColorChanged(Color oldColor, Color newColor) { textPlayerName.color = newColor; _playerMaterialClone = new Material(GetComponent<Renderer>().material) {color = newColor}; GetComponent<Renderer>().material = _playerMaterialClone; } /// <summary> /// プレイヤーの移動・回転処理 /// </summary> private void Move() { var moveX = Input.GetAxis(HORIZONTAL) * Time.deltaTime * MOVEX_COEFFICIENT; var moveZ = Input.GetAxis(VERTICAL) * Time.deltaTime * MOVEZ_COEFFICIENT; transform.Rotate(0, moveX, 0); transform.Translate(0, 0, moveZ); } } }
Part 10|PlayerName/PlayerColor の同期確認
NetworkPlayer プレハブを開き,Inspector に FloatingInfo,Text_PlayerNameを当てはめます. ParrelSync でプレイヤーの名前と色が同期されていることを確認しましょう.
お互い同期できていればOKです!
Part 11|簡単なテキストチャットの表示
空オブジェクトを作成し,SceneScriptと命名します. NetworkObjectコンポーネントをアタッチしてください. (忘れるとNetworkBehaviourを継承したスクリプトコンポーネントのネットワーク同期処理がうまく行きません)
SceneScriptスクリプトコンポーネントを作成します.
using MLAPI; using MLAPI.NetworkVariable; using UnityEngine; using UnityEngine.UI; namespace MLAPIQuickStart { public class SceneScript : NetworkBehaviour { [SerializeField, Tooltip("メッセージ表示用のテキスト")] private Text textMessage; [SerializeField, Tooltip("メッセージ送信用のボタン")] private Button buttonSendMessage; public PlayerScript PlayerScript { set => _playerScript = value; } private PlayerScript _playerScript; /// <summary> /// メッセージの同期変数 /// </summary> private readonly NetworkVariable<string> _networkMessage = new NetworkVariable<string> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); private void Awake() { // フック関数の設定 _networkMessage.OnValueChanged += OnMessageChanged; buttonSendMessage.onClick.AddListener(SendMessage); } private void OnDestroy() { buttonSendMessage.onClick.RemoveListener(SendMessage); } /// <summary> /// メッセージの設定 /// </summary> /// <param name="message">テキストメッセージ</param> public void SetMessage(string message) => _networkMessage.Value = message; /// <summary> /// メッセージ内容が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldMessage">旧メッセージ</param> /// <param name="newMessage">新メッセージ</param> private void OnMessageChanged(string oldMessage, string newMessage) => textMessage.text = newMessage; /// <summary> /// メッセージの送信 /// </summary> private void SendMessage() { if (_playerScript != null) { _playerScript.SubmitMessageServerRpc(); } } } }
PlayerScript スクリプトコンポーネントを更新します.
using UnityEngine; // NetworkBehaviour 継承に必要 using MLAPI; // RPCに必要 using MLAPI.Messaging; // NetworkVariable 利用に必要 using MLAPI.NetworkVariable; using Random = UnityEngine.Random; namespace MLAPIQuickStart { // MonoBehaviour ではなく NetworkBehaviour を継承する public class PlayerScript : NetworkBehaviour { [SerializeField, Tooltip("プレイヤー名のテキスト")] private TextMesh textPlayerName; [SerializeField,Tooltip("プレイヤー情報")] private GameObject floatingInfo; /// <summary> /// プレイヤーの色設定に利用するマテリアルのクローン /// </summary> private Material _playerMaterialClone; /// <summary> /// カメラのTransform /// </summary> private Transform _cameraTransform; private SceneScript _sceneScript; #region Constants private static readonly string HORIZONTAL = "Horizontal"; private static readonly string VERTICAL = "Vertical"; private static readonly float MOVEX_COEFFICIENT = 110.0f; private static readonly float MOVEZ_COEFFICIENT = 4.0f; #endregion /// <summary> /// プレイヤー名の同期変数 /// </summary> private readonly NetworkVariable<string> _networkPlayerName = new NetworkVariable<string> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); /// <summary> /// プレイヤー色の同期変数 /// </summary> private readonly NetworkVariable<Color> _networkPlayerColor = new NetworkVariable<Color> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}, Color.white); private void Awake() { // フック関数の設定 _networkPlayerName.OnValueChanged += OnNameChanged; _networkPlayerColor.OnValueChanged += OnColorChanged; // カメラのTransformのキャッシュ _cameraTransform = Camera.main.transform; _sceneScript = FindObjectOfType<SceneScript>(); } private void Start() { // ローカルプレイヤーの場合,カメラを一人称視点に設定し,プレイヤー名のテキストを画面下に表示する if (IsOwner) { _sceneScript.PlayerScript = this; var thisTransform = transform; thisTransform.position = new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5)); _cameraTransform.transform.SetParent(thisTransform); _cameraTransform.localPosition = new Vector3(0, 0, 0); floatingInfo.transform.localPosition = new Vector3(0, -0.3f, 0.6f); floatingInfo.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); var playerName = "Player" + Random.Range(100, 999); var color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); SubmitPlayerNameServerRpc(playerName); SubmitPlayerColorServerRpc(color); SubmitJoinedMessageServerRpc(); } } private void Update() { // ローカルプレイヤーではない場合,プレイヤー名のテキストをカメラに向けさせる if (!IsLocalPlayer) { floatingInfo.transform.LookAt(_cameraTransform); return; } // ローカルプレイヤーの場合,移動・回転処理を実行する Move(); } private void OnDestroy() { _networkPlayerName.OnValueChanged -= OnNameChanged; _networkPlayerColor.OnValueChanged -= OnColorChanged; } /// <summary> /// テキストチャットを送信する /// </summary> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] public void SubmitMessageServerRpc(ServerRpcParams serverRpcParams = default) { if (_sceneScript != null) { _sceneScript.SetMessage($"{_networkPlayerName.Value} says hello {Random.Range(10, 99)}"); } } /// <summary> /// プレイヤー名を設定する /// </summary> /// <param name="playerName">playerName</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerNameServerRpc(string playerName, ServerRpcParams serverRpcParams = default) => _networkPlayerName.Value = playerName; /// <summary> /// プレイヤー色を設定する /// </summary> /// <param name="playerColor">playerColor</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerColorServerRpc(Color playerColor, ServerRpcParams serverRpcParams = default) => _networkPlayerColor.Value = playerColor; /// <summary> /// 入室時メッセージを表示する /// </summary> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitJoinedMessageServerRpc(ServerRpcParams serverRpcParams = default) { if (_sceneScript != null) { _sceneScript.SetMessage($"{_networkPlayerName.Value} joined"); } } /// <summary> /// プレイヤー名が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldName">前のプレイヤー名</param> /// <param name="newName">現在のプレイヤー名</param> private void OnNameChanged(string oldName, string newName) => textPlayerName.text = newName; /// <summary> /// プレイヤー色が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldColor">前のプレイヤー色</param> /// <param name="newColor">現在のプレイヤー色</param> private void OnColorChanged(Color oldColor, Color newColor) { textPlayerName.color = newColor; _playerMaterialClone = new Material(GetComponent<Renderer>().material) {color = newColor}; GetComponent<Renderer>().material = _playerMaterialClone; } /// <summary> /// プレイヤーの移動・回転処理 /// </summary> private void Move() { var moveX = Input.GetAxis(HORIZONTAL) * Time.deltaTime * MOVEX_COEFFICIENT; var moveZ = Input.GetAxis(VERTICAL) * Time.deltaTime * MOVEZ_COEFFICIENT; transform.Rotate(0, moveX, 0); transform.Translate(0, 0, moveZ); } } }
メッセージ表示用のTextオブジェクト,送信用のButtonオブジェクトを作成します.以下,参考.
作成後,InspectorでSceneScriptのTextとButtonを埋めます.
以下の動画のように,入室時に「PlayerXXX joined」が, Send Message ボタン押下時に「Player XXX says hello XX」が同期表示されればOKです!
Part 12|武器のスイッチ実装
武器のスイッチを実装してみます. PlayerScriptコンポーネントを更新します.
using System.Collections.Generic; using System.Linq; using UnityEngine; // NetworkBehaviour 継承に必要 using MLAPI; // RPCに必要 using MLAPI.Messaging; // NetworkVariable 利用に必要 using MLAPI.NetworkVariable; using Random = UnityEngine.Random; namespace MLAPIQuickStart { // MonoBehaviour ではなく NetworkBehaviour を継承する public class PlayerScript : NetworkBehaviour { [SerializeField, Tooltip("プレイヤー名のテキスト")] private TextMesh textPlayerName; [SerializeField,Tooltip("プレイヤー情報")] private GameObject floatingInfo; [SerializeField, Tooltip("プレイヤーの武器リスト")] private List<GameObject> playerWeapons; /// <summary> /// プレイヤーの色設定に利用するマテリアルのクローン /// </summary> private Material _playerMaterialClone; /// <summary> /// カメラのTransform /// </summary> private Transform _cameraTransform; private SceneScript _sceneScript; private int _currentWeaponIndex; #region Constants private static readonly string HORIZONTAL = "Horizontal"; private static readonly string VERTICAL = "Vertical"; private static readonly float MOVEX_COEFFICIENT = 110.0f; private static readonly float MOVEZ_COEFFICIENT = 4.0f; #endregion /// <summary> /// プレイヤー名の同期変数 /// </summary> private readonly NetworkVariable<string> _networkPlayerName = new NetworkVariable<string> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); /// <summary> /// プレイヤー色の同期変数 /// </summary> private readonly NetworkVariable<Color> _networkPlayerColor = new NetworkVariable<Color> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}, Color.white); /// <summary> /// 武器番号の同期変数 /// </summary> private readonly NetworkVariable<int> _networkWeaponIndex = new NetworkVariable<int> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); private void Awake() { // フック関数の設定 _networkPlayerName.OnValueChanged += OnNameChanged; _networkPlayerColor.OnValueChanged += OnColorChanged; _networkWeaponIndex.OnValueChanged += OnWeaponChanged; // カメラのTransformのキャッシュ _cameraTransform = Camera.main.transform; _sceneScript = FindObjectOfType<SceneScript>(); // 武器は最初全て非表示にする foreach (var weapon in playerWeapons.Where(weapon => weapon != null)) { weapon.SetActive(false); } } private void Start() { // ローカルプレイヤーの場合,カメラを一人称視点に設定し,プレイヤー名のテキストを画面下に表示する if (IsOwner) { _sceneScript.PlayerScript = this; var thisTransform = transform; thisTransform.position = new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5)); _cameraTransform.transform.SetParent(thisTransform); _cameraTransform.localPosition = new Vector3(0, 0, 0); floatingInfo.transform.localPosition = new Vector3(0, -0.3f, 0.6f); floatingInfo.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); var playerName = "Player" + Random.Range(100, 999); var color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); SubmitPlayerNameServerRpc(playerName); SubmitPlayerColorServerRpc(color); SubmitJoinedMessageServerRpc(); } } private void Update() { // ローカルプレイヤーではない場合,プレイヤー名のテキストをカメラに向けさせる if (!IsLocalPlayer) { floatingInfo.transform.LookAt(_cameraTransform); return; } // ローカルプレイヤーの場合,移動・回転処理を実行する Move(); // 右クリックで武器変更処理を走らせる if (Input.GetButtonDown("Fire2")) { _currentWeaponIndex++; if (_currentWeaponIndex > playerWeapons.Count) { _currentWeaponIndex = 0; } ChangeActiveWeaponServerRpc(_currentWeaponIndex); } } private void OnDestroy() { _networkPlayerName.OnValueChanged -= OnNameChanged; _networkPlayerColor.OnValueChanged -= OnColorChanged; } /// <summary> /// テキストチャットを送信する /// </summary> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] public void SubmitMessageServerRpc(ServerRpcParams serverRpcParams = default) { if (_sceneScript != null) { _sceneScript.SetMessage($"{_networkPlayerName.Value} says hello {Random.Range(10, 99)}"); } } /// <summary> /// プレイヤー名を設定する /// </summary> /// <param name="playerName">playerName</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerNameServerRpc(string playerName, ServerRpcParams serverRpcParams = default) => _networkPlayerName.Value = playerName; /// <summary> /// プレイヤー色を設定する /// </summary> /// <param name="playerColor">playerColor</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerColorServerRpc(Color playerColor, ServerRpcParams serverRpcParams = default) => _networkPlayerColor.Value = playerColor; /// <summary> /// 入室時メッセージを表示する /// </summary> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitJoinedMessageServerRpc(ServerRpcParams serverRpcParams = default) { if (_sceneScript != null) { _sceneScript.SetMessage($"{_networkPlayerName.Value} joined"); } } /// <summary> /// 武器インデックスを設定する /// </summary> /// <param name="index">武器インデックス</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void ChangeActiveWeaponServerRpc(int index, ServerRpcParams serverRpcParams = default) => _networkWeaponIndex.Value = index; /// <summary> /// プレイヤー名が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldName">前のプレイヤー名</param> /// <param name="newName">現在のプレイヤー名</param> private void OnNameChanged(string oldName, string newName) => textPlayerName.text = newName; /// <summary> /// プレイヤー色が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldColor">前のプレイヤー色</param> /// <param name="newColor">現在のプレイヤー色</param> private void OnColorChanged(Color oldColor, Color newColor) { textPlayerName.color = newColor; _playerMaterialClone = new Material(GetComponent<Renderer>().material) {color = newColor}; GetComponent<Renderer>().material = _playerMaterialClone; } /// <summary> /// 武器インデックスが変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldIndex">前のインデックス</param> /// <param name="newIndex">現在のインデックス</param> private void OnWeaponChanged(int oldIndex, int newIndex) { if (0 < oldIndex && oldIndex < playerWeapons.Count && playerWeapons[oldIndex] != null) { playerWeapons[oldIndex].SetActive(false); } if (0 < newIndex && newIndex < playerWeapons.Count && playerWeapons[newIndex] != null) { playerWeapons[newIndex].SetActive(true); } } /// <summary> /// プレイヤーの移動・回転処理 /// </summary> private void Move() { var moveX = Input.GetAxis(HORIZONTAL) * Time.deltaTime * MOVEX_COEFFICIENT; var moveZ = Input.GetAxis(VERTICAL) * Time.deltaTime * MOVEZ_COEFFICIENT; transform.Rotate(0, moveX, 0); transform.Translate(0, 0, moveZ); } } }
Part 13|簡易的な武器オブジェクトの実装
NetworkPlayerプレハブを開きます. NetworkPlayerの子として空オブジェクトを生成し,WeaponHolderと命名します. WeaponHolderの子として,Cubeを生成し,Weapon1と命名します. Weapon1を複製して,Weapon2と命名します.
Weapon1とWeapon2のTransformは以下を参考にしてください.
Part 14|武器オブジェクト変更処理の確認
Inspector で PlayerWeapons を設定します. 注意なのはここでは,Element 0 は None に設定してください.
動作確認をしてみます.
武器の変更の同期が出来ていればOKです!
Part 15|SceneReference作成(微調整)
現在のコードのままでは,SceneScriptオブジェクトが非活性の際,Find処理に失敗します. ここで少し微調整を行います.空オブジェクトを作成し,SceneReferenceと命名します. 以下,SceneReference スクリプトコンポーネントを作成します.
using UnityEngine; namespace MLAPIQuickStart { public class SceneReference : MonoBehaviour { [SerializeField] private SceneScript sceneScript; public SceneScript SceneScript => sceneScript; } }
また,SceneScriptスクリプトコンポーネントで以下のコードを追加します.
public SceneReference SceneReference { set => _sceneReference = value; } private SceneReference _sceneReference;
PlayerScriptスクリプトコンポーネントで以下のコードを変更します.
変更前
_sceneScript = FindObjectOfType<SceneScript>();
変更後
_sceneScript = GameObject.Find("SceneReference").GetComponent<SceneReference>().SceneScript;
デグレの確認をし,今までと同じ動作をしているかどうか確認してください.問題なければ続きます.
Part 16 ~ 19|シーン遷移
今回ここは省略します.
Part 20|武器の中身の作成
Weapon1の子として,WeaponFirePositionを作成します.Weapon2も同様にします. WeaponFirePosition の Position.z は 0.5 くらいが丁度よいです.
Weapon スクリプトコンポーネントを作成し,Weapon1,Weapon2にアタッチします.
using UnityEngine; namespace MLAPIQuickStart { public class Weapon : MonoBehaviour { public float weaponSpeed = 15.0f; public float weaponLife = 3.0f; public float weaponCooldown = 1.0f; public int weaponAmmo = 15; public GameObject weaponBullet; public Transform weaponFirePosition; } }
Part 21|銃弾の作成
銃弾を作成します.SphereとCubeを作成し,Scale全てを0.2にします. それぞれにRigidbodyも忘れずに追加します.命名はWeaponBullet1,WeaponBullet2としました.
NetworkPlayerプレハブを開き,InspectorでWeaponの中身を埋めます.
Part 22|銃弾処理の確認
SceneScriptスクリプトコンポーネントを更新します. 弾数表示用のテキストと表示用のメソッドを追加します.
using MLAPI; using MLAPI.NetworkVariable; using UnityEngine; using UnityEngine.UI; namespace MLAPIQuickStart { public class SceneScript : NetworkBehaviour { [SerializeField, Tooltip("メッセージ表示用のテキスト")] private Text textMessage; [SerializeField, Tooltip("メッセージ送信用のボタン")] private Button buttonSendMessage; [SerializeField, Tooltip("銃弾の弾数表示用のテキスト")] private Text textAmmo; public PlayerScript PlayerScript { set => _playerScript = value; } private PlayerScript _playerScript; public SceneReference SceneReference { set => _sceneReference = value; } private SceneReference _sceneReference; /// <summary> /// メッセージの同期変数 /// </summary> private readonly NetworkVariable<string> _networkMessage = new NetworkVariable<string> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); private void Awake() { // フック関数の設定 _networkMessage.OnValueChanged += OnMessageChanged; buttonSendMessage.onClick.AddListener(SendMessage); } private void OnDestroy() { buttonSendMessage.onClick.RemoveListener(SendMessage); } /// <summary> /// メッセージの設定 /// </summary> /// <param name="message">テキストメッセージ</param> public void SetMessage(string message) => _networkMessage.Value = message; /// <summary> /// 銃弾の弾数を表示する /// </summary> /// <param name="value"></param> public void DisplayAmmo(int value) => textAmmo.text = $"Ammo: {value}"; /// <summary> /// メッセージ内容が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldMessage">旧メッセージ</param> /// <param name="newMessage">新メッセージ</param> private void OnMessageChanged(string oldMessage, string newMessage) => textMessage.text = newMessage; /// <summary> /// メッセージの送信 /// </summary> private void SendMessage() { if (_playerScript != null) { _playerScript.SubmitMessageServerRpc(); } } } }
適当にTextオブジェクトを作成し,Inspectorで textAmmo を埋めます.
次にPlayerScriptスクリプトコンポーネントを更新します.
using System.Collections.Generic; using System.Linq; using UnityEngine; // NetworkBehaviour 継承に必要 using MLAPI; // RPCに必要 using MLAPI.Messaging; // NetworkVariable 利用に必要 using MLAPI.NetworkVariable; using Random = UnityEngine.Random; namespace MLAPIQuickStart { // MonoBehaviour ではなく NetworkBehaviour を継承する public class PlayerScript : NetworkBehaviour { [SerializeField, Tooltip("プレイヤー名のテキスト")] private TextMesh textPlayerName; [SerializeField,Tooltip("プレイヤー情報")] private GameObject floatingInfo; [SerializeField, Tooltip("プレイヤーの武器リスト")] private List<GameObject> playerWeapons; /// <summary> /// プレイヤーの色設定に利用するマテリアルのクローン /// </summary> private Material _playerMaterialClone; /// <summary> /// カメラのTransform /// </summary> private Transform _cameraTransform; private SceneScript _sceneScript; /// <summary> /// 現在の武器インデックス /// </summary> private int _currentWeaponIndex; /// <summary> /// 現在の武器 /// </summary> private Weapon _activeWeapon; /// <summary> /// 武器の攻撃間隔 /// </summary> private float _weaponCooldownTime; #region Constants private static readonly string HORIZONTAL = "Horizontal"; private static readonly string VERTICAL = "Vertical"; private static readonly float MOVEX_COEFFICIENT = 110.0f; private static readonly float MOVEZ_COEFFICIENT = 4.0f; #endregion /// <summary> /// プレイヤー名の同期変数 /// </summary> private readonly NetworkVariable<string> _networkPlayerName = new NetworkVariable<string> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); /// <summary> /// プレイヤー色の同期変数 /// </summary> private readonly NetworkVariable<Color> _networkPlayerColor = new NetworkVariable<Color> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}, Color.white); /// <summary> /// 武器番号の同期変数 /// </summary> private readonly NetworkVariable<int> _networkWeaponIndex = new NetworkVariable<int> (new NetworkVariableSettings {WritePermission = NetworkVariablePermission.OwnerOnly}); private void Awake() { // フック関数の設定 _networkPlayerName.OnValueChanged += OnNameChanged; _networkPlayerColor.OnValueChanged += OnColorChanged; _networkWeaponIndex.OnValueChanged += OnWeaponChanged; // カメラのTransformのキャッシュ _cameraTransform = Camera.main.transform; _sceneScript = GameObject.Find("SceneReference").GetComponent<SceneReference>().SceneScript; // 武器は最初全て非表示にする foreach (var weapon in playerWeapons.Where(weapon => weapon != null)) { weapon.SetActive(false); } // 現在の武器の設定と銃弾の弾数を表示する if (_currentWeaponIndex < playerWeapons.Count && playerWeapons[_currentWeaponIndex] != null) { _activeWeapon = playerWeapons[_currentWeaponIndex].GetComponent<Weapon>(); _sceneScript.DisplayAmmo(_activeWeapon.weaponAmmo); } } private void Start() { // ローカルプレイヤーの場合,カメラを一人称視点に設定し,プレイヤー名のテキストを画面下に表示する if (IsOwner) { _sceneScript.PlayerScript = this; var thisTransform = transform; thisTransform.position = new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5)); _cameraTransform.transform.SetParent(thisTransform); _cameraTransform.localPosition = new Vector3(0, 0, 0); floatingInfo.transform.localPosition = new Vector3(0, -0.3f, 0.6f); floatingInfo.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); var playerName = "Player" + Random.Range(100, 999); var color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); SubmitPlayerNameServerRpc(playerName); SubmitPlayerColorServerRpc(color); SubmitJoinedMessageServerRpc(); } } private void Update() { // ローカルプレイヤーではない場合,プレイヤー名のテキストをカメラに向けさせる if (!IsLocalPlayer) { floatingInfo.transform.LookAt(_cameraTransform); return; } // ローカルプレイヤーの場合,移動・回転処理を実行する Move(); // 右クリックで武器変更処理を走らせる if (Input.GetButtonDown("Fire2")) { _currentWeaponIndex++; if (_currentWeaponIndex > playerWeapons.Count) { _currentWeaponIndex = 0; } ChangeActiveWeaponServerRpc(_currentWeaponIndex); } // 左クリックで発射処理を走らせる if (Input.GetButtonDown("Fire1")) { if (_activeWeapon && Time.time > _weaponCooldownTime && _activeWeapon.weaponAmmo > 0) { _weaponCooldownTime = Time.time + _activeWeapon.weaponCooldown; _activeWeapon.weaponAmmo--; _sceneScript.DisplayAmmo(_activeWeapon.weaponAmmo); FireWeaponServerRpc(); } } } private void OnDestroy() { _networkPlayerName.OnValueChanged -= OnNameChanged; _networkPlayerColor.OnValueChanged -= OnColorChanged; } /// <summary> /// テキストチャットを送信する /// </summary> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] public void SubmitMessageServerRpc(ServerRpcParams serverRpcParams = default) { if (_sceneScript != null) { _sceneScript.SetMessage($"{_networkPlayerName.Value} says hello {Random.Range(10, 99)}"); } } /// <summary> /// プレイヤー名を設定する /// </summary> /// <param name="playerName">playerName</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerNameServerRpc(string playerName, ServerRpcParams serverRpcParams = default) => _networkPlayerName.Value = playerName; /// <summary> /// プレイヤー色を設定する /// </summary> /// <param name="playerColor">playerColor</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitPlayerColorServerRpc(Color playerColor, ServerRpcParams serverRpcParams = default) => _networkPlayerColor.Value = playerColor; /// <summary> /// 入室時メッセージを表示する /// </summary> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void SubmitJoinedMessageServerRpc(ServerRpcParams serverRpcParams = default) { if (_sceneScript != null) { _sceneScript.SetMessage($"{_networkPlayerName.Value} joined"); } } /// <summary> /// 武器インデックスを設定する /// </summary> /// <param name="index">武器インデックス</param> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void ChangeActiveWeaponServerRpc(int index, ServerRpcParams serverRpcParams = default) => _networkWeaponIndex.Value = index; /// <summary> /// サーバーへ発射処理を走らせる /// </summary> /// <param name="serverRpcParams">ServerRpcParams</param> [ServerRpc(RequireOwnership = true)] private void FireWeaponServerRpc(ServerRpcParams serverRpcParams = default) => FireWeaponClientRpc(); /// <summary> /// 全クライアントへ発射処理を送る /// </summary> /// <param name="clientRpcParams">ClientRpcParams</param> [ClientRpc] private void FireWeaponClientRpc(ClientRpcParams clientRpcParams = default) { var bullet = Instantiate(_activeWeapon.weaponBullet, _activeWeapon.weaponFirePosition.position, _activeWeapon.weaponFirePosition.rotation); bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * _activeWeapon.weaponSpeed; if (bullet) { Destroy(bullet, _activeWeapon.weaponLife); } } /// <summary> /// プレイヤー名が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldName">前のプレイヤー名</param> /// <param name="newName">現在のプレイヤー名</param> private void OnNameChanged(string oldName, string newName) => textPlayerName.text = newName; /// <summary> /// プレイヤー色が変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldColor">前のプレイヤー色</param> /// <param name="newColor">現在のプレイヤー色</param> private void OnColorChanged(Color oldColor, Color newColor) { textPlayerName.color = newColor; _playerMaterialClone = new Material(GetComponent<Renderer>().material) {color = newColor}; GetComponent<Renderer>().material = _playerMaterialClone; } /// <summary> /// 武器インデックスが変更された時に呼ばれるフック関数 /// </summary> /// <param name="oldIndex">前のインデックス</param> /// <param name="newIndex">現在のインデックス</param> private void OnWeaponChanged(int oldIndex, int newIndex) { if (0 < oldIndex && oldIndex < playerWeapons.Count && playerWeapons[oldIndex] != null) { playerWeapons[oldIndex].SetActive(false); } if (0 < newIndex && newIndex < playerWeapons.Count && playerWeapons[newIndex] != null) { playerWeapons[newIndex].SetActive(true); _activeWeapon = playerWeapons[_networkWeaponIndex.Value].GetComponent<Weapon>(); if (IsLocalPlayer) { _sceneScript.DisplayAmmo(_activeWeapon.weaponAmmo); } } } /// <summary> /// プレイヤーの移動・回転処理 /// </summary> private void Move() { var moveX = Input.GetAxis(HORIZONTAL) * Time.deltaTime * MOVEX_COEFFICIENT; var moveZ = Input.GetAxis(VERTICAL) * Time.deltaTime * MOVEZ_COEFFICIENT; transform.Rotate(0, moveX, 0); transform.Translate(0, 0, moveZ); } } }
銃弾発射が同期できていればOKです!
終わりに
ここで終わりです.
何か漏れや誤りがあれば,ご連絡ください.
一部バグが残っていますが,ここでは軽微なので(ry