写点什么

使用 HTML5 开发体感游戏——VeloMaze 的开发简介

  • 2013-03-19
  • 本文字数:4637 字

    阅读完需:约 15 分钟

HTML5 现在越来越像一个游戏开发平台。但有时候,游戏领域对于如何应用 HTML5 的特性设置了相当多的限制条件,尤其是对那些访问硬件设备的接口更是如此。

在 2012 年 11 月初,我加入了 copypastel 小组,并决定分享我在第三届年度 NodeKO 竞赛中开发游戏的经历。尽管由于时间限制无法详解全项目,我相信结果仍旧值得与爱好游戏相关技术的读者们分享。接下来,我打算公开该游戏的技术背景,及其如何在多种网络技术基础之上构建整个项目。应用在该游戏中的技术有: Node.js express (静态内容服务), Socket.io (处理客户端和服务器端关于小球往复运动的通讯), Sylvester.js (物理引擎的矢量库)和 jQuery

那什么是 VeloMaze 呢?VeloMaze 是被许多点状恐龙(迅猛龙)占据的迷宫。迅猛龙希望小球能一直在迷宫中移动。由于迷宫的连续性,它可以说是没有终点的。但是每当你通过一级关卡,就会给你之后的玩家造成更多麻烦,因为他(她)会获得另一个小球!是不是很有趣?这就是迷宫中的生活。

这个游戏非常适合那些在同一个地方,而且每个人都有手机的团队。这在当今是很常见的。这里还有一段解说游戏系统要求的视频

系统运行最重要的条件就是加速计。加速计是测量加速度的设备。带有加速计的设备通常返回重力的角度或者重力的矢量数据。这在某些浏览器中有可能做到,比如在下列网贴中所提及的:

从描述系统要求的视频中可以注意到,某些笔记本电脑中也配有加速计。相当多新式的 MacBook Pro 笔记本为防止跌落时造成硬件损伤也安装了加速度计(我那台 2009 年买的笔记本中就安装着一个)。我觉得以笔记本旋转为基础的游戏开发领域目前还是少有人涉足的地带!下面的图表演示了应用程序架构在上层是如何搭建的。

游戏本身的开发相当容易,但全面支持所有的浏览器和加速度计组合需要做更多的工作,而我们的小组只拥有 48 小时的时间。因此,有些测试我们是没有做的,比如对最新版 Android 系统的测试;但是我惊喜的发现,我们的游戏在其中却运行的非常好!然而运气只是成功的一部分。在下面的篇幅中,我打算解析游戏玩法的编写,并解释究竟怎样使该游戏具有可玩性。

读取加速度计数据非常简单,不过标准的缺失使得该过程比预想的更加难以实现。首先,我们快速调查了小组内现有的各种不同的平台和浏览器组合,为适应各种组合方式,编写了如下代码:

复制代码
/* 这里检查游览器是否支持 DeviceOrientationEvent 事件(链接到 W3C)。*/
if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', function(e) {
// 我们从事件“e”中获取角度值并转化成弧度值。
leftRightAngle = e.gamma /90.0*Math.PI/2;
frontBackAngle = e.beta /90.0*Math.PI/2;
}, false);
} else if (window.OrientationEvent) { // 另一个选项是 Mozilla 版本同样的东西
window.addEventListener('MozOrientation', function(e) {
// 在这里将长度值当做一个单位,并转换成角度值,看起来运行的不错。
leftRightAngle = e.x * Math.PI/2;
frontBackAngle = e.y * Math.PI/2;
}, false);
} else {
// 自然地,没有浏览器支持的大多数人会获取这个。
setStatus('Your device does not support orientation reading. Please use Android 4.0 or later, iOS (MBP laptop
is fine) or similar platform.');
}

结果是,代码可以在版本较新的 Chrome 中正常运行,也有人反馈说说它也可以运行在较新版本的 iOS 上的 Safari 浏览器当中(但是我手头上的 Safari 并不支持)。我决定不再试图寻找那种能读取所有可能用的浏览器中加速度计数据的普适性解决方案,因为现实是我们在 Node 淘汰赛的编码环节中个只有 48 小时的时间,而当时游戏的架构还没有完成。

我决定使用 Sylvester,它是一个碰撞检测的向量和矩阵数学库。其实我也可以使用 Box2D JS 来节省时间,但是由于有过 Sylvester 的使用经验,并且所需的碰撞检测比较简单,我还是决定使用 Sylvester。检查小球是否落到洞里去的代码如下所示:

复制代码
function checkBallHole(ball, hole, dropped) {
// 用 Sylvester 定义洞和求的位置为矢量对象
var holeVector = $V([hole.x, hole.y]);
var ballVector = $V([ball.x, ball.y]);
// 在 Sylvester 中用向量简单的计算距离
if (ballVector.distanceFrom(holeVector) < hole.r) {
// 用球的位置作为变量执行回调函数
dropped(ballVector);
}
}

所以事实上这里没有什么复杂的:如果你的小球的中心位于洞内,那么就会触发“dropped”的函数。这段代码在每帧运行一次,那么以前开发过游戏的朋友都知道,这种实现方式可能会造成小球在这一帧内飞跃洞穴而没有掉进去。然而,在日常生活中我们知道,如果你用足够快的速度将小球推向洞穴,它是可以滑过而不掉落的,所以这不是个问题。

这个游戏中也有墙体,所以碰撞检测也是必须要做的。Sylvester 提供了一种目标与计算线状对象的放发,我用的就是这个。简单的代码如下:

复制代码
// 计算球和墙壁碰撞时的冲击矢量数据
function impactBallByWall(ball, wall) {
var ballVector = $V([ball.x, ball.y]);
// 定义墙体为线段 (x1,y1) (x2,y2)
var wallSegment = Line.Segment.create(
              $V([wall.sx, wall.sy]),
              $V([wall.dx, wall.dy]));
// 计算墙与球的最近点(几乎就要撞上的那个位置)
var collisionPoint = wallSegment.pointClosestTo(ballVector)
              .to2D(); // needed by sylvester to convert 3D to 2D vector
//sylvester 将矢量数据从 3D 转化成 2D 所需的变量,然后看这个距离在当前框架内为多少(并不是在两个框架之间差距多少)
var dist = collisionPoint.distanceFrom(ballVector);
// 天真的假设碰撞只发生在球和墙的距离小于球的半径的情况下
if (dist < ball.r) {
// 调整到一个合适的值。较大的逆质量值意味着更大的影响(和较小的质量)
var inverseMassSum = 1/100.0;
// 从球心到碰撞点的向量
var differenceVector = collisionPoint.subtract(ballVector);
var collisionNormal = differenceVector.multiply(1.0/dist);
// 球陷下去的部分相当于在墙内
var penetrationDistance = ball.r-dist;
// 碰撞时球的速率
var collisionVelocity = $V([ball.vx, ball.vy]);
// 从点属性中我们获得冲击速度
var impactSpeed = collisionVelocity.dot(collisionNormal);
if (impactSpeed >= 0) {
    // 计算冲击量。运动能量在每次碰撞是以 2-1-0.4=0.6 的倍率递减
    var impulse = collisionNormal.multiply(
               (-1.4)*impactSpeed/(inverseMassSum));
    // 冲击只会作用在球上,因为墙被设计为固定的
    var newBallVelocity = $V([ball.vx, ball.vy]).add(
               impulse.multiply(inverseMassSum));
    // 把值传回原来的对象
    ball.vx = newBallVelocity.e(1);
    ball.vy = newBallVelocity.e(2);
}
}
}

在实现小球和墙体的碰撞过程时我做了许多并非真实的假设(但是跟现实足够接近)。首先,墙体的厚度为零(而不是实际上的 5 像素),而且,我没有计算两帧之间发生了什么。很明显,这会导致游戏中球体有能力穿越墙体。通过创建球体在不同帧之间的运动线段并找出球体三角与墙体之间是否有交叉,就很可以容易的测试到是否会发生碰撞。那么我们就必须要计算小球和墙体发生碰撞的位置。在上文的代码段中,这个位置数据就存在变量“collisionPoint”内(见下图)。

我很喜欢 Ganvas 和 WebGL,但是我们计划使用 DOM 和 jQuery 来做渲染,因为我们除了制作球体滚动之外,不需要任何 Ganvas 和 WebGL 的特效(如果这样实现,其实是很优雅的,真可惜)。使用 DOM 渲染的场景在缩放时有点生硬,但它很容易实现。我写了下面的函数用于绘制游戏中的子画面。

复制代码
// 设置 DOM 元素属性以反映 sprite 对象
setElementPosition: function(element, sprite) {
// 同步 sprite 维数
sprite.width = (maze.getSquareWidth() * sprite.r * 2);
sprite.height = (maze.getSquareHeigth() * sprite.r * 2);
var x = sprite.x;
var y = sprite.y;
/* 在绝对定位中计算样式属性 left 和 top 的值
* 从而确保点(x,y)在 sprire 的中心位置(使距离计算更加简单)
*/
var newLeft = (x * maze.getSquareWidth()  - element.width() / 2.0);
var newTop = (y * maze.getSquareHeigth()  - element.height() / 2.0);
// 避免 sprite 因为受到传感器持续输入的影响而产生的颤抖
// 通过一个阈值判断是否显示球在屏幕上的移动。
// 这是一个相当大的阈值,对于某些设备来说应该选择较小的值。
if (thresholded(element.css('left') - newLeft, 5) !== 0) {
   // 设置 DOM 元素的 x 坐标位置
    element.css('left', parseInt(newLeft) + 'px');
}
if (thresholded(element.css('top') - newTop, 5) !== 0) {
    // 设置 DOM 元素的 y 坐标位置
    element.css('top', parseInt(newTop) + 'px');
}
// 设置 DOM 元素的大小。
element.css('width', sprite.width + 'px');
element.css('height', sprite.height + 'px');
// 球状 DOM 元素包含许多层(所有的 div),所以重置所有层。
element.find('div').each(function () {
$(this).css('width', sprite.width + 'px');
$(this).css('height', sprite.height + 'px');
});
// sprite 位置的调试信息。通过点击‘enter’显示调试信息。
element.find('.location').html('('+parseInt(sprite.x*10)/10.0+','+parseInt(sprite.y*10)/10.0+')');
},

我做了一个根据视角实时缩放的功能,因此在每个框架中的宽度和高度都是计算得到的。很不幸在游戏中没有体现出这点,因为我们尝试编程控制浏览器旋转失败了(没有用于此项功能的接口,所以这还需要破解)。所以我们最后决定,通知用户关闭手机浏览器的旋转功能,如下图所示:

所有的加速度计数据的读取,物理引擎的运行和 DOM 渲染都被归拢到一个主循环中了。我将所有的主循环的代码放置到函数“update”中并且每 100 毫秒运行一次(我知道这不够频繁,但是它在我的设备上运行的很好,所以就暂时忽略这个设定值吧),像这样:

复制代码
window.setInterval(function() { update(); }, 100);

客户端的所有源代码可以点击这里获取。

顺便提一句,我对于新式的视网膜MacBook Pros 非常失望,它没有加速计(就像我们某位玩家提到的),因为它们的SSD 驱动器没有可以移动的部件!所以也许以笔记本旋转为基础的游戏看起来要到此为止了。- @raimo_t

关于作者:

Raimo Tuisku 是 UserVoice(一种和用户交流的产品)的 API/ 集成系统开发者。他在去年完成了他的计算机科学硕士学位,是位有 10 年开发经验的资深开发人员。在前往旧金山湾区之前,他为三家芬兰软件公司开发产品和安全接口。除 Web 和集成之外,Raimo 还喜欢开发 3D 游戏,设计软件架构,开发社交移动版游戏,和环游世界会见新的朋友。你可以在
Twitter 上 @raimo_t 来获知他在 APIs、WebGL、HTML5 Canvas 和移动版 HTML5 游戏方面正在进行的工作。

查看英文原文: Developing Motoric Games with HTML5 - The Making of VeloMaze

2013-03-19 17:296279
用户头像

发布了 21 篇内容, 共 67441 次阅读, 收获喜欢 1 次。

关注

评论

发布
暂无评论
发现更多内容

斯图飞腾发布《如何将客户反馈转化为有价值的商业洞察》白皮书

Web JS SDK 架构解析

神策技术社区

技术 源码分析 神策数据

上线直接霸占GitHub榜一!腾讯内部spring全家桶笔记细节拉满!

Java 编程 架构 腾讯 面试

OCR开发者福音:PDF提取Excel文件算法开源啦

百度开发者中心

开源 最佳实践 开发者 方法论 OCR

科技的世界里没有“粉红税”

脑极体

百度世界大会2021: 与时代共振,AI让生活更好

百度大脑

人工智能

三年开发,跳槽腾讯三面终获Offer,定级T2-1(面试题+经验总结)

编程菌

Java 编程 程序员 面试 计算机

模块五作业

俊杰

架构实战营

Python开发篇——RSA加密算法和SHA1计算文件校验码

吴脑的键客

Python

硬核技术,带你走进3D点云车道线自动识别

澳鹏Appen

自动驾驶 机器学习 训练数据 3D点云 车道线标注

你真的懂Redis与MySQL双写一致性如何保证吗?

Linux服务器开发

MySQL redis 中间件 架构师 Linux服务器开发

阿里技术3面+HR面,奋战两个月,终斩获offer定级阿里P6+

编程菌

Java 编程 程序员 面试 计算机

百度商业大规模微服务分布式监控系统-凤睛

百度开发者中心

产品 最佳实践 方法论 经验分享 监控系统

ipfs国家认可吗?ipfs挖矿靠谱吗?

IPFS国家认可吗 ipfs挖矿靠谱吗

ASM 实现 Hook Lambda 和方法引用

神策技术社区

大前端 后端 asm 代码 神策数据

技术白皮书:现代企业架构设计

码语者

企业架构

云原生 | 混沌工程工具 ChaosBlade Operator Pod 篇

RadonDB

数据库 混沌工程

hadoop 基本原理与应用

神策技术社区

hadoop 程序员 Hadoop全分布式集群

分享 6 个JavaScript学习资源

devpoint

JavaScript GitHub 8月日更

神策数据微信小程序 SDK 功能介绍

神策技术社区

小程序 微信 代码 神策数据 维护

大二上半学期还挂科两门,大三上半学期就找到了外企实习工作,半年时间,我是怎么逆袭的?

编程菌

Java 编程 程序员 面试 计算机

分享我的华为面经,华为OD岗笔试+面试心得,本人已成功入职!

编程菌

Java 华为 程序员 面试 计算机

Android SDK 的 H5 打通方案演进

神策技术社区

大前端 后端 神策数据 shujv

博睿数据斩获“飞腾PCS认证集成商”,推动国产化生态建设再进一步!

博睿数据

基于 CODING CD + Nocalhost 在大型应用的 ChatOps 实践

CODING DevOps

DevOps 工具 CI/CD 开发测试 ChatOps

上游思维:如何定义成功?

石云升

读书笔记 8月日更 上游思维

Springboot通过@WebFilter日志双份打印BUG分享

FunTester

性能测试 springboot bug

第一次看房

escray

生活记录 8月日更

OceanBase 常见参数和变量究竟有什么本质区别?

OceanBase 数据库

数据库 oceanbase OceanBase 开源 OceanBase 社区版

iOS SDK 的 H5 打通方案演进 | 数据采集

神策技术社区

程序员 大前端 后端 数据 方案

文化与科技的交织,华为P50 Pro与一曲长城谣

脑极体

使用HTML5开发体感游戏——VeloMaze的开发简介_JavaScript_Raimo Tuisku_InfoQ精选文章