在基于 HTML5 Canvas 的游戏中处理用户输入

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

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

Kevin Moot, 软件开发人员, The Nerdery

Kevin Moot 照片Kevin Moot 从很小的时候就对计算机图形有浓厚的兴趣,那时他在自己的 Apple IIe(具有六种颜色的巨大矩阵以及令人震撼的 280x192 分辨率)上创建游戏。他还将 HTML5 的 Canvas 技术应用于几个前沿网站,HTML/CSS、JavaScript 和 .NET 也是他的专长。Kevin 目前是 The Nerdery 的一名交互软件开发人员。



2013 年 10 月 14 日

简介

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

常用缩略语

  • CSS:级联样式表
  • DOM:文档对象模型
  • HTML:超文本标记语言

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

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

您可以 下载本文中使用的示例的源代码。


事件类型

用户交互完全由浏览器的传统事件监听器模型处理。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.pageXe.pageY特性,很容易获得鼠标相对于整个浏览器窗口的位置。在本例中,原点 (0,0) 将位于整个浏览器窗口的左上角。

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

鼠标事件战略

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

  1. 计算页面上的 Canvas DOM 元素的 (x,y) 位置。
  2. 确定鼠标相对于整个文档的全局位置。
  3. 要将原点 (0,0) 放在 Canvas 元素的左上角,并有效地将全局坐标转换为相对坐标,需要了解第 2 步中计算的全局鼠标位置与第 1 步中计算的 Canvas 位置之间的区别。

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

图 1. 鼠标位置、全局坐标
该屏幕显示了一个窗口,其中的文档 x,y 坐标为 300,200,全局 x,y 坐标为 350,260

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

图 2. 转换为局部坐标后的鼠标位置
该屏幕显示了局部 x,y 坐标 50,60

清单 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 没有正确处理 ondragstartonselectstart事件(如果使用 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 应用程序的简单实现。

  1. 检测用户的鼠标是否单击了 Canvas 区域内的位置。
  2. 通知所有游戏对象,在一组给定的坐标上发生了一个单击事件。
  3. 对于每个游戏对象,在鼠标坐标和游戏对象的边框之间执行一次命中测试,确定鼠标坐标是否接触到该对象。

简单传播示例

单击事件处理函数可能类似于清单 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. 边界外单击 -- 命中测试未成功
该图显示单击位置为 x,y 坐标 50,60,而边界的 x,y 坐标为 120,150

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

图 5. 边界内单击 -- 命中测试成功
该图显示单击位置为 x,y 坐标 160,170,在边界 x,y 坐标 120,150 内

您可以为游戏对象定义一个类,如清单 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 自定义事件触发了一个合成事件。该合成事件仅由监听该特定事件的游戏对象处理。对于该示例:

  1. 像以前一样处理鼠标单击事件,执行所有必要的转换(比如将鼠标位置转换为局部坐标)。
  2. 触发一个包含已转换的鼠标坐标(作为参数)的合成事件。
  3. 任何负责处理一个单击事件的游戏对象都会设置一个监听器来监听该合成事件。

修改鼠标单击事件处理函数,以便只触发一个自定义事件。可为该自定义事件提供任何随意名称。在清单 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. 在一个三角形路径边界内单击
一个黑色三角形和一个单击 x,y 坐标 20,50 的鼠标指针

出于演示之目的,填充的路径已可视化。因为无需在屏幕上物理地呈现 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被替换为 touchstartmouseup被替换为 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 应用程序的用户输入提供了一个起点。


下载

描述名字大小
本文的代码清单article.listings.zip5KB

参考资料

学习

获得产品和技术

  • jQuery:下载这个流行的 JavaScript 库,它简化了 HTML 文档遍历、事件处理、动画和 Ajax 交互,以实现快速 Web 开发。
  • Modernizr:获取这个开源的 JavaScript 库,帮助您构建下一代受 HTML5 和 CSS3 支持的网站。
  • Kibo:使用这个流行、简单的 JavaScript 库处理键盘事件。
  • IBM 产品评估版:下载或 在 IBM SOA 沙盒中浏览在线试用版,上手使用来自 DB2®、Lotus®、Rational®、Tivoli®和 WebSphere®的应用程序开发工具和中间件产品。

讨论

  • developerWorks 社区:探索由浏览开发人员推动的博客、论坛、群组和维基,并与其他 developerWorks 用户进行交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=947555
ArticleTitle=在基于 HTML5 Canvas 的游戏中处理用户输入
publish-date=10142013