在Three.js上具有旧气氛的新游戏。 第二部分

第一部分中,我谈到了在使用Three.js为浏览器创建3D游戏的过程中遇到的问题。 现在,我将详细介绍解决编写游戏时遇到的一些重要问题,例如构建关卡,检测碰撞以及将图像调整为浏览器窗口的任何纵横比。


液位图


实际上,关卡本身是在3D编辑器中创建的,即它们的几何形状,纹理贴图,阴影烘焙等。 我在第一部分中描述了所有这些。 为什么还有其他计划? 事实是Three.js不提供任何物理引擎,因此我使用级别方案来识别障碍。


用于解决碰撞问题的Three.js仅提供光线追踪-确定对象几何形状相交的最简单方法。 原则上,它可以使用,我什至在其他项目之一中也使用过。 它是网站上浏览器中的虚拟城市。 您可以在城市周围移动,而不必穿过墙壁。


在移动过程中发生玩家与建筑物的几何形状相交的情况下,我在与墙壁相反的方向上以一定距离对玩家进行了排斥。 但是为此,对象必须是平行六面体。 我围绕一些复杂的对象创建了对撞机(我们将其称为隐形对象,它们起着障碍物的作用,并阻止玩家穿过它们),通过这些对撞机进行了计算。 而且某些建筑物的下部,简称为“盒子”,有时也被用作对撞机。


在几何形状复杂的对象上,光线跟踪可能不起作用或行为不当。 而且,作为一种解决方案,您可以将物体嵌入一个物体中,而不是一个,而是将几个小的不可见碰撞体以平行六面体的形式嵌入,并以彼此平行且彼此重叠的方式绘制,并具有100%的透明度,大致重复物体的形状。

在关于地牢的游戏中,关卡是一个带有切割动作的单个长物体,用于移动玩家。 实际上,要解决碰撞问题,可以在必要的地方贴上不可见的碰撞器并使用射线追踪。 但是,我决定走另一条路。


  • 首先,我想使创建对撞机阵列的过程自动化。
  • 其次,您只能使用有关碰撞器的信息,即它们在空间中的坐标,而不能在3D场景本身中加载一些多余的空对象。
  • 第三,由于游戏仅使用侧视图,并且其中一个坐标在移动时不会改变,因此您只能在两个坐标中使用交点的计算。
  • 第四,毕竟,实际上会有一个等级计划。 而且,从这样的方案开始,提出新的水平只是方便。 您可以在任何图形编辑器中简单地在屏幕上拖动块,构造新的走廊和障碍物,然后运行脚本并获取有关对撞机的信息。 即,部分地解决了关卡编辑器的问题。

我编写了一个脚本,该脚本采用了诸如水平方案文件(png)的名称和颜色之类的输入参数,其填充被解释为障碍。 默认的可用空间颜色是黑色。 为了通过脚本进行处理,每个级别的方案必须保存在单独的png文件中。 例如,对于最低级别,它看起来像这样:


我同意一个块应为80像素宽和48像素高。 这相当于3D世界中的4 x 2.4米。 可以制作40 x 24像素,即十倍,但在图片中看起来很小。

脚本在第一层的结果(图像被裁剪到右侧):


该脚本在浏览器中执行。 我认为html标记没有意义,它很基本:数据输入字段和开始按钮。 接下来,读取的图像显示在画布上。 作为脚本的结果,将在3D世界比例下的图片下方显示一个数组,其中包含每个块的左下角和右上角坐标,并为每个级别在脚本中指定了偏移量。 可以将这个数组复制并粘贴到对撞机列表中以在游戏中使用(在下面进行更多介绍),它将以某种常量存储。 坐标也出现在图片本身上,但在2D图像的参考帧中。 这些数字显示在每个块的中心,可让您检查计算中是否包括所有块。 就其本身而言,除了目视检查外,这些数字对于任何其他用途都不需要。 某些障碍物(例如玩家经过的栏)不应计算在内。 关于从计算中排除哪些对象-下文。

另外,例如,在第二层上,玩家可以在薄薄的水平板上行走。 必须考虑它们。 因此,您需要确保数字也出现在它们上。 在图中,使它们高2个像素。



现在,关于脚本如何考虑块:

  • 该方案由80x48块处理,在每个块中,水平从第2个像素到第79个像素,垂直从第2个像素到第47个像素取一个区域。 不使用第一个和最后一个像素,因此您可以在块周围创建一个宽度为1像素的黑框,这可以改善电路的视觉感知并有助于其创建。
  • 查看块顶行的所有像素。 如果其中有彩色像素,则在最终数组中,块的坐标从水平第一个像素到最后一个彩色像素,垂直到块的整个高度。 这将是一个空白块,部分或全部宽度。
  • 查看该块底行的所有像素。 如果其中有彩色,但顶行中没有彩色,则块的坐标从水平第一个到最后一个彩色像素,从底部到最后一个阵列垂直从3个像素移动。 这将是一个行走的平台。 一个街区内可以有几个水平平台。 仅在块的底部识别平台。 平台的坐标“下沉”到位于下方的块中,因此平台的表面与相邻块(而不是平台)处于同一水平面。
  • 由于仅考虑像素的上一行和下一行,因此不处理空白块内的列和其他修饰。 因此,您可以在块内放置装饰,图的说明,指针,列等,而不必担心这会以某种方式影响脚本的结果。

然后,将从数组中接收的所有坐标转换为3D世界的比例,再乘以其比例系数(在创建3D编辑器时选择该比例)。 该阵列已准备好在游戏中使用。 该脚本代码是匆忙编写的,因此它并不装作精美,但可以执行其任务。

代号
ap = { //      (  ),   3D   lvd: { 'lv01.png': { invw: false, invh: true, level_dw: -8.5, level_dh: -1.5 }, 'lv02.png': { invw: true, invh: true, level_dw: -19.5, level_dh: -5.5 } }, blockw: 80, //   2D blockh: 48, //   2D sc3d: 0.05, //,   3D  ex: 100, //  3D (-   ) v: { data: [] }, i: 0, par: {}, datai: [], resi: [], ars: [], fStopEncode: false, blockColor: function(cl) { document.getElementById('input_cl').value = cl; }, startEncode: function() { //      for (var key in ap.lvd) { ap.lvd[key].dw = ap.lvd[key].level_dw * ap.blockw; ap.lvd[key].dh = ap.lvd[key].level_dh * ap.blockh; }; document.getElementById('startbtn').style.display = 'none'; document.getElementById('startmsg').style.display = 'block'; var cl = document.getElementById('input_cl').value; var fld = document.getElementById('input_fld').value; var nm = document.getElementById('input_nm').value; ap.nm = nm; ap.par = { path: [fld + '/', nm], key: [nm], cl: aplib.hexToRgb(cl.substring(1, 7)) }; setTimeout(function() { ap.datai[ap.par.key] = new Image(); ap.datai[ap.par.key].onload = function() { ap.parseData(); }; ap.datai[ap.par.key].src = ap.par.path[0] + ap.par.path[1]; }, 500); }, stopEnode: function(e) { if (typeof ap !== "undefined") { if (e.keyCode == 27) { console.log('stop'); ap.fStopEncode = true; }; }; }, parseData: function() { ap.w = ap.datai[ap.par.key[0]].width, ap.h = ap.datai[ap.par.key[0]].height; aplib.initCanv(ap.w, ap.h); ctx.drawImage(ap.datai[ap.par.key[0]], 0, 0, ap.w, ap.h, 0, 0, ap.w, ap.h); ap.ars = []; ap.i = 0; setTimeout(function() { ap.parseData1(); }, 1000); }, parseData1: function() { if (ap.i < ap.par.key.length) { document.getElementById('info').innerHTML = '' + ap.nm; ap.blocksw = Math.floor(ap.w / ap.blockw); ap.blocksh = Math.floor(ap.h / ap.blockh); ap.ar = []; ap.arv = {}; ap.hi = 0; ctx.fillStyle = '#CCCCCC'; ap.parseData2(); } else { document.getElementById('startbtn').style.display = 'block'; document.getElementById('startmsg').style.display = 'none'; }; }, parseData2: function() { if (ap.hi < ap.blocksh) { ap.ar.push([]); ap.wi = 0; ap.parseData3(); } else { ap.parseData4(); }; }, parseData3: function() { var k = ''; if (ap.wi < ap.blocksw) { var fground = true, fvari = false, fempty = true; var upx1 = 0, upx2 = 0, dnx1 = 0, dnx2 = 0; var upxf = false, dnxf = false; for (var wii = 1; wii < ap.blockw - 2 + 2; wii++) { pixelDatai = ctx.getImageData(ap.wi * ap.blockw + wii, ap.hi * ap.blockh + 1, 1, 1).data; //  pixelDatai2 = ctx.getImageData(ap.wi * ap.blockw + wii, (ap.hi + 1) * ap.blockh - 3, 1, 1).data; //  if ((pixelDatai[0] == ap.par.cl.r) & (pixelDatai[1] == ap.par.cl.g) & (pixelDatai[2] == ap.par.cl.b)) { //   ground    if (upxf == false) { upxf = true; upx1 = wii; }; } else { //    if (upxf == true) { upx2 = wii + 1; upx1--; //   dy = -1; // 3D       1 ap.v.data.push([ap.wi * ap.blockw + upx1, ap.hi * ap.blockh + dy, ap.wi * ap.blockw + upx2, ap.hi * (ap.blockh) + ap.blockh - 1]); upxf = false; upx1 = 0; upx2 = 0; }; }; if ((pixelDatai2[0] == ap.par.cl.r) & (pixelDatai2[1] == ap.par.cl.g) & (pixelDatai2[2] == ap.par.cl.b)) { //   ground     if (upxf == false) { if (dnxf == false) { dnxf = true dnx1 = wii; }; }; } else { if (upxf == false) { if (dnxf == true) { dnx2 = wii + 1; dnx1--; //   dy = 2; // 3D    2 ap.v.data.push([ap.wi * ap.blockw + dnx1, (ap.hi + 1) * ap.blockh - 3 + dy, ap.wi * ap.blockw + dnx2, (ap.hi + 1) * ap.blockh - 3 + 2 + dy]); dnxf = false; dnx1 = 0; dnx2 = 0; }; }; }; }; if (ap.fStopEncode == true) { ap.hi = ap.h, ap.wi = ap.w, i = ap.par.key.length; }; setTimeout(function() { ap.wi++; ap.parseData3(); }, 10); } else { ap.hi++; ap.parseData2(); }; }, parseData4: function() { setTimeout(function() { var t, tw, tx, ty, ar = []; //  for (var i = 0; i < ap.v.data.length; i++) { ar = ap.v.data[i]; t = ar[0] + ';' + (ar[1]+1) + '<br/>' + ar[2] + ';' + (ar[3]+1); tw = ar[2] - ar[0]; tx = ar[0]; ty = ar[1] + Math.floor((ar[3] - ar[1]) / 2) - 0; aplib.Tex2Canvas(ctx, t, 'normal 10px Arial', 10, '#CCCCCC', tx, ty, tw, 0, 'center', 'top'); }; ap.parseData5(); }, 10); }, parseData5: function() { var t, tw, tx, ty, ar = [], n; //   3D var lv = ap.lvd[ap.nm]; for (var i = 0; i < ap.v.data.length; i++) { ar = ap.v.data[i]; ar[0] += lv.dw; ar[1] += lv.dh; ar[2] += lv.dw; ar[3] += lv.dh; if (lv.invh == true) { n = -ar[1]; ar[1] = -ar[3]; ar[3] = n; }; if (lv.invw == true) { n = -ar[0] ar[0] = -ar[2]; ar[2] = n; }; ar[0] = Math.round(ap.sc3d * ar[0] * ap.ex) / ap.ex; ar[1] = Math.round(ap.sc3d * ar[1] * ap.ex) / ap.ex; ar[2] = Math.round(ap.sc3d * ar[2] * ap.ex) / ap.ex; ar[3] = Math.round(ap.sc3d * ar[3] * ap.ex) / ap.ex; }; //    ap.v.data.sort(aplib.sortBy0); console.log(ap.v.data); document.getElementById('divresult').innerHTML = JSON.stringify(ap.v.data); } }; aplib = { hexToRgb: function(hex) { var arrBuff = new ArrayBuffer(4); var vw = new DataView(arrBuff); vw.setUint32(0, parseInt(hex, 16), false); var arrByte = new Uint8Array(arrBuff); return { r: arrByte[1], g: arrByte[2], b: arrByte[3], s: arrByte[1] + "," + arrByte[2] + "," + arrByte[3] }; }, //   canvas Tex2Canvas: function(ctx, t, font, lin, fcolor, x, y, w, h, haln, valn) { //left, right, center, center-lim- ctx.font = font; ctx.fillStyle = fcolor; var l = 0; var tx = x; var ftw = false; var tw = 1; var arr = t.split('<br/>'); for (var i = 0; i < arr.length; i++) { arr[i] = arr[i].split(' '); }; for (var i = 0; i < arr.length; i++) { var s = '', slen = 0, s1 = '', j = 0; while (j < arr[i].length) { var wordcount = 0; while ((slen < w) & (j < arr[i].length)) { s = s1; s1 = s + arr[i][j] + ' '; slen = ctx.measureText(s1).width; if (slen < w) { j++; wordcount++; } else { if (wordcount > 0) { s1 = s; } else { j++; }; }; }; ftw = false; tw = ctx.measureText(s1).width; if (haln == 'center') { tx = x + Math.round((w - tw) / 2); }; if (haln == 'right') { tx = x + Math.round((w - tw)); }; if (haln == 'center-lim') { if (tw > w) { tw = w; }; if (tw < 1) { tw = 1; }; tx = x + Math.round((w - tw) / 2); ftw = true; }; if (ftw == false) { ctx.fillText(s1, tx, l * lin + y); } else { ctx.fillText(s1, tx, l * lin + y, tw); }; if (s1 == '') { j = arr[i].length + 1; }; l++; s1 = ''; slen = 0; }; }; return Math.round(tw); }, // canvas initCanv: function(w, h) { function canvErr() { document.getElementById('divcanv').innerHTML = '<div style="height:130px"></div><div style="width:440px; border:#FFFFFF 1px solid; margin:10px; padding:4px; background-color:#000000"><p class="txterr">---> Error<br/>HTML5 Canvas is not supported!<br/>Please, update your browser!</p></div>'; }; if (w == 0) { w = 740; h = 680; }; elcanv = document.getElementById('divcanv'); elcanv.innerHTML = '<canvas id="canv" style="width:' + w + 'px; height:' + h + 'px; display:block;" width="' + w + '" height="' + h + '"></canvas>'; canvas1 = document.getElementById('canv'); if (!canvas1) { canvErr(); return 0; } else { if (canvas1.getContext) { ctx = canvas1.getContext('2d'); ctx.clearRect(0, 0, w, h); return 1; } else { canvErr(); }; }; }, sortBy0: function(i, ii) { if (i[0] > ii[0]) return 1; else if (i[0] < ii[0]) return -1; else return 0; } }; 


现在-关于游戏如何使用一系列块进行工作。 游戏使用相交的走廊(关卡)。 当玩家进入走廊时,将连接一个新的方块阵列:因此,对于每个走廊,都可以从其关卡方案中获得自己的阵列。 在玩家移动期间,将检查其坐标是否在每个块内。 如果他在任何街区内,我们都会发生碰撞。 但是随着玩家的每一次移动,我们都不需要寻找与该关卡的所有区块相交的地方,因为其中可能有很多。 创建仅包含距离播放器最近的块的数组。

 collisionsUpdate: function(x, y, dw, dh) { var coll = []; var o; for (var i = 0; i < ap.v.lv.d.length; i++) { o = ap.v.lv.d[i]; if ((o[0] >= x - ap.v.dw) & (o[2] <= x + ap.v.dw)) { if ((o[1] >= y - ap.v.dh) & (o[3] <= y + ap.v.dh)) { coll.push(o); }; }; }; ap.v.coll = coll; }, 

此处,在输入x处,y是玩家的当前坐标,dw,dh是您要水平和垂直搜索块的距离,例如12米和8米。 换句话说,将播放器周围的所有方块都放在24x16米的正方形内。 他们将参加冲突搜索。 ap.v.lv.d [i]是当前级别的块数组的元素,实际上,他本人也是定义一个块的边界的4个数字的数组-[x1,y1,x2,y2],因此,要检查平方在水平方向上,我们采用索引为0和2的元素,在垂直方向上为1和3的元素。如果存在匹配项,则将此块添加到ap.v.coll碰撞列表中。

当播放器移动时,我们将更新此碰撞列表,但是,为了节省性能,我们将不会在每一步(或更确切地说,渲染帧)执行此操作,而是在播放器离开ap.v.collwStep和ap.v.collhStep,例如8米和4米。 也就是说,当玩家从其原始位置水平或垂直经过某个路径时,我们将重新组装碰撞数组。 同时,让我们记住重新组装数组的位置,以便将其用于下一次迭代。 pers [ax]-这里用ax表示坐标轴(ax),它可以是x或z,具体取决于玩家所走的走廊的方向。

 //   if ((Math.abs(pers[ax] - ap.v.collw) > ap.v.collwStep) || (Math.abs(pers.y - ap.v.collh) > ap.v.collhStep)) { ap.v.collw = pers[ax]; ap.v.collh = pers.y; ap.collisionsUpdate(pers[ax], pers.y, 12, 8); }; 

为什么会有这样的困难? 为什么不使用整个碰撞阵列而不是蒸汽。 事实是,冲突检测是根据更加复杂的算法执行的,并且在帧的每个渲染中绝对不使用最接近的级别块(而不是最接近的级别块)来检查冲突是无济于事的。 (尽管这不准确。)

使用上面准备的碰撞数组,在帧的每个渲染中定义碰撞:

代号
 collisionsDetect: function(x, y, xOld, yOld, up) { //up=-1 -  var res = false, o; var collw = false, collh = false, collwi = false, collhi = false, collhsup = false, support = [], supportf = false, fw = false, upb = -1; var bub = -1, bubw = 0; var pw2 = ap.v.player.pw2, ph2 = ap.v.player.ph2, supportd = ap.v.supportd; for (var i = 0; i < ap.v.coll.length; i++) { o = ap.v.coll[i]; collwi = false; collhi = false; collhsup = false; fw = false; if ((x + pw2 >= o[0]) & (x - pw2 <= o[2])) { if ((y + ph2 > o[1]) & (y - ph2 < o[3])) { collwi = true; }; }; //     if ((xOld + pw2 >= o[0]) & (xOld - pw2 <= o[2])) { if ((yOld + ph2 > o[1]) & (yOld - ph2 < o[3])) { bub = i; if (Math.abs(xOld - o[0]) < Math.abs(xOld - o[2])) { bubw = -1; } else { bubw = 1; }; }; }; if ((x >= o[0]) & (x <= o[2])) { fw = true; //  i   }; if ((y + ph2 >= o[1]) & (y - ph2 <= o[3])) { if ((x > o[0]) & (x < o[2])) { collhi = true; //  if (y + ph2 > o[3]) { collhsup = true; supportf = true; support = o; upb = 1; }; //  if (y - ph2 < o[1]) { upb = -1; }; }; }; if ((y - ph2 >= o[3] + supportd - 0.11) & (y - ph2 <= o[3] + supportd + 0.001)) { if (fw == true) { collhi = true; collh = true; res = true; collhsup = true; supportf = true; support = o; }; }; if (collwi & collhi) { res = true; }; if (collwi) { collw = true; }; if (collhi) { collh = true; }; }; return { f: res, w: collw, h: collh, support: support, supportf: supportf, upb: upb, bub: bub, bubw: bubw }; }, 


这里的x,y,xOld,yOld是播放器的新坐标和当前坐标。 只需按一下按钮,就可以根据给定的速度来计算新的坐标,即这些坐标是可能的。 检查它们以查看它们是否落在冲突列表的任何块内。 如果它们掉落,则它们会退回旧的,并且玩家不会越过障碍物。 如果它们不跌倒,它们就会成为潮流。 pw2和ph2是玩家假想对撞机的宽度和高度的一半(玩家宽度/ 2,玩家高度/ 2)。 如果发生水平和垂直碰撞(collw,collh),播放器下方是否有支撑块(supportf),则发出输出-这样可以清楚地确定是否进一步开始跌倒动画,或者播放器是否简单地切换到相邻的块等等。 只是不要问为什么我在那里加0.001并减去0.11。 这是一个可怕的拐杖,可以防止掉落块状物以及与水平障碍物碰撞时产生的抖动影响。此功能有效,但需要以正常方式重写。 此功能的优化也缺失。

我认为遇到碰撞值得在这里结束。

很难说我的方法比光线追踪快多少,或者也许比光线追踪慢,但是对于后者,Three.js还存储了一系列参与碰撞系统的对象。 只是碰撞是通过发射光束及其与物体侧面平面的交点的方法确定的,而与我之间的冲突,是通过确定一个物体的坐标是否沿两个轴的每一个都在另一个物体的内部来确定的。

该游戏还具有触发某种动画(例如,与水接触触发鲨鱼的运动)的移动对象(鲨鱼)和标记对象。 所有这些对象也都参与碰撞,其中一些对象具有随时间变化的坐标。 奇怪的是,这里的一切都更加简单:在对象移动期间,将其坐标与玩家的坐标进行比较。


游戏手柄


通常,在浏览器中维护javascript游戏手柄并不是一件容易的事。 没有按钮按下和释放事件。 只有连接和断开设备的事件以及可以通过定期轮询获得的状态,然后将其与上一个事件进行比较,才可以进行。

该视频演示了Windows 8.1平板电脑和Windows 10 PC上浏览器中游戏手柄的操作。该平板电脑是旧版,于2014年发布,因此在游戏上关闭了动态照明。


为了轮询游戏手柄,使用了每100毫秒调用一次的函数。 使用我的库m3d.lib.globalTimer.addEvent的功能进行设置。

 m3d.lib.globalTimer.addEvent({ name: 'gamepad', ti: 100, f: function() { var st = m3d.gamepad.state(); if (st == false) { if (contr.gpDownFlag == true) { m3d.gamepad.resetH(); }; }; } }); 

这里的globalTimer是我编写的javascript setInterval计时器事件管理系统。 在那里,只需将一系列事件添加到需要以不同间隔调用的特定数组。 然后,将一个setInterval计时器设置为与所有事件中频率最高的事件对应的频率。 计时器轮询函数m3d.lib.globalTimer.update(),该函数遍历所有事件的列表,并运行即将执行的事件的功能。 添加或删除事件时,间隔频率也可以更改(例如,如果删除最快的事件)。

游戏还为每个游戏手柄按键定义了处理程序:“ a”代表轴(ax),“ b”代表按钮(按钮),而11是沿着十字的水平轴的左偏差(好像其按钮1), 12-沿十字的水平轴(如其按钮2)的右偏差,21和22-垂直轴。 例如:

['a',11],
['b',3]

表示将同时设置下一个功能,用于沿水平轴向左偏移和按钮3(向左)偏移。 好了,然后设置了一个功能,该功能将在按下按钮然后释放时执行。

  m3d.gamepad.setHandler( [ ['a', 11], ['b', 3] ], function(v) { if (contr.btState.lt == false) { contr.keyDownFlag = true; contr.btState.lt = true; contr.gpDownFlag = true; apcontrolsRenderStart(); }; }, function(v) { contr.btState.lt = false; m3d.contr.controlsCheckBt(); apcontrolsRenderStart(); } ); 

apcontrolsRenderStart()是一个函数,如果尚未运行渲染,它将启动它。 通常,对游戏手柄的支持与我的m3d库紧密相关,因此,如果我继续描述其所有功能,它将持续很长时间...

我只给您一部分-游戏手柄,其中我以最简单的方式实现了游戏手柄的初始化,处理程序的安装和状态轮询。

代号
 gamepad: { connected: false, gamepad: {}, gamepadKey: '', axesCount: 0, buttonsCount: 0, f: [], //  fup: [], //  fval: [], //      fupCall: [], //   buttons: [], //link to f [0.. ] axes: [], //link to f [0.. ] initCb: function() {}, resetH: function() {}, init: function(gp) { var f = false; for (var key in gp) { if (f == false) { if (gp[key] != null) { if (typeof gp[key].id !== "undefined") { f = true; this.connected = true; this.gamepad = gp[key]; this.gamepadKey = key; }; }; }; }; if (typeof this.gamepad.axes !== "undefined") { this.axesCount = this.gamepad.axes.length; }; if (typeof this.gamepad.buttons !== "undefined") { this.buttonsCount = this.gamepad.buttons.length; }; this.f = []; this.fup = []; this.fval = []; this.fupCall = []; this.axes = []; for (var i = 0; i < this.axesCount * 2; i++) { this.axes.push(-1); }; this.buttons = []; for (var i = 0; i < this.buttonsCount; i++) { this.buttons.push(-1); }; this.initCb(); }, setHandlerReset: function(f) { this.resetH = f; }, setHandler: function(ar, f, fup) { //ar['b',3] ['a',11] var fi, bt, ax, finext, finexta; finexta = false; for (var i = 0; i < ar.length; i++) { if (ar[i][0] == 'a') { ax = Math.floor(ar[i][1] / 10); bt = ar[i][1] - (ax * 10); bt = ax * 2 + bt - 3; fi = this.axes[bt]; if (fi == -1) { //   fi = this.f.length; if (finexta == false) { finexta = true; this.f.push(f); this.fup.push(fup); this.fval.push(0); this.fupCall.push(true); this.axes[bt] = fi; } else { fi--; this.f[fi] = f; this.fup[fi] = fup; this.axes[bt] = fi; }; } else { this.f[fi] = f; this.fup[fi] = fup; }; } else if (ar[i][0] == 'b') { bt = ar[i][1] - 1; fi = this.buttons[bt]; if (fi == -1) { //   fi = this.f.length; if (finexta == false) { finexta = true; this.f.push(f); this.fup.push(fup); this.fval.push(0); this.fupCall.push(true); this.buttons[bt] = fi; } else { fi--; this.f[fi] = f; this.fup[fi] = fup; this.buttons[bt] = fi; }; } else { this.f[fi] = f; this.fup[fi] = fup; }; }; }; }, state: function() { var pressed = false; var fi, fval, axesval; for (var i = 0; i < this.fval.length; i++) { this.fval[i] = 0; }; //   var gp = navigator.getGamepads()[this.gamepadKey]; for (var i = 0; i < this.axesCount; i++) { axesval = Math.round(gp.axes[i]); if (axesval < 0) { pressed = true; fi = this.axes[i * 2]; if (fi != -1) { this.fval[fi] = gp.axes[i]; this.fupCall[fi] = true; }; } else if (axesval > 0) { pressed = true; fi = this.axes[i * 2 + 1]; if (fi != -1) { this.fval[fi] = gp.axes[i]; this.fupCall[fi] = true; }; }; }; for (var i = 0; i < this.buttonsCount; i++) { if (gp.buttons[i].pressed == true) { pressed = true; fi = this.buttons[i]; if (fi != -1) { this.fval[fi] = 1; this.fupCall[fi] = true; }; }; }; for (var i = 0; i < this.fval.length; i++) { fval = this.fval[i]; if (fval != 0) { this.f[i](this.fval[i]); } else { if (this.fupCall[i] == true) { this.fupCall[i] = false; this.fup[i](this.fval[i]); }; }; }; return pressed; } }, //gamepad 


通常,游戏中对游戏手柄的支持仍不完整:仅实现了对最简单游戏手柄的支持,但没有实现例如在XBox中使用的那种,因为我没有。 如果得到了,我将对其进行编程并使用它。 可以调整角色的速度,也就是说,可以在步进到运行的范围内以任意速度移动。 这是通过从轴获取分数参数来实现的。 我的游戏板仅返回整数-1和1。此外,我的游戏板有一个令人讨厌的叉号,当向左或向右按​​下时,同时向下或向上按下。 因此,我没有使用十字架的顶部和底部,而是使用游戏手柄右侧的按钮进行了复制。...在游戏发行时,我计划创建游戏手柄的多个配置文件。 另外,在连接多个游戏手柄的情况下,到目前为止仅将使用后者。

响应式屏幕


该游戏的宽高比为16:9。 但是我添加了±10%的自动水平调整,以便在扩展的浏览器窗口中侧面没有这种黑条:


就像这样:


在全屏模式下,将有真正的16:9。 可以使图像总体适应浏览器窗口的任何纵横比,但是我没有这样做,因为低的宽窗口会导致太大的视角,从游戏角度来看这是不好的:遥远的死角,物体,敌人将立即可见以及玩家不需要看到的其他所有内容。 因此,我将自己限制在16:9的±10%范围内。 但是,对于狭窄的显示器(4:3),我仍然意识到可以通过按Y键从4:3切换到16:9到适应模式。 但不要更宽-因此,同样,不要破坏游戏玩法。 也就是说,您可以以经典的16:9比例播放,也可以通过水平裁剪将图像放大到窗口的高度。 虽然,这也不是很好,例如在街机游戏中,当某些东西从侧面飞向玩家时。 反应几乎没有时间了。 但是您总是可以快速返回经典模式。


下面的视频显示了屏幕的适应性以及游戏中使用的所有热键:


实际上,宽高比是在游戏设置中设置的。

 aspect1:{w:1280, h:720, p:10}, //16x9 +- 10% aspect2:{w:960, h:720, p:34}, //4x3 +- 34% 

在游戏中,当您按Y时,它会切换:

 contr.btCodesDn[89] = function() { //'y' if (m3dcache.setup.aspect.swch == 1) { m3dcache.setup.aspect = m3dcache.setup.aspect2; m3dcache.setup.aspect.swch = 2; } else { m3dcache.setup.aspect = m3dcache.setup.aspect1; m3dcache.setup.aspect.swch = 1; }; m3d.core.onWindowResize(0); m3d.contr.renderAll(); }; 

我的图书馆有一个挂在调整大小窗口上的事件。 这是其中的一部分:

代号
 m3dcache.v.vw = window.innerWidth; m3dcache.v.vh = window.innerHeight; m3dcache.v.vclipw = 0; m3dcache.v.vcliph = 0; if (typeof m3dcache.setup.aspect !== "undefined") { if ((m3dcache.setup.aspect.w == 0) || (m3dcache.setup.aspect.h == 0)) {} else { var o = m3d.lib.inBlock(0, 0, m3dcache.setup.aspect.w, m3dcache.setup.aspect.h, 0, 0, m3dcache.v.vw, m3dcache.v.vh, 'center', 'center', 'resize'); if (typeof m3dcache.setup.aspect.p !== "undefined") { if (o.clipx > 0) { ow = ow * (m3dcache.setup.aspect.p / 100 + 1); if (ow > m3dcache.v.vw) { ow = m3dcache.v.vw; }; o = m3d.lib.inBlock(0, 0, ow, oh, 0, 0, m3dcache.v.vw, m3dcache.v.vh, 'center', 'center', 'resize'); }; }; m3dcache.v.vclipw = o.clipx; m3dcache.v.vcliph = o.clipy; var margx = o.clipx + 'px', margy = o.clipy + 'px'; document.getElementById('m3dcontainer').style.marginLeft = margx; document.getElementById('m3dcontainer').style.marginTop = margy; if (document.getElementById('renderer') !== null) { document.getElementById('renderer').style.marginLeft = margx; document.getElementById('renderer').style.marginTop = margy; }; m3dcache.v.vw = ow; m3dcache.v.vh = oh; }; }; 


m3d.lib.inBlock也是我的库的功能,该函数将一个矩形内切到另一个矩形中,并带有诸如居中,缩放或裁切之类的参数,并显示内切矩形的新尺寸以及在此过程中形成的字段的大小。 基于此信息,将放置窗口的div容器。 “渲染器”是3D场景的块上下文元素。 然后根据获得的参数在此处缩放画布。

UI显示在容器中单独的画布元素上。 通常,文档树由三个具有绝对位置的透明DIV块组成(或多或少,取决于游戏的需要):底部是3D场景的画布,上方是IU的画布,顶部用于为界面元素和其他视觉效果制作动画。 也就是说,UI不是以3D呈现,而是在其knavas或图层上呈现。 将图层组合成一张图片的任务留给浏览器。 要使用UI,我在库中有一个特殊的对象。 简要地-本质如下。 加载了具有UI元素(呈png格式,具有透明性)的Sprite列表。 从那里开始,获取必要的元素-背景,按钮。 然后使用js drawImage函数(img,ix,iy,iw,ih,x,y,w,h)将它们绘制在中间画布上。 即,来自图像的必要片段被显示在屏幕上的必要位置。 按钮显示在附加到它们的背景之上-它们的所有位置和大小都在UI配置中设置。 调整窗口大小时,将重新计算目标画布(在其上显示元素)上的元素位置,具体取决于该元素在水平或垂直方向上居中还是在屏幕的某个角或面上对齐。 这将创建一个不依赖于屏幕的纵横比的自适应UI。 仅需要在水平和垂直方向上设置最小可能的分辨率,并且不低于该分辨率,以使元素彼此不重叠。 我将再次讨论UI,因为文章篇幅很大,而且我仍在研究UI,因为仍然缺少很多我需要的功能。 例如,在高分辨率显示器上,界面看起来会很小。 您可以将元素的大小乘以某个系数,具体取决于屏幕分辨率。 另一方面,也许不需要屏幕上的大按钮? 如果屏幕分辨率很大,那么屏幕本身就很大。


您可以给程序员一个选择-是根据窗口的大小动态缩放IU还是将元素分布在角落。 在动态尺寸的情况下,还存在其自身的问题-例如,界面以太大的比例显示时的“肥皂”。 如果您以故意很大的分辨率制作界面元素的精灵,那么它们将占用大量空间,并且可能对小型设备也没有用-它们仍然不需要大的精灵,但是它们会消耗内存。

我认为今天就足够了。 还有什么要考虑的,如何实现这一点或那一点。 同时,我离开了编程一段时间,做了晋升。 我计划参加一对独立的展示会,并积极参与社交网络游戏的推广,因为自11月以来,我计划去众筹平台:我将需要3D图形和骨骼动画领域的专家才能完成游戏。

在以下文章中,我将讨论移动设备浏览器中的触摸控件-并非每个人都将游戏手柄或键盘连接到平板电脑,关于针对低功耗设备优化3D图形的内容等等。

Source: https://habr.com/ru/post/zh-CN472272/


All Articles