国民游戏王者荣耀的真实地图开发之路
👉腾小云导读
相信很多人都玩过王者荣耀,大家在欣赏其华丽的游戏界面以及炫酷的游戏技能时,是否好奇过王者荣耀的地图是怎样开发出来的?在开发的历程中,都有哪些问题?是怎样解决的?本文将从其地图设计到完成的整个流程讲解王者荣耀地图轻量解决方案,希望可以给你带来灵感。
👉看目录点收藏,随时涨技术
1 项目背景
2 技术方案演进
2.1 地图方案选型
2.2 技术方案选型
3 项目架构设计
3.1 整体结构
3.2 UI框架
3.3 数据传输
3.4 小结
4 项目中问题以及解决方案
4.1 三端坐标系统一
4.2 Anroid点击事件处理
4.3 Anroid沉浸式问题处理
4.4 Anroid点9图功能支持
4.5 联调流程优化
5 总结
01
项目背景
地图展示作为游戏 LBS 社交的基础能力,是王者荣耀地图技术落地中需要突破和解决的事情。
地图能力是地图开放平台的核心能力,在经过第一次沟通后,明确了几个核心需求:王者地图UI的展示 、POI 点省市县排行、热门街区排行 、定位能力的输出。
并且也明确了由地图团队提供 Unity 上的地图展示方案,由王者团队、阿波罗团队以及地图团队共同开发该项目。
接下来就进入了技术方案的调研和设计阶段。
02
技术方案演进
2.1 地图方案选型
地图展示作为游戏 LBS 社交的基础能力,是当前方案中最需要突破和解决的事情。按照《王者荣耀》的整体计划,留给调研设计、研发和联调也就只有 1 个月的时间,在技术选型上更多的是结合当前已有的地图能力对外输出。
从现状出发,地图开放平台对外输出移动端地图 sdk,使用平台分为 Android 端和 ios 端,在效果上可以分为两类,2D 版本和 3D 版本。区别如下:
2D 版本的地图提供了基础的地图展示能力,3D 版本的地图可以支持更酷炫的建筑物拔起效果以及无极缩放等,在体验上更酷炫,但所占用的包大小更大。
android:
包大小 | 包增量 | |
栅格1.2.8 | 221K | 115K |
矢量4.0.1 | jar包2.3M(包括资源文件1.1M),so库1.3M(armV7a) | 2.2M(armv7a) |
ios:
代码段 | |
栅格1.2.7 | 321K(arm64) |
矢量4.0.0 | 1490K (arm64) |
从王者系统的第一期需求效果图来看,2D 版本的地图是完全可以满足的。而王者对于包大小也有严格的要求。
基于此,我们把地图支持的项目目标定义为:为王者荣耀提供基于 2D 效果的轻量级游戏解决方案。
2.2 技术方案选型
2.2.1 第一阶段 原生View挂载可行性分析
明确了使用 2D 地图 sdk 对外输出后,需要解决的是如何将两个平台 ( Android 和ios )的原生 View 和 Unity 的 View 结合在一起。
Unity 与原生的 andorid 和 ios 相互调用,在技术上是可行的。之前王者内部是有一些页面由各个团队提供的原生 view 支持(主要是一些独立的 webview 页面,如英雄故事,王者规则等)。
2.2.1.1 Android可行性分析
Android一般情况有三种方式实现地图:
1)启动新的 Activty,展示一个全新的页面;
2)使用 WindowManager,在游戏 Activity 之上显示一个新页面;
3)加载原生 View,需要将原始View挂载到游戏 Activity 之上。
第一种方案一开始就被pass了。由于已明确了 Unity 业务逻辑,上层负责 UI 展示,而展示地图时,Unity 侧还需要进行一些逻辑处理。新起一个 Activity, 在体验上和逻辑上都行不通。
第二个方案和第三个方案原则上都行得通,两种方案也都做了验证。本文介绍的是第三种方案。
原理如下:
public class UnityPlayerNativeActivity extends NativeActivity
{ protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code // Setup activity layout @Override protected void onCreate (Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); getWindow().takeSurface(null); setTheme(android.R.style.Theme_NoTitleBar_Fullscreen); getWindow().setFormat(PixelFormat.RGB_565); mUnityPlayer = new UnityPlayer(this); if (mUnityPlayer.getSettings ().getBoolean ("hide_status_bar", true)) getWindow ().setFlags (WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(mUnityPlayer); mUnityPlayer.requestFocus(); } ..............................
}
这个是 Android 中 Unity 中 Activity 的基类,而 mUnityPlayer 也是通过 setContentView 加载的,也就是加载到 DecorView 上。所以只需要再将 Native 的View 加载上去就可以了:
ViewGroup rootView = (ViewGroup)activity.getWindow().getDecorView();
ViewGroup.LayoutParams param = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
rootView.addView(mView, param);
2.2.1.2 ios可行性分析
ios 侧可以通过将原生View挂载在地图的 Window 上。
/*** 获取场景挂载点 (keyWindow)* @return 挂载点*/
+ (UIView *)getMountPoint{UIWindow *window = [UIApplication sharedApplication].keyWindow;NSAssert(window != nil, @"window must not be nil");return window;
}
/*** 挂载到 keywindow.*/
- (void)mount{UIView *mountPoint = [[self class] getMountPoint];NSAssert([self underlyingView].superview == nil , @"scene super view must be nil");[mountPoint addSubview:[self underlyingView]];
}
以上方案均在 Unity 侧验证通过。
2.2.2 第二阶段 View层级关系
从整体需求来看,上层不仅仅是一个单独的地图,还要有很多的 UI 元素:
那么上面的按钮、其它元素如何去做呢?
理想的方案:由地图单纯的提供地图以及地图上的标注元素,上面的元素仍然由Unity 侧进行绘制。这样只需要将地图的显示插入到 Unity 的层级中。可以看一下Unity 的原理。
Android 侧由于 Activity 加载的是 UnityPlayer,这里可以看一下 UnityPlayer 的代码:
private SurfaceView n;
public class UnityPlayer extends FrameLayout implementscom.unity3d.player.a.aaa {public UnityPlayer(final ContextWrapper m) {super((Context) m);this.n = new SurfaceView((Context) m);this.n.getHolder().addCallback((SurfaceHolder.Callback) new SurfaceHolder.Callback() {public final void surfaceCreated{//缺省**************************}public final void surfaceChanged{//缺省**************************}public final void surfaceDestroyed(final SurfaceHolder surfaceHolder) {
//缺省**************************}});this.n.setFocusable(true);this.n.setFocusableInTouchMode(true);//缺省**************************}
}
其实内部的 Unity 在渲染原理上是一个 SurfaceView 。比较容易理解,由于地图渲染使用的是普通的 sdk,和view层级不在一个级别,而且要将原生的 View 放到 SurfaceView 下面进行展示,也是做不到的,起初有一个很好的比喻可以解释,SurfaceView 会将屏幕扣出一个洞,然后进行绘制,因此只要这块区域通过SurfaceView 进行了绘制,普通View就没办法进行渲染了。
如果是 SurfaceView,基于 OPenGl 渲染的 3D 地图 sdk 就成了可选方案,但需要解决如何将 Unity 和 Native 两层渲染打通,这里会涉及到大量的改动以及接口封装,考虑到方案调研和研发的时间成本以及包大小的控制,前期不必在这个方案上做深究。可以得出如下结论:
如果上次使用原生的 View 进行地图渲染,那么在此地图上的所有 UI 元素,都必须使用原生 View 进行绘制。
2.2.3 第三阶段 技术设计原则
确认了上层都使用了端上的原生 View 进行绘制,那么这次的需求就不再只是地图的能力支持了。而需要考虑到业务逻辑的变化性,将王者层和地图平台层进行明确划分:地图平台团队负责 UI 渲染部分,王者团队负责具体的产品交互和业务逻辑,阿波罗团队负责 Native 和 Unity 之间的桥接中转。
这样,就存在了 Unity 调用原生 Android/ios 以及 Native 调用 Unity 的一系列调用。阿波罗团队将会承担中间的通道中大量的中转任务。中转过程中,涉及大量的数据结构。一旦结构发生变化,就需要 Unity 以及原生的 Android 和 ios 平台进行数据格式的调整。
为了降低维护三个平台数据结构的复杂度,同事们提出引入 JCE 作为 Unity 和Android/ios 的数据结构头文件。结合公司内部的JCE语法和编译平台,就可以做到维护一份标记语言。
加入 JCE 后,就可以彻底把阿波罗团队解放出来,使其更专注于数据通道的实现,改变后的三层结构是这样的:
2.2.4 第四阶段 技术方案确认
讨论完可行性和数据交互协议以后,团队一开始就是准备上层按照王者的具体需求去实现 UI 展示效果,然后数据由王者来填充。
这个阶段时间不长,王者团队又提出:能否定制一些按钮的显示位置,文字大小等。毕竟很多时候需求会有变化,这就涉及到一个思考:
为什么要定制 UI?为什么不做一套通用的UI框架来实现王者的需求?
开始这样思考的时候,已经按照之前的计划排期了。整体的研发为三周时间,第一周完成首页面的开发,后面两周都做联调。做整体架构和具体实现也就只有一周时间。
我仍记得这个场景:当时我们团队几个人到了会议室,已是某个周五下午的5点多,整个会议室充斥着沿这个思路去策划方案的兴奋!没有理由不去做。
我和同事放下「狠话」说:“做不出来,晚上不回去!”结果,我们奋战到第二天日出。
为了纪念那个周五晚上,我们把这个方案名叫做 Friday。
03
项目架构设计
由于目标从实现具体的页面,转向实现一套跨平台的 UI 框架,那么就需要考虑这套 UI 框架如何去定义、去创建。
3.1 整体架构
整体架构和最初的思路没有太大出入,地图团队提供一套完整的 native UI 框架以及实际的渲染方案,王者团队负责业务逻辑以及绘制逻辑,而作为通道的阿波罗团队负责数据中转。
3.2 UI框架
关于如何设计 UI 框架、Android、ios、react native、小程序等等,市面上很多事物都有一套设计规则。本人的理念是源于一本书(记得好像是一本杂志)。
当你打开这本书,你就进入了这个为你贴身打造的场景(Scene),每一页(Page)都是为你定制的内容,有文字(Label)、图片(ImageView)、各种图文混排的组合......
因此,我们将整体的 UI 分为三层:Scene、Page、View 控件。
Scene 场景:Friday Engine 提供一个场景,所有的UI展示都在该场景中。 Page 页面:Scene中可以添加多个 Page。可以是全屏的,也可以自定义大小。 View 控件:在每个 Page 中可以添加多个View控件,来实现实际展示效果。目前包括:Label、Button、ImageView、MapView、Tableiew、LoadingView、TextBox 等。 |
坐标系:
有了这三层框架,下一步就是如何将View控件放到指定的位置,这就需要有标准的坐标系。整体的坐标系定位是基于父元素左上角为(0,0)的点。有了坐标系,想把控件放到位置,还需要知道这个控件的大小,因此,需要有控件的宽高:
Z轴:
有了坐标系和 view 宽高,控件就可以绘制到指定区域了,但出现的层级关系如何处理,谁在上谁在下呢?这就需要纵向层级属性:ZIndex。如图,地图在下方,其他元素在上方。
控件以 ZIndex 为 order 确认纵向层级,同一层级的控件按照显示范围依次绘制。存在遮挡区域的不同控件,通过设定不同的 Z 轴 index 进行层级划分,默认为 0,越往上数值越高。
View 控件集合:
控件 | 含义 |
UKLabel | 文本控件 |
UKButton | 按钮控件 |
UKImageView | 图片控件 |
UKMapView | 地图控件 |
UKTableView | 列表控件 |
UKCheckbox | 单选框控件 |
UKLoadingView | loading控件 |
UKViewGroup | View组合控件 |
并且定义了 View 的通用属性:
属性 | 含义 |
id | 文本控件 |
rect | 显示区域 |
backgroundcolor | 背景色值 |
UKMapView | 地图控件 |
zIndex | 纵向index |
invisible | 是否隐藏 |
这里有一个有意思的点。通用属性里有一行:invisible、bool 值。含义是:是否隐藏。
这里没有用 visible:是否显示。简单解释一下,中间通过 JCE 数据格式进行数据传输,默认不填数据,bool 值默认是 false。那么假设这里设置的是 visible,而用户没有设置该属性的话,值就默认是 false,不显示了。这可不是用户想要的,用户还是希望默认是显示的。于是便有了这样的定义。
下面是一个文本控件的JCE格式示例:
struct UKLabel {// View通用属性0 require UKInt id; //唯一标示1 optional UKInt zIndex;//z轴索引2 require UKRect rect;//显示区域,坐标,宽高3 optional UKBool invisible;//是否隐藏4 optional UKColor backgroundColor;//背景色// 文本属性.5 optional UKString text; //文本6 optional UKColor textColor; //文本颜色7 optional UKColor highlightedTextColor;//高亮时的颜色8 optional UKFont font;//字体9 optional UKTextAlignment textAlignment;//文本位置,居中,居左等10 optional UKEllipsis ellipsis;//文本省略方式};
3.3 数据传输
UI框架大致如上,而如何将这一整套框架运转起来呢——数据驱动。
设想一下荣耀页面整体的运转流程:
王者用户点击荣耀战区,会进入荣耀地图页面。那么这时候,需要进入该场景,也就需要创建一个 Scene。然后需要加载一个页面,就是一个Page。之后在 Page 上添加地图 View、添加按钮、添加图片、添加文字等元素。经过这些元素的添加,整个页面就显示出来了。 |
然后,接受用户的事件,譬如说一个按钮的点击,点击事件获取到以后,就需要进行下一步的处理,譬如修改某个文本,设置某个图片的元素等等,也就是会继续向该框架发送下一个指令。
总结来说,要做两件事:Unity 向 Friday 发送指令,Friday 将用户事件回调给 Unity。这两件事情可以归纳为:方法调用和事件回调。
这里要解决两个问题:
1)如何通过数据完成方法调用和事件回调?
2)如何找到对应的调用对象?
3.3.1 方法调用
举一个例子,设置文本控件的文字,正常的方法调用是这样的:
class UKLabel{/**设置文本*/public void setText(UKString text){if(text != null){setText(text.getval())}}
}UKLabel label = new UKLabel();
label.setText("hello world");
那么如何去解决呢,方法如下:
如上图所示,方法名对应数据的变量名,参数对应数据的参数值,参数类型就对应的是数据的参数类型,是否被调用就对应变量值是否为空,这样就完成了一个普通方法的调用。
下一个问题:多个参数如何处理?既然参数类型对应的是变量类型,那么多个参数只要设计一个结构体进行存储即可。
按照这套规则,我们可以看到,同时可以有多个方法被调用。这大大增加了使用的灵活性,减少冗余数据的出现。而顺序则是按照既定或协商好的顺序执行。
方法可以调用了,接下来就是修改文本,但修改哪一个文本控件的文字呢?
这就需要找到指定的文本控件。如前面的 Label 的 JCE 数据所示,所有的 View 控件都是有一个 id 的,而且所有的 View id 要求必须唯一,而且 id 的规则是由外部调用者(王者)决定的。这就解决了方法调用对象的问题,通过 id 索引,找到对应的View控件,从而调用到该控件支持的方法,完成完整的方法调用。
因此,一个方法调用包括两部分:方法目标(Target)、方法体(Method)。
Target 中包含该对象的 id,方法体包含具体的方法数据。而这里还需要解决一个问题,因为拿到的数据虽然有对象,通过对象也能知道该对象的类型,并且拿到该对象类型支持的方法类型,也能把方法体解析出来。但为了方便,还是直接将方法类型封装在 target 里,便于快速解析,如:
由于所有数据都进行了 JCE 格式的压缩,数据以二进制的形式通过阿波罗团队在Unity 和 Friday 之间传递,对外暴露的接口在 android 侧是下面这个样子:
/*** 对外调用接口* @param target* 消息目标,jce格式化后的数据* @param method* 数据参数,jce格式化后的数据* */public void call(byte[] target, byte[] method);
所有的方法调用都是通过该通道传输。
3.3.2 事件回调
方法调用完成后,另一块就是看各种事件如何传递给 Unity 侧。如一个点击事件:一个TableView 的某一项被点击、CheckBox 某一项被选中、某个地图上的标注被点击等等。
如何构造回调事件,需要解决两个问题:
1)是谁发生了点击或状态变化 2)发生的变化是什么 |
关于1):因为每个对象都有了唯一的标识,所以向外输出时,可以将该id对外发布。而为了外部解析的便捷,也将回调的对象类型和数据类型一起回调给 Unity。
示例如下:
struct UKCallbackTarget {0 require UKInt targetID;//回调时间的id1 require UKTargetType targetType;//回调的对象类型 如:Button,TableView2 require UKCallbackType callbackType;//回调数据类型,如点击或者状态变化
};
关于2):对应不同的点击事件,定义了不同的回调类型,并且将所需的数据封装起来一起回传。如 TableView 的点击回调数据类型,需要回调 Unity 哪一条被点击:
struct UKTableViewCallbackData_Clicked {0 require UKInt idx; //被点击的item的index
};
其他的回调也都是类似,同方法调用,回调对外提供的接口为:
/*** 回调,目前支持点击回调,或者事件回调* @param target* 回调事件对象* @param @data* 回调事件数据* */public void callback(byte[] target, byte[] data);
而阿波罗团队只需要将方法调用和事件回调中的两份数据传递给王者团队,即可完成通道作用。
3.4 小结
通过UI的框架和方法的调用以及回调系统的设计和研发,整体的设计架构也就基本搭建完成了,剩下的就是不同UI控件的具体实现和接口输出了。这一部分是在第一周研发的前期完成,包括文本、图片、TableView、按钮等控件等,通过这些已经可以基本模拟出王者第一个页面的显示。第一周的研发工作也基本告一段落,下一步就是”开赴成都,与王者团队会师“!
04
遇到的问题和解决方案
第一周时,团队准备了详细的设计方案和使用文档,以为可以轻轻松松去联调了。结果还是遇到了很多问题。
4.1 三端坐标系统一
Untiy 有自己的一套坐标系,拿到的坐标系在 Android 侧既不是 dp 也不是像素,在 ios也是一样。当时自己和同事的第一反应是找一下 Unity 的坐标系原理,确认其和端上的转换关系,只有这样才能把控件绘制到王者游戏中想要的位置。
我们在不同的设备上测试了一下,没有找到什么规律,也查找了 Unity 坐标相关的文档,短时间内没有找到解决问题的思路。Andorid 和 ios 建立的都是以像素为单位的坐标系,如果寄希望于上层 Unity 以终端的设备为单位的坐标系去设置所有控件的宽高、位置等属性,对于 Unity 是很大的负担。
但无论坐标系是怎么样的,都是一个基于平面的坐标系,而屏幕宽高比是一致的。如王者在 Vivo XPlay5 获取的屏幕宽高(横屏)是:
size: {width: {val: 1280}height: {val: 720}
}
而终端通过以下代码获取屏幕宽高:
WindowManager wm = this.getWindowManager();
ScreenUtils.width = wm.getDefaultDisplay().getWidth();
ScreenUtils.height = wm.getDefaultDisplay().getHeight();
结果:width:2560;height:1440;手机屏幕密度是 3.5
由于王者所有的UI元素都是基于范围为(1280*720)的坐标系建立的,而手机端的显示都是基于(2560*1440)的坐标系建立的,但比例是一样的,只需要将所有的坐标做一个比例映射就可以解决。
4.2 Android 点击事件处理
4.2.1 原生 View 无法获取焦点
在加载 Android 原生 View 后会出现一个问题,从UI层级上看,原生页面在上,Unity 页面在下,但上层却没有收到点击事件。经过和阿波罗团队的沟通,得出了解决问题的思路和方案:
我们知道,Android 程序都是运行在 dalvik/art 虚拟机上的,而 Unity 程序是运行在(mono/il2cpp)上。当一个Unity应用想要用到 Andorid 的方法时,毫无疑问,这个应用就需要两套虚拟机同时运行,即两个虚拟机运行在同一个进程中。 那么,Unity 与 Android 之间的交互,其实就是两个 VM 之间的相互调用,如下图: 如上图所示,Unity 通过 UnityEngine 提供的 API 调用 Android 的方法;Android 借助 com.unity.player 包提供的 API 调用 Unity 的方法。 |
点击事件是先由 Unity 侧先收到,如果需要传递到 Android 侧,可以设置:统一转发机制允许将事件传播到 DalvikVM。需在AndroidManifest.xml 文件中的 activity 子节点下增加如下两行代码。
<meta-data android:name="android.app.lib_name" android:value="unity" />
<meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
通过此方式,将点击事件传递到 Android 侧。点击传递如下:
这样 Android 侧的 View 就可以接收到事件了。
4.2.2 Unity侧点击事件处理
通过以上方法解决了 Andorid 侧无法获取点击事件的问题,但如上图所示,Unity 侧还是会收到事件,这样会触发一些 Unity 的点击逻辑。这是所有人都不希望的,最后在王者团队和阿波罗团队讨论后,采用 Unity 官方论坛的一条解答方案对此问题进行了解决:
在展示 android 页面时,在 Unity 侧添加一个蒙版,Untiy 此时不处理该事件,而是直接转移到 Android 侧。来源:
http://answers.unity3d.com/questions/829687/android-plugin-touch-issues-with-forwardnativeeven.html The answer goes the same as in this question: "You have two possible solutions here: create a separate window for your view, for example a Dialog; create your own activity which shows your view. The reason is that UnityPlayerNativeActivity consumes all these events, because the input loop is associated with Unity window." |
4.3 Android沉浸式问题处理
王者在 Andorid 侧采用了沉浸式模式,沉浸式在显示界面上,默认情况下是全屏的,状态栏和导航栏都不会显示。而当需要用到状态栏或导航栏时,只需要在屏幕顶部向下拉,或者在屏幕右侧向左拉,状态栏和导航栏才会显示出来,此时界面上任何元素的显示或大小都不会受影响。过一段时间后如果没有任何操作,状态栏和导航栏又会自动隐藏起来,重新回到全屏状态。
举例来说非沉浸式,部分沉浸式(状态栏隐藏),完全沉浸式:
很多 Android 手机是有虚拟按键的,但效果上打开王者荣耀的效果,边缘的虚拟按键以及顶部的状态栏都是不显示的。这里有两个小细节,如下:
屏幕宽高
获取屏幕宽高,一开始是通过上面提到的方法获得:
WindowManager wm = this.getWindowManager();
ScreenUtils.width = wm.getDefaultDisplay().getWidth();
ScreenUtils.height = wm.getDefaultDisplay().getHeight();
在王者没有设置沉浸式模式的时候,是没有问题的。但该宽高是不包括虚拟按键的宽高的,这就导致王者在设置沉浸式以后,出现显示不全屏的问题,边上少了一块。
那我们看一下如何设置沉浸模式:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}@Overridepublic void onWindowFocusChanged(boolean hasFocus) {super.onWindowFocusChanged(hasFocus);if (hasFocus && Build.VERSION.SDK_INT >= 19) {View decorView = getWindow().getDecorView();decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);}}}
其实也是通过 Activity 的 DecorView 进行设置的沉浸模式,那 DecorView 的宽高肯定在该处也会变成全屏大小了,通过测试确实如此,由此也解决了显示少一部分区域的问题。
WindowManager
现在王者荣耀里有很多其他的原生页面(Android/ios),使用的是 webview 进行显示独立的信息。譬如说英雄传说,世界起源等页面,在目前的展示上似乎没有达到沉浸式的效果,这里方法上根据一些相关团队的研发介绍,应该是通过WindowManager 的方式添加的,做了一些测试,但没有达到需要的效果。
以下是通过添加 WindowManager 的方法:
WindowManager windowManager = activity.getWindowManager();if (mScene.getParent() != null) {windowManager.removeView(mScene);}try {windowManager.addView(mScene, params);} catch (WindowManager.BadTokenException e) {e.printStackTrace();} catch (WindowManager.InvalidDisplayException e) {e.printStackTrace();}public WindowManager.LayoutParams createLayoutParams(int left, int top, int width, int height) {WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();windowParams.gravity = Gravity.LEFT | Gravity.TOP;windowParams.flags = windowParams.flags| WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED| WindowManager.LayoutParams.FLAG_FULLSCREEN| WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;windowParams.width = width;windowParams.height = height;windowParams.x = left;windowParams.y = top;windowParams.format = PixelFormat.TRANSLUCENT;windowParams.softInputMode |= WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;if (mActivityReference.get() != null) {windowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;} else {windowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;}windowParams.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;return windowParams;}
添加的方式我们请教了相关开发人员。后面添加了些代码,想以此去解决虚拟按键显示的问题,如上图所示,进行了一些尝试:
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;windowParams.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
这起到了一定的效果,但在有虚拟按键的手机上,进入页面后会先闪一下虚拟键盘然后消失,体验上不够好。我们通过 DecorView 方式进行添加,则不存在该问题,因此,也就没有更换方案。
这里还是蛮有意思的,感兴趣的开发者可以想一下解决方案。WindowManager 的方案是不需要考虑点击事件传递的,这一点对于方案来说应该是更方便,方案迁移上也更好。
4.4 Android 点9图功能支持
这个课题很有意思,如何将一张普通图片以点 9 的形式提供拉伸、缩放的能力?
Unity 里提供了大量的类似使用方式,只提供普通图和拉伸点,来实现拉伸效果。这种方式也很快在 ios 里得到了验证和实现。而在 android 里,如何做到这种效果呢?
一张普通的图如何实现点9的效果,网上的解答基本都是从 NinePatch 的原理讲起,反向推导输出方案。
这一块其实可以看一下点9图的编译过程,也是很有意思。最后编译后的图并不是点 9,而是一张 png 图片,并且携带了 ninepatchConfig 的信息。那么此时的思路其实就是伪造一份 NinePatchConfig,就可以实现普通图的效果了。
再看 NinePatchDrawable 的构造方法:
/*** Create drawable from raw nine-patch data, setting initial target density* based on the display metrics of the resources.*/public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk,Rect padding, String srcName) {this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), res);}
其实,支持这一思路的可行性,只需要构造 chunk 的二进制流,就可以伪装成点 9 图的效果。
拿到一张点 9 图,android 是通过 NinePatch 进行处理,点 9 图无非是在普通图上打几个点,作为拉伸的依据,即 NinePatchConfig,然后交由 Native 层进行处理,NInePatch 的代码不多:
// NinePatch chunk.
class NinePatchChunk {public static final int NO_COLOR = 0x00000001;public static final int TRANSPARENT_COLOR = 0x00000000;public Rect mPaddings = new Rect();public int mDivX[];public int mDivY[];public int mColor[];private static void readIntArray(int[] data, ByteBuffer buffer) {for (int i = 0, n = data.length; i < n; ++i) {data[i] = buffer.getInt();}}private static void checkDivCount(int length) {if (length == 0 || (length & 0x01) != 0) {throw new RuntimeException("invalid nine-patch: " + length);}}public static NinePatchChunk deserialize(byte[] data) {ByteBuffer byteBuffer =ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());byte wasSerialized = byteBuffer.get();if (wasSerialized == 0) return null;NinePatchChunk chunk = new NinePatchChunk();chunk.mDivX = new int[byteBuffer.get()];chunk.mDivY = new int[byteBuffer.get()];chunk.mColor = new int[byteBuffer.get()];checkDivCount(chunk.mDivX.length);checkDivCount(chunk.mDivY.length);// skip 8 bytesbyteBuffer.getInt();byteBuffer.getInt();chunk.mPaddings.left = byteBuffer.getInt();chunk.mPaddings.right = byteBuffer.getInt();chunk.mPaddings.top = byteBuffer.getInt();chunk.mPaddings.bottom = byteBuffer.getInt();// skip 4 bytesbyteBuffer.getInt();readIntArray(chunk.mDivX, byteBuffer);readIntArray(chunk.mDivY, byteBuffer);readIntArray(chunk.mColor, byteBuffer);return chunk;}
}
由此反向寻求解决方案,将打的上下左右的点去反推二进制数据的构造方法。但实际使用时,没有达到理想的效果。上面两个开源项目是 StackOverflow 里提的比较多的,第二个开源项目中的核心代码:
public class NinePatchBitmapFactory {// The 9 patch segment is not a solid color.private static final int NO_COLOR = 0x00000001;// The 9 patch segment is completely transparent.private static final int TRANSPARENT_COLOR = 0x00000000;public static NinePatchDrawable createNinePathWithCapInsets(Resources res, Bitmap bitmap, int top, int left, int bottom, int right, String srcName) {ByteBuffer buffer = getByteBuffer(top, left, bottom, right);NinePatchDrawable drawable = new NinePatchDrawable(res, bitmap, buffer.array(), new Rect(), srcName);return drawable;}public static NinePatch createNinePatch(Resources res, Bitmap bitmap, int top, int left, int bottom, int right, String srcName) {ByteBuffer buffer = getByteBuffer(top, left, bottom, right);NinePatch patch = new NinePatch(bitmap, buffer.array(), srcName);return patch;}private static ByteBuffer getByteBuffer(int top, int left, int bottom, int right) {//Docs check the NinePatchChunkFileByteBuffer buffer = ByteBuffer.allocate(56).order(ByteOrder.nativeOrder());//was translatedbuffer.put((byte)0x01);//divx sizebuffer.put((byte)0x02);//divy sizebuffer.put((byte)0x02);//color sizebuffer.put(( byte)0x02);//skipbuffer.putInt(0);buffer.putInt(0);//paddingbuffer.putInt(0);buffer.putInt(0);buffer.putInt(0);buffer.putInt(0);//skip 4 bytesbuffer.putInt(0);buffer.putInt(left);buffer.putInt(right);buffer.putInt(top);buffer.putInt(bottom);buffer.putInt(NO_COLOR);buffer.putInt(NO_COLOR);return buffer;}}
我们看一个简单的示例:
原图是一个 144*72 的 png 图片,我们希望达到的点 9 效果:
希望作为按钮去实现该效果,可以先实现横向的拉伸效果,按照中间显示的区域做拉伸。通过以上代码达到的效果如下:
如图所示,点9图是我们的目标效果,直接拉伸会造成图片虚缈,不符合要求。而通过以上开源代码得到的效果周边似乎少了一圈,虽然看上去没有任何拉伸变虚的问题,但也不符合要求。
如何解决这个问题?仿佛是很棘手的事情。这是在王者荣耀开发第一周时遇到的。当时本着先实现效果的目标,再另找方法。
思路:点 9 无非是根据拉伸点(本文涉及的是两个拉伸点),将一张图分成九块,每块做不同的处理。
边缘四个角不做变化,中上,中下,左中,右中,以及中部做不同的处理,以达到拉伸效果。这部分研发复杂度偏高,没有达到完美的效果。
还是要重新跟进源码。继续看 NinePatchDrawable 的源码:
/*** Set the density scale at which this drawable will be rendered. This* method assumes the drawable will be rendered at the same density as the* specified canvas.** @param canvas The Canvas from which the density scale must be obtained.** @see android.graphics.Bitmap#setDensity(int)* @see android.graphics.Bitmap#getDensity()*/public void setTargetDensity(@NonNull Canvas canvas) {setTargetDensity(canvas.getDensity());}/*** Set the density scale at which this drawable will be rendered.** @param metrics The DisplayMetrics indicating the density scale for this drawable.** @see android.graphics.Bitmap#setDensity(int)* @see android.graphics.Bitmap#getDensity()*/public void setTargetDensity(@NonNull DisplayMetrics metrics) {setTargetDensity(metrics.densityDpi);}/*** Set the density at which this drawable will be rendered.** @param density The density scale for this drawable.** @see android.graphics.Bitmap#setDensity(int)* @see android.graphics.Bitmap#getDensity()*/public void setTargetDensity(int density) {if (density == 0) {density = DisplayMetrics.DENSITY_DEFAULT;}if (mTargetDensity != density) {mTargetDensity = density;computeBitmapSize();invalidateSelf();}}
而在绘制的时候:
@Overridepublic void draw(Canvas canvas) {final NinePatchState state = mNinePatchState;Rect bounds = getBounds();int restoreToCount = -1;final boolean clearColorFilter;if (mTintFilter != null && getPaint().getColorFilter() == null) {mPaint.setColorFilter(mTintFilter);clearColorFilter = true;} else {clearColorFilter = false;}final int restoreAlpha;if (state.mBaseAlpha != 1.0f) {restoreAlpha = getPaint().getAlpha();mPaint.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));} else {restoreAlpha = -1;}final boolean needsDensityScaling = canvas.getDensity() == 0;if (needsDensityScaling) {restoreToCount = restoreToCount >= 0 ? restoreToCount : canvas.save();// Apply density scaling.final float scale = mTargetDensity / (float) state.mNinePatch.getDensity();final float px = bounds.left;final float py = bounds.top;canvas.scale(scale, scale, px, py);if (mTempRect == null) {mTempRect = new Rect();}// Scale the bounds to match.final Rect scaledBounds = mTempRect;scaledBounds.left = bounds.left;scaledBounds.top = bounds.top;scaledBounds.right = bounds.left + Math.round(bounds.width() / scale);scaledBounds.bottom = bounds.top + Math.round(bounds.height() / scale);bounds = scaledBounds;}final boolean needsMirroring = needsMirroring();if (needsMirroring) {restoreToCount = restoreToCount >= 0 ? restoreToCount : canvas.save();// Mirror the 9patch.final float cx = (bounds.left + bounds.right) / 2.0f;final float cy = (bounds.top + bounds.bottom) / 2.0f;canvas.scale(-1.0f, 1.0f, cx, cy);}state.mNinePatch.draw(canvas, bounds, mPaint);if (restoreToCount >= 0) {canvas.restoreToCount(restoreToCount);}if (clearColorFilter) {mPaint.setColorFilter(null);}if (restoreAlpha >= 0) {mPaint.setAlpha(restoreAlpha);}}
明显使用了 Density 的属性进行了绘制,于是开发人员对原有的代码进行了修改,加入了屏幕密度的修改:
float density = (context.getResources().getDisplayMetrics().density);Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int)(bitmap.getWidth() * density), (int)(bitmap.getHeight() * density), true);ByteBuffer buffer = getByteBufferFixed((int)(top * density), (int)(left * density), (int)(bottom * density), (int)(right * density));NinePatchDrawable drawable = new NinePatchDrawable(context.getResources(), scaledBitmap, buffer.array(), new Rect(), null);
改进后,得到的效果如下:
通过加入 density 属性,完美解决了边缘处效果的问题,结论就是还是要检查源码。
这里我提一个问题:以上的方案解决了图片拉伸的问题,那如果该图片需要做压缩,该如何处理呢?有兴趣的开发者可以思考一下~
4.5 联调流程优化
由于只提供了 Android 和 ios 的库,所以就出现一个问题,Unity 的研发无法在Windows 上进行调试。而出现问题也不能很方便的走查。编译一次端上的包,需要一到两个小时,一个小问题也很难快速解决。每次尝试修改,都需要重新打包。
跟进问题方法:打印 log,起初就是通过打印一些必要的 log 跟进问题,然后遍包查 log。
第一次改进,通过调试走查问题:当时通过编译的包,在 ios 上对 C#编译后的C代码进行 Debug 调试,以此来跟进问题的具体原因,减少了编码次数。
第二次改进,数据还原:这个问题还是得想办法解决,思路源自数据协议。
这一套基于数据的渲染引擎,只是让王者生成了数据,而数据只是通过阿波罗团队转接一次。那到 Android 和 ios 侧就可以还原出来,那完全不需要编包才能做。
在这里进行一个尝试,写一个 Demo,在 Windows 上编译生成数据以及资源文件,交给 Android 侧,通过,就直接将二进制的文件进行解析并将页面还原出来,这样就规避掉了编译的过程、快速的走查调用时可能产生的问题。
而在跟进问题时,也可以通过记录文件、还原文件进行 Debug。这样 Debug 就变成了一个很简单的 Android Demo 的项目了,更加快速便捷。
Demo 示意图(点击显示view后显示王者界面):
通过一系列的改进,从一开始查问题需要 1-2 个小时甚至更长,到最后大约 10 分钟左右就可以搞定,而且一次还可以查多个问题。
05
总结
王者地图支持项目是一个充满故事的项目。经过和其他王者开发人员的并肩作战,也确实领会到了一个产品是如何走到今天的。能够感受那种氛围,也是一个不错的体验。
整个项目从可行性分析,到第一周研发准备,再到去成都进行联调和最核心部分的开发,这段时间总共只有四周的时间。而涉及的人员也相当的多,期间得到了很多王者开发人员的支持和帮助,非常感谢一路走来的战友们!
我们基于上述项目不断扩展迭代技术,形成了新的基于游戏引擎的可视化方案。如果各位感兴趣,可以在公众号后台回复「可视化方案」,查看完整的 Wemap 腾讯地图产业版白皮书。让你轻松了解数字地图底座。
以上是本次分享全部内容,欢迎大家在评论区分享交流。如果觉得内容有用,欢迎转发~
聊一聊是哪个瞬间让你走上了程序员这条道路?在评论区分享你的故事,我们将选取1则最有创意的分享,送出腾讯云开发者-文化衫1件(见下图)。5月24日中午12点开奖。
相关文章:

国民游戏王者荣耀的真实地图开发之路
👉腾小云导读 相信很多人都玩过王者荣耀,大家在欣赏其华丽的游戏界面以及炫酷的游戏技能时,是否好奇过王者荣耀的地图是怎样开发出来的?在开发的历程中,都有哪些问题?是怎样解决的?本文将从其地…...

浅谈IDC数据中心综合布线第二篇——结构化布线
数据中心网络在当今的业务中扮演着越来越重要的作用,提供数据的存储、管理、共享、交换、应用等功能。在数据中心中,大量的数据在服务器、交换机、存储设备之间通过物理层的光缆(仅讨论光纤布线)进行传输。数据表明,在…...

电脑格式化后数据恢复软件EasyRecovery16
EasyRecovery是一款由Kroll Ontrack公司开发的专业数据恢复软件,旨在帮助用户从各种数据丢失情况下恢复文件。无论是因为误删除、格式化、分区丢失、系统崩溃还是其他原因导致的数据丢失,EasyRecovery都具有强大的恢复功能。 EasyRecovery提供了多种恢复…...

(2020)End-to-end Neural Coreference Resolution论文笔记
2020End-to-end Neural Coreference Resolution论文笔记 Abstract1 Introduction2 Related Work3 Task4 Model4.1 Scoring Architecture4.2 Span Representations5 Inference6 Learning7 Experiments7.1 HyperparametersWord representationsHidden dimensionsFeature encoding…...
kafka命令
查询kafka版本信息 kafka-configs.sh --describe --bootstrap-server localhost:9092 --version 查看所有topic [rootm10 bin]# kafka-topics.sh --list --zookeeper localhost:2181 __consumer_offsets kahn-topic-1 my_topic x_topic-1 创建一个topic,名为x_top…...
mybatis多表查询
多表查询有哪些情况 Mybatis 支持多表查询,常见的多表查询方式包括使用嵌套查询和关联查询 嵌套查询 嵌套查询是指在 SQL 语句中嵌套另外一个查询语句,可以用于在一个表中查询与另一表相关的数据。例如,在一个订单表中同时需要查询该订单所属…...

kafka 从入门到精通
kafka 安装 zookeeper模式 创建软件目录 mkdir /opt/soft cd /opt/soft下载 wget https://downloads.apache.org/kafka/3.4.0/kafka_2.13-3.4.0.tgz解压 tar -zxvf kafka_2.13-3.4.0.tgz 修改目录名称 mv kafka_2.13-3.4.0 kafka配置环境变量 vim /etc/profileexport K…...
写PPT没有思路, 这些底层方法论让你灵感爆棚……
作为一个10年经验的策划人,以下是个人多年经验,看完绝对对你写PPT会有很大帮助! 首先,有很多新手写PPT有一个不好的习惯,就是喜欢直接上手就打开PPT开始啪啪啪打字。 这是非常错误的,这就等于你是想到哪写…...

【小沐学Python】Python实现Web服务器(Flask+Vue+node.js,web单页增删改查)
文章目录 1、简介1.1 flask1.2 vue 2、开发2.1 新建flask项目2.2 安装flask库2.3 新建flask的主脚本2.4 新建Vue项目2.5 安装vue项目依赖项2.6 新增组件Ping.vue2.7 Ping.vue增加HTTP请求2.8 美化vue前端页面2.9 新增组件Books.vue2.10 flask增加路由Books2.11 Books.vue增加HT…...

甘肃非煤矿山电子封条 智慧矿山 opencv
甘肃非煤矿山电子封条 智慧煤矿接入国家矿山安全平台是通过pythonopencv网络模型,甘肃非煤矿山电子封条pythonopencv网络模型对关键位置(回风井口、运人井口、车辆出入口)对现场人员行为、数量、穿戴着装及设备状态各数据进行实时监控分析。p…...

工业识别与定位系统源码解决方案
工厂人员定位系统源码,工业领域定位系统源码 近年来人员定位系统在工业领域的发展势头迅猛,工业识别与定位成为促进制造业数字化的关键技术。通过实时定位可以判断所有的人、物、车的位置。实时定位系统要适用于复杂工业环境,单一技术是很难…...

PCL学习之滤波算法
前言 点云滤波作为常见的点云处理算法,一般是点云处理的第一步,对后续处理有很重要作用。滤波 有很多方面也有很多种功能,比如去除噪声点、离群点、点云平滑以及空洞、数据压缩等 原始点云数据往往包含大量散列点、孤立点,在获取…...
第二章 链表
目录 一、移除链表元素二、设计链表三、反转链表四、两两交换链表中的节点五、删除链表倒数第N个节点六、链表相交七、环形链表Ⅱ 一、移除链表元素 Leetcode 203 class Solution { public:ListNode* removeElements(ListNode* head, int val) {ListNode* dummyHead new Lis…...
Spring Security OAuth2实现单点登录:简化多个系统之间的登录流程
Spring Security OAuth2实现单点登录:简化多个系统之间的登录流程 一、介绍OAuth21. OAuth2的定义和作用2. OAuth2的优点和使用场景 二、Spring Security1. Spring Security的介绍2. Spring Security的特点和优势 三、OAuth2与Spring Security的结合1. OAuth2在Spri…...
语义分析器
语义分析器(Semantic Analyzer)是编译器中的一个重要组成部分,它负责对源代码进行语义分析,检查源代码是否符合语义规范,并进行错误处理和类型推导等操作。 举个例子,假设有以下的源代码: int…...

爬虫基本原理
爬虫基本原理 1.1获取网页1.1.1提取信息1.1.2保存数据 1.2请求1.2.1 请求方法1.2.2 请求网址1.2.3 请求头1.2.4请求体1.3响应 1.1获取网页 爬虫首先要做的工作就是获取网页,这里就是获取网页的源代码。源代码里包含了网页的部分有用信息,所以只要把源代…...

常见电子元器件和电路
目录 常见电子元器件一览表(字母标志)NTC(负温度系数热敏电阻)压敏电阻X2电容(抑制电源电磁干扰用电容器)泄放电阻共模电压共模电感整流桥滤波电容RCD吸收二极管Y电容整流器的原理输出整流肖特基二极管 功率晶体管(GTR,三极管)双极型晶体管(BJTÿ…...
English Learning - L3 Lesson1 VOA-Color 译文
听碎 VOA NOW, THE VOA SPECIAL ENGLISH PROGRAM WORDS AND THEIR STORIES Every people has its own way of saying things, its own special expressions. Many everyday American expressions are based on colors. 各国人民都有自己说话的方式,有自己独特的表…...

如何在linux中配置JDK环境变量
在linux系统部署皕杰报表,因皕杰报表是一款纯java报表工具,运行时需要jre环境,所以要在服务器上配置三个jdk环境变量path、classpath、JAVA_HOME。 那么为什么要配置jdk环境变量呢?因为java软件运行时要用到一些java命令ÿ…...

横截面收益率(二) 阿尔法策略是如何构建的
资本资产定价模型自从首次被提出以来在金融经济学中一直处于中心地位。 在一系列简化假定条件下,资本资产定价模型表明,任何证券的收益率与该证券 的系统性风险(或者贝塔值)呈线性关系。因此,依据资本资产定价模型横截…...
MS8312A 车规 精密、低噪、CMOS、轨到轨输入输出运算放大器,用于传感器、条形扫描器
MS8312A 车规 精密、低噪、CMOS、轨到轨输入输出运算放大器,用于传感器、条形扫描器 简述 MS8312A 是双通道的轨到轨输入输出单电源供电运放。它们具有低的失调电压、低的输入电压电流噪声和宽的信号带宽。 低失调、低噪、低输入偏置电流和宽带宽的特性结合使得 …...
【Elasticsearch】映射:fielddata 详解
映射:fielddata 详解 1.fielddata 是什么2.fielddata 的工作原理3.主要用法3.1 启用 fielddata(通常在 text 字段上)3.2 监控 fielddata 使用情况3.3 清除 fielddata 缓存 4.使用场景示例示例 1:对 text 字段进行聚合示例 2&#…...
《视觉SLAM十四讲》自用笔记 第三讲:三维空间刚体运动
第三讲 三维空间刚体运动 3.0 目标 1.理解三维空间的刚体运动描述方式:旋转矩阵、变换矩阵、四元数和欧拉角。 2.掌握 Eigen 库的矩阵、几何模块使用方法。 3.1 旋转矩阵 3.1.1 点和向量,坐标系 三维空间中,刚体的运动可以用两个概念来…...

【第七篇】 SpringBoot项目的热部署
简介 本文介绍了热部署(Hot Deployment)的概念、使用场景及在IDEA中的配置方法。热部署可在不重启应用的情况下动态更新代码,提升开发效率,适用于调试、微服务架构和自动化测试等场景。文章详细说明了热部署的实现步骤(…...

Python Day46
Task: 1.不同CNN层的特征图:不同通道的特征图 2.什么是注意力:注意力家族,类似于动物园,都是不同的模块,好不好试了才知道。 3.通道注意力:模型的定义和插入的位置 4.通道注意力后的特征图和热力…...

基于51单片机的红外防盗及万年历仿真
目录 具体实现功能 设计介绍 资料内容 全部内容 资料获取 具体实现功能 具体功能: (1)实时显示年、月、日、时、分、秒、星期信息; (2)红外传感器(仿真中用按键模拟)检测是否有…...
mybatis的if判断==‘1‘不生效,改成‘1‘.toString()才生效的原因
mybatis的xml文件中的if判断‘1’不生效,改成’1’.toString()才生效 Mapper接口传入的参数 List<Table> queryList(Param("state") String state);xml内容 <where><if test"state ! null and state 1">AND EXISTS(select…...
el-table表格增加序号列index vue2和vue3的写法
<el-table><!--每页从1开始的序号--><el-table-column label"序号" width"60" align"center" type"index" /><!--一直递增的序号 vue2写法--><el-table-column label"序号" width"60"…...

[文献阅读] Emo-VITS - An Emotion Speech Synthesis Method Based on VITS
[文献阅读]:An Emotion Speech Synthesis Method Based on VITS 在VITS基础上通过参考音频机制,获取情感信息,从而实现的情感TTS方式。 摘要 VITS是一种基于变分自编码器(VAE)和对抗神经网络(GAN…...
网络协议通俗易懂详解指南
目录 1. 什么是网络协议? 1.1 协议的本质 1.2 为什么需要协议? 1.3 协议分层的概念 2. TCP协议详解 - 可靠的信使 📦 2.1 TCP是什么? 2.2 TCP的核心特性 🔗 面向连接 🛡️ 可靠传输 📊 流量控制 2.3 TCP三次握手 - 建立连接 2.4 TCP四次挥手 - 断开连接…...