保持Tizen Web应用程序的高度可移植性
PUBLISHED
简介
在阅读这篇文章之前,建议您熟悉以下几篇文章:“Canvas2D移动网页游戏开发 - 基础篇”和“Canvas2D移动网页游戏开发 - 实现篇”,在这两篇文章中,讲解了Cnavas2D API,游戏循环和自适应游戏循环的概念。 同时也对网页游戏基础架构和类图进行了简要描述。
Tizen是一个兼容HTML5的平台,因此,只要你在编程时遵循一定的规则,那你的程序会有很高的兼容性。 有两类基本的HTML兼容平台:
- 一种平台是需要进行特殊的封装才能启动HTML5应用程序。 这类平台包括:Android,IOS和Windows Mobile。 我们把这类平台成为原生HTML5不支持的平台。
- 还有一类平台不需要经过封装就能支持HTML5。 这类平台包括:Tizen,webOS,FirefoxOS和桌面浏览器。 本文成这类平台为原生HTML5支持的平台。
本文将讲解一些指南和基本概念,它们能让你在PC浏览器和Android平台上运行你的Tizen网页游戏。 在Android平台上,你需要一个带有WebView对象实例的封装的应用程序,WebView是HTML5应用程序的运行时库。 我们将单独讲解怎样在Android平台上启动应用程序 - “在Android平台上启动Tizen应用程序”。
在原生HTML5支持的平台上启动
特征检测
当你在多个HTML5支持的平台上启动应用程序时,尽管有很多的W3C规范,但对于不同的平台,各种规范的支持是不同的。 假如我们的目标平台是Tizen平台,你必须要知道这个平台支持HTML5的哪些特性。 但是当你的HTML5应用程序运行在多个不同的平台上时,我们就不知道哪些HTML5的特性会被支持。 这就是为什么我们需要在使用之前检查每一个HTML5功能的可用性。 在“Canvas2D移动应用程序开发 - 基础篇”中,我们介绍了用纯JavaScript进行特性检测的方法。 在我们的应用程序中,我们将使用一个叫Modernizr的外部库来进行特性验证,这个库会返回一个布尔值用来标示特性的可用性。 在Earth Guard的情况下,我们需要检查Canvas 2D和localStorage的可用性 - 请参考./js/modules/view.js中的view模块:
checkEnviroment : function() { game.log("checkEnviroment"); var canvas = document.getElementById('game'); var ctx = canvas.getContext && canvas.getContext('2d'); width = canvas.width; height = canvas.height; if (!Modernizr.canvas || !Modernizr.localstorage) { this.resize(); this.error("Earth Guard cannot be launched on this device/browser!"); return false; } game.setContext(canvas, ctx); $("#container").height(height).width(width); if (!this.isItMobile()) { $('head').append('<link rel="stylesheet" href="./css/menu.desktop.css" type="text/css" />'); } return true; },
只要它们中的任何一个不可用,就会显示一个错误消息,同时应用程序就不能启动。 这适用于IE8或更低版本的情况。
画布缩放
我们的应用程序依赖于画布HTML5元素。 将你的画布和画布容器的分辨率设置为最好,并且能进行扩展,这样对进行下面的工作是很有好处的。 此处的最高分辨率是:1280X720,同时也是Tizen 2.0.0支持的最高分辨率。 不幸的是,不少设备或桌面要求的分辨率比较低。 我们用简单的缩放算法使画布能满足所有的屏幕。
定义最大分辨率:
maxWidth = 720
maxHeight = 1280
运行应用程序的设备的屏幕尺寸如下:
devWidth = 480
devHeight = 800
我们将画布的高度设置为设备的高度:
newHeight = devHeight = 800
计算缩放因子:
scale = devHeight/maxHeight = 0.625
计算新的画布宽度:
newWidth = scale * devWidth = 300
用这种方法使画布适应低分辨率的屏幕。 幸运的是,画布API提供了缩放函数。
context.scale(x_sale_factor, y_scale_factor);
上面的方法在view模块中被实现了。
resize : function() { if (config.resolution.scaled.use) { var scaleWidth = width; var scaleHeight = config.resolution.scaled.height; } else { var scaleWidth = $(window).width(); var scaleHeight = $(window).height(); } scale = scaleHeight / height; var newWidth = Math.floor(scale * width); $("#game").attr("height", scaleHeight).attr("width", newWidth); if (game.getCtx()) game.getCtx().scale(scale, scale); $("#container").height(scaleHeight).width(newWidth); sWidth = newWidth; sHeight = scaleHeight; // Scale main font size. sFontSize = Math.round(fontSize * scale); $(document.body).css('fontSize', sFontSize + 'px'); },
图 1:画布缩放举例
DOM还是画布?
应用程序必须运行在具有任何分辨率的桌面或移动设备上。 这就给UI界面的缩放带来了问题。 前面提到过,画布元素的比例已经计算好了,理论上来说,在画布上进行渲染是没有问题的。 由于以下原因,在画布上放置UI界面(菜单,记分牌等)不是一种好的方法。
- 画布上的元素不容易添加事件监听器。 这是因为游戏是以保留模式设计的(参考“二维画布移动网页游戏开发 - 基础篇”)。 如果要添加事件监听器,你需要创建一个DOM元素来覆盖画布上的UI元素,然后在给它们添加点击事件。 因此,这是毫无意义的。
- 画布上缩放过的UI元素会有一些无效的块。
在实例应用程序中,我们将在DOM中创建一个菜单和记分牌,并且为UI创建单独的缩放机制。
UI缩放
在开始进行UI缩放之前,你需要进行正确的视口设置。 你需要在index.html头部定义一个视口meta标签。 对于实例应用程序,我们使用下面的设置:
<meta name="viewport" content="user-scalable=no, width=720, height=1280, target-densitydpi=device-dpi">
在上面的代码中视口的尺寸被设置为720x1280px与设备的DPI相等。 在我们的应用程序开发之初,我们认为高清是我们的应用程序的完整分辨率。 你可能会问:如果设备不支持这么高的分辨率?在这种情况下,它将被降低到设备的最大高度和宽度。 在后面的段落中,我们将解释如何执行UI缩放,使我们的应用程序能以较低的分辨率显示。
UI中的两个元素需要被缩放:
- 字体
- UI尺寸
字体缩放可以通过以下方式来实现:
- 选择参考分辨率(在我们的案例中是HD),并准备你的用户界面与基准字体。 以参考分辨率来开发设备上的应用程序。 对于Earth Guard,参考的字体大小为25px。
- 使用CSS2的实体选择器来为HTML的实体分配参考字体大小(请参阅./css/menu.css)。 在Earth Guard中,25px字体大小对于整个HTML文档来说就是100%的大小。 通过这种方法,我们能够通过百分比确定HTML元素字体的大小。 请参考下面的代码,并注意CSS的font-size属性。
.form-menu label { display: block; height: 2em; font-family: 'Arial', sans-serif; font-weight: normal; font-size: 120%; line-height: 2em; }
- 在此之后,我们需要扩展的参考字型,以反映所有的屏幕分辨率。 我们已经有了画布缩放的比例因子。 现在我们需要做的唯一的事情是将这个因子应用到参考字体的大小。 我们是用view模块的resize方法来实现的(./js/modules/view.js)。 请参考下面的代码:
resize : function() { … // Scale main font size. sFontSize = Math.round(fontSize * scale); $(document.body).css('fontSize', sFontSize + 'px'); },
按照上面的方法使我们能够根据当前的屏幕分辨率更改所有用户界面元素的字体大小。
不幸的是,UI界面的缩放只成功了一半。 我们需要做的下一件事就是缩放UI的尺寸。 幸运的是CSS2.1有em单元。 1em等于当前元素的当前字体大小。 em单元将字体大小和缩放了的UI界面尺寸进行绑定。
请按照下面的方法来缩放用户界面尺寸:
- 使用参考字体和参考分辨率在设备上开发你的应用程序。
- 用em单元来定义下列元素的尺寸:margins,paddings和sizes。
- 应用字体缩放方法。
下面我们展示了可缩放应用程序用户界面的整个开发过程。
图2:可缩放应用程序的用户界面开发过程
用户输入处理
为了让您的应用程序具有可移植性,你应该提供不同平台下的不同输入方法。 我们将使用两个输入控制机制:
- 键盘和鼠标为桌面用户的输入进行处理。
- 陀螺仪和粗磨时间为移动用户的输入进行处理。
桌面输入处理
桌面的用户输入处理是简单的。 我们只需使用window.addEventListener方法为操作分配事件。 我们创建了一个简单的按键注册机制。 请参阅./js/modules/inputHandler.js模块。 下面你可以找到一个代码片段,负责按键注册:
var KEY_CODES = { 37 : 'left', 39 : 'right', 32 : 'fire', 40 : 'down', 38 : 'up', 13 : 'enter' };
每个按键都对应一个在游戏模块中定义的game.keys条目数组。 当按键被按下时,对应的布尔值就会变成true,松开按键时,布尔值就会变成false。 所有存储在数组中的布尔值都会被运用到子画面中。 例如,game.keys['fire']在PlayerShip的step方法中被检查。
下面你可以找到一个代码,负责分配在game.keys数组中的值分配,该值对应于键盘上按下的键。
window.addEventListener('keydown', function(e) { resetDirection(); if (KEY_CODES[e.keyCode]) { game.keys[KEY_CODES[e.keyCode]] = true; e.preventDefault(); } }, false); window.addEventListener('keyup', function(e) { resetDirection(); if (KEY_CODES[e.keyCode]) { game.keys[KEY_CODES[e.keyCode]] = false; e.preventDefault(); } }, false);
当用户按下向上或向下的按钮,我们使用e.preventDefault()来关闭浏览器的默认滚动行为。
移动设备输入处理
对于移动设备,例如Tizen或Android,需要开发一套单独的输入处理机制。 它基于在W3C中定义的“deviceorientation”事件。 下面你可以看到移动设备输入处理的开发过程。
图3:用面向设备的W3C API开发移动设备输入处理的过程
创建角度的归一化表格和算法
对于Android设备,Tizen设备和Tizen模拟器这些设备,有一个通用移动输入处理机制是非常重要的。 不幸的是每个平台在相同的范围内会返回不同的角度值。 为了调试和开发简易化,你必须统一固定范围内的旋转角度值。
- gamma角度范围[-90, 90]
- beta角度范围:[-180, 180]
图4:设备按照gamma角度旋转的说明
图5:设备按照beta角度旋转的说明
按Y轴旋转设备会改变gamma角度。 下表描述了设备的方向的变化如何影响了陀螺仪的情况下返回值:Android设备,Tizen设备和Tizen模拟器。
图6:设备由gamma角度旋转
设备位置变化 |
(1 → 2) |
(2 → 3) |
(3 → 4) |
(4 → 1) |
Android设备 |
[0, 90] |
[90,180] |
[180,270] |
[-90,0] |
Tizen设备 |
[0, -90] |
[-90, -180] |
[180, 90] |
[90, 0] |
Tizen模拟器 |
[0, 90] |
[90, 180] |
[-180, -90] |
[-90,0] |
标准化 |
[0, 90] |
[-90, 0] |
[0, 90] |
[-90,0] |
“设备位置的改变”行显示发生了什么变化(设备的位置显示在表格上面的图中)。 “Android Device”,“Tizen Device”和“Tizen Simulator”行显示不同设备的陀螺仪返回的值。 “Normalized”行显示了设置的方向和移动速度的值。
现在,让我们来看看beta角:
图7:设备由beta角旋转
第一个位置显示装置在桌子上。 第二个位置是桌子的垂直面。
设备位置变化 |
(1 → 2) |
(2 → 3) |
(3 → 4) |
(4 → 1) |
Android设备 |
[0, 90] |
[90, 0] |
[0, -90] |
[-90, 0] |
Tizen设备 |
[0, -90] |
[-90, -180] |
[180, 90] |
[90, 0] |
Tizen模拟器 |
[0, 90] |
[90,180] |
[-180, -90] |
[-90, 0] |
标准化 |
[0, -90] |
[-90, -180] |
[180, 90] |
[90, 0] |
我们实现了一个简单的角度标准化算法。 请参阅./js/modules/inputHandler.js模块。
创建算法来处理陀螺仪的不确定性角度范围。
在一些旋转范围内,deviceorientation API返回数据不准确,从而导致倾斜的移动。 当归一化的β值在<65 - 115>的范围时,通常我们观察到的伽玛值不准确。 为了减少对PlayerShip运动测量的不准确性,我们需要去除偏差大的值,然后在计算角度的平均值。 下面是处理陀螺仪不确定性的流程图:
图8:平均gamma角度的计算
为了实时读取角度值,需要添加面向设备的事件监听器。
现在来注册面向设备的事件监听器。 当事件触发时,你就会得到事件的属性。 在这种情况下,只有beta和gamma属性起作用。
window.addEventListener('deviceorientation', function(e) { var normGamma = e.gamma; var normBeta = e.beta; … });
归一化的值被分配到actualAngle数组:
// Set normalized values for angles actualAngle[1] = normBeta; actualAngle[2] = normGamma;
创建代码来存储初始设备的角度作为参考位置
读取设备的初始位置是很重要的。 如果不能读取初始位置,那用户就只能在水平位置玩游戏了。
在实例代码中,我们用下面的方法来读取设备的初始位置:
if (startAngle[1] === undefined) startAngle[1] = actualAngle[1]; if (startAngle[2] === undefined) startAngle[2] = actualAngle[2];
最初的手机位置被设置时,startAngle []数组的元素是不确定的。 清除角的初始值的函数是clearStartPosition()。
clearStartPosition : function() { startAngle[1] = undefined; startAngle[2] = undefined; }
它会被触发:
- 在菜单中点击“New Game”或者“Resume”后,
- 游戏在后台运行时。
通过创建数学函数来绑定角度和元素移动。
在标准化角度的实际和初始化值后,你要用setDirection()函数设置方向和移动速度。
var setDirection = function(delta, keyOn, keyOff) { if (delta > 90) delta = 90; // 90 degrees maximum angle if (delta > config.angleOffset) { game.keys[KEY_CODES[keyOn]] = true; direction[KEY_CODES[keyOn]] = delta / 90; } else { game.keys[KEY_CODES[keyOn]] = false; direction[KEY_CODES[keyOn]] = 0; } game.keys[KEY_CODES[keyOff]] = false; direction[KEY_CODES[keyOff]] = 0; };
第一个参数,“delta”,定义了角度的初始值和当前值之间的差。 它应小于或等于90度。 “keyOn”和“keyOff”值是按键的代码表示。 每个键代表一个方向。 direction[]数组里面指定了按照某一方向移动的速度的系数。 这些系数在playerShip.js类中用来设置位置:
位置:
new_position - 新的X和Y的位置,
position - 上次X和Y的位置,
dt = 2,5
位置:
maxVel - ship的最大速度,
coefficient - 从direction[]数组中取得的系数。
如果“delta”参数要比config.angleOffset大的话,合适的按键(“keyOn”)就会设置为true,这个按键的方向值也会被计算出来。 其他情况下,“keyOn”是false,方向值为0。 在上面的两种情况下,“keyOff”始终未false,且方向值始终为0。
setDirection()函数由以下函数触发:
- setRightAndLeft()
- setUpAndDown()
下面将会讲述这两个函数。
可以使用setUpAndDown()函数垂直运动的参数:
var setUpAndDown = function() { var angle0 = startAngle[1]; // initial beta var angle1 = actualAngle[1]; // actual beta var delta = angle1 - angle0; if (Math.abs(delta) < 180){ if (delta < 0) { // down setDirection(Math.abs(delta), key[2], key[3]); } else { // up setDirection(Math.abs(delta), key[3], key[2]); } } else { if (angle0 < 0) { delta += 360; // up setDirection(Math.abs(delta), key[3], key[2]); } else { delta = 360 - delta; // down setDirection(Math.abs(delta), key[2], key[3]); } } };
setRightandLeft()函数可以设置水平运动的参数:
var setRightAndLeft = function(angle) { var angle0 = startAngle[2]; var angle1 = angle; var delta; delta = angle1 - angle0; if (delta > 0) { // left setDirection(Math.abs(delta), key[0], key[1]); } else { // right setDirection(Math.abs(delta), key[1], key[0]); } }
setRightAndLeft()函数就不作说明了。 “key”是KEY_CODES运动的数组。 KEY_CODES数组的每个元素代表方向。
/** * Array of key codes for ship's movement */ var key = [39, 37, 40, 38]; // right, left, down, up var KEY_CODES = { 37 : 'left', 39 : 'right', 32 : 'fire', 40 : 'down', 38 : 'up', 13 : 'enter' };
setRightAndLeft()函数由setHorizontal()函数触发:
var setHorizontal = function() { if (actualAngle[1] > (90 - off) && actualAngle[1] < (90 + off)) { // add actual angle position to gammaVec array gammaVec.push(actualAngle[2]); // if gammaVec array contains 20 elements -> count ship's movement parameter if (gammaVec.length == 20) { countsForUncertainRange(20); } } else { // if gammaVec array contains at least 20 elements -> count ship's movement parameter if (gammaVec.length >= 6) { countsForUncertainRange(gammaVec.length); } else { // in other case clear array gammaVec = []; setRightAndLeft(actualAngle[2]); } } };
在上面的代码中,“off”变量定义了一半的范围,在这个范围中,陀螺仪的值是不确定的。 setHorizontal()函数会检查actualAngle[1] 的值是否在 (90 - off, 90 + off)范围内。 如果
- 在这个范围内,那这个值就会被存在gammaVec[]数组中。 如果gammaVec[]数组的长度为20,就会计算该数组中元素的平均值。
- 如果不在这个范围内,就会检查gammaVec[]数组的长度。 如果
- 长度大于等于6,就会计算该数组的元素的平均值,
- 如果小于6,gammaVec数组就会清空,并且actualAngle[2] 会用来作为setRightAndLeft()函数的参数。
总结
保持Tizen网页应用程序的兼容性对于提高市场占有率是非常重要的。 在这篇文章中,我们描述了在Tizen平台上启动HTML5应用程序的概念和方法。 如果你想了解怎样在Tizen平台上启动不支持HTML5的应用程序,请参考 - “在Android平台上启动Tizen应用程序”。