关于后端:国民游戏王者荣耀的真实地图开发之路

2次阅读

共计 24555 个字符,预计需要花费 62 分钟才能阅读完成。

👉腾小云导读

置信很多人都玩过王者光荣,大家在观赏其富丽的游戏界面以及炫酷的游戏技能时,是否好奇过王者光荣的地图是怎么开发进去的?在开发的历程中,都有哪些问题?是怎么解决的?本文将从其地图设计到实现的整个流程解说王者光荣地图轻量解决方案,心愿能够给你带来灵感。

👉看目录点珍藏,随时涨技术

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:

ios:

从王者零碎的第一期需要效果图来看,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 implements  
    com.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 控件汇合:

并且定义了 View 的通用属性:

这里有一个有意思的点。通用属性里有一行: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;// 回调工夫的 id  
    
  1 require UKTargetType targetType;// 回调的对象类型 如:Button,TableView  
    
  2 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-to…
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 {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
    }  
  
    @Override  
    public 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 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);  
  
        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 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);

        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();}  
    }

而在绘制的时候:

@Override  
    public 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 点开奖。

正文完
 0