Unity读书系列《Unity3D游戏开发》——拓展编辑器(一)

news2024/11/23 20:22:01

文章目录

  • 前言
  • 一、扩展Project视图
    • 1、右键扩展菜单(Asset)
    • 2、监听事件
    • 3、拓展布局
  • 二、扩展Hierarchy视图
    • 1、拓展菜单(GameObject)
    • 2、拓展布局
    • 3、重写菜单
  • 三、扩展Inspector视图
    • 1、扩展原生组件
    • 2、扩展继承组件
  • 四、扩展Scene视图
    • 1、绘制辅助元素
    • 2、辅助UI
    • 3、常驻辅助UI
  • 五、扩展Game视图
  • 总结


前言

本篇文章是对前文关于编辑器拓展的探讨的延伸。即使内置的Unity编辑器再强大,也无法满足所有不同产品和游戏的需求。为了解决这个问题,Unity提供了编辑器拓展的API接口。我们可以通过代码反射的方式修改内置的系统编辑器,同时,游戏开发者也可以利用EditorGUI接口编写适合自己的专属游戏编辑器。这涵盖了从简单的一键换字体、材质、一键打包、管理、优化,到复杂的技能编辑器、关卡编辑器等功能。

特别需要注意的是,由于内容涉及较多且较为复杂,会分2节进行详细讨论。在本文的第一部分中,我们将总结最基础和最实用的编辑器拓展知识。

本文所有代码均在Gitee参考工程,如有需要请自取。


一、扩展Project视图

Project视图是掌握Unity项目的生死大权的地方,包括创建、删除等重要操作。在这里,我们可以通过右键点击实现Asset菜单的拓展。在进行这项任务之前,首先需要将脚本文件保存到名为Editor的文件夹下,并引入UnityEditor命名空间。

1、右键扩展菜单(Asset)

右键创建物体

using UnityEngine;
using UnityEditor;

public class AssetEditor
{
    [MenuItem("Assets/Tools/CreateSphere",false,1)]//数值越小越靠前
    static void Createxx() {
        GameObject.CreatePrimitive(PrimitiveType.Sphere);
    }
}

如图我点击CreateSphere按钮就创建了一个球体到场景当中。
在这里插入图片描述

2、监听事件

在大型或规范的项目中,通常会有严格的项目规范,包括对资源的归类等方面。例如,如果你将贴图移动到了脚本文件夹,项目可能会判断这样的操作是不合法的,并阻止你进行修改。
1、监听资源的删除、创建、移动、保存等操作,在进行操作后会输出绑定的委托。

    //监听事件
    [InitializeOnLoadMethod]
    static void InitializeOnLoadMethod() {
	EditorApplication.projectChanged += delegate ()
	{
	    Debug.Log("怎么回事,老弟。你是不是刚动了资源?");
	};
    }

嘿嘿,知识点还没完,[InitializeOnLoadMethod]写在方法 前则会使该方法在C#代码编译完成后首先调用。
2、当需要重新具体的删除、创建方法时必须继承UnityEditor.AssetModificationProcessor,具体方法如下:

public class AssetEventEditor : UnityEditor.AssetModificationProcessor
{
    //监听事件
    [InitializeOnLoadMethod]
    static void InitializeOnLoadMethod()
    {
        EditorApplication.projectChanged += delegate ()
        {
            Debug.Log("怎么回事,老弟。你是不是刚动了资源?");
        };
    }

    //监听"双击左键打开资源"事件
    public static bool IsOpenForEdit(string assetPath, out string message)
    {
        message = null;
        Debug.LogFormat("assetPath:{0}", assetPath);
        return true;//true表示该资源可以打开,false表示不允许打开

    }

    //监听"资源即将被创建"事件
    public static void OnWillCreateEdit(string path)
    {
        Debug.LogFormat("创建资源的路径:{0}", path);
    }

    //监听"资源即将被保存"事件
    public static string[] OnWillSaveAssets(string[] paths) {
        if (paths != null)
        {
            Debug.LogFormat("保存资源的路径:{0}",string.Join(",",paths));
        }
        return paths;
    }

    //监听"资源即将被移动"事件
    public static AssetMoveResult OnWillMoveAsset(string oldPath,string newPath) {
      
        Debug.LogFormat("资源从路径{0}移动到路径{1}", oldPath,newPath);
        return AssetMoveResult.DidNotMove;//DidNotMove表示可以移动,DidMove表示不可以移动
    }
    //监听"资源即将被删除"事件
    public static AssetDeleteResult OnWillDeleteAsset(string assetPath) {
      
        Debug.LogFormat("资源从路径{0}删除", assetPath);
        return AssetDeleteResult.DidNotDelete;//DidNotDelete表示可以移动,DidDelete表示不可以移动
    }

}

3、拓展布局

选中资源后出现按钮,并监听按钮的点击事件

    //选中资源后出现按钮,并监听按钮的点击事件
    [InitializeOnLoadMethod]
    static void InitializeOnLoadMethod()
    {
	EditorApplication.projectWindowItemOnGUI = delegate (string guid, Rect selectionRect)
	{
	    //在Project试图中选择一个资源
	    if (Selection.activeObject && guid == AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(Selection.activeObject)))
	    {
		//设置拓展按钮区域
		float width = 80f;
		selectionRect.x += (selectionRect.width - width);
		selectionRect.y += 2;
		selectionRect.width = width;
		GUI.color = Color.red;
		//点击事件
		if (GUI.Button(selectionRect,"click"))
		{
		    Debug.LogFormat("点击:{0}", Selection.activeObject.name);
		}
		GUI.color = Color.white;

	    }
	};
    }

在这里插入图片描述

二、扩展Hierarchy视图

在Hierarchy(层次)视图中,右键点击相当于打开菜单栏的GameObject栏目。

1、拓展菜单(GameObject)

细心的读者已经看出来了,下面代码对比上面写的仅仅将菜单栏目从"Assets"换成了"GameObject"。

    //右键创建物体
    [MenuItem("GameObject/Tools/CreateSphere", false, 1)]//数值越小越靠前
    static void Createxx()
    {
        GameObject.CreatePrimitive(PrimitiveType.Sphere);
    }

2、拓展布局

粗心的读者这下也已经看出来了,下面的代码复刻了之前的代码,将 EditorApplication 后的 GUI 委托修改为 Hierarchy 窗口专属的,并将参数从资源的 GUID 变为 instanceID 实例 ID。此外,按钮引入了本地图片。在各种插件中,编辑器引入图片的操作屡见不鲜,有时为了资源规范会整理插件的图标和图片位置,别忘了根据实际情况修改相关代码。
在这里插入图片描述

3、重写菜单

通过以上学习,我们了解了如何在原有基础上扩展编辑器。那么,能否完全重写呢?当然可以。

1、下面,我们将学习如何重新创建 Image 的逻辑。因为在创建 Image 时,Unity 默认会自动勾选 RaycastTarget,如果我们不需要它具有点击功能,就会有额外的性能开销。使用下面的代码,我们可以创建不勾选 RaycastTarget 的 Image。

  //创建Image默认不勾选RaycastTarget
  [MenuItem("GameObject/UI/Image0")]
  static void CreateImage() {
      if (Selection.activeTransform)
      {
          if (Selection.activeTransform.GetComponentInParent<Canvas>())
          {
              Image image = new GameObject("image").AddComponent<Image>();
              image.raycastTarget = false;
              image.transform.SetParent(Selection.activeTransform, false);
              //设置选中状态
              Selection.activeTransform = image.transform;
          }
      }
  }

完整版会有检测视图是否有Canvas组件,没有则自动创建等功能。

2、重写菜单:

   //重写菜单
   [InitializeOnLoadMethod]
   static void StartInitializeOnLoadMethod()
   {
       EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
   }

   static void OnHierarchyGUI(int instanceID, Rect selectionRect)
   {
       //Event.current监听当前事件,如果监听到鼠标抬起则执行自定义事件(也就是我们的自定义菜单)
       if (Event.current != null && selectionRect.Contains(Event.current.mousePosition) && Event.current.button == 1 && Event.current.type <= EventType.MouseUp)
       {
           GameObject selectedGameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
           //判断是否满足条件
           if (selectedGameObject)
           {
               Vector2 mousePosition = Event.current.mousePosition;
               EditorUtility.DisplayPopupMenu(new Rect(mousePosition.x, mousePosition.y, 0, 0), "Window/Test", null);
               Event.current.Use();
           }
       }
   }

   [MenuItem("Window/Test/Test1")]
   static void Test1()
   {

   }

   [MenuItem("Window/Test/Test2")]
   static void Test2()
   {

   }

重写完成后,右键视图中的实例将会弹出自定义菜单
在这里插入图片描述

三、扩展Inspector视图

Inspector(检视)视图是用来展示组件及资源的详细信息面板。unity自身提供的各类组件的面板能够满足我们正常的需求,但我们偶尔会希望在某些面板上添加快捷按钮或者某些逻辑。

1、扩展原生组件

摄像机是典型的原生组件,我们CustomEditor进行自定义组件,重写OnInspectorGUI在base.OnInspectorGUI()这个原有元素接口上下添加按钮。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Camera))]
public class CameraEditor : Editor
{
    public override void OnInspectorGUI()
    {
	if (GUILayout.Button("拓展按钮-上"))
	{

	}
        base.OnInspectorGUI(); 
        if (GUILayout.Button("拓展按钮-下"))
        {

        }
    }
}

如下便绘制了两个按钮,不过要注意该组件限制了按钮必须加在最上面或者最下面。
在这里插入图片描述

2、扩展继承组件

1、Unity将大量的Editor绘制方法封装进了DLL,通常来讲我们无法调用其中方法。想要解决可以使用反射获取内部对象,然后调用想要使用的未公开的方法。

using System.Reflection;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Transform))]
public class TransformEditor : Editor
{
    private Editor m_Editor;
    private void OnEnable()
    {
        m_Editor = Editor.CreateEditor(target, Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector", true));
    }
    public override void OnInspectorGUI()
    {
        if (GUILayout.Button("拓展按钮"))
        {
        }
        m_Editor.OnInspectorGUI();//原有信息面板
        // base.OnInspectorGUI();

    }
}

2、Context菜单
点击组件的设置按钮(或鼠标右键),会弹出Context菜单,里面有Copy、Reset等操作按钮。我们有时候想对特定组件进行自定义的操作,例如我想在Transform的Context菜单添加NewContext按钮,只需更改MenuItem里第一个参数为"CONTEXT/Transform/NewContext"接口。想给Camer加就将Transform替换成Camera,想给所有组件加就替换成"Compoment"。

    [MenuItem("CONTEXT/Transform/New Context")]
    static void NewContext(MenuCommand menuCommand)
    {
        Debug.LogFormat("组件名称:{0}",menuCommand.context.name);
    }

下面演示如何重写特定脚本的系统方法。作者建议最好延迟一帧以防止在编辑模式下代码同步出现问题。在测试 Unity 2021 版本时,貌似没有出现问题。此外,需要注意的是,书中的部分接口可能已经过时,个人已经替换成最新的版本(以2021.3为准)。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ContextScript : MonoBehaviour
{

    [ContextMenu("Remove Component")]
    void RemoveComponent()
    {
        Debug.Log("RemoveComponent");
        //等一帧再删除自己,防止引擎底层错误
        UnityEditor.EditorApplication.delayCall = delegate () {
            DestroyImmediate(this);
        };
    }
}

脚本中使用宏定义(使用宏定义的原因是为了在发布后剔除无效代码),联动脚本中的变量在编辑模式下实现功能。下面代码就让NewContext按钮操作了脚本中的变量——将ContextScript脚本中的str变量从"原始"改成了"原神"。

using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

public class ContextScript : MonoBehaviour
{
    public string str = "原始";
#if UNITY_EDITOR
    //宏定义操作脚本变量
    [MenuItem("CONTEXT/ContextScript/New Context")]
    static void NewContext(MenuCommand menuCommand)
    {
        ContextScript contextScript = menuCommand.context as ContextScript;
        contextScript.str = "原神";
    }
#endif
}

请添加图片描述

四、扩展Scene视图

Unity的Scene视图是一个用于编辑场景的窗口。在Scene视图中,你可以直观地查看、编辑和组织你的游戏场景。

1、绘制辅助元素

场景编辑中我们有时会需要线段、不同形状的元素来帮助我们快速编辑。下面我们将使用Gizmos.cs工具类绘制简单元素。

using UnityEngine;

public class GizmoScirpt : MonoBehaviour
{ 
	//在鼠标点击到脚本挂载的物体的身上的时候运行
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        //画线
        Gizmos.DrawLine(transform.position, Vector3.one);
        //立方体
        Gizmos.DrawCube(Vector3.one, Vector3.one);
    }
}

在这里插入图片描述
我们发现未点击挂载脚本的物体时,立方体和线条消失了。如果想让绘制的物体一直出现,可以使用 OnDrawGizmos 方法。具体用法有很多,比如技能范围展示、地形和陷阱的实际范围绘制等,都能使项目更高效进行。

   //不依赖对象,会一直执行
   private void OnDrawGizmos()
   {
       Gizmos.DrawSphere(transform.position, 2.0f);
   }

2、辅助UI

我们在Scene视图中可以在各种组件中添加EditorGUI以获得便利。下面演示如何在Scene中给Camer添加位置信息。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(Camera))]
public class GUIEditor : Editor
{
    private void OnSceneGUI()
    {
        Camera camera = target as Camera;
	if (camera != null)
	{
	    Handles.color = Color.red;
	    Handles.Label(camera.transform.position, camera.transform.position.ToString());

	    Handles.BeginGUI();
	    GUI.backgroundColor = Color.red;
	    if (GUILayout.Button("click",GUILayout.Width(200f)))
	    {
		Debug.LogFormat("click = {0}", camera.name);
	    }
	    GUILayout.Label("Label");
	    Handles.EndGUI();
	}
    }
}

在这里插入图片描述

最后注意如果你的脚本不生效,可能是该脚本与CameraEditor脚本互斥冲突,因为都用了"[CustomEditor(typeof(Camera))]",默认先创建的脚本会生效。

3、常驻辅助UI

常驻辅助UI或者说固定辅助UI,顾名思义,无需游戏对象即可常驻Scene视图,有些类似OnDrawGizmosSelected和OnDrawGizmos。

using UnityEngine;
using UnityEditor;

//常驻辅助UI
public class GUIEditor2 : MonoBehaviour
{
    [InitializeOnLoadMethod]
    static void InitializeOnLoadMethod() {
        SceneView.duringSceneGui += delegate (SceneView sceneView)
        {
            Handles.BeginGUI();
            GUI.Label(new Rect(0, 0, 50f, 50f), "标题");
            GUI.Button(new Rect(0, 20f, 50f, 50f), AssetDatabase.LoadAssetAtPath<Texture>("Assets/unity.png"));
            Handles.EndGUI();
        };
    }
}

如下,Scene视图左上角多出了一个UI
在这里插入图片描述

五、扩展Game视图

通常来讲运行游戏才能执行脚本的生命周期。如果想在非运行模式下也可以执行脚本,在脚本上添加[ExecuteInEditMode],那么该脚本可以在编辑模式下生效,如果不想在发布后出现可以使用宏定义来剔除。

using UnityEngine;

#if UNITY_EDITOR

//编辑器模式下依然执行生命周期
[ExecuteInEditMode]
public class GameScript : MonoBehaviour
{
    private void OnGUI()
    {
	if (GUILayout.Button("Click"))
	{
	    Debug.Log("Click");
	}
	GUILayout.Label("Click!");
    }
}

#endif

总结

累死了,本篇详细讲述了 Unity 编辑器的五大视图的拓展方法。原本想分为多篇,但为了整体性,将其整合在一起。下一篇文章字数减少但会更加深入地探讨,并详细解释面板和编辑器源码的相关内容。有不愿透露姓氏的杨姓砖家建议认真阅读完本篇并亲自进行代码试验,然后再查看下一篇。
创作不易,觉得有用的请大家多点赞、评论、收藏,毕竟不收钱,甚至说不定因为哪个知识点恰巧能在面试里帮助到你,提升你的薪资,哈哈。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1415878.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【多线程】ThreadLocal 作为类的私有静态字段实践

ThreadLocal 通常作为类的私有静态字段存在的主要原因是为了确保每个线程都能够拥有自己独立的 ThreadLocal 变量。 以下是一些原因&#xff1a; 线程隔离&#xff1a; ThreadLocal 的设计目的是为了实现线程隔离&#xff0c;即每个线程都可以独立地管理自己的变量&#xff0c…

华为机考入门python3--(3)牛客3-明明的随机数

分类&#xff1a;集合、排序 知识点&#xff1a; 集合添加元素 set.add(element) 集合转列表 list(set) 列表排序 list.sort() 题目来自【牛客】 N int(input().strip()) nums set()for i in range(N):nums.add(int(input().strip()))# 集合转列表 nums_list l…

3 JS类型 值和变量

计算机对value进行操作。 value有不同的类型。每种语言都有其自身的类型集合。编程语言的类型集是该编程语言的基本特性。 value需要保存一个变量中。 变量的工作机制是变成语言的另一个基本特性。 3.1概述和定义 JS类型分为&#xff1a; 原始类型和对象类型。 原始类型&am…

最高20倍!压缩ChatGPT等模型文本提示,极大节省AI算力

最高20倍&#xff01;压缩ChatGPT等模型文本提示&#xff0c;极大节省AI算力_信息_段落_问题 在长文本场景中&#xff0c;ChatGPT 等大语言模型经常面临更高算力成本、更长的延迟以及更差的性能。为了解决这三大难题&#xff0c;微软开源了 LongLLMLingua。 据悉&#xff0c;L…

如何在docker容器中安装Elasticsearch中的IK分词器

目录 &#xff08;1&#xff09;准备IK分词器的压缩包 &#xff08;2&#xff09;进入docker容器 &#xff08;3&#xff09;移动ik分词器到指定文件夹 &#xff08;4&#xff09;解压分词器压缩包 &#xff08;5&#xff09;测试IK分词器是否安装成功 &#xff08;1&#…

【Image captioning】论文阅读八—ClipCap: CLIP Prefix for Image Captioning_2021

中文标题&#xff1a;ClipCap: CLIP前缀用于图像描述&#xff08;ClipCap: CLIP Prefix for Image Captioning&#xff09; 文章目录 1. 介绍2. 相关工作3. 方法3.1 综述3.2 语言模型微调3.3 映射网络架构3.4 推理 4. 结果5. 结论 摘要&#xff1a;图像描述是视觉语言理解中的…

C语言——操作符详解2

目录 0.过渡0.1 不创建临时变量&#xff0c;交换两数0.2 求整数转成二进制后1的总数 1.单目表达式2. 逗号表达式3. 下标访问[ ]、函数调用( )3.1 下标访问[ ]3.2 函数调用( ) 4. 结构体成员访问操作符4.1 结构体4.1.1 结构体的申明4.1.2 结构体变量的定义和初始化 4.2 结构体成…

SpringBoot 配置类解析

全局流程解析 配置类解析入口 postProcessBeanDefinitionRegistry逻辑 processConfigBeanDefinitions逻辑 执行逻辑解析 执行入口 ConfigurationClassPostProcessor.processConfigBeanDefinitions()方法中的do while循环体中 循环体逻辑 parse方法调用链 doProcessConfigurat…

【C++中STL】list链表

List链表 基本概念构造函数赋值和交换大小操作插入和删除数据存取反转和排序 基本概念 将数据进行链式存储 链表list是一种物理存储单元上非连续的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接实现的&#xff0c;链表是由一系列结点组成&#xff0c;结点的组…

Android studio环境搭建过程异常

异常&#xff1a;Connect timed out 创建新项目时&#xff0c;提示time out 解决方案&#xff1a;修改gradle下载地址&#xff0c;使用国内镜像地址 distributionUrlhttps\://services.gradle.org/distributions/gradle-8.2-bin.zip修改成distributionUrlhttps\://mirrors.c…

fatal error:require():Failed opening required

今天部署网站遇到了个错误 fatal error:require():Failed opening required 这个错误经常遇到 大多是网站 是开启了 open_basedir 但今天这个错误很神奇 先说解决方法 1. 检测一下是不是真的 不存在这个文件 即使100%确定 也建议你再仔细看一下 这个文件存不存在 今天我遇…

日常学习之:如何使用 dockerfile 将 vue 的单独前端项目通过 docker 的方式部署到 heroku上

文章目录 需求描述开始操作准备阶段&#xff1a;准备 server.js 文件并安装依赖&#xff0c;将 vue 项目包装成单独的服务器制作 server.js安装 server.js 需要的依赖 构建 Dockerfileheroku container 链接和部署其他细节 需求描述 你想用 vue 构建前端&#xff0c;用 django…

终端录屏神器Asciinema慎用教程

1.效果 2.安装 centos yum install asciinema ubuntu apt-get install asciinema 3.使用 asciinema rec kali.cast #录制文件 asciinema play kali.cast #播放文件 asciinema upload kali.cast #上传文件 详细说明:只使用 asciinema rec 也是可以的,ctrlD结束录屏 4.…

2024年数学建模美赛 分析与编程

2024年数学建模美赛 分析与编程 1、本专栏将在2024年美赛题目公布后&#xff0c;进行深入分析&#xff0c;建议收藏&#xff1b; 2、本专栏对2023年赛题&#xff0c;其它题目分析详见专题讨论&#xff1b; 2023年数学建模美赛A题&#xff08;A drought stricken plant communi…

尚无忧球馆助教系统源码,助教小程序源码,助教源码,陪练系统源码

特色功能&#xff1a; 不同助教服务类型选择 助教申请&#xff0c;接单&#xff0c;陪练师入住&#xff0c;赚取外快 线下场馆入住 设置自己服务 城市代理 分销商入住 优惠券 技术栈&#xff1a;前端uniapp后端thinkphp 独立全开源

Redis 的二进制安装与包管理安装, 全发行版 Linux 通用

博客原文 文章目录 Redis 简介二进制编译安装获取源码包编译安装移动配置文件到安装目录下配置 redis 为后台启动将 redis 加入到开机启动设置 redis 密码 (可选)修改 bind启动 redis apt 安装更换阿里源(可选)安装 redis修改配置文件 Redis 简介 Redis&#xff08;全称为Remot…

【Go】Channel底层实现 ②

文章目录 channel底层实现channel发送、接收数据有缓冲 channelchannel 先写再读channel 先读再写(when the receiver comes first) 无缓冲channelchannel存在3种状态&#xff1a; channel底层实现 // channel 类型定义 type hchan struct {// channel 中的元素数量, lenqcoun…

1.26学习总结

连通性判断 DFS连通性判断步骤&#xff1a; 1.从图上任意一点u开始遍历&#xff0c;标记u已经走过 2.递归u的所有符合连通条件的邻居点 3.递归结束&#xff0c;找到了的所有与u的连通点&#xff0c;就是一个连通块 4.然后重复这个步骤找到所有的连通块 BFS连通性判断步骤…

opencv学习二值分析

内容来源于《opencv4应用开发入门、进阶与工程化实践》 二值分析&#xff1a; 常见的二值化方法&#xff1a; 基于全局阈值&#xff08;threshold&#xff09;得到的二值图像&#xff1b;基于自适应阈值&#xff08;adaptiveThreshold&#xff09;得到的二值图像&#xff1…

RPC教程 7.服务发现与注册中心

0.前言 这一节的内容只能解决只有一个服务的情况。要是有多个服务(即是多个结构体&#xff09;这种就解决不了&#xff0c;也即是没有服务ip地址和服务实例的映射关系。 1.为什么需要注册中心 在上一节中&#xff0c;客户端想要找到服务实例的ip,需要硬编码把ip写到代码中。…