腾小云导读
相信许多人都玩过王者荣耀,我们在欣赏其华丽的游戏界面以及炫酷的游戏技能时,是否猎奇过王者荣耀的地图是怎样开发出来的?在开发的进程中,都有哪些问题?是怎样处理的?本文将从其地图规划到完结的整个流程讲解王者荣耀地图轻量处理计划,期望可以给你带来灵感。
看目录点收藏,随时涨技能
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.4Anroid点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, 在体会上和逻辑上都行不通。
第二个计划和第三个计划准则上都行得通,两种计划也都做了验证。本文介绍的是第三种计划。
原理如下:
publicclassUnityPlayerNativeActivityextendsNativeActivity
{
protectedUnityPlayer mUnityPlayer;// don't change the name of this variable; referenced from native code
// Setup activity layout
@OverrideprotectedvoidonCreate(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 =newUnityPlayer(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 =newViewGroup.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 = [UIApplicationsharedApplication].keyWindow;
NSAssert(window !=nil,@"window must not be nil");
returnwindow;
}
/**
* 挂载到 keywindow.
*/
- (void)mount{
UIView*mountPoint = [[selfclass] getMountPoint];
NSAssert([selfunderlyingView].superview ==nil,@"scene super view must be nil");
[mountPoint addSubview:[selfunderlyingView]];
}
以上计划均在 Unity 侧验证经过。
2.2.2 第二阶段 View层级联系
从全体需求来看,上层不仅仅是一个独自的地图,还要有许多的 UI 元素:
那么上面的按钮、其它元素怎么去做呢?
抱负的计划:由地图单纯的供给地图以及地图上的标示元素,上面的元素仍然由Unity 侧进行制作。这样只需求将地图的显现刺进到 Unity 的层级中。可以看一下Unity 的原理。
Android 侧由于 Activity 加载的是 UnityPlayer,这儿可以看一下 UnityPlayer 的代码:
privateSurfaceView n;
publicclassUnityPlayerextendsFrameLayoutimplements
com.unity3d.player.a.aaa{
publicUnityPlayer(finalContextWrapper m){
super((Context) m);
this.n =newSurfaceView((Context) m);
this.n.getHolder().addCallback((SurfaceHolder.Callback)newSurfaceHolder.Callback() {
publicfinalvoidsurfaceCreated{
//缺省**************************
}
publicfinalvoidsurfaceChanged{
//缺省**************************
}
publicfinalvoidsurfaceDestroyed(finalSurfaceHolder 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 UKFontfont;//字体
9 optional UKTextAlignment textAlignment;//文本方位,居中,居左等
10 optional UKEllipsis ellipsis;//文本省掉方法
};
3.3 数据传输
UI结构大致如上,而怎么将这一整套结构作业起来呢——数据驱动。
想象一下荣耀页面全体的作业流程:
王者用户点击荣耀战区,会进入荣耀地图页面。那么这时分,需求进入该场景,也就需求创立一个 Scene。然后需求加载一个页面,便是一个Page。之后在 Page 上增加地图 View、增加按钮、增加图片、增加文字等元素。经过这些元素的增加,整个页面就显现出来了。 |
然后,承受用户的作业,比如说一个按钮的点击,点击作业获取到今后,就需求进行下一步的处理,比如修正某个文本,设置某个图片的元素等等,也便是会持续向该结构发送下一个指令。
总结来说,要做两件事:Unity 向 Friday 发送指令,Friday 将用户作业回调给 Unity 。这两件作业可以概括为:方法调用和作业回调。
这儿要处理两个问题:
1)怎么经过数据完结方法调用和作业回调?
2)怎么找到对应的调用方针?
3.3.1 方法调用
举一个例子,设置文本控件的文字,正常的方法调用是这样的:
classUKLabel{
/**
设置文本
*/
publicvoidsetText(UKString text){
if(text !=null){
setText(text.getval())
}
}
}
UKLabel label =newUKLabel();
label.setText("hello world");
那么怎么去处理呢,方法如下:
如上图所示,方法名对应数据的变量名,参数对应数据的参数值,参数类型就对应的是数据的参数类型,是否被调用就对应变量值是否为空,这样就完结了一个一般方法的调用。
下一个问题:多个参数怎么处理?已然参数类型对应的是变量类型,那么多个参数只需规划一个结构体进行存储即可。
依照这套规矩,咱们可以看到,一同可以有多个方法被调用。这大大增加了运用的灵活性,削减冗余数据的呈现。而次序则是依照既定或协商好的次序履行。
方法可以调用了,接下来便是修正文本,但修正哪一个文本控件的文字呢?
这就需求找到指定的文本控件。如前面的 Label 的 JCE 数据所示,一切的 View 控件都是有一个 id 的,并且一切的 View id 要求必须仅有,并且 id 的规矩是由外部调用者(王者)决议的。这就处理了方法调用方针的问题,经过 id 索引,找到对应的View控件,从而调用到该控件支撑的方法,完结完好的方法调用。
因而,一个方法调用包括两部分:方法方针(Target)、方法体(Method)。
Target 中包括该方针的 id,方法体包括详细的方法数据。而这儿还需求处理一个问题,由于拿到的数据尽管有方针,经过方针也能知道该方针的类型,并且拿到该方针类型支撑的方法类型,也能把方法体解析出来。但为了方便,仍是直接将方法类型封装在 target 里,便于快速解析,如:
由于一切数据都进行了 JCE 格局的压缩,数据以二进制的形式经过阿波罗团队在Unity 和 Friday 之间传递,对外露出的接口在 android 侧是下面这个姿态:
/**
* 对外调用接口
*@paramtarget
* 音讯方针,jce格局化后的数据
*@parammethod
* 数据参数,jce格局化后的数据
* */
publicvoidcall(byte[] target,byte[] method);
一切的方法调用都是经过该通道传输。
3.3.2 作业回调
方法调用完结后,另一块便是看各种作业怎么传递给 Unity 侧。如一个点击作业:一个TableView 的某一项被点击、CheckBox 某一项被选中、某个地图上的标示被点击等等。
怎么结构回调作业,需求处理两个问题:
1)是谁产生了点击或状况改变2)产生的改变是什么 |
关于1): 由于每个方针都有了仅有的标识,所以向外输出时,可以将该id对外发布。而为了外部解析的便捷,也将回调的方针类型和数据类型一同回调给 Unity。
示例如下:
struct UKCallbackTarget {
0requireUKInt targetID;//回调时刻的id
1requireUKTargetType targetType;//回调的方针类型 如:Button,TableView
2requireUKCallbackType callbackType;//回调数据类型,如点击或许状况改变
};
关于2): 对应不同的点击作业,界说了不同的回调类型,并且将所需的数据封装起来一同回传。如 TableView 的点击回调数据类型,需求回调 Unity 哪一条被点击:
structUKTableViewCallbackData_Clicked{
0require UKInt idx;//被点击的item的index
};
其他的回调也都是类似,同方法调用,回调对外供给的接口为:
/**
* 回调,现在支撑点击回调,或许作业回调
*@paramtarget
* 回调作业方针
*@param@data
* 回调作业数据
* */
publicvoidcallback(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.htmlThe answer goes the same as in thisquestion:“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();
在王者没有设置沉溺式形式的时分,是没有问题的。但该宽高是不包括虚拟按键的宽高的,这就导致王者在设置沉溺式今后,呈现显现不全屏的问题,边上少了一块。
那咱们看一下怎么设置沉溺形式:
publicclassMainActivityextendsAppCompatActivity{
@Override
protectedvoidonCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
publicvoidonWindowFocusChanged(booleanhasFocus){
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();
}
publicWindowManager.LayoutParamscreateLayoutParams(intleft,inttop,intwidth,intheight){
WindowManager.LayoutParams windowParams =newWindowManager.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;
returnwindowParams;
}
增加的方法咱们请教了相关开发人员。后面增加了些代码,想以此去处理虚拟按键显现的问题,如上图所示,进行了一些测验:
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.
*/
publicNinePatchDrawable(Resources res, Bitmap bitmap,byte[] chunk,
Rect padding, String srcName){
this(newNinePatchState(newNinePatch(bitmap, chunk, srcName), padding), res);
}
其实,支撑这一思路的可行性,只需求结构 chunk 的二进制流,就可以伪装成点 9 图的作用。
拿到一张点 9 图,android 是经过 NinePatch 进行处理,点 9 图无非是在一般图上打几个点,作为拉伸的依据,即 NinePatchConfig,然后交由 Native 层进行处理,NInePatch 的代码不多:
// NinePatch chunk.
classNinePatchChunk{
publicstaticfinalintNO_COLOR =0x00000001;
publicstaticfinalintTRANSPARENT_COLOR =0x00000000;
publicRect mPaddings =newRect();
publicintmDivX[];
publicintmDivY[];
publicintmColor[];
privatestaticvoidreadIntArray(int[] data, ByteBuffer buffer){
for(inti =0, n = data.length; i < n; ++i) {
data[i] = buffer.getInt();
}
}
privatestaticvoidcheckDivCount(intlength){
if(length ==0|| (length &0x01) !=0) {
thrownewRuntimeException("invalid nine-patch: "+ length);
}
}
publicstaticNinePatchChunkdeserialize(byte[] data){
ByteBuffer byteBuffer =
ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bytewasSerialized = byteBuffer.get();
if(wasSerialized ==0)returnnull;
NinePatchChunk chunk =newNinePatchChunk();
chunk.mDivX =newint[byteBuffer.get()];
chunk.mDivY =newint[byteBuffer.get()];
chunk.mColor =newint[byteBuffer.get()];
checkDivCount(chunk.mDivX.length);
checkDivCount(chunk.mDivY.length);
// skip 8 bytes
byteBuffer.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 bytes
byteBuffer.getInt();
readIntArray(chunk.mDivX, byteBuffer);
readIntArray(chunk.mDivY, byteBuffer);
readIntArray(chunk.mColor, byteBuffer);
returnchunk;
}
}
由此反向寻求处理计划,将打的上下左右的点去反推二进制数据的结构方法。但实际运用时,没有到达抱负的作用。上面两个开源项目是 StackOverflow 里提的比较多的,第二个开源项目中的核心代码:
publicclassNinePatchBitmapFactory{
// The 9 patch segment is not a solid color.
privatestaticfinalintNO_COLOR =0x00000001;
// The 9 patch segment is completely transparent.
privatestaticfinalintTRANSPARENT_COLOR =0x00000000;
publicstaticNinePatchDrawablecreateNinePathWithCapInsets(Resources res, Bitmap bitmap,inttop,intleft,intbottom,intright, String srcName){
ByteBuffer buffer = getByteBuffer(top, left, bottom, right);
NinePatchDrawable drawable =newNinePatchDrawable(res, bitmap, buffer.array(),newRect(), srcName);
returndrawable;
}
publicstaticNinePatchcreateNinePatch(Resources res, Bitmap bitmap,inttop,intleft,intbottom,intright, String srcName){
ByteBuffer buffer = getByteBuffer(top, left, bottom, right);
NinePatch patch =newNinePatch(bitmap, buffer.array(), srcName);
returnpatch;
}
privatestaticByteBuffergetByteBuffer(inttop,intleft,intbottom,intright){
//Docs check the NinePatchChunkFile
ByteBuffer buffer = ByteBuffer.allocate(56).order(ByteOrder.nativeOrder());
//was translated
buffer.put((byte)0x01);
//divx size
buffer.put((byte)0x02);
//divy size
buffer.put((byte)0x02);
//color size
buffer.put((byte)0x02);
//skip
buffer.putInt(0);
buffer.putInt(0);
//padding
buffer.putInt(0);
buffer.putInt(0);
buffer.putInt(0);
buffer.putInt(0);
//skip 4 bytes
buffer.putInt(0);
buffer.putInt(left);
buffer.putInt(right);
buffer.putInt(top);
buffer.putInt(bottom);
buffer.putInt(NO_COLOR);
buffer.putInt(NO_COLOR);
returnbuffer;
}
}
咱们看一个简略的示例:
原图是一个 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.
*
*@paramcanvas The Canvas from which the density scale must be obtained.
*
*@seeandroid.graphics.Bitmap#setDensity(int)
*@seeandroid.graphics.Bitmap#getDensity()
*/
publicvoidsetTargetDensity(@NonNull Canvas canvas){
setTargetDensity(canvas.getDensity());
}
/**
* Set the density scale at which this drawable will be rendered.
*
*@parammetrics The DisplayMetrics indicating the density scale for this drawable.
*
*@seeandroid.graphics.Bitmap#setDensity(int)
*@seeandroid.graphics.Bitmap#getDensity()
*/
publicvoidsetTargetDensity(@NonNull DisplayMetrics metrics){
setTargetDensity(metrics.densityDpi);
}
/**
* Set the density at which this drawable will be rendered.
*
*@paramdensity The density scale for this drawable.
*
*@seeandroid.graphics.Bitmap#setDensity(int)
*@seeandroid.graphics.Bitmap#getDensity()
*/
publicvoidsetTargetDensity(intdensity){
if(density ==0) {
density = DisplayMetrics.DENSITY_DEFAULT;
}
if(mTargetDensity != density) {
mTargetDensity = density;
computeBitmapSize();
invalidateSelf();
}
}
而在制作的时分:
@Override
publicvoiddraw(Canvas canvas){
finalNinePatchState state = mNinePatchState;
Rect bounds = getBounds();
intrestoreToCount = -1;
finalbooleanclearColorFilter;
if(mTintFilter !=null&& getPaint().getColorFilter() ==null) {
mPaint.setColorFilter(mTintFilter);
clearColorFilter =true;
}else{
clearColorFilter =false;
}
finalintrestoreAlpha;
if(state.mBaseAlpha !=1.0f) {
restoreAlpha = getPaint().getAlpha();
mPaint.setAlpha((int) (restoreAlpha * state.mBaseAlpha +0.5f));
}else{
restoreAlpha = -1;
}
finalbooleanneedsDensityScaling = canvas.getDensity() ==0;
if(needsDensityScaling) {
restoreToCount = restoreToCount >=0? restoreToCount : canvas.save();
// Apply density scaling.
finalfloatscale = mTargetDensity / (float) state.mNinePatch.getDensity();
finalfloatpx = bounds.left;
finalfloatpy = bounds.top;
canvas.scale(scale, scale, px, py);
if(mTempRect ==null) {
mTempRect =newRect();
}
// Scale the bounds to match.
finalRect 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;
}
finalbooleanneedsMirroring = needsMirroring();
if(needsMirroring) {
restoreToCount = restoreToCount >=0? restoreToCount : canvas.save();
// Mirror the 9patch.
finalfloatcx = (bounds.left + bounds.right) /2.0f;
finalfloatcy = (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 的特点进行了制作,所以开发人员对原有的代码进行了修正,参加了屏幕密度的修正:
floatdensity = (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 =newNinePatchDrawable(context.getResources(), scaledBitmap, buffer.array(),newRect(), null);
改善后,得到的作用如下:
经过参加 density 特点,完美处理了边际处作用的问题,结论便是仍是要查看源码。
这儿我提一个问题:以上的计划处理了图片拉伸的问题,那假如该图片需求做压缩,该怎么处理呢? 有爱好的开发者可以考虑一下~
4.5 联调流程优化
由于只供给了 Android 和 ios 的库,所以就呈现一个问题,Unity 的研制无法在Windows 上进行调试。而呈现问题也不能很方便的走查。编译一次端上的包,需求一到两个小时,一个小问题也很难快速处理。每次测验修正,都需求从头打包。
跟进问题方法:打印 log,起初便是经过打印一些必要的 log 跟进问题,然后遍包查 log。
榜首次改善,经过调试走查问题:其时经过编译的包,在 ios 上对 C#编译后的C代码进行 Debug 调试,以此来跟进问题的详细原因,削减了编码次数。
第2次改善,数据复原:这个问题仍是得想方法处理,思路源自数据协议。
这一套根据数据的烘托引擎,仅仅让王者生成了数据,而数据仅仅经过阿波罗团队转接一次。那到 Android 和 ios 侧就可以复原出来,那完全不需求编包才能做。
在这儿进行一个测验,写一个 Demo,在 Windows 上编译生成数据以及资源文件,交给 Android 侧,经过,就直接将二进制的文件进行解析并将页面复原出来,这样就规避掉了编译的进程、快速的走查调用时或许产生的问题。
而在跟进问题时,也可以经过记录文件、复原文件进行 Debug。这样 Debug 就变成了一个很简略的 Android Demo 的项目了,愈加快速便捷。
Demo 示意图(点击显现view后显现王者界面):
经过一系列的改善,从一开端查问题需求 1-2 个小时乃至更长,到最后大约 10 分钟左右就可以搞定,并且一次还可以查多个问题。
05、总结
王者地图支撑项目是一个充溢故事的项目。经过和其他王者开发人员的并肩作战,也的确体会到了一个产品是怎么走到今天的。可以感触那种氛围,也是一个不错的体会。
整个项目从可行性剖析,到榜首周研制预备,再到去成都进行联调和最核心部分的开发,这段时刻总共只需四周的时刻。而触及的人员也适当的多,期间得到了许多王者开发人员的支撑和协助,非常感谢一路走来的战友们!
咱们根据上述项目不断扩展迭代技能,形成了新的根据游戏引擎的可视化计划。假如各位感爱好,可以在大众号(点这儿进入开发者社区,右边扫码即可进入大众号)后台回复 「可视化计划」,查看完好的 Wemap 腾讯地图工业版白皮书。让你轻松了解数字地图底座。
以上是本次共享全部内容,欢迎我们在谈论区共享交流。假如觉得内容有用,欢迎转发~
聊一聊是哪个瞬间让你走上了程序员这条路途? 在大众号(点这儿进入开发者社区,右边扫码即可进入大众号)谈论区共享你的故事,咱们将选取1则最有构思的共享,送出腾讯云开发者-文化衫1件(见下图)。5月24日正午12点开奖。