Mirror官方案例操作
一、导入Mirror
在unity商城订阅Mirror https://assetstore.unity.com/packages/tools/network/mirror-129321
使用unity创建工程 (推荐版本:目前建议使用 Unity 2020 或 2021 LTS 版本;超出这些版本的可能可以运行,但用户需自行承担风险,尤其是预览版或测试版。) 并导入Mirror。
二、初步设置
新建一个场景并放在Build Settings里
新建一个空物体,命名为NetworkManager 并添加以下三个组件:
NetworkManager ( 对整个网络游戏对象进行管理。)
KCPTransport (TelepathyTransport is older, you do not need KCP and Telepathy)
NetworkManagerHUD ( 一个启动的面板,方便客户端或服务器端启动停止。)
NetworkManager组件中把场景拖拽到Offline 和 Online中(拖拽的场景必须在Build Settings里)
三、设置场景
添加一个简单的Plane 作为地面 设置 position(0,-1,0) 比例(2,2,2)
添加一个空物体 为他添加组件:NetworkStartPosition (作为出生点)复制多个放在Plane的各个角
四、创建玩家
创建一个胶囊 并添加组件:NetworkTransform(Reliable) 其中Sync Direction选项选为Client To Server 自动会再添加一个NetworkIdentity
在 NetworkTransform勾选 Client Authority (我没找到)
重命名胶囊为:Player 新建PlayerScript代码挂在Player上
把Player这个物体拽成预制体,并在场景里删了他
Network Manager中的Player Prefab选择Player
五、PlayerScript
添加以下代码到playerscript里
using Mirror;
using UnityEngine;
namespace QuickStart
{
public class PlayerScript : NetworkBehaviour
{
public override void OnStartLocalPlayer()
{
Camera.main.transform.SetParent(transform);
Camera.main.transform.localPosition = new Vector3(0, 0, 0);
}
void Update()
{
if (!isLocalPlayer) { return; } //如果不是本地玩家就return
float moveX = Input.GetAxis("Horizontal") * Time.deltaTime * 110.0f;
float moveZ = Input.GetAxis("Vertical") * Time.deltaTime * 4f;
transform.Rotate(0, moveX, 0);
transform.Translate(0, 0, moveZ);
}
}
}
六、第一次play
点击Play 然后点击左上角 Host (server + client)测试人物移动 成功后可以发布一版本联机试试效果。
七、添加玩家名称
在player预制体中,创建一个空的GameObject
命名FloatingInfo
y轴位置设置为1.5
缩放x轴 -1
添加3DText子物体
八、更新PlayerScripts代码
using Mirror;
using UnityEngine;
namespace QuickStart
{
public class PlayerScript : NetworkBehaviour
{
public TextMesh playerNameText;
public GameObject floatingInfo;
private Material playerMaterialClone;
[SyncVar(hook = nameof(OnNameChanged))]//当playerName变了之后调用OnNameChange
public string playerName;
[SyncVar(hook = nameof(OnColorChanged))]
public Color playerColor = Color.white;
void OnNameChanged(string _Old, string _New)
{
playerNameText.text = playerName;
}
void OnColorChanged(Color _Old, Color _New)
{
playerNameText.color = _New;
playerMaterialClone = new Material(GetComponent<Renderer>().material);
playerMaterialClone.color = _New;
GetComponent<Renderer>().material = playerMaterialClone;
}
public override void OnStartLocalPlayer()//当该对象成为本地玩家时调用的方法
{
Camera.main.transform.SetParent(transform);
Camera.main.transform.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);
string name = "Player" + Random.Range(100, 999);
Color color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f));
CmdSetupPlayer(name, color);
}
[Command] //以Cmd
开头的方法或标记[Command]
的方法,只能由客户端调用,在服务器上执行
public void CmdSetupPlayer(string _name, Color _col)
{
// player info sent to server, then server updates sync vars which handles it on all clients
playerName = _name;
playerColor = _col;
}
void Update()
{
if (!isLocalPlayer)
{
// make non-local players run this
floatingInfo.transform.LookAt(Camera.main.transform);
return;
}
float moveX = Input.GetAxis("Horizontal") * Time.deltaTime * 110.0f;
float moveZ = Input.GetAxis("Vertical") * Time.deltaTime * 4f;
transform.Rotate(0, moveX, 0);
transform.Translate(0, 0, moveZ);
}
}
}
八、第二次Play
这时候测试人头上会有随机数字的名字
九、场景
新建一个空物体命名为SceneScript。并新建一个SceneScript.cs挂在这个物体上。也挂载NetworkIdentity代码。
创建一个button 和 text
修改PlayerScript代码
添加以下内容
private SceneScript sceneScript;
void Awake()
{
//allow all players to run this
sceneScript = GameObject.FindObjectOfType<SceneScript>();
}
[Command]
public void CmdSendPlayerMessage()
{
if (sceneScript)
sceneScript.statusText = $"{playerName} says hello {Random.Range(10, 99)}";
}
[Command]
public void CmdSetupPlayer(string _name, Color _col)
{
//player info sent to server, then server updates sync vars which handles it on all clients
playerName = _name;
playerColor = _col;
sceneScript.statusText = $"{playerName} joined.";
}
public override void OnStartLocalPlayer()
{
sceneScript.playerScript = this;
//. . . . ^ new line to add here
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
SceneScript代码如下:
using Mirror;
using UnityEngine;
using UnityEngine.UI;
namespace QuickStart
{
public class SceneScript : NetworkBehaviour
{
public Text canvasStatusText;
public PlayerScript playerScript;
[SyncVar(hook = nameof(OnStatusTextChanged))]
public string statusText;
void OnStatusTextChanged(string _Old, string _New)
{
//called from sync var hook, to update info on screen for all players
canvasStatusText.text = statusText;
}
public void ButtonSendMessage()
{
if (playerScript != null)
playerScript.CmdSendPlayerMessage();
}
}
}
十、第三次play
这个时候play
点击右上角button会有个says hello
十一、添加武器切换
添加以下代码到PlayerScript.cs
private int selectedWeaponLocal = 1;
public GameObject[] weaponArray;
[SyncVar(hook = nameof(OnWeaponChanged))]
public int activeWeaponSynced = 1;
void OnWeaponChanged(int _Old, int _New)
{
// disable old weapon
// in range and not null
if (0 < _Old && _Old < weaponArray.Length && weaponArray[_Old] != null)
weaponArray[_Old].SetActive(false);
// enable new weapon
// in range and not null
if (0 < _New && _New < weaponArray.Length && weaponArray[_New] != null)
weaponArray[_New].SetActive(true);
}
[Command]
public void CmdChangeActiveWeapon(int newIndex)
{
activeWeaponSynced = newIndex;
}
void Awake()
{
// disable all weapons
foreach (var item in weaponArray)
if (item != null)
item.SetActive(false);
}
添加武器切换代码,要放在!isLocalPlayer检查后面
void Update()
{
if (!isLocalPlayer)
{
// make non-local players run this
floatingInfo.transform.LookAt(Camera.main.transform);
return;
}
float moveX = Input.GetAxis("Horizontal") * Time.deltaTime * 110.0f;
float moveZ = Input.GetAxis("Vertical") * Time.deltaTime * 4f;
transform.Rotate(0, moveX, 0);
transform.Translate(0, 0, moveZ);
if (Input.GetButtonDown("Fire2")) //Fire2 is mouse 2nd click and left alt
{
selectedWeaponLocal += 1;
if (selectedWeaponLocal > weaponArray.Length)
selectedWeaponLocal = 1;
CmdChangeActiveWeapon(selectedWeaponLocal);
}
}
玩家按鼠标右键 →
本地修改 selectedWeaponLocal(临时记录) →
调用 CmdChangeActiveWeapon(将本地值发给服务器) →
服务器修改 activeWeaponSynced(权威同步变量) →
Mirror 自动将 activeWeaponSynced 同步到所有客户端 →
所有客户端触发 OnWeaponChanged(根据同步值切换武器模型)
十二、武器制作
双击player进入预制体进行制作
添加空物体WeaponHolder,位置旋转设置0,0,0
添加Cube子物体并移除Cube碰撞体
重命名Cube为Weapon1并调整参数
复制Weapon1并重命名为Weapon2
修改参数
点击Player把武器拖给代码位置
十三、第四次Play
运行后点击鼠标切换武器。
十四、小调整(新增SceneReference)
因为使用 GameObject.Find() 可能无法保证找到SceneScript。NetworkIdentity场景对象被禁用,它们会被禁用,直到玩家处于“就绪”状态(就绪状态通常在玩家生成时设置)。
创建一个名为 SceneReference.cs 的新脚本
using UnityEngine;
namespace QuickStart
{
public class SceneReference : MonoBehaviour
{
public SceneScript sceneScript;
}
}
打开SceneScript.cs并添加以下变量。
public SceneReference sceneReference;
现在,在 Unity 场景中创建一个游戏对象,将其命名为 SceneReference,并添加新脚本。在两个游戏对象上将引用设置为彼此。因此,SceneReference 可以与 SceneScript 通信,SceneScript 可以与 SceneReference 通信。
打开PlayerScript.cs并将 Awake 函数覆盖为以下内容:
void Awake()
{
//allows all players to run this
sceneScript = GameObject.Find(“SceneReference”).GetComponent<SceneReference>().sceneScript;
}
Mirror的核心用法
public class Player : NetworkBehaviour{
// 自动同步,只能在服务器上被修改
[SyncVar] public int health = 100;
// 列表
SyncList<Item> inventory = new SyncList<Item>();
// 只有服务器或客户端执行
[Server] void LevelUp() {}
[Client] void Animate() {}
void Update(){
// 运行时检查是在服务器还是在客户端
if (isServer) Heal();
if (isClient) Move();
}
// 零开销远程调用
[Command] void CmdUseItem(int slot) {} // 客户端到服务器
[ClientRpc] void RpcRespawn() {} // 服务器到所有的客户端
[TargetRpc] void Hello() {} // 服务器到单个客户端
}
- RPC( Remote Procedure CallsRemote Procedure Calls):跨网络执行操作,也叫远程过程调用,Mirror的网络系统中的RPC分为两种:Command和ClientRpc。
- Command:在客户端被调用,在服务端运行。
- ClientRpc:在服务端被调用,在所有客户端运行。
- TargetRpc:在服务端被调用,在某个客户端运行。
- SyncVars
- 一个特性,被修饰的字段会自动同步,且其只能在服务端被修改,同步方向为从服务端到客户端。当一个游戏对象被生成后,或者有新的玩家加入时,他们就会得到最新的状态信息。该特性带有一个hook(SyncVar Hooks)参数,在服务器上的值被修改时在所有的客户端调用这个钩子函数。
- 同样的还有SyncLists和SyncDictionary等,不过用法略微不同,详情请看 SyncLists 以及 SyncDictionary
- NetworkManager
网络管理器,管理网络游戏对象的组件,其一些主要的功能包括:- 游戏状态管理
- 游戏对象的生成管理
- 场景管理
- 调试信息
- 自定义等
以下是该组件的生命周期:
- NetworkBehaviour
- 网络行为组件,可以在这里实现联网对象的网络行为,做法是新建一个类继承这个组件。配合NetworkIdentity组件一起使用,一般在这里使用Command、ClientRpc、SyncVars等高级API(high-level API),以下是这个组件的生命周期:
负责处理游戏对象在网络中的数据传输,确保游戏状态的一致性和实时性。