最终效果
文章目录
最终效果前言关于Mirror导入Mirror插件
注意事项基本使用一、创建场景的NetworkManager网络管理器1、NetworkManager组件2、NetworkManagerHUD组件3、KcpTransport组件
二、创建一个玩家1、为玩家添加NetworkIdentity组件2、为玩家添加NetworkTransform组件
三、添加玩家初始生成位置四、玩家控制1、NetworkBehaviour2、书写人物控制脚本
五、同步摄像机六、同步不同角色的名字和颜色修改七、同步动画八、同步子弹方法一方法二方法三
九、聊天功能十、场景同步切换十一、重新绘制一个HUD界面十二、查找服务器十三、角色死亡复活十四、自己编一个network manager十五、AOI迷雾效果实现十六、开房间的功能
连接线上服务器(待续)1、linux服务器2、然后就可正常打包客户端,连接服务器游玩了
后续源码参考完结
前言
终于来了,之前很多人私信我,想看关于如何实现多人游戏的流程,这不就来了。关于Mirror插件其实我已经关注很久了,最近才有时间把它整理出来。
有些同学做了一个单机版的小Demo,想改成局域网多人联机版,要处理好多复杂的同步问题,比如物理碰撞、状态同步等等,这个对于Unity萌新来说,不大友好。有没有什么好用的网络库可以让开发更高效呢?有,那就是:Mirror!
注:在Unity 5.1 ~ Unity2018中你可以使用UNet(全称Unity Networking),到Unity 2019之后UNet就被废弃了,Mirror就是来替代UNet的。你在网上搜到的Unity Netwoking的教程就是UNet,它已经过时了,不要再使用UNet了!
关于Mirror
Mirror是Unity的高级网络 API,支持不同的低级传输(UDP、TCP、KCP等等)。 使用 Mirror,客户端、服务端是在同一个工程中的,这就是为什么它叫Mirror。也就是说它没有一个独立的服务端,而是由一台客户端作为Host,它既是客户端又是服务端,其他客户端连接这台Host客户端。
Mirror是一个简单高效的开源的unity多人游戏网络框架,Mirror在Unity商店中是免费的。
官方文档地址:https://mirror-networking.gitbook.io/docs
导入Mirror插件
建议从Asset Store上下载Mirror版本,因为GitHub的版本不一定稳定。
Asset Store地址:https://assetstore.unity.com/packages/tools/network/mirror-129321 将Mirror插件添加到自己的账号中,然后回到Unity,在Package Manager中就可以下载了
注意事项
场景内所有挂在了你的代码的物体都会默认添加NetworkIdentity,但network manager组件与network identity组件放在一个物体上会报错。一般游戏内除了包含NetworkManager组件的物体都要挂载NetworkIdentity组件,包括即将孵化的。
如果物体内有network manager组件但是没有Kcp transport组件,会报错。
如果场景内有多个network manager组件,会报错。一个场景中只能有一个激活的NetworkManager(它是单例模式的)。
如果角色预制体拖不进player prefab栏,可能是没有挂在network identity组件。
角色代码必须有if (!isLocalPlayer) return;否则。。。后果自己知道。
基本使用
一、创建场景的NetworkManager网络管理器
在起始场景中创建一个空游戏对象,然后添加新创建的网络管理器组件(NetworkManager、NetworkManagerHUD、KcpTransport组件)。 KcpTransport组件挂载在networkManager的transport上 并配置Scene场景,offline和online是离线界面和游戏界面,比如说我们新建一个offline场景,在里面放一个network manager的network managerHUD,然后再新建一个online场景,把他们都注册到build setting(生成设置)里的build里的场景栏中(拖进去),offline在上。然后我们进入offline场景,运行,点击host,便会进入online(在线场景)。
1、NetworkManager组件
NetworkManager是管理多个客户端连接的组件。它是多人联机游戏的核心控制组件。一个场景中只能有一个激活的NetworkManager(它是单例模式的)。
连接的服务端IP地址在NetworkManager中进行设置,Max Connections是最大连接数。(注意:任何一个客户端都可以同时是一个服务端)
2、NetworkManagerHUD组件
NetworkManagerHUD组件是下面这个GUI的逻辑,通过它我们可以方便地进行测试。
3、KcpTransport组件
Mirror帮我们封装了各种不同等级的传输协议(各种Transport组件),常用的是KcpTransport和TelepathyTransport。 KcpTransport是使用可靠UDP协议,TelepathyTransport是使用TCP协议。 Transport组件中可以设置端口号、最大延迟等等参数:
二、创建一个玩家
1、为玩家添加NetworkIdentity组件
创建一个玩家物体Player,为玩家添加networkIdentity组件,NetworkIdentity组件提供了游戏物体在网络中的唯一标识(ID)。
游戏运行过程中,我们在Inspector视图中预览到NetworkIdentity的信息。 一般游戏内除了包含NetworkManager组件的物体都要挂在此组件,包括即将孵化的。 这东西只有俩选项:
一个勾选框Server Only,意思是只有服务端能操作,大家根据自己的需要勾选。第二个是visible,里面有三个选项:默认(Default)、强制隐藏(Force Hidden)、强制显示(ForceShown),个人感觉没啥用,大家默认就行。
只有挂载了networkIdentity,网络中枢才能识别到这个物件,并对之进行同步。接下来将Player作为一个预制体保存,并在场景中删除,后拖拽预制体到网络中枢(networkManager)的Player Prefab插槽中,以后它的产生就完全依靠网络中枢在连接到主机后自动生成。 自动创建播放器(Auto Create Player):默认勾选,勾选的话当连接服务器时会自动生成上面的“玩家预制件”。
注:如果角色预制体托不进player prefab栏,可能是没有挂在NetworkIdentity组件
2、为玩家添加NetworkTransform组件
为玩家添加NetworkTransform,NetworkTransform组件会通过网络自动同步position、rotation和scale。带NetworkTransform组件的物体必须也带NetworkIdentity组件。
Mirror 目前提供2种Network Transform:
Reliable:低带宽,与Rpcs/Cmds/等相同的延迟。
Unreliable:高带宽,极低延迟
使用Reliable,除非需要超低延迟。
添加并勾选networkTransport的Client Authority属性。 我们可以设置Positon、Rotation、Scale同步的敏感度(新版本貌似只能设置Rotation了) 为了让同步有一个平滑效果(不会一卡一卡的),我们可以勾选平滑差值(当然默认就是勾选的)
注:后面我们会利用玩家body的Scale进行翻转,这里给body也加上Network Transform代码,记勾选SyncScale.
三、添加玩家初始生成位置
创建几个空物体作为玩家的初始生成位置,添加Network Start Position脚本,并将该物体拖动到合适的位置。 并在NetworkManager中选择随机(Random)或者轮询(Round Robin)的出生点选择方式。
1.Random:生成为随机(可能相同的生成位置将被两个或更多玩家使用)
2.Round Robin:循环(使用每个可用位置,直到客户端数超过生成点数)。
效果
四、玩家控制
1、NetworkBehaviour
玩家控制脚本需要继承NetworkBehaviour。在书写玩家控制脚本之前,我觉得有必要介绍一下NetworkBehaviour。
NetworkBehaviour脚本处理具有NetworkIdentity组件的游戏对象,NetworkBehaviour的子类中可以处理高级API功能,例如Commands、ClientRpc's、SyncEvents、SyncVars。
NetworkBehaviour组件具有以下功能:
Synchronized variables:同步变量Network callbacks:网络回调Server and client functions:服务端和客户端函数Sending commands:发送命令Client RPC calls:客户端远程过程调用Networked events:网络事件
NetworkBehaviour提供了一些 网络回调:
OnStartServer回调 这个回调函数只在服务端调用,当在服务端生成一个游戏对象,或者服务端启动时被回调。OnStopServer回调 这个回调函数只在服务端调用,当在服务端销毁一个游戏对象,或者服务端停止时被回调。OnStartClient回调 这个回调函数只在客户端调用,当客户端生成一个游戏对象,或者客户端连接到服务端时被回调。OnStopClient回调 这个回调函数只在客户端调用,当服务端销毁一个游戏对象时被回调。OnStartLocalPlayer回调 这个回调函数只在客户端调用,当客户端生成一个玩家对象时被回调。OnStartAuthority回调 这个回调函数只在客户端调用,当游戏对象拿到控制权时。OnStopAuthority回调 这个回调函数只在客户端调用,当游戏对象失去控制权时。
标记服务端函数或客户端函数: 在NetworkBehaviour中,我们可以使用下面这些注解对函数进行标注。
[Server]、[ServerCallback]表示函数为服务端函数,只在服务端执行;[ServerCallback],它与[Server]一样,只能在服务端调用,只是没有Warning输出而已。[Client]、[ClientCallback]表示为客户端函数,只在客户端执行。
Command 命令: 使用[Command]注解对函数进行标记,表示这个函数是由客户端调用,由服务端来执行。 被[Command]标记的函数约定以Cmd开头。
Client RPC 客户端远程过程调用: 使用[ClientRpc]注解对函数进行标记,表示这个函数是由服务端调用,在所有与服务端连接的客户端执行。 被[ClientRpc]标记的函数约定以Rpc开头。
TargetRpc在指定客户端远程过程调用: TargetRpc在服务端调用,在指定的与服务端连接的客户端执行,该方法至少有一个,NetworkConnection的形参,用来确定是在哪一个客户端执行,并且方法名称以"Target"开头。
比如某个客户端角色得分加分
NetworkIdentity netIdentity = GetComponent TargetShowMessage(netIdentity.connectionToClient, 1); [TargetRpc] private void TargetShowMessage(NetworkConnection target, int count) { sumCount += count;//加分 } Networked Events 网络事件(观察者模式): 类似于Client RPC调用,不同之处是它触发的是事件。 使用[SyncEvent]对事件进行标记。被[SyncEvent]标记的事件变量必须以Event开头,例EventTakeDamage。例子可以参见官方手册:https://mirror-networking.gitbook.io/docs/guides/synchronization/syncevent Mirror提供的函数注解如下(部分注解我们上面已做了介绍),具体的注解可以参见Mirror官方手册:https://mirror-networking.gitbook.io/docs/guides/attributes 2、书写人物控制脚本 网络同步需要注意的一些事情: 1.需要用到联网功能的脚本中都要添加using Mirror来使用相应API,并且继承NetworkBehaviour而不是MonoBehaviour。 2.涉及到玩家输入时,首先先要进行isLocalPlayer的判断,通过islocalplayer来判断是否具有当前对象的权限 为控制游戏对象,添加一个简单的人物控制脚本为PlayerControl.cs,继承NetworkBehaviour。 其中移动的同步会自动通过NetworkTransform进行同步,所以我们只需对本地坦克进行控制即可。 using UnityEngine; using Mirror; public class PlayerControl : NetworkBehaviour //MonoBehaviour --> NetworkBehaviour { private Rigidbody2D rb; // 刚体组件 void Start() { rb = GetComponent } //速度:每秒移动5个单位长度 public float moveSpeed = 5; void Update() { // isLocalPlayer是父类NetworkBehaviour的属性,用于判断当前NetworkBehaviour对象是否为本地对象; if (!isLocalPlayer) return; //不应操作非本地玩家 Move(); } void Move() { //通过键盘获取水平轴的值,范围在-1到1 float horizontal = Input.GetAxisRaw("Horizontal"); rb.velocity = new Vector2(horizontal * moveSpeed, rb.velocity.y); // 设置刚体速度 if (horizontal != 0) { transform.GetChild(0).localScale = new Vector3(-horizontal, 1, 1); // 翻转角色 } } } 效果 五、同步摄像机 对于在每个客户端独立生成的对象(这里以每位玩家的camera为例),需要将start方法修改为OnStartLocalPlayer(),这样可以避免多个客户端的摄像机被修改为同一台。 OnStartLocalPlayer:仅在client执行,当脚本所在物体为玩家角色时调用,用来设置跟踪相机,角色初始化等 public override void OnStartLocalPlayer() { rb = GetComponent //摄像机与角色绑定 Camera.main.transform.SetParent(transform); Camera.main.transform.localPosition = new Vector3(0, 0, Camera.main.transform.position.z); } 效果 六、同步不同角色的名字和颜色修改 同步变量需要添加同步变量的标记[SyncVar(hook=nameof(FunctionExecOnClient))],当同步变量发生变化时就会调用后面的FunctionExecOnClient方法 当服务器的场景中的一个SyncVar的值发生变化时,就同步给其它所有客户端。 对于同步变量的修改,使用[Command]标记(针对方法的标记,方法名以Cmd开头) using TMPro; public TMP_Text nameText; //需要把name和颜色同步给其他玩家,添加同步变量的标记[SyncVar(hook=nameof(FunctionExecOnClient))] [SyncVar(hook = nameof(OnPlayerNameChanged))] public string playerName; [SyncVar(hook = nameof(OnPlayerColorChanged))] private Color playerColor; //申明OnPlayerNameChanged和OnPlayerColorChanged这两个方法 //第一个变量(oldstr)是同步变量修改前的值,第二个(newstr)是同步变量修改后的值 private void OnPlayerNameChanged(string oldstr, string newstr) { nameText.text = newstr; } private void OnPlayerColorChanged(Color oldCor, Color newCor) { nameText.color = newCor; } void Update() { if (!isLocalPlayer) return; //不应操作非本地玩家 Move(); if (Input.GetKeyDown(KeyCode.Space)) { //随机生成颜色和名字 ChangedColorAndName(); } } public override void OnStartLocalPlayer() { //。。。 //开始就随机生成颜色和名字 ChangedColorAndName(); } //player 的随机名称和颜色 private void ChangedColorAndName() { //随机名称和颜色 var tempName = $"Player{Random.Range(1, 999)}"; var tempColor = new Color(Random.Range(0, 1f), Random.Range(0, 1f), Random.Range(0, 1f), 1); //同步变量进行修改 CmdSetupPlayer(tempName, tempColor); } //对于同步变量的修改,使用[Command]标记(针对方法的标记,方法名以Cmd开头) //通过这个方法同时对name和颜色进行修改 [Command] private void CmdSetupPlayer(string name, Color color) { playerName = name; playerColor = color; } 效果 七、同步动画 挂载Network Animator组件 private Animator anim; // 动画组件 anim = gameObject.GetComponentInChildren public override void OnStartLocalPlayer() { //。。。 anim = gameObject.GetComponentInChildren } void Update() { if (!isLocalPlayer) return; //不应操作非本地玩家 //。。。 //攻击动画控制 if (Input.GetMouseButtonDown(0)) { anim.SetTrigger("isAttack"); anim.SetBool("isIdle", false); }else{ anim.SetBool("isIdle", true); } } 八、同步子弹 bomb就是普通的炸弹预制体 方法一 [ClientRpc]关键字,服务端可以向所有的连接的客户端发送同步指令,方法名也需要Rpc开头。 public GameObject bomb;//炸弹预制体 void Update() { if (!isLocalPlayer) return; //不应操作非本地玩家 //。。。 //生成炸弹 if (Input.GetMouseButtonDown(1)) { Cmdshoot(); } } [Command] private void Cmdshoot() { RpcWeaponFire(); } [ClientRpc] private void RpcWeaponFire() { GameObject b = Instantiate(bomb, transform.position, Quaternion.identity); b.transform.Translate(1, 0, 0);//防止子弹撞到角色 b.GetComponent } 效果 方法二 NetworkManager最下面有个列表(Registered Spawnable Prefab),他是用来放游戏中需要孵化的物体的,比如说enemy(敌人),bullet(子弹)啊,都给它拖进去 ps:记得给炸弹添加Network Identity组件,不然拖不进去 public GameObject bomb;//炸弹预制体 void Update() { if (!isLocalPlayer) return; //不应操作非本地玩家 //。。。 //生成炸弹 if (Input.GetMouseButtonDown(1)) { Cmdshoot(); } } [Command] private void Cmdshoot() { GameObject b = Instantiate(bomb, transform.position, Quaternion.identity); b.transform.Translate(1, 0, 0);//防止子弹撞到角色 b.GetComponent Destroy(b, 2.0f);//两秒后删除 NetworkServer.Spawn(b);//服务器孵化,同步客户端 } 效果 问题 你会发现客户端给炸弹施加的AddForce力并没有效果,原因是我们没有添加同步刚体的组件,给炸弹添加Network Rigidbody 2D组件 效果 方法三 子弹预设上挂以下脚本 NetworkIdentity:因为炮弹也是一个网络对象,所以它需要NetworkIdentity组件; 炮弹的Transform信息不使用NetworkTransform进行同步,而是通过Rigibody刚体组件的力来使炮弹飞行,所以只需要同步一下力即可,在Projectile脚本中实现炮弹的逻辑。 开炮需要由服务端来执行, void Update() { // ... // 检测玩家是否按下射击键 if (Input.GetKeyDown(shootKey)) { // 调用射击命令 CmdFire(); } } // 这个方法在服务器端执行 [Command] void CmdFire() { // 在发射点位置和方向实例化炮弹预制体 GameObject projectile = Instantiate(projectilePrefab, projectileMount.position, transform.rotation); // 在网络上生成炮弹对象,使其在所有客户端同步 NetworkServer.Spawn(projectile); // 调用客户端RPC方法,在所有客户端上播放射击动画 RpcOnFire(); } // 这个方法在所有观察此坦克的客户端上执行 [ClientRpc] void RpcOnFire() { // 设置动画器的"Shoot"触发器,播放射击动画 animator.SetTrigger("Shoot"); } 炮弹脚本 炮弹也是一个网络对象,它的行为脚本也必须继承NetworkBehaviour, // Projectile.cs public class Projectile : NetworkBehaviour { } 炮弹预设实例化后,需要给Rigibody一个力,从而让炮弹向前飞行, // Projectile.cs void Start() { rigidBody.AddForce(transform.forward * force); } 炮弹需要有一个生命周期控制,超过5秒自动销毁,执行NetworkServer.Destroy(gameObject)来销毁对象,前面介绍了OnStartServer回调函数只在服务端调用,当在服务端生成一个游戏对象,或者服务端启动时被回调。 // Projectile.cs public override void OnStartServer() { Invoke(nameof(DestroySelf), destroyAfter); } [Server] void DestroySelf() { NetworkServer.Destroy(gameObject); } 我们看到这里有一个[Server]注解,它表示只有服务端可以调用此函数。 九、聊天功能 新增ChatController脚本 using UnityEngine; using Mirror; using TMPro; using UnityEngine.UI; public class ChatController : NetworkBehaviour { public TMP_InputField chatText;//输入框 public Button chatBtn;//发送按钮 public GameObject chatInfo;//聊天框内容预制体 public GameObject chatFrame;//聊天框 public PlayerController playerController ; [SyncVar(hook = nameof(OnChatTextStringChanged))] public string chatTextString; private void OnChatTextStringChanged(string oldstr, string newstr) { //添加聊天内容 GameObject ci = Instantiate(chatInfo); ci.GetComponent ci.transform.SetParent(chatFrame.transform); } void Awake() { chatBtn.onClick.AddListener(SendBtn); } public void SendBtn() { if (player != null) { playerController.CmdSendPLayerMessage(chatText.text); } } } 修改PlayerController,调用传送人物名字 private ChatController chatController; void Awake() { chatController = FindObjectOfType } public override void OnStartLocalPlayer() { //。。。 chatController.playerController = this; } [Command] public void CmdSendPLayerMessage(string message) { if (chatController != null) { chatController.chatTextString = playerName + "说:" + message; } } 绘制UI页面,记得添加Network Identity组件 记得给聊天的UI canvas挂载Network Identity脚本 效果 十、场景同步切换 新建三个场景NetworkManager对象上 新增ScenceController 代码,控制NetworkManagerHUD的显隐 using UnityEngine; using UnityEngine.UI; using Mirror; using UnityEngine.SceneManagement; public class ScenceController : MonoBehaviour { private void Update() { Scene scene = SceneManager.GetActiveScene(); //控制NetworkManagerHUD的显隐 if(scene.name == "Main"){ GetComponent }else{ GetComponent } } //开始游戏,场景切换 public void ButtonLoadScene() { SceneManager.LoadScene("SampleScene1"); } } Main场景为游戏开始页面,默认就放一个按钮,按钮调用ButtonLoadScene方法,Network Manager只需要在初始场景挂载即可(及Main场景),前面代码已经控制了NetworkManagerHUD的显隐,报错HUD视图你不在主场景显示 挂载对应的场景 SampleScene1和SampleScene2场景基本没啥区别,更前面的游戏页面一样,删除原本的NetworkManager对象,防止与主界面Main场景的冲突 新增ButtonChangeScene方法,控制游戏内的场景切换,方法挂载在SampleScene1和SampleScene2场景的场景切换按钮上 //同步切换场景 public void ButtonChangeScene() { if (isServer) { var scene = SceneManager.GetActiveScene(); NetworkManager.singleton.ServerChangeScene ( scene.name == "SampleScene1" ? "SampleScene2" : "SampleScene1" ); } else { Debug.Log("你不是host"); } } 效果 十一、重新绘制一个HUD界面 NetworkManagerHUD(需要配合Network Manager组件),他会自动绘制一个GUI: Host(主机):相当于又是服务器又是客户端。 Client:连接服务端,后面是服务端IP地址,localhost为本地端口,相当于自己连接自己。 Server Only:只当服务端。 但是,这个UI界面不太好看,所以我们一般不用这个组件,都会自己制作GUI。 在场景中新增三个按钮 新增MyNetworkManagerHUD 代码,挂载在游戏页面,实例代码 using UnityEngine; using UnityEngine.UI; using Mirror; public class MyNetworkManagerHUD : MonoBehaviour { private NetworkManager networkManager; // 创建 NetworkManager 对象 public GameObject btn; public GUISkin mySkin; private GameObject startHost;//启动网络主机按钮 private GameObject startClient;//启动网络客户端按钮 private GameObject stopHost;//停止网络主机或客户端按钮 void Awake() { networkManager = FindObjectOfType startHost = GameObject.Find("StartHost"); startClient = GameObject.Find("StartClient"); stopHost = GameObject.Find("StopHost"); startHost.GetComponent startClient.GetComponent stopHost.GetComponent } private void Update() { // GetComponent if (!NetworkClient.isConnected && !NetworkServer.active) // 检查客户端和服务器的连接状态 { startHost.SetActive(true); startClient.SetActive(true); stopHost.SetActive(false); } else { startHost.SetActive(false); startClient.SetActive(false); stopHost.SetActive(true); } } private void OnStartHost() { networkManager.StartHost(); // 启动网络主机 } private void StartClient() { networkManager.StartClient(); // 启动网络客户端 } private void StopHost() { networkManager.StopHost(); // 停止网络主机或客户端 } } 当然,原来的NetworkManagerHUD组件就没用了,可以删除了,记得同步删除前面控制NetworkManagerHUD显隐的代码 运行效果 十二、查找服务器 使用network discoveryHUD+network discovery组件替换原来的NetworkManagerHUD 这个network discovery组件也需要配合network manager使用,他可以列出局域网内所有的服务器,其中有个transport栏,我们需要把与network manager组件在一起的Kcp transport组件拖进去,不然无法运行。 network discoveryHUD,与networkmanagerHUD差不多,唯独少了一个Client,多了一个find server 作用是点击find server就会 把局域网内的所有比赛(服务器)列出来,但仅限局域网。 运行效果 十三、角色死亡复活 // 角色是否死亡的标志 [SyncVar(hook = nameof(OnIsDeadChanged))] public bool isDead = false; // 当角色死亡状态改变时的回调方法 void OnIsDeadChanged(bool oldValue, bool newValue) { if (newValue) { // 执行死亡逻辑,例如播放死亡动画、禁用角色控制等 Debug.Log("Player has died."); Destroy(gameObject, 2f); // 延迟2秒后销毁角色对象 } } void Update() { if (Input.GetKeyDown(KeyCode.T)) { CmdDestroyPlayerServer(); // 创建一个新的Camera对象 GameObject cameraObject = new GameObject("Main Camera"); // 添加Camera组件到对象上 Camera cameraComponent = cameraObject.AddComponent // 设置摄像机的位置和旋转 cameraComponent.transform.position = new Vector3(0, 0, -10f); cameraComponent.transform.rotation = Quaternion.identity; } } [Command] private void CmdDestroyPlayerServer() { isDead = true; } 其他地方书写复活方法(重新生成角色) public class GameManager : MonoBehaviour { void Update() { if (Input.GetKeyDown(KeyCode.R)) { //让当前客户端复活角色 NetworkClient.AddPlayer(); } } } 效果 十四、自己编一个network manager 这里简单实现一个创建角色的功能 using Mirror; using UnityEngine; using UnityEngine.UI; public class MyNetworkManager : NetworkManager//继承network manager类 { public InputField myname; // 输入玩家名称的InputField // 在服务器启动时调用 public override void OnStartServer() { Debug.Log("启动服务器"); // 启动服务器 base.OnStartServer(); // 注册CreateMMOCharacterMessage消息的处理方法 NetworkServer.RegisterHandler } public override void OnStopServer() { Debug.Log("关闭服务器"); // 关闭服务器 base.OnStopServer(); } public override void OnServerConnect(NetworkConnectionToClient conn) { Debug.Log("处理连接事件"); // 处理连接事件 base.OnServerConnect(conn); } public override void OnServerDisconnect(NetworkConnectionToClient conn) { Debug.Log("处理断开事件"); // 处理断开事件 base.OnServerDisconnect(conn); } // CreateMMOCharacterMessage消息结构体 public struct CreateMMOCharacterMessage : NetworkMessage { public string playername; // 玩家名称 } // 在客户端连接到服务器时调用 public override void OnClientConnect() { base.OnClientConnect(); // 在这里发送消息,或者您想要的任何其他地方 CreateMMOCharacterMessage characterMessage = new CreateMMOCharacterMessage { // playername = myname.text // 设置玩家名称为InputField中的文本 playername = "测试" // 设置玩家名称为InputField中的文本 }; NetworkClient.Send(characterMessage); // 发送消息给服务器 } // 创建角色的方法,在收到CreateMMOCharacterMessage消息时调用 // 参数conn:与服务器的连接;参数message:接收到的消息 void OnCreateCharacter(NetworkConnectionToClient conn, CreateMMOCharacterMessage message) { //PlayerPrefab是在Network Manager的Inspector中指定的, //但您可以在每个比赛中使用不同的预制体,例如: GameObject gameobject = Instantiate(playerPrefab); // 实例化玩家预制体 //根据游戏的需要,应用消息中的数据 Player player = gameobject.GetComponent player.playerName = message.playername; // 将玩家名称赋值给Playercode组件中的playername变量 //将此游戏对象添加为连接上的玩家的控制对象 NetworkServer.AddPlayerForConnection(conn, gameobject); } } 去除原来的NetworkManager,挂载自己写的代码,去除自动的创建角色的勾选,不然开始会创建两个主角 运行效果,正常 十五、AOI迷雾效果实现 将Spatial Hashing Interest Management组件与您的网络管理器相同的对象中 参数: 可见距离 多久显示出来 3d 2d切换 调试滑块 走近才显示物品 十六、开房间的功能 官方其实已经有个demo,在Mirror/Examples/MultipleMatches/Scenes/Main,我们可以先看看效果, 连接线上服务器(待续) 1、linux服务器 修改线上服务器ip和端口号 对应端口记得去服务器加安全组白名单 去除HUD脚本组件 书写代码,控制是服务器还是客户端 先打包一个服务器,所以先勾选是服务器 然后打包一个Linux服务器端程序 ps:没有的记得先去安装unity模块 把服务器端程序全部上传到服务器上 先把可执行文件添加权限 运行可执行程序 2、然后就可正常打包客户端,连接服务器游玩了 打包记得去除AppIsServer的勾选和修改Target Platfom为windows 连接成功 后续 Mirror的研究就暂且研究到这里,个人开发者可能没那么多金额购买好的服务器,本地局域网联机成为一种不错选择,后续我再补充这方面的知识,可以关注期待一下 源码 很遗憾源码我并不想免费分享,我也建议大家能自己手动去敲代码,逐步实现和理解每一块功能。项目实现所涉及的主要功能思路和代码我也已经毫无保留的分享在文章中了,当然,如果你真的需要的话,源码我也放出来了,收个辛苦费,就当作你对我不断创作的支持。力量随微,心暖人。您的每一次支持都是我创作的最大动力!!! https://gf.bilibili.com/item/detail/1107096120 参考 https://blog.csdn.net/linxinfa/article/details/118888064 完结 赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,以便我第一时间收到反馈,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦! 好了,我是向宇,https://xiangyu.blog.csdn.net 一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~