求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
在基于 HTML5 Canvas 的游戏中处理用户输入
 
作者 Kevin Moot,火龙果软件    发布于 2013-11-15
 

捕获用于游戏开发的键盘、鼠标和触摸事件

当涉足 HTML5 游戏世界时,人们很容易低估管理键盘、鼠标和基于触摸的输入的复杂性。本文将探讨用来处理基于 HTML Canvas 的游戏中用户交互的一些基本技术。学习如何处理键盘和鼠标事件,如何阻止 Web 浏览器的默认事件行为,以及如何向游戏对象的某种逻辑表示传播事件。此外,还将学习如何处理 iPhone 和 iPad 等移动设备上与设备无关的(device-agnostic)输入。

简介

令拥有 Flash 或 Silverlight 背景的开发人员感到惊讶的是,为 HTML5 Canvas 编写的应用程序在处理用户输入方面并没有什么特立独行之处。实质上,从启用了 JavaScript 的 Web 浏览器诞生之初开始,HTML 用户输入就涉及到使用内置于浏览器中的事件处理系统;HTML5 在检测和处理用户输入方面没有任何特殊之处。例如,浏览器提供了低级反馈来表明用户单击的坐标 (x,y),就这么简单。

处理用户交互与其他任何低级游戏架构没什么不同。没有内置的抽象来通知您用户何时与已在 Canvas 上呈现的一个具体对象进行了交互。这对您希望处理这些事件的方式提供了强大的低级控制力度。只要您可以避免各种浏览器缺陷,最终就能根据一个独特应用程序来调优事件处理,从而实现最高效率 —而不是受到特定实现的禁锢。

在本文中,将会学习处理基于 HTML Canvas 的游戏中的用户交互的一些技术。文中的示例演示了如何处理键盘、鼠标和基于触摸的事件。向游戏对象传播事件的战略和移动兼容性也会有所涉及。

常用缩略语

1.CSS:级联样式表

2.DOM:文档对象模型

3.HTML:超文本标记语言

事件类型

用户交互完全由浏览器的传统事件监听器模型处理。HTML5 的出现并不新鲜;它采用了与自 Netscape Navigator 诞生初期就已经使用的事件模型。

实质上,可以将交互式应用程序或游戏视为处理用户输入的浏览器事件模型与处理图形输出的 Canvas 的结合。除非您亲自将它们结合在一起,二者之间没有逻辑关系。

您将利用事件监听器可附加到 <canvas>元素自身的事实。因为 <canvas>元素是一个块级元素,从浏览器的角度讲,这与将事件监听器附加到 <div>或其他任何块级元素上没有任何区别。

键盘事件

监听和处理的最简单的事件类型是键盘事件。它们不依赖于 Canvas 元素或用户的鼠标位置。键盘事件只需您在文档级别上监听按键、释放和按住事件。

监听键盘事件

事件监听器模型可能因为浏览器实现不同而各不相同,所以实现模型的最快捷的方式是使用一个库来规范化事件的处理。以下示例使用了 jQuery 绑定事件。这通常是最简单的开始方式,但考虑到 jQuery 在兼容遗留浏览器方面涉及的工作量水平,性能可能会受到影响。另一个流行的库(专为加速跨浏览器键盘事件处理而编写)是 Kibo(请参见 参考资料)。

清单 1 演示了对键事件的监听,以及如何基于按下的键而采取适当的措施。

清单 1. 处理键盘事件

$(document.body).on('keydown', function(e) { 
switch (e.which) {
// key code for left arrow
case 37:
console.log('left arrow key pressed!');
break;

// key code for right arrow
case 39:
console.log('right arrow key pressed!');
break;
}
});

如果应用程序在一个 Web 浏览器的环境中运行,那么一定要牢记一些有意义的键盘组合键。尽管定义某些常见组合键的行为来替换它们的默认浏览器行为(比如 Ctrl+R)在技术上是可行的,但这种做法受到了强烈反对。

鼠标事件

鼠标事件比键盘事件更复杂。您必须知道 Canvas 元素在浏览器窗口中的位置,以及用户光标的位置。

监听鼠标事件

使用 e.pageX和 e.pageY特性,很容易获得鼠标相对于整个浏览器窗口的位置。在本例中,原点 (0,0) 将位于整个浏览器窗口的左上角。

当用户光标未在 Canvas 区域中时,您通常不会太关心用户输入。因此,最好考虑将原点 (0,0) 放在 Canvas 元素的左上角。在理想情况下,您希望在与 Canvas 区域相关的局部坐标系统内工作,而不希望在与整个浏览器窗口相关的全局坐标系统中工作。

鼠标事件战略

执行以下步骤,将全局窗口坐标转换为局部 Canvas 坐标。

计算页面上的 Canvas DOM 元素的 (x,y) 位置。

确定鼠标相对于整个文档的全局位置。

要将原点 (0,0) 放在 Canvas 元素的左上角,并有效地将全局坐标转换为相对坐标,需要了解第 2 步中计算的全局鼠标位置与第 1 步中计算的 Canvas 位置之间的区别。

图 1 给出了您需要捕获的有关全局坐标系统的信息示例。

图 1. 鼠标位置、全局坐标

图 2 显示了将鼠标位置转换为局部坐标后的结果。

图 2. 转换为局部坐标后的鼠标位置

清单 2 给出了确定局部鼠标坐标的方法。它假设您已经在标记中定义了一个 Canvas 元素,如下所示:<canvas id="my_canvas"></canvas>。

清单 2. 处理鼠标事

var canvas = $('#my_canvas'); 

// calculate position of the canvas DOM element on the page

var canvasPosition = {
x: canvas.offset().left,
y: canvas.offset().top
};

canvas.on('click', function(e) {

// use pageX and pageY to get the mouse position
// relative to the browser window

var mouse = {
x: e.pageX - canvasPosition.x,
y: e.pageY - canvasPosition.y
}

// now you have local coordinates,
// which consider a (0,0) origin at the
// top-left of canvas element
});

非常规的浏览器行为

在计算机游戏中,您通常不希望任何默认浏览器行为干扰您的操作。例如,您不希望拖动鼠标来执行文本选择,通过单击鼠标右键来打开上下文菜单,或者滚动鼠标滚轮来上下翻页。

图 3 给出了一个在用户单击并拖动浏览器中的一个图像时可能出现的情况的示例。尽管从总体上讲,默认浏览器行为对拖放应用程序很有用,但这不是您的游戏中想要的行为。

图 3. 拖动图像时的默认浏览器行为

在所有事件处理函数中,添加一个 preventDefault()行,并从该函数返回 false。清单 3 中的代码将完成此任务,防止发生默认操作和事件发生。

清单 3. 阻止默认行为

 canvas.on('click', function(e) { 
e.preventDefault();

var mouse = {
x: e.pageX - canvasPosition.x,
y: e.pageY - canvasPosition.y
}

//do something with mouse position here

return false;
});

即使对于清单 3 中的代码,当用户在 DOM 元素上发起一个拖动事件时,您仍然可能遇到多种不合意的副作用,比如 I 型光标外观、文本选择等。拖动事件问题在图像上通常更常见,一种不错的想法是将它也应用于 Canvas 元素,以阻止拖动和选择。清单 4 给出了一个通过添加少量 CSS 来阻止选择的 CSS 规则。

清单 4. 阻止选择的建议样式

image, canvas { 
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
}

覆盖桌面行为

一般来讲,一个不错的想法是覆盖拖动和选择事件,确保浏览器的默认拖动和选择行为不会出现。

清单 5 中的代码特意未使用 jQuery 来附加事件。jQuery 没有正确处理 ondragstart和 onselectstart事件(如果使用 jQuery 来附加事件,事件处理函数可能从不触发)。

清单 5. 取消拖动和选择事件

var canvasElement = document.getElementById('my_canvas'); 

// do nothing in the event handler except canceling the event
canvasElement.ondragstart = function(e) {
if (e && e.preventDefault) { e.preventDefault(); }
if (e && e.stopPropagation) { e.stopPropagation(); }
return false;
}

// do nothing in the event handler except canceling the event
canvasElement.onselectstart = function(e) {
if (e && e.preventDefault) { e.preventDefault(); }
if (e && e.stopPropagation) { e.stopPropagation(); }
return false;
}

覆盖移动行为

在移动设备上,阻止用户缩放和平移浏览器窗口通常很重要(缩放和平移常常是移动浏览器处理触摸手势的默认行为)。

您可以将 user-scalable=no添加到 viewport元标记来阻止缩放行为。例如:

 <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1" />

要禁用使用手势对文档或窗口的所有移动,可以将清单 6 中的事件监听器附加到 document.body 文件中。这会从根本上取消用户在点击 Canvas 或游戏区域外任何位置时的所有默认浏览器行为。

清单 6. 取消移动设备窗口移动

document.body.ontouchstart = function(e) { 
if (e && e.preventDefault) { e.preventDefault(); }
if (e && e.stopPropagation) { e.stopPropagation(); }
return false;
}

document.body.ontouchmove = function(e) {
if (e && e.preventDefault) { e.preventDefault(); }
if (e && e.stopPropagation) { e.stopPropagation(); }
return false;
}

传播到游戏对象

对于您希望捕获的每种事件类型,只需向 Canvas 附加一个事件监听器。例如,如果您需要捕获单击和鼠标移动事件,只需向 Canvas 附加一个单击事件监听器和一个鼠标移动事件监听器。这些事件监听器只需附加一次,所以通常会在初始化应用程序期间附加这些事件。

如果您需要将事件监听器捕获的所有有用信息都传播给在 Canvas 上呈现的对象,则必须为系统构建您自己的逻辑。在本例中,这样一个系统会负责将单击或鼠标移动事件传播到关注其中某个事件的处理的所有游戏对象。

当每个游戏对象获知其中一个事件后,该游戏对象首先需要确定该单击或鼠标移动事件是否与它们有关。如果有关,那么该游戏对象需要确定鼠标坐标是否在它自己的边界内。

传播战略

您的具体战略将会取决于游戏类型。例如,2D 磁贴集可能具有与 3D 空间不同的战略。

以下步骤给出了一个可用于简单 2D 应用程序的简单实现。

检测用户的鼠标是否单击了 Canvas 区域内的位置。

通知所有游戏对象,在一组给定的坐标上发生了一个单击事件。

对于每个游戏对象,在鼠标坐标和游戏对象的边框之间执行一次命中测试,确定鼠标坐标是否接触到该对象。

简单传播示例

单击事件处理函数可能类似于清单 7。该示例假设您已设置了某种类型的结构来跟踪一个空间中的所有游戏对象。所有游戏对象的位置和尺寸存储在一个名为 gameObjectArray的变量中。

清单 7. 单击事件处理函数向游戏对象传播

// initialize an array of game objects 
// at various positions on the screen using
// new gameObject(x, y, width, height)

var gameObjectArray = [
new gameObject(0, 0, 200, 200),
new gameObject(50, 50, 200, 200),
new gameObject(500, 50, 100, 100)
];

canvas.on('click', function(e) {
var mouse = {
x: e.pageX - canvasPosition.x,
y: e.pageY - canvasPosition.y
}

// iterate through all game objects
// and call the onclick handler of each

for (var i=0; i < gameObjectArray.length; i++) {
gameObjectArray[i].handleClick(mouse);
}
});

下一步是确保每个游戏对象都能够执行命中测试,从而确定鼠标坐标是否接触到了游戏对象的边框区域。图 4 给出了一个未成功的命中测试的示例。

图 4. 边界外单击 -- 命中测试未成功

 

图 5 显示了一个成功的命中测试。

图5. 边界内单击 -- 命中测试成功

您可以为游戏对象定义一个类,如清单 8 所示。命中测试在 onclick()函数内执行,它测试对象的矩形边框与作为参数传入的鼠标坐标之间是否接触。

清单 8. 游戏对象类和命中测试

function gameObject(x, y, width, height) { 
this.x = x;
this.y = y;
this.width = width;
this.height = height;

// mouse parameter holds the mouse coordinates
this.handleClick = function(mouse) {

// perform hit test between bounding box
// and mouse coordinates

if (this.x < mouse.x &&
this.x + this.width > mouse.x &&
this.y < mouse.y &&
this.y + this.height > mouse.y) {

// hit test succeeded, handle the click event!
return true;
}

// hit test did not succeed
return false;
}
}

提高传播效率

在许多情形下,可以构建一种更高效的实现。例如,在具有数千个游戏对象的游戏中,您当然希望避免在每次触发一个事件时对屏幕中的每个游戏对象进行测试。

下面的示例使用 jQuery 自定义事件触发了一个合成事件。该合成事件仅由监听该特定事件的游戏对象处理。对于该示例:

像以前一样处理鼠标单击事件,执行所有必要的转换(比如将鼠标位置转换为局部坐标)。

触发一个包含已转换的鼠标坐标(作为参数)的合成事件。

任何负责处理一个单击事件的游戏对象都会设置一个监听器来监听该合成事件。

修改鼠标单击事件处理函数,以便只触发一个自定义事件。可为该自定义事件提供任何随意名称。在清单 9 中,它名为 handleClick。

清单 9. 触发一个自定义事件

canvas.on('click', function(e) { 
var mouse= {
x: e.pageX - canvasPosition.x,
y: e.pageY - canvasPosition.y
}

//fire off synthetic event containing mouse coordinate info
$(canvas).trigger('handleClick', [mouse]);
});

如清单 10 所示,游戏对象类已被修改。我们没有定义 onclick函数,而只是监听 handleClick事件。只要触发 handleClick事件,监听该事件的所有游戏对象都会触发它们相应的事件处理函数

清单 10. 处理一个自定义事件

function gameObject(x, y, width, height) { 
var self = this;
this.x = x;
this.y = y;
this.width = width;
this.height = height;

$(canvas).on('handleClick', function(e, mouse) {

// perform hit test between bounding box
// and mouse coordinates

if (self.x < mouse.x &&
self.x + self.width > mouse.x &&
self.y < mouse.y &&
self.y + self.height > mouse.y) {

// hit test succeeded, handle the click event!

}
});
}

高级命中测试

一定要考虑在多个游戏对象彼此层叠时会发生什么。如果用户单击一个层叠了多个游戏对象的点,那么您需要确定如何处理该行为。例如,您通常只希望触发最近的对象的事件处理函数,忽略它之下的其他对象。

要处理这种层叠情况,您需要知道每个层叠的游戏对象的顺序或深度。Canvas 不会公开深度的任何逻辑表示,所以您需要获得控制权,生成必要的逻辑来处理这种情况。

为了介绍深度的概念,有必要为所有游戏对象分配一个 z 索引来表示它们的深度。清单 11 显示了一个示例。

清单 11. 向游戏对象添加一个 z 索引

function gameObject(x, y, zIndex, width, height) { 
var self = this;
this.x = x;
this.y = y;
this.zIndex = zIndex;
this.width = width;
this.height = height;

//...
}

为了方便深度测试,您需要执行排序。在清单 12 中,对存储游戏对象的示例结构进行了排序,让具有最高 z 索引的游戏对象显示在列表的最前端。

清单 12. 对游戏对象数组进行排序

// sort in order such that highest z-index occurs first 
var sortedGameObjectArray = gameObjectArray.sort(function(gameObject1, gameObject2) {
if (gameObject1.zIndex < gameObject2.zIndex) return true;
else return false;
})

最后,在 click函数中,通过切换来迭代这个有序数组中的所有游戏对象。

只要一个游戏对象的命中测试提供了积极的结果,就会立即中断测试,使单击事件不会继续传播下去。如果未终止测试,就会像在清单 13 中一样,在更深的深度中,处理 click事件的不合意的游戏对象行为将会继续下去。

清单 13. 在命中测试成功时中断

canvas.on('click', function(e) { 
var mouse = {
x: e.pageX - canvasPosition.x,
y: e.pageY - canvasPosition.y
}

for (var i=0; i < sortedGameObjectArray.length; i++) {
var hitTest = sortedGameObjectArray[i].onclick(mouse);

// stop as soon as one hit test succeeds
if (hitTest) {
break; // break out of the hit test
}
}
});

不规则的游戏对象边界

对矩形边框执行命中测试通常是最简单和最高效的方法,但在许多情况下这还不够。如果游戏对象具有更加不规则的形状,对一个三角形或多边形边框执行测试可能更有意义。在这些情况下,您需要将游戏对象事件处理函数中的命中测试逻辑换为更高级的命中检测形式。通常,您会参考游戏碰撞理学来实现合适的逻辑。

Canvas API 提供了一个名为 IsPointInPath()的有趣函数,它会自行执行多边形碰撞测试。实质上

IsPointInPath(x, y)允许您测试给定的 (x,y) 点是否落在一个任意路径(基本上是一个多边形边界)内。如果提供的 (x,y) 坐标落入当前路径(在 Canvas 上下文中定义)内,则会返回 true。

使用 isPointInPath()

图 6 显示了一种有必要对一个非矩形路径测试鼠标坐标的情形。在本例中,该路径是一个简单的三角形路径。

图 6. 在一个三角形路径边界内单击

出于演示之目的,填充的路径已可视化。因为无需在屏幕上物理地呈现 IsPointInPath()的路径就可以返回一个有用的结果,所以定义该路径就已经足够了,无需调用 fill()或 stroke()来实际绘制该路径。清单 14 显示了详细信息。

清单 14. 使用 isPointInPath执行命中检测

$(canvas).on('handleClick', function(e, mouse) { 

// first, define polygonal bounding area as a path
context.save();
context.beginPath();
context.moveTo(0,0);
context.lineTo(0,100);
context.lineTo(100,100);
context.closePath();

// do not actually fill() or stroke() the path because
// the path only exists for purposes of hit testing
// context.fill();

// perform hit test between irregular bounding area
// and mouse coordinates
if (context.isPointInPath(mouse.x, mouse.y)) {
// hit test succeeded, handle the click event!

}
context.restore();
});

自行编写碰撞算法常常比使用 IsPointInPath()更高效,但它是一个建立原型和快速开发的不错工具。

移动兼容性

为了使示例游戏兼容移动设备,您需要处理触摸事件而不是鼠标事件。

尽管手指点击也可以被移动浏览器解释为单击事件,但仅监听移动浏览器上的单击事件通常不是一种好方法。更好的方法是附加特定触摸事件的监听器,确保获得最佳的响应能力。

检测触摸事件

您可以编写一个帮助器函数,它首先检测设备是否支持触摸事件,然后相应地返回鼠标坐标或触摸坐标。这使得调用函数可以采用与设备无关的方式处理输入坐标,无论您是在桌面上还是在移动平台上。

清单 15 给出了一个与设备无关的函数的示例,它将会捕获鼠标和触摸事件,并规范化响应。

清单 15. 规范化鼠标和触摸事件

function getPosition(e) { 
var position = {x: null, y: null};

if (Modernizr.touch) { //global variable detecting touch support
if (e.touches && e.touches.length > 0) {
position.x = e.touches[0].pageX - canvasPosition.x;
position.y = e.touches[0].pageY - canvasPosition.y;
}
}
else {
position.x = e.pageX - canvasPosition.x;
position.y = e.pageY - canvasPosition.y;
}

return position;
}

在检测触摸支持时,该示例使用了 Modernizr 库(请参见 参考资料)。Modernizr 库将对触摸支持的检测简化为测试变量 Modernizr.touch,如果设备支持触摸事件,那么该变量将会返回 true。

与设备无关的事件处理函数

在应用程序初始化期间,您可以将指定定义事件监听器的代码替换为一个支持触摸的设备和鼠标输入的独立分支。将鼠标事件映射到一个等效的触摸事件非常简单。例如,mousedown被替换为 touchstart,mouseup被替换为 touchend。

清单 16 给出了一个使用 Modernizr 映射等效的鼠标 / 触摸事件的示例。它还使用了清单 15 中定义的 getPosition()函数。

清单 16. 使用规范化的鼠标 / 触摸事件

var eventName = Modernizr.touch ? 'touchstart' : 'click'; 

canvas.on(eventName, function(e) {
e.preventDefault();

var position = getPosition(e);
//do something with position here

return false;
});

除非您需要处理更高级的操作,比如捏合和轻击,在直接从桌面应用程序直接移植鼠标事件时,此方法通常会良好地运行。假设使用一个单点触摸系统;如果需要执行多点触摸检测,则需要使用其他一些代码(这不属于本文的介绍范围)。

结束语

在本文中,您学习了如何处理键盘和鼠标事件,以及如何取消不合意的浏览器行为。本文还探讨了向游戏对象传播事件的战略,回顾了针对命中测试的更高级的考虑因素,以及一个解决移动兼容性问题的简单方法。尽管用户输入的范围不属于本文的讨论范畴,但典型的用户输入场景为创建可靠的、与设备无关的库来处理 HTML5 应用程序的用户输入提供了一个起点。

 
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
 
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
 
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
 
分享到
 
 


十天学会DIV+CSS(WEB标准)
HTML 5的革新:结构之美
介绍27款经典的CSS框架
35个有创意的404错误页面
最容易犯的13个JavaScript错误
设计易理解和操作的网站
更多...   


设计模式原理与应用
从需求过渡到设计
软件设计原理与实践
如何编写高质量代码
单元测试、重构及持续集成
软件开发过程指南


东软集团 代码重构
某金融软件服务商 技术文档
中达电通 设计模式原理与实践
法国电信 技术文档编写与管理
西门子 嵌入式设计模式
中新大东方人寿 技术文档编写
更多...