You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中如何基于属性自动添加预处理指令实现客户端/服务端代码剥离?

问题描述

我开发了一个极简的多人游戏网络代码框架,客户端与服务端代码共存于同一类中,类实例同时运行在服务端与客户端。这些类中需同步的属性可通过可配置的[SynchronizeOverNetwork]标记:

[SynchronizeOverNetwork(Authority = Authority.Server)]
public ReconcileMoveData ReconcileMoveData {
  // Code should not appear on server, but on client
  get => new ReconcileMoveData {
    Position = transform.position,
    Rotation = transform.rotation.eulerAngles.z,
    LinearVelocity = _body.velocity,
    AngularVelocity = _body.angularVelocity,
  };
  // Code should not appear on client, but on server
  set {
    transform.position = value.Position;
    transform.rotation = Quaternion.Euler(new Vector3(0, 0, value.Rotation));
    _body.velocity = value.LinearVelocity;
    _body.angularVelocity = value.AngularVelocity;
  }
}

在游戏初始化阶段我会识别这些属性并进行全局管理,根据Authority的设置(Authority.Server或Authority.Client)决定属性的get/set方法仅在服务端或客户端执行。为避免代码分析问题(服务端代码永不发布)并减小客户端代码体积,我希望在客户端构建时剥离服务端代码,服务端构建时剥离客户端代码。Unity提供了UNITY_SERVER预处理指令用于标记服务端专属代码,但手动为每个get/set添加该指令易出错、繁琐且冗余。请问是否可通过预处理构建步骤(或其他方式),基于属性配置自动添加这些指令(不修改源文件,仅处理编译前的代码),或通过其他方式标记代码以实现自动剥离?

解决方案

以下是几种可行的实现思路,按推荐优先级排序:

1. Roslyn代码分析器(编译阶段自动注入指令)

这是最精准且无侵入的方案,利用Roslyn的编译时语法树修改能力,完全不需要手动修改源文件:

  • 编写一个自定义Roslyn分析器,扫描项目中带有[SynchronizeOverNetwork]属性的属性成员;
  • 根据属性的Authority参数,自动给对应的get/set块包裹预处理指令:
    • 当Authority = Authority.Server时,给get块包裹#if !UNITY_SERVER/#endif,给set块包裹#if UNITY_SERVER/#endif;
    • 当Authority = Authority.Client时,逻辑反过来,get块用服务端指令,set块用客户端指令;
  • 分析器会在Unity编译代码时动态修改语法树,最终编译后的代码会自动包含这些指令,源文件完全不受影响。

2. 构建前代码生成工具

写一个轻量的C#工具,作为Unity构建的预处理步骤运行:

  • 工具用Roslyn语法分析API遍历项目中目标代码文件,识别带有[SynchronizeOverNetwork]标记的属性;
  • 在对应的get/set块前后插入预处理指令,生成临时的修改后代码文件;
  • 配置Unity的PreBuild事件(比如用[InitializeOnLoad]或者构建管线回调),在编译前触发这个工具,让Unity用临时文件进行编译,编译完成后自动清理临时文件;
  • 注意要给临时文件设置忽略规则,避免被版本控制工具捕获。

3. IL织入(编译后代码裁剪)

用IL织入工具(比如Mono.Cecil)在编译后直接修改IL代码,彻底移除不需要的代码:

  • 编写一个PostBuild处理工具,扫描编译后的程序集,找到带有[SynchronizeOverNetwork]属性的属性;
  • 根据当前构建目标(Server/Client),直接移除对应平台不需要的get或set方法的IL指令,甚至可以直接删除整个方法体;
  • 通过Unity的PostProcessBuild事件触发这个工具,确保构建完成后输出的程序集已经裁剪掉冗余代码。

4. 自定义宏简化手动标记(低成本替代方案)

如果不想搞复杂的工具链,可以定义一套简化的宏来减少手动标记的工作量和错误率:

  • 在项目的Assembly-CSharp.csproj或者全局宏定义中添加:
    #define SERVER_AUTH_GET #if !UNITY_SERVER
    #define SERVER_AUTH_GET_END #endif
    #define SERVER_AUTH_SET #if UNITY_SERVER
    #define SERVER_AUTH_SET_END #endif
    
    #define CLIENT_AUTH_GET #if UNITY_SERVER
    #define CLIENT_AUTH_GET_END #endif
    #define CLIENT_AUTH_SET #if !UNITY_SERVER
    #define CLIENT_AUTH_SET_END #endif
    
  • 然后在属性中直接使用这些宏:
    [SynchronizeOverNetwork(Authority = Authority.Server)]
    public ReconcileMoveData ReconcileMoveData {
      SERVER_AUTH_GET
      get => new ReconcileMoveData { ... };
      SERVER_AUTH_GET_END
      SERVER_AUTH_SET
      set { ... };
      SERVER_AUTH_SET_END
    }
    
  • 这种方式比直接写原生#if指令简洁很多,也降低了手动写错的概率。

内容的提问来源于stack exchange,提问作者Sebastian Barth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 04:52:40