有了Leap Motion,大家可以跟游戏手柄说拜拜了。
现代智能手机凭借着以手势与触控为核心的新型用户界面为我们带来前所未有的使用体验。随着时间的不断推移,我们几乎可以肯定地认为,在计算的未来中触控技术将扮演愈发重要的关键性角色。不过话虽如此,一系列艰难挑战仍然横亘在开发者面前、使他们很难在台式设备上充分发挥触控机制的巨大优势。
深度相机与3D位置追踪器的出现隐隐给触控式互动操作的迅速普及指出了一条光明的发展道路。虽然目前的最新一代技术仍然远远无法与《少数派报告》中的展示效果相提并论,但其中蕴含的可观潜力却绝对不容置疑。
就在去年,我们曾经对Leap Motion控制器进行过一番评测——这款设备利用高精度动作捕捉与手指追踪技术成功在任何标准化台式机上实现了手势输入机制。作为一套经由标准USB端口连接的设备,Leap Motion控制器通过其内置摄像头与红外LED来捕捉用户手指与手部的细微活动。Leap Motion软件会对图像数据加以处理,并将这些信息翻译成手势以及触控事件。
Leap Motion提供一套指向性极广的SDK,旨在帮助开发人员更轻松地在自己的应用程序当中实现对该设备的支持。它支持跨越多种平台的一系列不同编程语言——除了原生桌面软件之外,Leap Motion还提供一套JavaScript库,从而通过构建Leap兼容性网站的方式将开发成果在Web浏览器上加以呈现。
Leap Motion SDK非常容易上手,掌握之后能为我们带来极为丰厚的回报。它以抽象化方式消除了控制器所固有的大部分复杂因素,通过高级别API实现了对手部及手指追踪数据的捕捉。在今天的文章中,我们将共同探讨如何构建起一款能够充分发挥Leap Motion追踪功能优势的前端Web应用程序。作为初始内容,我们先来说明如何在HTML Canvas元素上渲染手指定位点,而后再一步步讨论怎样利用Pixi.js图形库来配合Leap Motion控制机制开发出简单的2D游戏。
正式开始
Leap Motion JavaScript库依赖于WebSockets将来自控制器的数据提交并显示在用户的Web浏览器之上。WebSockets标准的设计目标在于允许JavaScript代码运行在网页当中,从而确保网页与远程服务器之间始终保持有长效连接——这一特性通常被用于创建基于浏览器的聊天客户端以及其它一些实时类Web应用程序。
当用户部署好了自己的Leap Motion设备、并为其安装了附带的软件及驱动程序之后,其中作为内置软件组件之一的轻量级WebSocket服务器就会运行在用户计算机的后台进程当中。由Leap Motion控制器捕捉到的数据会被发往WebSocket服务器,这是为了能够让相关数据直接交由Web浏览器使用、而无需另行安装额外的浏览器插件。Leap Motion JavaScript库与本地WebSocket服务器相连,负责捕捉数据并利用部分简单API进行打包以保证其易于使用。
作为开发工作的第一步,让我们首先创建一个网页,并在其中载入Leap Motion JavaScript库、获取来自设备的数据并记录下浏览器调试控制台中的部分数据:
- <html>
- <head>
- <script src="http://js.leapmotion.com/leap-0.4.2.js"></script>
- </head>
- <body>
- </body>
- <script type="text/javascript">
- Leap.loop(function(frame) {
- if (frame.pointables.length > 0)
- console.log(frame.pointables);
- });
- </script> </html>
上述代码的head元素中包含一个script标签,它的作用是从公司的CDN处下载Leap Motion JavaScript库。Leap Motion针对生产使用环境推出一套精简版本,此外还针对开发用途提供非精简版本。在这里我们使用的是非精简版本,这是为了能够在需要使用浏览器的JavaScript调试器时能够更轻松地对代码进行单步调试。大家可以点击此处访问Leap Motion官方网站,并在这里下载到前面提到的这两种版本。
在第二个script标签中,也就是页面body之下,我们利用Leap.loop方法对来自设备的数据进行捕捉。Leap Motion驱动程序会发出数据“帧(frame)”,这些帧也就是经过处理的控制器视频流快照。该软件每秒大约会产生30帧数据,从而持续不断地为应用程序的运行提供必要信息。被传递至该loop中的匿名函数在每次接收到新帧时都会执行一次。
Leap Motion API大大简化了对手部、手指以及工具位置的检测流程。这里的“工具”被认定为一种延长状物体,例如铅笔,其中一端由用户把持在手中。在Leap Motion的表述体系当中,通用术语“指向物(pointable)”被用于描述作为工具或者手指存在的对象。帧对象当中包含一项名为pointables的属性,用于显示一系列显示在帧内的指向物对象。
在前面的示例中,每一帧内所包含的一系列pointables都被输出至控制台当中。如果大家检查这些被推送至控制台的指向物对象,就会发现其中有多项属性被用于描述显示信息——例如指向物的长度与宽度、指向物末端的空间坐标以及该指向物末端的移动速度等。
在火狐开发者控制台中查看指向物对象。
计算标准化手指位置
利用指向物数据,我们将建立一套简单的演示范例,用户可以利用它通过移动手指控制屏幕上某个元素的位置。第一步,我们需要获取指尖的具体位置。下面的示例代码为如何获取单个指向物的原始坐标:
- Leap.loop(function(frame)
- { if (frame.pointables.length > 0)
- { var position = frame.pointables[0].tipPosition;
- console.log("X: " + position[0] + " Y: " + position[1]);
- }
- });
在多数情况下,大家都会优先使用Leap Motion软件所提供的自动稳定功能,而不太可能直接使用原始tipPosition数据。Leap Motion控制器会以极高的精度对图像中的活动对象加以检测,并从中甄别出使用者手部几乎难以察觉的细微摇晃及活动。另一项名为stabilizedTipPosition的备选属性则允许我们收集同样的数据,但在结果中过滤掉上述细微活动。现在我们已经获得了用户手指的物理坐标,接下来要做的就是将其与浏览器窗口内的位置进行关联。
在Leap Motion的术语体系中,由控制器加以追踪的大型虚拟空间被称为互动框(interaction box)。帧对象的interactionBox属性当中包含多种方法与属性,它们负责提供与互动框及其维度相关的具体信息。它采用一种便捷的标准化方法,即将某个物理点的原始坐标换算成能够代表该点在互动框中相对位置的等效数值。
标准化定位机制非常实用,因为它能帮助我们获取手指的原始位置、并将其与应用程序窗口中的对应点加以映射。标准化坐标使用浮点数值格式,具体数值在0到1之间浮动。要想获取浏览器窗口中对应点的相对数值,大家只需将X与Y值同目标区域的高度与宽度相乘即可:
- Leap.loop(function(frame) {
- if (frame.pointables.length > 0) {
- var position = frame.pointables[0].stabilizedTipPosition;
- var normalized = frame.interactionBox.normalizePoint(position);
- var x = window.innerWidth * normalized[0];
- var y = window.innerHeight * (1 - normalized[1]);
- console.log("X: " + x + " Y: " + y);
- }
- });
在屏幕上绘制手指位置
利用标准化定位机制,现在我们已经可以在网页上绘制手指的具体位置。为了简单起见,这篇文章将只向大家展示如何利用DOM元素实现这一目标。只需创建一个具备绝对位置的div,而后将其top与left属性分别设置为对应的X与Y坐标,我们就能将Leap Motion loop中的标准化函数用于定位:
- <html>
- <head>
- <script src="http://js.leapmotion.com/leap-0.4.2.js"></script>
- <style type="text/css">
- #position {
- width: 25px;
- height: 25px;
- position: absolute;
- background-color: blue;
- }
- </style>
- </head>
- <body>
- <div id="position"></div>
- </body>
- <script type="text/javascript">
- Leap.loop(function(frame) {
- if (frame.pointables.length > 0) {
- var position = frame.pointables[0].stabilizedTipPosition;
- var normalized = frame.interactionBox.normalizePoint(position);
- var element = document.getElementById("position");
- element.style.left = window.innerWidth * normalized[0];
- element.style.top = window.innerHeight * (1 - normalized[1]);
- }
- });
- </script>
- </html>
这样一来,当用户在Leap Motion控制器前方移动自己的手指时,屏幕上的div元素也将随之发生位移。在目前为止的示例中,我们只涉及单一手指的处理,也就是使用pointables数组中的第一个元素。在接下来的示例中,我们将逐步探寻数组中第一个元素的作用、从而使每根手指都成为可操作对象。这一次,我们将在HTML 5 Canvas上描绘手指位置、而不再使用DOM元素:
- <html>
- <head>
- <script src="http://js.leapmotion.com/leap-0.4.2.js"></script>
- </head>
- <body>
- <canvas id="canvas" width="800" height="600"></canvas>
- </body>
- <script type="text/javascript">
- var canvas = document.getElementById("canvas");
- var ctx = canvas.getContext("2d");
- Leap.loop({frameEventName: "animationFrame"}, function(frame) {
- ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
- frame.pointables.forEach(function(pointable) {
- var position = pointable.stabilizedTipPosition;
- var normalized = frame.interactionBox.normalizePoint(position);
- var x = ctx.canvas.width * normalized[0];
- var y = ctx.canvas.height * (1 - normalized[1]);
- ctx.beginPath();
- ctx.rect(x, y, 20, 20);
- ctx.fill();
- });
- });
- </script>
- </html>
在上面的示例中,我们利用forEach方法对pointable项目进行了遍历。它会对每一个标准化位置进行识别,而后将其作为矩形绘制在屏幕当中。此外,clearRect方法在处理每一帧图像时都会被调用一次,旨在确保前一帧所绘制的矩形切实得到清除。
上述示例当中还引入了另一项新功能,即frameEventName选项。在默认状态下,Leap.loop回调将被调用至每一个提取自Leap Motion控制器的帧数据。对frameEventName选项中的“animationFrame”值进行设置会改变这一行为机制,从而令Leap.loop与浏览器的绘制周期保持一致。它会利用浏览器的requestAnimationFrame API,从而确保该回调只在浏览器准备进行绘制时被调用。
检测手势
除了手指位置,Leap Motion SDK还能够识别出其它几种手势动作,其中包括扫动与点触。Leap Motion帧对象当中包含一个手势属性,能够提取从帧数据中检测出的一系列手势信息。以下示例代码显示了如何对扫动手势进行迭代、对其起始与结束位置进行标准化处理并最终将结果绘制在Canvs当中:
- var canvas = document.getElementById("canvas");
- var ctx = canvas.getContext("2d");
- var options = {
- enableGestures: true, frameEventName: "animationFrame" };
- Leap.loop(options, function(frame) {
- ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
- frame.gestures.forEach(function(gesture) {
- if (gesture.type != "swipe") return;
- var start = frame.interactionBox.normalizePoint(gesture.startPosition);
- var end = frame.interactionBox.normalizePoint(gesture.position);
- var startX = ctx.canvas.width * start[0];
- var startY = ctx.canvas.width * (1 - start[1]);
- var endX = ctx.canvas.width * end[0];
- var endY = ctx.canvas.width * (1 - end[1]);
- ctx.beginPath();
- ctx.moveTo(startX, startY);
- ctx.lineTo(endX, endY);
- ctx.stroke();
- });
- });
Leap Motion SDK在默认状态下不会显示手势。为了获取手势数据,我们必须将选项对象的enableGestures属性设为true,而后再将其传递至Leap.loop方法当中。如果该选项没有经过设置,那么frame.gestures数组将直接为空。
每一种手势都具备一项type属性,用于描述该手势的相关性质。在forEach loop当中,首先利用条件表达式来忽略不属于扫动的手势动作。接下来,利用interactionBox对手势的起始与结束位置进行标准化处理。最后,利用标准Canvas绘制API来描绘由起始位置到结束位置的线条。
在面对Leap Motion设备进行手部扫动操作时,大家应该能够在Canvas上看到自己的动作轨迹线条。手势检测在更为广义的互动操作之下起着非常关键的作用。举例来说,大家可以通过向左或者向右的扫动操作帮助用户导航至相册界面的上一个或者下一个显示条目当中。
为了找到更多明确的黑色方块,大家往往尽快得焦头烂额。
下页精彩内容继续
#p#
利用Leap Motion与Pixi.js创建一款简单游戏
现在我们已经介绍了关于Leap Motion开发的基础知识,是时候尝试建立一些更具现实意义的实验性成果了。在接下来的示例中,我们将一步步帮助大家构建起一款能够与Leap Motion控制器相对接的互动游戏。在该游戏中,大家可以移动自己的手指来控制屏幕上的宇宙飞船。玩家必须小心驾驶自己的飞船,从而避免与敌人的船只相撞。
我们利用Pixi.js——一套简单的2D图形库——进行游戏创建,它能够帮助大家轻松创建并操作HTML Canvas元素。底层Leap Motion控制方案则基于文章前面提到的单手指操作示例。在游戏中,经过标准化X与Y坐标处理的手指位置将被映射到画面中的飞船之上,也就是玩家所需要操作的对象。
为了帮助大家更轻松地遵照示例进行,我们在代码当中穿插注释来帮助各位读者顺利理解。下面来看游戏代码:
- <html>
- <head>
- <script src="assets/pixi.dev.js" type="text/javascript">
- </script>
- <script src="http://js.leapmotion.com/leap-0.4.2.js">
- </script>
- <style type="text/css">
- #game {
- margin-left: auto;
- margin-right: auto;
- width: 800px;
- height: 600px;
- }
- </style>
- </head>
- <body>
- <div id="game"></div>
- </body>
- <script type="text/javascript">
- var stageWidth = 800;
- var stageHeight = 600;
- //创建关卡与渲染引擎
- var stage = new PIXI.Stage(0xFFFFFF);
- var render = new PIXI.autoDetectRenderer(stageWidth, stageHeight); document.getElementById("game").appendChild(render.view);
- // 创建动态星空背景
- var spacebg = PIXI.Texture.fromImage("assets/spacebg.png");
- var space = new PIXI.TilingSprite(spacebg, stageWidth, stageHeight);
- stage.addChild(space);
- // 创建飞船对象
- var rocket = PIXI.Sprite.fromImage("assets/rocketship.png");
- stage.addChild(rocket);
- //这条变量用于控制游戏状态。
- //当飞船与短文碰撞时,此变量将被设置为“true”,
- //这时draw loop中止并显示“游戏结束”字样
- var collided = false;
- //在计时器中设置初始值
- //更多关于时间与游戏速度的详细信息
- var timer = window.performance.now();
- //设置Leap Motion控制器loop
- var options = {frameEventName: "animationFrame"};
- var controller = Leap.loop(options, function(frame) {
- //我希望游戏中的各个元素能够始终以同样的速度进行移动。
- //如果激活了draw loop,
- //游戏速度将根据帧速率进行变化。
- //我利用渲染时间差来确定实际距离
- //场景中的每个元素都应该以每一帧为单位进行移动。
- var now = window.performance.now();
- var delta = Math.min(now - timer, 100);
- timer = now;
- //让星空背景保持移动
- space.tilePosition.x -= 0.2 * delta;
- //检查“游戏结束”是否处于活动状态
- if (collided) {
- //创建并显示“游戏结束”标题
- var caption = new PIXI.Text("Game Over", {
- font: "50px Helvetica", fill: "red"
- });
- caption.x = stageWidth / 2 - caption.width / 2;
- caption.y = stageHeight / 2;
- stage.addChild(caption);
- //渲染该帧并调用返回结果
- //场景中的各元素保持静态
- return render.render(stage);
- }
- //遍历关卡当中的每一艘敌方船只
- space.children.forEach(function(child) {
- // 使敌方船只向前移动
- child.x -= 0.2 * delta;
- //测宇宙飞船的中心点是否处于敌方船只的边界当中。
- //这是一种非常简单的处理方式。
- //大家也可以选择采用更为智能的多碰撞检测机制
- //即利用“hitArea”设置更为精确的碰撞边界。
- //如果用户与敌机接触,则变更“collided”值以结束游戏
- if (child.getBounds().contains(rocket.x, rocket.y))
- collided = true;
- //移除移动至屏幕边缘以外的敌方船只
- if (child.x < -child.width)
- space.removeChild(child);
- });
- //找到刚刚被添加到关卡当中的宇宙飞船
- var last = space.children[space.children.length - 1];
- //如果屏幕上不存在宇宙飞船,则加入一艘新的船只
- //如果刚刚添加的船只距离右侧边缘超过250px,则再增加一艘新的船只。
- //这就是我们在游戏中派遣敌方船只的方式。在实际游戏中,
- //大家可以进一步调低250px这一空间设置以增加游戏难度
- if (space.children.length == 0 || last.x < (stageWidth - 250)) {
- var item = PIXI.Sprite.fromImage("assets/enemy.png");
- item.y = Math.floor((Math.random() * (stageHeight - 100)));
- item.x = stageWidth;
- space.addChild(item);
- }
- if (frame.pointables.length > 0) {
- //获取标准化手指位置
- var pos = frame.pointables[0].stabilizedTipPosition;
- var normPos = frame.interactionBox.normalizePoint(pos, true);
- //将宇宙飞船移动至标准化手指位置
- rocket.x = stageWidth * normPos[0];
- rocket.y = stageHeight * (1 - normPos[1]);
- }
- // 渲染场景
- render.render(stage);
- });
- </script>
- </html>
如大家所见,Leap Motion SDK让创建实时手势交互游戏变得相对简单了一些。利用代码示例中所涉及的开发技术,大家可以很轻松地创建出多种不同类型的游戏或者应用程序。如果大家拥有自己的Leap Motion控制器,不妨尝试利用沿X轴进行手指操控的方式开发“打砖块”或者“是男人就下一百层”之类的游戏。大家还可以通过多种方式对上述代码进行深入扩展,从而添加更多更为丰富的功能。举例来说,我们可以利用指向物的touchZone属性让宇宙飞船在识别到用户的指向划动操作时发射激光武器。
“手指?在向前行进时,我们不需要手指。”
我们在GitHub资源库中发布了本篇文章中所用到的全部代码内容,感兴趣的朋友可以点击此处进行查看。除此之外,作为奖励内容我们还利用Three.js制作了一份粗糙的WebGL示例,它能够显示出手指位置在三维空间中的追踪轨迹——点击此处查看。
游戏中所使用的图像资源来自Daniel Cook发布的SpaceCute图像合集,大家可以点击此处进行下载。如果各位希望使用其它用于游戏原型设计的免费图形资源,也不妨点击此处看看Cook同志收集的其它好东西。背景图像来自OpenGameArt的宇宙飞行游戏入门素材,点击此处查看。
总结
Leap Motion硬件也许仅仅算是迈向未来的第一步(但却绝对堪称是‘人类的一大步’),很明显其实际表现与《少数派报告》等电影中展示的效果还存在着巨大的差距,不过它的出现已经标志着我们向手势驱动类计算系统吹响了进军的号角。由于引入了三维空间概念,这扇刚刚开启的大门完全有可能引领我们走入更为丰富也更为复杂的交互新时代——这要比我们习以为常的平面触控机制更加激动人心。
这份教程当中涉及到了JavaScript使用场景,不过Leap Motion还提供面向更多其它不同编程环境的多种SDK。大家既可以轻松创建原生应用,也可以根据实际需要使用脚本化语言。
另外值得一提的是,除了本篇教程中所提到的功能之外、Leap Motion还提供更多有待发掘的珍贵资源。要详细了解Leap Motion SDK所支持的各项功能,我们强烈建议大家点击此处查看官方发布的说明文档。
请大家继续期待我们本系列专题中的下一篇文章,届时我们将向大家展示如何利用Pixi.js创建一款更为完整的游戏成品。我们将向其中添加更多游戏性功能——包括更为深入的卷轴滚动周期以及游戏循环机制。此外,我们还会探讨如何利用新的HTML 5 Gamepad API让玩家能够通过传统的游戏主机手柄进行操作。
核子可乐译,点击查看原文。