-
-
-
Exception: no aot assembly found. please run HybridCLR/Generate/All or HybridCLR/Generate/AotDlls to generate aot dlls before runing HybridCLR/Generate/MethodBridge
-
CopyStrippedAOTAssemblies
-
这个问题是因为Unity 的构建缓存(Library/Bee)
-
删除项目中的Library/Bee强制重新编译
-
HybridCLRData\AssembliesPostIl2CppStrip\StandaloneWindows64无法生成dll
-
-
无法正确检测catalog变化从而进行更新
-
Play Mode Script要使用Existing build,否则不会检测远程的catalog
-
-
启动闪退
-
清除Addressables缓存,重新生成
-
-
自定义宏在编辑器中不生效
-
将构建平台切换到PC
-
Regenerate Project Files
-
-
网络变量热更问题
-
对于客户端和服务端NetworkManger的hash值必须相同,否则会misMatch无法正常连接
-
双端的NetworkVariable必须统一持有
-
-
AOT泛型问题
-
HybirdCLR有两种解决方案(其实还有一种商业方案,只是相比第二种节约内存)
-
在AOT程序集中调用一次这个泛型,例如 new List<Type>
-
HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly加载该容器所在程序集
-
-
-
彻底清理Addressables脏数据
-
清理PersistentDataPath中的addressable
-
清理ServerData中的addressable
-
清理Library中的addressable
-
Build -> Clean All
-
-
Unable to open archive file: “xxxx/xxxx/aa/xxxx” ,Failed to read data for the AssetBundle “aa/ xxxx”
-
需要将Addressable Group全设置为Remote,包括Default Local Group
-
-
-
Netcode快速入门
-
下载Netcode For GameObject插件
-
新建空物体,加组件Network Manager
-
选择Unity Transport
-
DefaultNetworkPrefabs存放需要实例化的预制体
-
Player存放玩家预制体,开始时生成
-
-
使用Network相关组件在要同步的物体上
-
C#代码要继承NetworkBehavior
-
重要属性:IsOwner,IsServer
-
重要特性:[ServerRpc(RequireOwnership = false)] , [ClientRpc]
-
重要类(自动同步变量数据):NetworkVariable<float> speed = new NetworkVariable<float>(2);
// 需要GameManager 开始时Editor作为服务端,exe作为客户端
public class GameManager : MonoBehaviour
{
void Start()
{
#if UNITY_EDITOR
NetworkManager.Singleton.StartServer();
#else
NetworkManager.Singleton.StartClient();
#endif
}
}
public class PlayerControl : NetworkBehaviour
{
public float speed = 2f;
void Update()
{
// 这里判断isOwner,否则多个窗口会同时执行操作
if (IsOwner)
{
float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");
Vector2 dir = new Vector2(x, y).normalized;
print(dir);
HandlerMovementServerRpc(dir);
}
}
// 加上此特性,表示客户端调用服务端操作
[ServerRpc(RequireOwnership = false)]
private void HandlerMovementServerRpc(Vector2 dir)
{
transform.Translate(new Vector3(dir.x, 0, dir.y) * speed * Time.deltaTime);
}
}
-
-
环境搭建
-
URP参数设置
-
自定义宏定义相关
-
Editor MenuItem自动打包服务端,客户端exe
public class BuildMenuItem
{
private static string buildPath = "Builds";
private static string ServerPath = "Server";
private static string ClientPath = "Client";
[MenuItem("Project/Build/All")]
public static void All()
{
// 注意先后顺序
Server();
Client();
}
[MenuItem("Project/Build/Server")]
public static void Server()
{
Debug.Log("开始构建服务端");
// 筛选场景,筛掉客户端相关场景
List<string> list = new List<string>(EditorSceneManager.sceneCountInBuildSettings);
for(int i = 0;i < EditorSceneManager.sceneCountInBuildSettings;++ i)
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(i);
if(scenePath != null && !scenePath.Contains("Client"))
{
Debug.Log("正在加载场景:" + scenePath);
list.Add(scenePath);
}
}
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions()
{
scenes = list.ToArray(), // 转成string[] scene
target = BuildTarget.StandaloneWindows64, // 打包目标平台Win64
subtarget = (int)StandaloneBuildSubtarget.Server, // 子目标平台Server
locationPathName = new DirectoryInfo(Application.dataPath).Parent.FullName + '/' + buildPath + '/' + ServerPath + "/Server.exe" // exe打包生成路径
};
BuildPipeline.BuildPlayer(buildPlayerOptions);
Debug.Log("完成构建服务端");
}
[MenuItem("Project/Build/Client")]
public static void Client()
{
Debug.Log("开始构建客户端");
List<string> list = new List<string>(EditorSceneManager.sceneCountInBuildSettings);
for (int i = 0; i < EditorSceneManager.sceneCountInBuildSettings; ++i)
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(i);
if (scenePath != null && !scenePath.Contains("Server"))
{
Debug.Log("正在加载场景:" + scenePath);
list.Add(scenePath);
}
}
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions()
{
scenes = list.ToArray(),
target = BuildTarget.StandaloneWindows64,
subtarget = (int)StandaloneBuildSubtarget.Player,
locationPathName = new DirectoryInfo(Application.dataPath).Parent.FullName + '/' + buildPath + '/' + ClientPath + "/Client.exe"
};
BuildPipeline.BuildPlayer(buildPlayerOptions);
Debug.Log("完成构建客户端");
}
}
-
-
客户端热更新(Addressable下载资源(资源热更)和热更Dll,HybirdCLR通过Dll进行代码热更)
-
将HybirdCLR包放入Package后Install
-
-
自定义程序集HotUpdate放入Setting进行Hybird编译,获取生成的HotUpdate.dll(需要一个cs脚本与自定义程序集在同一个文件夹)
-
构建服务端开始需要代码禁用HybridCLR.Editor.SettingsUtil.Enable = false,完成时再开启
-
解决AOT泛型问题
-
自定义生成DllBytes的方法(包含AOT和HotUpdate),需要提前在Setting中设置name,若生成的HotUpdate中没有自己想要的dll文件,则手动copy过去并重命名.bytes
[MenuItem("Project/Build/GenerateDllBytesFiles")]
public static void GenerateDllBytesFiles()
{
Debug.Log("开始创建DllBytes");
string sourceDllDir = System.Environment.CurrentDirectory + '/' + SettingsUtil.GetHotUpdateDllsOutputDirByTarget(EditorUserBuildSettings.activeBuildTarget);
string aotDllDir = Application.dataPath + "/Scripts/HotUpdate/DllBytes/AOT";
string hotUpdateDllDir = Application.dataPath + "/Scripts/HotUpdate/DllBytes/HotUpdate";
foreach (string dllName in SettingsUtil.AOTAssemblyNames)
{
File.Copy(sourceDllDir + '/' + dllName + ".dll", aotDllDir + '/' + dllName + ".dll.bytes", true);
}
foreach (string dllName in SettingsUtil.HotUpdateAssemblyNamesExcludePreserved)
{
File.Copy(sourceDllDir + '/' + dllName + ".dll", hotUpdateDllDir + '/' + dllName + ".dll.bytes", true);
}
AssetDatabase.Refresh();
Debug.Log("完成创建DllBytes");
}
-
自定义ClientUpdate方法,标准的HybirdCLR + Addressables热更新方法
[MenuItem("Project/Build/UpdateClient")]
public static void UpdateClient()
{
Debug.Log("开始构建客户端更新包");
CompileDllCommand.CompileDllActiveBuildTarget();
GenerateDllBytesFiles();
// Addressable更新包
// 拿到上一次的更新路径
string path = ContentUpdateScript.GetContentStateDataPath(false);
// 拿到Addressable的总配置文件
AddressableAssetSettings addressableAssetSettings = AddressableAssetSettingsDefaultObject.Settings;
// 根据配置文件和上一次的路径来进行update
ContentUpdateScript.BuildContentUpdate(addressableAssetSettings, path);
Debug.Log("完成构建客户端更新包");
}
-
IIs模拟资源服务器进行热更新
-
需要设置Project Setting的All downloads over HTTP为Allow,才能从IIS上访问下载资源
-
设置好MIME类型,.bundle .bytes .hash
-
创建Test程序,加载HotUpdate.dll.bytes进行反射热更新
public class HotUpdateTest : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Debug.Log("开始");
TextAsset textAsset = Addressables.LoadAssetAsync<TextAsset>("HotUpdate.dll").WaitForCompletion();
System.Reflection.Assembly.Load(textAsset.bytes);
Addressables.InstantiateAsync("GameObject").WaitForCompletion();
Debug.Log("结束");
}
-
-
创建VersionInfo,进入客户端优先更新显示版本信息
-
热更新UI界面
-
核心功能准备工作
-
创建服务端实体对象,用于后续测试玩家和服务端之间的AOI效果
-
在客户端显示延迟,服务端对象数量,其他客户端对象数量
-
-
热更新完善
-
断点传续
-
客户端热更新的检测是通过Check Catalog的变化来决定是否进行更新的,如果上一次客户端启动只是更新了catalog没有更新实际内容,则下一次启动就不会继续更新,导致游戏出错
-
使用Load和Save在本地存储一个State类,通过State类的succeed成员来判断是否需要继续进行更新,如果为false或者null(首次加载不存在State)则需要删除catalog,以便进行更新的catalog
-
-
-
服务端AOI
-
只加载周围区块(通常是九宫格)的服务端内容,以节省性能
-
使用容器存储客户端和服务端对象
-
Dictionary<Vector2Int, HashSet<NetworkObject>> chunkServerObjectDic
-
Dictionary<Vector2Int, HashSet<ulong>> chunkClientDic
-
-
当离开范围时,item.NetworkHide(clientA)
-
当进入范围时,item.NetworkShow(clientB)
-
-
客户端空间管理
-
四叉树空间管理
-
QuadTree存有RootNode,Node再包含四个更小的Node,递归创建
-
ClientMapManager管理四叉树
-
Dictionary<Vector2Int, TarrainController>管理各块Terrain,Controller封装了相关显隐加载方法及倒计时销毁(超出视角范围时先是隐藏,过段时间才会销毁)
-
流程:
-
利用得到的视野Planes切片,初始化周围起始区块
-
初始化区块使用bool CheckVisual(Bounds bounds),是在QuadTreeNode中递归调用检测来决定当前Terrain是否需要Enable(从整个rootNode开始递归检测)
-
bool CheckVisual(Bounds bounds) 不仅要检测相机视野内,为了保证游戏体验还要扩大检测包围盒的范围,使加载区域变宽,还要检测是否为相机周围的九宫格
-
-
每帧检测是否所有区块是否有需要销毁的,销毁的要从字典移除(移除清单用List缓冲一下)
-
-
-
第三人称角色控制
-
客户端,服务端启动逻辑梳理
-
分为ClientOnGameScene和ServerOnGameScene
-
加上GameSceneManager,IsClient则实例化ClientOnGameScene,IsServer则实例化ServerOnGameScene
-
-
程序集梳理
-
AOT(无需热更的程序集)
-
HotUpdate(热更,存放客户端)
-
Common(热更,存放客户端服务端公共部分)
-
Server(为热更程序集,实际不用热更,不用Addressable,因为需要引用的Common为热更程序集,所以Server也必须要为热更程序集)
-
-
PlayerContorller分块并处理AOI
-
使用partial将PlayerContorller类分为公共,客户端,服务端三块
-
客户端的移动逻辑要ServerRpc请求服务端进行计算
-
AOI逻辑同样在服务端处理
-
因为AOI在Server程序集,而PlayerController在Common程序集,无法直接使用AOI
-
要通过AOIUtility使用EventSysten来进行搭桥调用
-
-
-
状态机
-
角色Idle,Move等状态的表现均在状态机中实现
-
根运动利用OnAnimationMove配合animator.deltaPosition等来手动实现移动,而不是勾选RootMotion
-
这里使用Action<Vector3, Quaternion> action
-
在OnAnimatorMove中调用action?.Invoke(animator.deltaPosition, animator.deltaRotation);
-
在状态机中设定事件,如在MoveState中设Action作用characterController.Move(deltaVector);
-
在角色下线时要注意销毁StateMachine,否则还会update导致空引用
-
-
-
网络变量热更问题
-
有自定义类型网络变量时,用NetworkVariableSerializationBinder,让NetworkVariable识别如何读写复制这个新类型
-
因为NetworkVariable的变量类型必须在编译期确定,而热更新类型肯定是不能确定的,所以这里的热更新类型变量只能由服务端来处理同步(服务端不需要热更新,打包时会取消HybridCLR),客户端为了防止报错需要重写一下同步函数
// 这里直接修改源码也可以,这里采用不修改源码的方式
public class NetVariable<T> : NetworkVariable<T>
{
// 这里是构造函数,全部复用父类的就好
public NetVariable(T value = default,
NetworkVariableReadPermission readPerm = DefaultReadPerm,
NetworkVariableWritePermission writePerm = DefaultWritePerm) : base(value, readPerm, writePerm) { }
public override bool IsDirty()
{
// 这里客户端肯定是null,不重写是要报错的
if (NetworkVariableSerialization<T>.AreEqual == null) return false;
return base.IsDirty();
}
}
-
-
第三人称角色移动受到相机影响
-
我们有自己移动的Vector3方向,现在要得到相机的Vector3方向
-
float yEuler = mainCamera.transform.eulerAngles.y; 得到相机Y旋转角度
-
Vector3 newDir = Quaternion.Euler(new Vector3(0, yEuler, 0)) * dir; 使用欧拉角 * 方向得到旋转后的方向
-
-
现在需要在角色移动时,讲角色的朝向Smooth移到正确的位置
-
float tanRad = Mathf.Atan2(player.inputData.dir.x, player.inputData.dir.y); 得到目标弧度
-
float tanDeg = Mathf.Rad2Deg * tanRad; 转化成目标角度
-
nowEulerY = Mathf.SmoothDampAngle(nowEulerY, tanDeg, ref currentVelocity, 0.1f); 丝滑变换
-
-
-
-
-

评论(1)