Mirror Quick Start Project
1、Basic scene setup
- 从asset store中导入mirror
- 创建新场景、创建空物体,重命名为NetworkManager,添加3个组件
-
- NetworkManager
-
- KCPTransport (TelepathyTransport is older, you do not need KCP and Telepathy)
-
- NetworkManagerHUD
- 由于只有一个场景,将这个场景放到NetworkManager组件中的Offline and Online scene,放入之前一定要先将场景放到build setting中
-
创建一个平面plane,位置:0,-1,0,;缩放:2,2,2;加上一个材质,可以添加mirror中包含的material
- 创建一个空物体,重置位置:0,0,0,命名为:spawnPoint,添加NetworkStartPosition组件,多复制几个,移动到不同的位置做出生点。
2、Player Movement
- 创建胶囊体capsule,重命名为:Player,重置位置,添加组件NetworkTransform 和Network Identity,勾选NetworkTransform中的Client Authority
- 创建脚本PlayerScript,添加到Player上,拖到project面板成预制体,从场景中删除Player
- 将Player预制体,拖到NetworkManager组件中的player prefab,将 spawn method设置为 Round Robin
-
编写脚本
-
using Mirror; using UnityEngine; // 脚本需要继承NetworkBehaviour public class PlayerScript : NetworkBehaviour { // OnStartLocalPlayer 类似star(),只在本地玩家上被调用 public override void OnStartLocalPlayer() { // 将主相机的设为玩家的子对象 Camera.main.transform.SetParent(transform); Camera.main.transform.localPosition = new Vector3(0, 0, 0); } void Update() { // 如果不是本地玩家,返回,不执行下面的代码 if (!isLocalPlayer) { 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); } }
打包,多开几个运行,会发现多个玩家(第一人称视角)
3、name and color
- 进入到玩家的预制体中,创建一个空对象重命名为FloatingInfo,调整位置(0,1.5,0),缩放x:-1(为了下面的名字显示正常)
- 在FloatingInfo中,GameObject - 3D Object - 3D Text,设置属性
- 更新脚本,运行,你会发现颜色不同,名字不同的玩家
using Mirror; using UnityEngine; // 脚本需要继承NetworkBehaviour public class PlayerScript : NetworkBehaviour { // 存放玩家姓名 public TextMesh playerNameText; // 存放悬浮姓名的父对象 public GameObject floatingInfo; // 存放 private Material playerMaterialClone; // SynVar用于同步服务器和所有客户端的变量,变量只能在服务器上更改 // hook允许你创建一个在客户端的方法,当客户端上接受到更新的信息后,执行这个方法 [SyncVar(hook = nameof(OnNameChanged))] public string playerName; [SyncVar(hook = nameof(OnColorChanged))] public Color playerColor = Color.white; // 名字发生改变 void OnNameChanged(string _old, string _new) { playerNameText.text = playerName; } // 颜色发生变化TODO void OnColorChanged(Color _old, Color _new) { playerNameText.color = _new; playerMaterialClone = new Material(GetComponent<Renderer>().material); playerMaterialClone.color = _new; GetComponent<Renderer>().material = playerMaterialClone; } // OnStartLocalPlayer 类似star(),只在本地玩家上被调用 public override void OnStartLocalPlayer() { // 将主相机的设为玩家的子对象 Camera.main.transform.SetParent(transform); Camera.main.transform.localPosition = new Vector3(0, 0, 0); // 设置floatingInfo的位置和缩放 floatingInfo.transform.localPosition = new Vector3(0, -0.3f, 0.6f); floatingInfo.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f); // 生成一个随机数和player做拼接,作为玩家名字 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,从客户端调用,但是在服务器上运行 [Command] void cmdSetupPlayer(string _name, Color _color) { //玩家信息发送到服务器,然后服务器更新所有客户端上SyncVar变量 playerName = _name; playerColor = _color; } void Update() { // 如果不是本地玩家,返回,不执行下面的代码 if (!isLocalPlayer) { // 让所有非本地玩家执行这个方面,名字朝向摄像机 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); } }
4、Scene script with canvas buttons (顺便测试synvar)
- 实现功能:点击按钮,能够改变玩家姓名,并且能够(相当于全世界广播)修改scene Text,变成玩家的名字+say hello+随机数字
- A scene networked object也可以访问和调整,创建一个空对象,重命名ceneScript,绑定新创建的脚本SceneScript.cs
- 创建一个按钮Button、一个Text、一个InputField,自己调整位置
-
在SceneScript.cs脚本中添加脚本,将public类型的变量拖拽好,在Button按钮中绑定sceneScript物体中的ButtonSendMessage()方法
using Mirror; using UnityEngine; using UnityEngine.UI; public class SceneScript : NetworkBehaviour { // canvas上canvasText中显示的内容 public Text canvasStatusText; // 存储PlayerScript.cs脚本 public PlayerScript playerScript; // 存储输入的名字 public Text playerNameText; // SynVar变量 [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.cs中的cmdSetupPlayer方法 if (playerNameText.text != "") { playerScript.cmdSetupPlayer("player:" + playerNameText.text, playerScript.playerColor); } else { playerScript.cmdSetupPlayer(playerScript.playerName, playerScript.playerColor); } // 执行玩家脚本中的CmdSendPlayerMessage方法 playerScript.CmdSendPlayerMessage(); } } }
- 在PlayerScript.cs中添加以下代码
-
// 存放SceneScript.cs脚本 private SceneScript sceneScript; void Awake() { //所有的玩家都执行这个方法,找到第一个激活带有SceneScript的object sceneScript = GameObject.FindObjectOfType<SceneScript>(); } // 发送消息的方法,里面的statusText也是synvar型变量 [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
- 测试代码,点击按钮,会发现在服务器修改synvar类型的变量,服务器会自动更新到所有的客户端
5、Weapon switching
- 功能:武器切换
- 在PlayerScript.cs中添加以下代码,武器切换的控制是在本地客户端,所以需要isLocalPlayer判断,武器变化也是需要所有客户端更新的,所以需要synvar,也要创建一个武器库
-
// 武器的状态 private int selectedWeaponLocal = 1; // 武器库 public GameObject[] weaponArray; // 存储激活的武器 [SyncVar(hook = nameof(OnWeaponChanged))] public int activeWeaponSynced = 1; // 新武器可见,旧武器不可见 void OnWeaponChanged(int _Old, int _New) { // 将旧武器(不为空,在范围内)设置为disable if (0 < _Old && _Old < weaponArray.Length && weaponArray[_Old] != null) weaponArray[_Old].SetActive(false); // 新武器(不为空,在范围内)设置成enable if (0 < _New && _New < weaponArray.Length && weaponArray[_New] != null) weaponArray[_New].SetActive(true); } // 改变激活的武器索引 [Command] public void CmdChangeActiveWeapon(int newIndex) { activeWeaponSynced = newIndex; } void Awake() { // 将所有武器设置不可见 foreach (var item in weaponArray) if (item != null) item.SetActive(false); }
- 制作两个武器,或者直接导入模型创建,打开玩家预制体,创建空对象WeaponsHolder,在WeaponsHolder放两个武器,拖到脚本上
-
-
打包,运行,你应该看到每个玩家交换武器,无论你的玩家装备了什么,都会在新加入的玩家身上自动显示(sync var and hook magic)
6、network scene object tweak(网络场景对象调整)
- 这里我们将做一个小的调整,因为使用GameObject.Find()可能无法保证找到网络标识场景对象。在下图中,您可以看到我们的NetworkIdentity场景对象被禁用,因为它们被禁用,直到玩家处于“就绪”状态(就绪状态通常在玩家生成时设置)。
-
因此,我们选择的解决方法是让GameObject.Find()获取非网络场景对象,它将这些网络标识场景对象作为预设变量。
- Create a new script called SceneReference.cs, and add this one variable.
-
using UnityEngine; public class SceneReference : MonoBehaviour { public SceneScript sceneScript; }
- 打开SceneScript.cs and 添加下面代码
-
public SceneReference sceneReference;
- 现在在Unity场景中创建一个空游戏对象,将其命名为SceneReference,并添加新脚本。在两个场景游戏对象上,设置彼此的引用。因此,SceneReference可以与SceneScript互相对话
-
打开PlayerScript.cs并将Awake功能改写为:
//所有的玩家都执行这个方法,找到第一个激活带有SceneScript的object //sceneScript = GameObject.FindObjectOfType<SceneScript>(); //通过SceneReference找到sceneScript sceneScript = GameObject.Find("SceneReference").GetComponent<SceneReference>().sceneScript;
7、Menu and scene switching
- 菜单和场景切换,这里我们将从带有play按钮的offline菜单转到带有back按钮和主机/加入HUD的游戏列表,再转到您的在线地图,然后再转到另一张供host切换的地图。
-
public void ButtonChangeScene() { //判断是否是服务器 if (isServer) { //返回当前的场景 Scene scene = SceneManager.GetActiveScene(); //如果场景的名字是MyScene,就切换到MyOtherScene、 if (scene.name == "MyScene") ///通过NetworkManager更改服务器场景和所有客户端场景。 NetworkManager.singleton.ServerChangeScene("MyOtherScene"); //如果不是就切换到MyScene else NetworkManager.singleton.ServerChangeScene("MyScene"); } else Debug.Log("You are not Host."); }
- 创建按钮,更改位置,添加onclick事件
-
然后将您的NetworkManager拖到您的项目中,使其成为一个预制件,可以将项目整理到文件夹中,一个文件夹用于脚本、预制、场景、纹理等:
-
保存,然后复制您的MyScene,重命名以生成Menu、GamesList和MyOtherScene,然后将它们添加到build settings中,首先是Menu。
-
打开 Menu场景,删除一些物体,在这里,你可以添加分数、联系人、新闻等,创建一个空对象Menu,绑定创建的Menu.cs脚本,编写脚本,对按钮绑定事件
-
using UnityEngine; using UnityEngine.SceneManagement; public class Menu : MonoBehaviour { // 点击按钮,执行LoadScene,加载GamesList场景。 public void LoadScene() { SceneManager.LoadScene("GamesList"); } }
-
打开GamesList场景,删除一些东西,保留NetworkManager预制体,创建空对象,绑定创建GamesList.cs的脚本,添加代码,在按钮上绑定事件
-
using UnityEngine; using UnityEngine.SceneManagement; public class GamesList : MonoBehaviour { public GameObject netM; // 点击按钮,执行LoadScene,加载Menu场景。 public void LoadScene() { // 删除NetworkManage,在切换场景的时候NetworkManage是不销毁的 Destroy(netM); SceneManager.LoadScene("Menu"); } }
-
打开MyOtherScene场景,修改一些东西,看起来和MyScene场景不一样,作为第二张地图
-
打开在project面板中的NetworkManager预制体,将离线场景和在线场景更改,会自动同步所有在场景中的NetworkManager预制件
-
构建并运行,按菜单上的Play进入GamesList,然后单击Host(对于玩家1)。对于玩家2,按菜单上的Play,然后按GamesList上的client connect。
现在,主机可以在地图1和地图2之间更改场景,如果有人断开或停止游戏,将加载菜单场景以重新开始。 - 问题:每次修改场景,玩家的名字、颜色都会变化
8、Weapon firing
- 射击,打开玩家预制体,每把武器都场景一个空对象,重命名,作为武器的子物体
- 创建脚本Weapon.cs,编写脚本,绑定到每把武器上
-
using System.Collections; using System.Collections.Generic; using UnityEngine; 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; }
- 创建两种子弹,在这里用的是正方体和球体两种,缩放scale为0.2, 0.2, 0.2,,变成预制体,拖到脚本中
-
在SceneScript.cs中,添加以下代码
-
// 显示弹药量的Text public Text canvasAmmoText; // 更新弹药量Text方法 public void UIAmmo(int _value) { canvasAmmoText.text = "Ammo: " + _value; }
- 在canvas中创建一个Text,拖拽到SceneScript中. 在MyScene(地图1)和MyOtherScene(地图2)上都这样做,因为我们还没有链接或预制画布和场景脚本来自动更新每个地图上的更改。
-
打开PlayerScript.cs, 添加以下变量:
-
// 存储weapon组件 private Weapon activeWeapon; // 射击的时间间隔 private float weaponCooldownTime;
- 在OnWeaponChanged方法中,添加下面代码
-
void OnWeaponChanged(int _Old, int _New) { // 将旧武器(不为空,在范围内)设置为disable if (0 < _Old && _Old < weaponArray.Length && weaponArray[_Old] != null) weaponArray[_Old].SetActive(false); // 新武器(不为空,在范围内)设置成enable if (0 < _New && _New < weaponArray.Length && weaponArray[_New] != null) weaponArray[_New].SetActive(true); // 获取Weapon组件 activeWeapon = weaponArray[activeWeaponSynced].GetComponent<Weapon>(); // 如果是本利玩家,显示新弹药量 if (isLocalPlayer) sceneScript.UIAmmo(activeWeapon.weaponAmmo); }
- 在Awake()方法中,在末尾添加
-
// 所有的玩家会执行一下代码,如果武器索引在length中,并且武器不为空 if (selectedWeaponLocal < weaponArray.Length && weaponArray[selectedWeaponLocal] != null) { // 获取组件 activeWeapon = weaponArray[selectedWeaponLocal].GetComponent<Weapon>(); // 显示弹药量 sceneScript.UIAmmo(activeWeapon.weaponAmmo); }
- 在 Update()中, 末尾添加
if (Input.GetButtonDown("Fire1")) { // 如果武器不为空并且时间大于射击冷却时间并且弹药量大于0,射击 if (activeWeapon && Time.time > weaponCooldownTime && activeWeapon.weaponAmmo > 0) { // 更改射击冷却时间 weaponCooldownTime = Time.time + activeWeapon.weaponCooldown; // 弹药量-1 activeWeapon.weaponAmmo -= 1; // 更新弹药量 sceneScript.UIAmmo(activeWeapon.weaponAmmo); // 射击 CmdShootRay(); } }
- 在 Update()后面创建两个方法
-
[Command] void CmdShootRay() { //告诉所有客户端,这个这个玩家射击 RpcFireWeapon(); } [ClientRpc] void RpcFireWeapon() { //生成子弹 GameObject bullet = Instantiate(activeWeapon.weaponBullet, activeWeapon.weaponFirePosition.position, activeWeapon.weaponFirePosition.rotation); // 速度 bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * activeWeapon.weaponSpeed; // 子弹销毁 Destroy(bullet, activeWeapon.weaponLife); }