当前位置:网站首页 > 技术博客 > 正文

laya教程



  • 实体组件系统(ECS)
    • 一、 什么是ECS
    • 二、组件脚本的内置方法
      • 2.1 组件的生命周期方法
      • 2.2 组件的事件方法
    • 三、组件在IDE的暴露方式
      • 3.1 组件脚本的识别@regClass()
      • 3.2 组件属性的识别@property()
      • 3.3 IDE中执行生命周期方法@runInEditor
      • 3.4 @classInfo()
    • 四、代码中使用属性
      • 4.1 节点类型方式
      • 4.2 组件类型的使用
      • 4.3 Prefab类型属性

Author:Charley 、谷主、孟星煜

ECS是Entity-Component-System(实体-组件-系统)的简写,这是一种基于数据驱动的游戏设计模式。

LayaAir的ECS,将场景中每一个有着唯一ID的显示对象节点都被看做一个个的实体。每一个实体都可以为其添加一个或多个不同的组件系统脚本。

在这里,组件系统是组件与系统两个组成部分,组件只包含数据,不包含逻辑,游戏对象的逻辑行为由系统控制,所以系统是实体的逻辑控制部分,组件是系统与外界的数据接口部分。LayaAir通过装饰器将接口暴露在IDE中,方便开发者直观的传入数据。引擎为组件系统提供的生命周期方法与事件方法可以作为系统逻辑控制的入口。

开发者通过继承引擎的组件脚本类,可以实现组件系统脚本的完整功能,我们通常将组件系统脚本简称为组件脚本。然后通过IDE或者代码的方式添加到实体上,实现完整的ECS功能。

组件脚本,在原则上是解耦且具有单一职责的,这样方便多个实体可以共用同一个组件系统。

继承引擎的组件脚本类之后,就可以直接使用引擎为组件脚本提供内置的生命周期方法与事件方法,这些方法可以用于组件脚本逻辑的执行入口。如下图所示:

2-1

(图2-1)组件脚本的生命周期方法

生命周期方法是指在物体的创建、销毁、激活、禁用等过程中,会自动调用的方法。当使用自定义的组件脚本时,可以实现如下生命周期方法,方便快速开发业务逻辑。可以在每个方法中打印一条日志,方便开发者进行测试。

名称 条件 onAdded 被添加到节点后调用,和Awake不同的是即使节点未激活onAdded也会调用 onReset 重置组件参数到默认值,如果实现了这个函数,则组件会被重置并且自动回收到对象池,方便下次复用。如果没有重置,则不进行回收复用 onAwake 组件被激活后执行,此时所有节点和组件均已创建完毕,此方法只执行一次 onEnable 组件被启用后执行,比如节点被添加到舞台后 onStart 第一次执行onUpdate之前执行,只会执行一次 onUpdate 每帧更新时执行,尽量不要在这里写大循环逻辑或者使用getComponent方法 onLateUpdate 每帧更新时执行,在onUpdate之后执行,尽量不要在这里写大循环逻辑或者使用getComponent方法 onPreRender 渲染之前执行 onPostRender 渲染之后执行 onDisable 组件被禁用时执行,比如从节点从舞台移除后 onDestroy 手动调用节点销毁时执行

在代码中的使用如下:

下面以“2D入门示例”中的一个子弹脚本为例,讲解生命周期方法,以下是此脚本文件的代码:

在游戏中,将子弹添加到舞台上时,每次添加到舞台都得有初速度,但如果将onEnable()换成onAwake(),那么这个初速度就会失效。onUpdate()是每帧执行一次,子弹超出屏幕,则移除子弹,此处的 if 条件判断是每一帧都会判断一次。onDisable()是节点从舞台移除后触发,当子弹超出屏幕被移除时,就触发这个方法,这里是回收子弹到对象池了。

事件方法是指在某些特定的情况下,会根据条件自动触发的方法。当使用自定义的组件脚本时,可以通过事件方法方便快速开发业务逻辑。

2.2.1 物理事件

使用物理引擎时,我们有时候需要根据物理的碰撞事件在代码中实现逻辑交互。为此,LayaAir引擎在物理碰撞的首次发生时、持续碰撞时以及退出碰撞时,均会触发这些特定的物理事件方法。这些方法在引擎中默认不包含具体实现,可以视为待开发者重写的方法。开发者只需继承类,并在其中重写这些方法,就可以实现自定义的逻辑响应,替代默认的空W实现。

这样,通过重写这些特定方法,开发者可以根据物理碰撞的具体阶段执行相应的游戏逻辑或者交互效果,从而使得游戏或应用能够在遇到物理碰撞时,有更自然、更真实的响应。下面表格罗列了全部的物理事件方法:

名称 条件 onTriggerEnter 3D物理触发器事件与2D物理碰撞事件,开始碰撞时执行,仅执行一次 onTriggerStay 3D物理触发器事件与2D物理碰撞事件(不支持传感器),持续碰撞时执行,每帧都执行 onTriggerExit 3D物理触发器事件与2D物理碰撞事件,结束碰撞时执行,仅执行一次 onCollisionEnter 3D物理碰撞器事件(不适用2D),开始碰撞时执行,仅执行一次 onCollisionStay 3D物理碰撞器事件(不适用2D),持续碰撞时执行,每帧都执行 onCollisionExit 3D物理碰撞器事件(不适用2D),结束碰撞时执行,仅执行一次

通过以上表格可以看出,2D物理事件只有三个事件方法,而3D物理事件,则分为触发器事件和碰撞器事件两类,有六个事件方法。

碰撞器事件是指反生物理反馈的事件,触发器事件是只有物理事件的触发,但没有实际物理碰撞反馈的一种事件,这与2D碰撞体启用了传感器的效果是一样的,只不过2D物理无论是碰撞反馈事件还是启用了传感器的无反馈物体事件,都是只用onTrigger事件。

特别提醒的是,2D物理启用传感器之后,onTriggerStay事件是不被触发的,这一点需要新手开发者注意。

在脚本中的事件使用示例如下:

基于上面的代码示例,为3D模型添加脚本。如动图2-2所示。

2-2

(动图2-2)

2.2.2 鼠标事件

名称 条件 onMouseDown 鼠标按下时执行 onMouseUp 鼠标抬起时执行 onRightMouseDown 鼠标右键或中键按下时执行 onRightMouseUp 鼠标右键或中键抬起时执行 onMouseMove 鼠标在节点上移动时执行 onMouseOver 鼠标进入节点时执行 onMouseOut 鼠标离开节点时执行 onMouseDrag 鼠标按住一个物体后,拖拽时执行 onMouseDragEnd 鼠标按住一个物体,拖拽一定距离,释放鼠标按键后执行 onMouseClick 鼠标点击时执行 onMouseDoubleClick 鼠标双击时执行 onMouseRightClick 鼠标右键点击时执行

在代码中的使用如下:

下面以onMouseDown和onMouseUp为例,在自定义的组件脚本“Script.ts”中加入以下代码:

如图2-3所示,将组件脚本添加到Scene2D的属性面板后,先不勾选 Mouse Through,因为如果勾选它,Scene2D下鼠标事件将不会响应。如果是一个3D场景,它会传递到Scene3D中。

2-3

(图2-3)

运行项目,如动图2-4所示,当鼠标按下时执行onMouseDown,打印“onMouseDown”;松开鼠标,鼠标弹起时执行onMouseUp,打印“onMouseUp”。

2-4

(动图2-4)

2.2.3 键盘事件

名称 条件 onKeyDown 键盘按下时执行 onKeyPress 键盘产生一个字符时执行 onKeyUp 键盘抬起时执行

在代码中的使用如下:

注意:onKeyPress是产生一个字符时执行,例如字母“a”、“b”,“c”等。像上、下、左、右键,F1、 F2等不是字符输入的按键,就不会执行此方法。

在LayaAir 3.0 IDE中,如果想在IDE内展示组件脚本的属性,需要通过装饰器的规则来实现。

开发者编写的组件脚本,需要在类定义之前使用装饰器的标识,示例代码如下所示:

如动图3-1所示,只有使用了上述的这个装饰器标识,开发者自定义的组件脚本才会被IDE识别为组件,可以被节点(实体)的所添加。

3-1

(动图3-1)

一个TS文件只能有一个类使用@regClass() 。

标记了@regClass()的类,在IDE环境内都会被编译,但最终发布时,如果这个类没有被其他类引用,也没有被添加到节点上,或者所在的预制体/场景没有发布,则这个类会被裁剪。

3.2.1 组件属性的常规使用

当开发者想将组件的属性,通过IDE暴露给外界编辑来传入数据。需要在类属性定义之前使用装饰器的标识,示例代码如下所示:

是IDE识别组件属性并显示到IDE属性面板上的装饰器标识,类型是装饰器属性标识必须携带的参数。

如果我们不需要给属性写一个tips说明,也不需要给属性重新定义一个在IDE里显示的别名,等需求。那按上面示例的简写方式即可。

如果简写方式有语法警告,请用新版本IDE,并通过IDE的功能来解决,或者使用标准写法来解决。

3.2.2 属性访问器的装饰器使用

有的时候,开发者会通过属性访问器(getter)和属性设置器(setter)来控制属性的读写行为。

当属性访问器和属性设置器同时存在时,装饰器的属性标识直接用于属性访问器之前即可,此时的组件属性与上一小节中介绍的常规使用方式一样,都是可读写的。

如果,该脚本只有属性访问器,那这个属性则是只读的,仅可以在IDE中显示,但不能编辑。

getter和setter同时存在的装饰器使用示例代码如下:

3.2.3 是否序列化保存

通过装饰器定义为组件属性后,默认状态下,属性名与值都会被序列化保存到组件被添加的场景文件或预制体文件里。例如,scene.ls里添加完自定义组件,通过vscode打开这个scene.ls,可以找到序列化保存后的组件属性名称与值,效果如动图3-2所示。

3-2

(动图3-2)

序列化保存后,不仅方便在IDE中直观查看与编辑组件属性值。在运行阶段,也可以直接使用序列化存储的值,对于结构复杂的数据,直接使用序列化的值还可以节省数据结构生成带来的开销。所以,有些时候,即便是不需要在属性面板上显示与编辑,也可以通过装饰器设置为组件属性,将值序列化存储在场景或预制体文件中。

但是,也有的时候,我们的组件属性只是为了方便在IDE中理解与调整,在使用的时候,这些值其实用不到,所以,还提供了是否序列化保存的控制。当装饰器属性定义的时候,对象参数中传入serializable为false,那么该属性就不会被序列化

例如,开发者的需求是序列化保存弧度值,但弧度值在人为调整数值的时候并不直观,此时,可以在IDE里直接输入角度值但不保存,仅将转换后的弧度值存起来。示例代码如下:

3.2.4 组件属性是否在IDE中显示

在默认情况下,装饰器属性规则只会对非下划线的类属性标记为IDE的组件属性。

对于有下划线的属性,其实是不会被显示到IDE里,此时该组件属性的价值只剩下将值保存到场景文件中了,这一点上文有所提及,示例也有应用。

带下划线的属性如果没有序列化保存到场景文件的需求,那就不必使用装饰器了。

假如,开发者想对有下划线的属性,也要显示到IDE上,也可以做到。将修饰器属性标识的传入对象中,设置参数private为false即可。

示例代码如下:

private参数不仅可以使得下划线属性显示,也可以通过将private设置为true,使得不带下划线的属性,不在IDE的属性面板出现。

这里,我们将前文的弧度转换示例稍作修改,代码如下:

3.2.5 装饰器属性标识的类型

装饰器属性标识的类型支持引擎对象类型(例如:Laya.Vector3、Laya.Sprite3D、Laya.Camera等)、自定义的对象类型(需要标记)、以及TS语言的基本类型。

3.2.5.1 引擎对象类型

引擎对象类型的理解比较简单,暴露组件属性之后,直接传入对应类型的值就可以。例如Laya.Sprite3D就只能传入3D节点,试图拖入2D节点或拖入资源都是禁止的。

常用的引擎对象类型使用示例如下:

如动图3-3所示,将场景中已经添加好的Image拖入到@property暴露的Image属性入口中,这样就获取到了此节点,然后可以在脚本中使用代码控制Image的属性了(参考4.1节)。

3-3

(动图3-3)

3.2.5.2 自定义对象类型

自定义对象类型,就是设置一个自定义的引入对象。按该对象的装饰器属性标识来暴露组件属性。

例如,下面这两个TS代码:

组件脚本MyScript中引用了Animal对象 ,并将装饰器属性标识的类型设置为Animal,尽管Animal不是继承于Laya.Script的组件脚本,但由于被组件脚本MyScript所引用并暴露给IDE,所以Animal类定义之前也需要标记,该类下使用了标识的属性,也可以出现在IDE属性面板中。

3.2.5.3 TS语言基本类型

最后就是常用的TS语言基本类型,不过需要注意的是,基本类型需要使用字符串的方式来描述,只有数字、字符串、布尔类型,可以用其对象类型来标记。

类型 类型书写示范 类型说明 数字类型 "number" 也可以用Number来标记该类型 单行字符串文本类型 "string" 也可以用String来标记该类型 布尔值类型 "boolean" 也可以用Boolean来标记该类型 整数类型 "int" 等价于 { type: Number, fractionDigits: 0 } 正整数类型 "uint" 等价于 { type: Number, fractionDigits: 0 , min: 0 } 多行字符串文本类型 "text" 等价于 { type: string, multiline: true } 任意类型 "any" 类型只会被序列化,不能显示和编辑。 类型化数组类型 Int8Array、Uint8Array、
Int16Array、Uint16Array、
Int32Array、Uint32Array、Float32Array 支持7种类型化数组类型 数组类型 ["number"]、["string"] 用中括号包含数组元素类型,

使用示例代码如下:

示例效果如动图3-4所示:

3-4

(动图3-4)

3.2.6 组件属性值的输入控件

IDE内置了number(数字输入)、string(字符串输入)、boolean(多选框)、color(颜色框+调色盘+拾色器)、vec2(XY输入组合)、vec3(XYZ输入组合)、vec4(XYZW输入组合)、asset(选择资源),这些输入控件。

通常情况下,IDE会根据组件属性类型自动选择对应的属性值输入控件。

但在某些情况下,也需要强制指定输入控件。例如,数据类型是string,但其实它表达的是颜色,用默认编辑string的控件不适合,需要在这里设置组件属性标识的参数inspector为“color”。示例代码如下:

注意:按照以上方法得到的颜色,是2D组件的颜色值,例如:rgba(217, 232, 0, 1)

效果如动图3-5所示:

3-5

(动图3-5)

如果inspector参数为null,则不会为属性构造属性输入控件,这与hidden参数设置为true不同。hidden为true是创建但不可见,inspector为null则是完全不创建。

3.2.7 组件属性分类与排序

组件的属性默认会统一显示在以组件脚本名称的属性分类栏目下,效果如图3-6所示:

3-6

(图3-6)

如果开发者想对组件内的某些属性进行归类,可以通过装饰器属性标识的对象参数catalog来实现,示例代码如下:

通过上面的代码可以看出,当为多个属性(c和d)设置相同的catalog名称(“adv”),就会按catalog名称进行分类。效果如图3-7所示:

3-7

(图3-7)

如果我们想给这个分类再起个中文别名,可以通过参数catalogCaption来实现,示例代码如下(更改上述示例的d属性):

效果如图3-8所示:

3-8

(图3-8)

在面对多个组件属性分类的时候,我们还可以通过参数catalogOrder对栏目的显示顺序自定义排序。数值越小显示在前面,不提供则按属性出现的顺序。示例代码如下:

效果如图3-9所示:

3-9

(图3-9)

属性分类名称catalogCaption与属性分类排序catalogOrder,在任意一个catalog相同名称的属性里配置即可,无需所有的属性都配置一次。

3.2.8 装饰器属性标识参数总结

上文介绍了常用装饰器属性标识的参数作用(加粗为上文出现过的),这里我们概述总结一下全部的参数。

参数名 参数使用示例 说明 name name: "abc" 一般不需要设定 type type: "string" 组件属性可输入值的类型,参照上文的介绍 caption caption: "角度" 组件属性的别名,常用中文,可以不设置,默认会用组件属性名 tips tips: "这是一个文本对象,只能输入文本哦" 组件属性的Tips说明,用于进一步描述该属性的作用等用途 catalog catalog:"adv" 为多个属性设置相同的值,可以将它们显示在同一个栏目内 catalogCaption catalogCaption:"高级组件" 属性分类栏目的别名,不提供则直接使用栏目名称 catalogOrder catalogOrder:0 栏目的显示顺序,数值越小显示在前面。不提供则按属性出现的顺序 inspector inspector: "color" 属性值输入控件,内置有:number,string,boolean,color,vec2,vec3,vec4,asset hidden hidden: "!data.a" true隐藏,false显示。可以直接使用布尔值,也可以使用表达式,通过将条件表达式放到字符串里,获得布尔类型的运算结果。
字符串表达式内,data是一个固定名字的变量,它是当前类型的所有已注册属性的数据集合。表达式内可以使用所有js语法,但不能引用引擎相关类型,也不能使用Laya等全局对象。 readonly readonly: "data.b" true表示只读。可以直接使用布尔值,也可以使用表达式,通过将条件表达式放到字符串里,获得布尔类型的运算结果。(表达式的格式同上) validator validator: "if (value == data.text1) return '不能与text1值相同' " 可以使用表达式,将表达式放到字符串里。例如示例中,若在IDE中输入的值和text1的值相等,就会显示”不能与text1值相同“ serializable serializable: false 控制组件属性是否序列化保存,true:序列化保存,false:不序列化保存 multiline multiline: true 字符串类型时,是否为多行输入,true:是,false:不是 password password: true 是否密码输入,true:是,false:不是。密码输入会隐藏输入的内容 submitOnTyping submitOnTyping: false 如果设置为true,那么每次输入一个字符,就会提交一次。如果设置为false,那么只有当输入完成后,并且点击其它地方,让文本输入框失去焦点时,才会提交一次。 prompt prompt: "文本提示信息" 在输入文本前,文本框内会有一个提示信息 enumSource enumSource: [{name:"Yes", value:1}, {name:"No",value:0}] 组件属性以下拉框的形式来展示与输入值 reverseBool reverseBool: true 反转布尔值,当属性值为true时,多选框显示为不勾选 nullable nullable: true 是否允许null值,默认为true min min: 0 数字类型时,数字的最小值 max max: 10 数字类型时,数字的最大值 range range: [0, 5] 数字类型时,组件属性在一个范围内以滑动杆的方式显示与输入值 step step: 0.5 数字类型时,在输入框的鼠标滑动或滚轮滚动的最小更改精度值 fractionDigits fractionDigits: 3 数字类型时,属性值的小数点后保留几位 percentage percentage: true 将range参数设置为[0,1]时,可以让percentage为true,显示为百分比 fixedLength fixedLength: true 数组类型时,固定数组长度,不允许修改 arrayActions arrayActions: ["delete", "move"] 数组类型时,可限制数组可以进行的操作。如果不提供,表示数组允许所有操作,如果提供,则只允许列出的操作。提供的类型有:"append","insert" ,"delete" ,"move" elementProps elementProps: { range: [0, 10] } 对数组类型属性适用。这里可以定义数组元素的属性 showAlpha showAlpha: false 颜色类型时,表示是否提供透明度a值的修改。true表示提供,false表示不提供 defaultColor defaultColor: "rgba(217, 232, 0, 1)" 颜色类型时,定义一个非null时的默认颜色值 colorNullable colorNullable: true 颜色类型时,设置为true可显示一个checkbox决定颜色是否为null isAsset isAsset: true 说明此属性是引用一个资源 assetTypeFilter assetTypeFilter: "Image" 资源类型时,设置加载的资源类型 useAssetPath useAssetPath: true 属性类型是string,并且进行资源选择时,这个选项决定属性值是资源原始路径还是res://uuid这样的格式。默认false,如果是true,则是资源原始路径,一般不使用,因为如果资源改名,路径会丢失。 position position: "before x" 属性显示的顺序默认是在类型定义里出现的顺序,position可以人为改变这个顺序。可以使用的句型有:"before x"、"after x"、"first"、"last" private private:false 控制组件属性是否显示在IDE里,false:显示,true:不显示 addIndent addIndent:1 增加缩进,单位是层级,注意不是像素 onChange onChange: "onChangeTest" 当属性改变时,调用名称为onChangeTest的函数。函数需要在当前组件类上定义

代码示例如下(只列出上文没有介绍过的):

3.2.9 装饰器属性标识特殊用法

除了以上列出的基本参数属性,@property还有一些特殊的组合用法。

  • 类型属性嵌套数组或字典

示例如下:

它的一个重要应用就是实现动态下拉框。前面3.2.8节介绍过有两种方式可以实现下拉框:一是设置属性类型为Enum,二是通过设置enumSource为数组。这两种方式都可以实现固定的下拉选项列表,但如果想让选项列表是动态的,可以使用以下方式:

除了在IDE属性面板上暴露组件属性,开发者还可以通过装饰器标识 来让组件在IDE内加载时也可以触发生命周期方法(onEnable、onStart等所有的组件脚本生命周期方法)。示例代码如下:

除非有特别的需求,我们并不建议这样做,一方面是因为静态物体更有利于IDE内进行编辑。另一方面是因为场景编辑器为了性能优化,帧率刷新要比正常运行慢很多,因此效果会与正常运行有明显差异。

装饰器标识主要有两个作用:

3.4.1 加入IDE的组件列表

开发者的自定义组件脚本默认都位于面板的的下面,如动图3-10所示。

3-10

(动图3-10)

如果我们想在这个中,将该组件加入自己定义的组件列表分类中,可以使用装饰器标识,示例代码如下所示:

然后我们保存代码,回到IDE,会发现自定义的分类已出现在组件列表中。如动图3-11所示。

3-11

(动图3-11)

3.4.2 属性分组

假设用装饰器暴露了A、B、C、D、E,5个属性,显示效果如下:

3-12

(图3-12)

当属性比较多时,可以将属性分组显示,就要用到@classInfo()了,@classInfo()可以为类型添加非数据类型的属性。例如,将BC两个属性显示在一个组里,实现方式如下:

其中,members指定了属于这个分组的属性名称列表。如果属性较多,也可以用这种格式 [ "b~c" ],表示从属性b到属性c之间所有的属性。position是可选的,指示这个分组显示在哪里。显示效果如下:

3-13

(图3-13)

前文已经介绍了组件组件的添加与识别。相信有一定基础的开发者已经可以直接使用LayaAir的实体组件系统了。

但针对一些新手开发者朋友,本小节通过几种常用类型的属性使用示例,进一步帮助大家理解组件化开发的基础。

LayaAir分为2D节点与3D节点类型,当设置为2D节点Laya.Sprite时,不能将3D节点作为其属性值。当设置为3D节点Laya.Sprite3D时,不能将2D节点作为其属性值。

4.1.1 2D节点的使用

首先,如动图4-1所示,将场景中已经添加好的2D节点Sprite拖入到@property暴露的属性入口中,这样就获取到了此节点。

4-1

(动图4-1)

然后就可以在脚本中使用代码改变节点的属性了,例如,给Sprite添加纹理等,示例代码如下所示:

效果如图4-2所示:

4-2

(图4-2)

4.1.2 3D节点的基础使用

首先,如动图4-3所示,将场景中已经添加好的3D节点Cube拖入到@property暴露的属性入口中,这样就获取到了此节点。

4-3

(动图4-3)

然后就可以在脚本中使用代码改变节点的属性了,例如,可以让Cube绕自身旋转,示例代码如下所示:

效果如动图4-4所示:

4-4

(动图4-4)

4.1.3 3D节点的进阶使用

通过暴露@property( { type :Laya.Sprite3D } )节点类型属性,来拖入particle节点,可以获得particle节点对象。transform可以直接修改,而simulationSpeed属性则通过getComponent(Laya.ShurikenParticleRenderer).particleSystem的方式获取。

通过暴露@property( { type : Laya.ShurikenParticleRenderer } )组件类型属性,来拖入particle节点,可以获得particle的ShurikenParticleRenderer组件。transform可以通过(this.p3dRenderer.owner as Laya.Sprite3D)修改,而simulationSpeed属性则通过this.p3dRenderer.particleSystem的方式获取。

不能通过直接使用Laya.ShuriKenParticle3D作为属性类型,因为IDE无法识别,只有节点和组件类型可以识别。

就算将type类型设置为Laya.Sprite3D,这样IDE虽然标识了属性是Sprite3D节点,但也无法转换为Laya.ShuriKenParticle3D对象。

当使用Laya.Prefab作为属性时,例如:

此时,需要按动图4-5所示,从assets目录下,拖入prefab资源。运行时会直接获取到加载实例化后的prefab。

4-5

(动图4-5)

版权声明


相关文章:

  • 左连接查询sql语句 实例2024-11-09 10:29:59
  • left join和right join和inner join的区别2024-11-09 10:29:59
  • psr代码规范2024-11-09 10:29:59
  • 一个usb接口最多能连接的设备数2024-11-09 10:29:59
  • 图像处理中的数学方法2024-11-09 10:29:59
  • 虚拟机系统都有哪些2024-11-09 10:29:59
  • 微信小程序源码平台2024-11-09 10:29:59
  • py文件是啥2024-11-09 10:29:59
  • 弹性盒子样式2024-11-09 10:29:59
  • android中textview属性2024-11-09 10:29:59