Unity游戏模组开发入门:基于MelonLoader的实践指南 📅 2026/8/6 10:57:06 1. 项目概述为什么选择MelonLoader作为你的模组开发起点如果你是一个Unity游戏爱好者看着社区里大神们制作的各种炫酷模组Mod心痒难耐却苦于不知从何下手那么你来对地方了。今天我们不谈那些复杂的底层原理和让人望而生畏的逆向工程就从一个对新手极其友好的工具——MelonLoader开始手把手带你敲开Unity游戏模组开发的大门。我最初接触模组开发时也经历过面对一堆晦涩难懂的注入工具和反编译代码的迷茫期直到发现了MelonLoader它那种“开箱即用”的友好感让我感觉模组开发的门槛瞬间降低了一大截。简单来说MelonLoader是一个专门为Unity游戏设计的模组加载器。它的核心价值在于为你处理了最复杂、最易出错的部分将你编写的自定义代码模组安全、稳定地“注入”到正在运行的游戏进程中。你可以把它想象成一个“万能适配器”你只需要关心你想给游戏添加什么新功能、修改什么逻辑而如何让游戏“听”你的话这部分脏活累活MelonLoader替你包了。这对于新手而言意义重大因为你无需从一开始就深入理解Windows的进程内存管理、Unity引擎的Mono或IL2CPP运行时细节就能快速看到自己的代码在游戏中生效这种即时反馈是保持学习动力的关键。为什么我强烈推荐新手从MelonLoader入手首先它的社区生态非常成熟。无论是《英灵神殿》Valheim、《腐蚀》Rust还是《森林之子》Sons Of The Forest大量热门Unity游戏都有基于MelonLoader的庞大模组社区。这意味着你有海量的现成模组可以参考学习遇到问题也更容易找到解决方案。其次它的配置和开发流程已经高度标准化。通过其官方安装器几乎可以一键完成环境部署其提供的项目模板能让你在Visual Studio里快速创建一个结构清晰的模组项目省去了大量搭建编译环境、配置依赖的麻烦。最后也是最重要的一点MelonLoader采用了独特的“双运行时”架构这不仅是它的技术亮点更是新手福音我们稍后会详细拆解这一点。2. 核心原理拆解理解MelonLoader的“双运行时”架构在深入动手之前我们有必要花点时间理解MelonLoader的核心工作原理。这不仅能让你在后续开发中知其所以然快速定位问题更能让你明白为什么MelonLoader比一些传统的注入方式如单纯的DLL注入更稳定、更强大。其核心秘密就在于“双运行时”架构。2.1 传统模组加载的困境在MelonLoader出现之前为Unity游戏制作模组通常有两种主流方式。第一种是“补丁式”直接修改游戏的程序集文件Assembly-CSharp.dll等这相当于直接给游戏动手术缺点是更新麻烦游戏一更新你的模组就失效且多个模组之间容易冲突。第二种是“运行时注入”比如通过一些通用注入器将你的DLL加载到游戏进程。这种方式虽然灵活但Unity游戏运行在Mono或IL2CPP这样的托管环境或AOT编译环境中单纯注入一个原生DLL常常会遇到找不到Unity引擎API、无法与游戏对象交互等问题稳定性很差对开发者要求极高。2.2 MelonLoader的解决方案托管域Domain与插件PluginMelonLoader的智慧在于它没有粗暴地直接注入代码而是在游戏原有的运行时环境旁边巧妙地“搭建”了一个新的、受控的运行时环境。这就是“双运行时”的由来。第一运行时游戏主运行时这是游戏原本的运行时环境由Unity引擎创建负责执行游戏的所有核心逻辑。第二运行时MelonLoader运行时这是由MelonLoader自身创建并管理的一个独立的托管域AppDomain。你的所有模组MelonMod都运行在这个独立的域中。你可以把游戏主运行时想象成公司的“核心生产区”而MelonLoader运行时则是公司批准设立的“创新研发实验室”。这个实验室有独立的资源和规则但可以通过特定的、安全的通道与核心生产区进行数据和指令交换。这样做的好处显而易见隔离与稳定你的模组代码运行在独立的域中即使你的模组崩溃抛出未处理的异常理论上也不会导致整个游戏进程崩溃最多是你的模组功能失效。这极大地提高了稳定性。兼容性与管理MelonLoader运行时预先加载了正确版本的Unity引擎程序集、Mono/IL2CPP运行时库等。这意味着你的模组代码可以直接引用UnityEngine、UnityEngine.UI这些命名空间就像在开发一个普通的Unity脚本一样无需担心环境缺失。同时MelonLoader提供了统一的API来管理模组的生命周期加载、初始化、更新、卸载。通信桥梁MelonLoader通过一系列“挂钩”Hooks和“事件”Events机制在游戏运行时和你的模组运行时之间建立了通信桥梁。例如它可以监听游戏每帧的更新事件Update、场景加载事件等并通知你的模组代码让你的代码能在正确的时机执行。2.3 针对IL2CPP的特别优化Unity游戏有两种脚本后端Mono和IL2CPP。IL2CPP会将C#代码提前编译AOT为C再编译为本地机器码性能更高但传统的基于反射的模组技术几乎完全失效。MelonLoader通过集成Il2CppAssemblyUnhollower现在通常内置于其核心等工具自动为IL2CPP编译的游戏生成对应的C#包装程序集。这使得你依然可以用C#来为IL2CPP游戏编写模组仿佛它还是一个Mono游戏一样这是MelonLoader在现代Unity游戏中依然能大放异彩的关键。注意虽然MelonLoader尽力屏蔽了底层差异但在为IL2CPP游戏开发模组时你仍可能遇到一些在Mono游戏中不会出现的问题例如部分通过反射访问的私有字段/方法可能需要不同的处理方式或者需要等待社区更新对应的“解映射”Unhollower库来支持新版本的游戏。选择模组开发目标时最好先到MelonLoader的Wiki或游戏社区查看兼容性列表。3. 环境准备与安装从零开始搭建你的模组开发工作台理论说再多不如动手一试。接下来我们一步步搭建一个完整的MelonLoader模组开发环境。这个过程我已经重复过几十遍总结了一套最顺畅的流程。3.1 第一步准备目标游戏与开发工具选择目标游戏挑选一个你熟悉且热爱、并且已知支持MelonLoader的Unity游戏。例如《英灵神殿》就是一个绝佳的选择其模组生态极其繁荣。确保你的游戏是通过Steam等正规渠道获取的完整版并且已经更新到最新稳定版本。安装.NET SDKMelonLoader及其模组项目基于.NET框架。你需要安装.NET 6.0或更高版本的SDK。前往微软官网下载并安装即可。安装后在命令行输入dotnet --version确认安装成功。安装集成开发环境IDE强烈推荐使用Visual Studio 2022社区版免费。在安装时务必勾选“.NET桌面开发”和“使用Unity的游戏开发”这两个工作负载。VS对C#和NuGet包管理的支持最为完善能极大提升开发效率。3.2 第二步安装MelonLoader到游戏这是将MelonLoader运行时“植入”目标游戏的过程。绝对不要手动去解压游戏文件或修改原始游戏程序集。下载MelonLoader安装器访问MelonLoader的GitHub发布页下载最新版本的MelonLoader.Installer.exe。运行安装器以管理员身份运行安装器非必须但可以避免一些权限问题。点击Select按钮浏览并选择你的游戏主程序.exe文件。例如对于《英灵神殿》就是valheim.exe。MelonLoader会自动检测游戏使用的Unity版本和脚本后端Mono/IL2CPP。点击Install按钮。安装器会自动完成以下工作在游戏目录下创建MelonLoader文件夹存放所有核心文件。备份原始的游戏主程序通常重命名为GameName_Original.exe。将自身注入生成一个新的启动入口。安装成功后你通常会看到游戏目录下多了一个MelonLoader文件夹并且原来的GameName.exe可能变成了一个很小的引导程序。实操心得安装完成后务必先直接运行一次游戏通过Steam启动或直接点击新的.exe。如果MelonLoader安装成功游戏启动时通常会先显示一个MelonLoader的控制台窗口里面会打印加载日志然后游戏正常启动。这个控制台是调试模组的生命线请确保它能正常弹出。如果游戏无法启动或闪退请检查游戏版本是否与MelonLoader版本兼容或者查看MelonLoader文件夹下的日志文件。3.3 第三步创建你的第一个MelonLoader模组项目现在我们来创建第一个模组代码项目。安装项目模板MelonLoader提供了官方的Visual Studio项目模板。打开命令行CMD或PowerShell运行以下命令安装模板dotnet new install MelonLoader.ModTemplate创建新项目打开Visual Studio 2022。选择“创建新项目”。在搜索框中输入“MelonLoader”你应该能看到“MelonLoader Mod”这个模板。选择它点击下一步。为你的项目命名例如MyFirstValheimMod选择合适的位置然后点击创建。理解项目结构模板创建的项目已经为你搭建好了基础框架。主要关注以下几个文件MyFirstValheimMod.cs这是你的主模组类文件。模板已经包含了一个继承自MelonMod的类骨架。MyFirstValheimMod.csproj项目文件其中已经引用了必要的NuGet包如MelonLoader和UnityEngine等。manifest.json这是模组的“身份证”至关重要。它定义了模组的基本信息游戏和MelonLoader都靠它来识别你的模组。3.4 第四步配置模组清单manifest.json打开manifest.json文件你会看到类似以下的内容。你需要根据你的模组信息进行修改{ $schema: https://raw.githubusercontent.com/LavaGang/MelonLoader/master/ManifestSchema.json, Name: MyFirstValheimMod, Author: YourName, Version: 1.0.0, Description: 这是我的第一个模组用来学习MelonLoader开发。, Game: { Id: valheim, // 游戏ID可在MelonLoader Wiki查找 Name: Valheim, Version: * // 支持的游戏版本“*”表示所有版本也可指定如“0.217.14” }, LoaderVersion: 0.6.1 }Name模组的内部名称通常与项目名一致不要包含空格和特殊字符。Author你的名字。Game.Id这是关键。必须填写正确MelonLoader才会为对应的游戏加载你的模组。你可以在MelonLoader的官方Wiki或游戏社区找到该ID。例如英灵神殿的ID就是valheim。LoaderVersion指定你的模组兼容的MelonLoader最低版本。通常保持模板默认值即可。4. 核心开发实战编写一个简单的功能模组环境搭好了我们来点真格的。我们将编写一个简单的模组在《英灵神殿》中按下F1键在屏幕左上角显示一个自定义的文本标签。这个例子涵盖了模组开发中最基础的几个概念生命周期、用户输入检测和GUI绘制。4.1 模组生命周期与基础框架打开你的主模组类文件如MyFirstValheimMod.cs。模板已经为你生成了如下结构using MelonLoader; using UnityEngine; namespace MyFirstValheimMod { public class MyFirstValheimMod : MelonMod { public override void OnInitializeMelon() { // 模组初始化时调用一次早于所有游戏系统初始化。 // 适合进行配置加载、基础数据准备。 MelonLogger.Msg(我的第一个模组初始化成功); } public override void OnLateInitializeMelon() { // 在游戏大部分系统初始化完成后调用一次。 // 适合进行需要访问游戏对象的操作。 } public override void OnUpdate() { // 每一帧调用一次相当于Unity MonoBehaviour的Update方法。 // 这是检测按键、更新逻辑的主要位置。 } public override void OnGUI() { // 每一帧调用一次用于绘制IMGUI即时模式GUI。 // 这是绘制屏幕UI的主要位置。 } } }OnInitializeMelon你的模组“出生”时第一个被调用的地方。在这里打印日志、加载配置文件是标准操作。OnUpdate这是模组的“心跳”。游戏每渲染一帧这里的方法就会被调用一次。所有需要持续检测或更新的逻辑都应该放在这里比如检测按键。OnGUI这是绘制简单屏幕UI的地方。它使用的是Unity传统的IMGUI系统虽然功能不如UGUI强大但对于模组显示一些调试信息、简单按钮来说非常方便快捷。4.2 实现按键触发与GUI显示现在我们在模组中添加功能在OnUpdate中检测F1键用一个布尔变量控制显示/隐藏在OnGUI中根据这个布尔变量绘制文本。using MelonLoader; using UnityEngine; namespace MyFirstValheimMod { public class MyFirstValheimMod : MelonMod { private bool _showText false; // 控制文本是否显示的开关 public override void OnInitializeMelon() { MelonLogger.Msg(我的第一个模组初始化成功按F1切换屏幕文本显示。); } public override void OnUpdate() { // 检测F1键是否在本帧被按下 if (Input.GetKeyDown(KeyCode.F1)) { _showText !_showText; // 切换显示状态 MelonLogger.Msg($文本显示状态切换为{_showText}); } } public override void OnGUI() { // 只有当开关打开时才绘制 if (_showText) { // 定义一个在屏幕左上角的矩形区域 (x, y, 宽度, 高度) Rect textRect new Rect(10, 10, 300, 50); // 设置GUI的文本颜色为黄色 GUI.color Color.yellow; // 在指定区域绘制一个标签 GUI.Label(textRect, 【我的模组】正在运行\n祝你模组开发愉快); // 恢复GUI颜色避免影响游戏其他UI GUI.color Color.white; } } } }代码解析与注意事项输入检测Input.GetKeyDown(KeyCode.F1)是Unity的标准输入API在MelonLoader环境中可以直接使用。这得益于MelonLoader运行时正确加载了UnityEngine程序集。GUI绘制GUI.Label是IMGUI的API。Rect定义了绘制区域的位置和大小坐标原点(0,0)在屏幕左上角。记住OnGUI每帧调用非常频繁里面的逻辑要尽量轻量。日志输出MelonLogger.Msg()是MelonLoader提供的日志工具。它的输出会显示在游戏启动时的MelonLoader控制台窗口中是调试时最重要的手段。比起Unity的Debug.Log它更可靠尤其是在游戏发布版本中。状态管理我们使用私有字段_showText来管理UI的显示状态。这是一种简单的状态机思想在模组开发中非常常用。4.3 编译、部署与测试编译项目在Visual Studio中选择“生成” - “生成解决方案”或按F6。如果一切正常会在项目的bin\Debug\net6.0或你配置的生成目录下生成一个.dll文件例如MyFirstValheimMod.dll。部署模组找到你的游戏安装目录下的MelonLoader文件夹。进入MelonLoader\Mods子文件夹。这是所有模组存放的标准位置。将你刚刚编译生成的MyFirstValheimMod.dll文件复制到这个Mods文件夹内。运行测试启动游戏通过Steam或直接点击被MelonLoader修改后的exe。观察启动时的控制台窗口你应该能看到类似[INFO] Loading Mod: MyFirstValheimMod.dll以及你写的“我的第一个模组初始化成功”的日志。进入游戏场景后按下F1键。同时观察控制台会打印状态切换的日志。按下F1后你应该能在游戏屏幕的左上角看到黄色的提示文字。再次按下F1文字消失。恭喜你你已经成功创建并运行了你的第一个MelonLoader模组。这个过程虽然简单但包含了从环境搭建、代码编写到部署测试的完整闭环是后续所有复杂模组开发的基础。5. 深入进阶挂钩Hooking与事件Events系统只会显示UI和检测按键是远远不够的。模组的真正威力在于与游戏逻辑交互修改玩家属性、生成物品、监听游戏事件等。这就需要用到MelonLoader更核心的功能挂钩Hooking和事件Events。5.1 理解挂钩Hooking的概念挂钩简单说就是“拦截”游戏原有的方法调用。比如游戏有一个方法Player.TakeDamage(float damage)我们通过挂钩可以在游戏执行这个方法之前或之后插入我们自己的代码逻辑比如读取伤害值、修改伤害值实现减伤或增伤、甚至阻止这次伤害。MelonLoader通过集成强大的库如HarmonyX来提供挂钩能力。HarmonyX是一个.NET下的函数修补库它允许你以非破坏性的方式修改其他程序集的方法。5.2 使用HarmonyX进行方法挂钩假设我们想修改《英灵神殿》中玩家吃东西恢复的生命值。我们首先需要知道游戏内处理吃东西的方法是哪个。这通常需要借助反编译工具如dnSpy, ILSpy或社区已有的逆向工程文档来查找。这里我们假设目标方法是Player.ConsumeFood(ItemDrop.ItemData item)。添加HarmonyX包引用在Visual Studio中右键点击你的项目 - “管理NuGet程序包”搜索并安装Lib.HarmonyHarmonyX的NuGet包名。创建补丁类在你的项目中新建一个类例如Patches.cs。using HarmonyLib; using MelonLoader; namespace MyFirstValheimMod { // 使用HarmonyPatch特性来声明我们要挂钩的类和方法 [HarmonyPatch(typeof(Player), nameof(Player.ConsumeFood))] // 假设Player类有ConsumeFood方法 internal class PlayerConsumeFoodPatch { // Prefix补丁在原方法执行前运行 [HarmonyPrefix] private static bool Prefix(Player __instance, ItemDrop.ItemData item) { // __instance 是调用该方法的Player实例即“this” // item 是原方法的参数 MelonLogger.Msg(${__instance.GetPlayerName()} 正在尝试食用{item.m_shared.m_name}); // 我们可以在这里修改item的数据比如让食物恢复量翻倍 // 假设恢复健康度的字段是 m_health if (item.m_shared.m_health 0) { float originalHealth item.m_shared.m_health; item.m_shared.m_health * 2.0f; // 恢复量翻倍 MelonLogger.Msg($已将 {item.m_shared.m_name} 的健康恢复从 {originalHealth} 提升至 {item.m_shared.m_health}); } // 返回 true 表示继续执行原方法返回 false 则会跳过原方法的执行 return true; } // Postfix补丁在原方法执行后运行 [HarmonyPostfix] private static void Postfix(Player __instance, ItemDrop.ItemData item) { MelonLogger.Msg(${__instance.GetPlayerName()} 食用 {item.m_shared.m_name} 完毕。); // 可以在这里做一些后续处理比如给玩家添加一个自定义的Buff } } }在模组初始化时应用补丁修改你的主模组类在OnInitializeMelon中创建Harmony实例并应用所有补丁。private HarmonyLib.Harmony _harmony; public override void OnInitializeMelon() { MelonLogger.Msg(我的第一个模组初始化成功); // 创建Harmony实例参数是你的模组ID确保唯一即可 _harmony new HarmonyLib.Harmony(com.yourname.MyFirstValheimMod); // 应用所有通过[HarmonyPatch]声明的补丁 _harmony.PatchAll(); MelonLogger.Msg(Harmony补丁已应用。); }重要警告与心得挂钩是强大但危险的工具。挂钩错误的方法、在补丁中编写低效或崩溃的代码都可能导致游戏不稳定甚至崩溃。务必注意精确匹配方法签名[HarmonyPatch]中的方法名和参数类型必须与游戏中的方法完全一致。一个参数类型不匹配就会导致挂钩失败。使用前缀Prefix谨慎返回false返回false会阻止原方法执行除非你完全清楚后果例如想完全取代某个功能否则通常应返回true。性能考量Prefix/Postfix方法会被频繁调用里面的逻辑必须高效避免复杂计算或内存分配。依赖社区与文档寻找正确的方法签名是模组开发中最耗时的一步。积极利用游戏的反编译资源、社区Wiki如Valheim的ValheimLib文档和Discord频道能节省大量时间。5.3 利用MelonLoader事件系统除了主动挂钩MelonLoader还提供了更易用的事件系统用于监听一些常见的、游戏生命周期相关的事件。这些事件由MelonLoader内部管理你只需要订阅即可。public override void OnInitializeMelon() { // 订阅场景加载完成事件 Il2CppInterop.Runtime.Il2CppEvents.OnSceneWasLoaded OnSceneLoaded; // 订阅游戏更新事件另一种方式与OnUpdate类似但机制不同 MelonEvents.OnUpdate.Subscribe(OnMyUpdate, -1000); // 优先级参数 } private void OnSceneLoaded(int buildIndex, string sceneName) { MelonLogger.Msg($场景加载完成: {sceneName} (索引: {buildIndex})); // 可以在这里进行场景特定的初始化比如在主菜单场景加载模组配置界面 } private void OnMyUpdate() { // 另一种处理每帧更新的方式 if (Input.GetKeyDown(KeyCode.F2)) { MelonLogger.Msg(通过事件系统检测到F2键按下); } } // 记得在模组卸载时取消订阅防止内存泄漏虽然MelonLoader会处理但这是好习惯 public override void OnDeinitializeMelon() { Il2CppInterop.Runtime.Il2CppEvents.OnSceneWasLoaded - OnSceneLoaded; MelonEvents.OnUpdate.Unsubscribe(OnMyUpdate); }事件系统比挂钩更简洁但只覆盖了MelonLoader预设的一些通用点。要实现具体的游戏逻辑修改挂钩仍然是不可替代的核心工具。6. 调试、排查与社区资源开发过程中你一定会遇到各种问题模组不加载、游戏崩溃、功能不生效等。高效的调试和排查能力是模组开发者的核心技能。6.1 调试三板斧日志Logging是你的眼睛MelonLogger是你的第一道防线。在代码的关键节点方法入口、条件分支、循环开始结束添加日志。它有不同级别MelonLogger.Msg(普通信息); // 白色文本 MelonLogger.Warning(警告信息); // 黄色文本 MelonLogger.Error(错误信息); // 红色文本启动游戏时务必打开MelonLoader控制台窗口查看日志输出。断点调试需要额外配置对于复杂问题日志可能不够。你可以配置Visual Studio进行远程调试或使用dnSpy等工具动态调试。但这需要更复杂的设置对于新手前期优先用好日志。隔离测试当功能复杂后新建一个干净的、只包含最小功能复现代码的测试模组来排除其他模组或复杂代码的干扰。6.2 常见问题排查速查表问题现象可能原因排查步骤模组根本未加载1..dll文件未放入MelonLoader\Mods目录。2.manifest.json中的Game.Id错误。3. 模组依赖的MelonLoader版本不兼容。1. 检查文件位置。2. 核对manifest.json。3. 查看控制台启动日志是否有加载失败的错误信息。游戏启动时崩溃1. 模组代码在OnInitializeMelon中有未处理的异常。2. Harmony挂钩的方法签名错误。3. 与其他模组冲突。1. 查看崩溃前控制台的最后几条错误日志。2. 注释掉所有Harmony补丁代码逐一启用测试。3. 移出其他所有模组单独测试你的模组。功能不生效但模组已加载1. 挂钩的目标方法名或参数类型错误。2. 补丁类未被PatchAll()应用。3. 逻辑条件判断有误如按键检测代码写错键位。1. 在补丁的Prefix第一行加日志确认补丁是否被执行。2. 确认_harmony.PatchAll()被调用且无异常。3. 仔细检查代码逻辑用日志输出中间变量值。游戏运行一段时间后崩溃内存泄漏、事件未取消订阅、在错误线程操作Unity对象。1. 检查所有事件订阅确保在OnDeinitializeMelon中取消。2. 避免在非主线程直接调用Unity API如GameObject.Instantiate。6.3 不可或缺的社区与资源MelonLoader官方GitHub仓库、Wiki和Discord是获取最新信息、文档和帮助的首选地。游戏特定的模组社区例如英灵神殿的Valheim ModdingDiscord频道和ValheimLib项目。这里聚集了针对该游戏的逆向工程成果、共享的库和大量有经验的开发者。反编译工具dnSpy或ILSpy是查看游戏程序集代码、寻找目标方法和字段的必备工具。使用它们需要一些.NET和C#基础。已有模组源码在GitHub上搜索你感兴趣的游戏模组阅读它们的源代码是最好的学习方式。看看别人是如何挂钩、如何设计架构的。7. 从入门到精通的路径规划掌握了以上基础你已经可以制作很多有趣的模组了。但要成为真正的“模组大师”还需要在以下方向深入深入理解HarmonyX学习更高级的补丁技术如Transpiler中间语言注入来修改方法内部的IL指令实现更复杂的逻辑修改。掌握游戏对象GameObject与组件Component学习如何在运行时查找、实例化、修改游戏中的对象。这是制作新物品、新NPC、新建筑的基础。配置与数据持久化使用MelonPreferences来为你的模组创建可配置的选项让其他玩家可以自定义你的模组行为。网络同步如果你的模组功能涉及多人游戏需要学习游戏本身的网络同步机制并确保你的修改在所有玩家客户端上保持一致这是一项高级且富有挑战性的任务。性能优化确保你的模组不会导致游戏卡顿。避免在Update或OnGUI中进行重型操作使用缓存合理管理资源。模组开发是一个融合了逆向工程、软件开发和社区互动的独特领域。它既需要耐心和细心去探索游戏的内部机制也需要创造力和编程能力去实现你的想法。从今天这个按下F1显示文字的小模组开始一步步去探索、去创造吧。当你第一次看到自己编写的代码真正改变了心爱游戏的行为时那种成就感是无与伦比的。记住社区是你最大的后盾多读、多问、多实践你很快就能从新手成长为能够贡献精彩内容的模组开发者。