基于canvas雷霆_在基于HTML5 Canvas的游戏中处理用户输入

基于canvas雷霆

具有Flash或Silverlight背景的开发人员通常会惊讶于为HTML5 Canvas编写的应用程序在处理用户输入方面没有提供特殊的便利。 从本质上讲,自最早启用JavaScript的Web浏览器以来,HTML用户输入就涉及使用内置于浏览器中的事件处理系统。 HTML5并没有特定于检测和处理用户输入的内容。 例如,浏览器可以提供低级反馈,指示用户单击了哪个坐标(x,y),并且仅此而已。

处理用户交互与其他任何低级游戏体系结构都没有不同。 当用户与在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浏览器的环境中进行的,那么请务必牢记明智的键盘组合。 尽管从技术上可能会定义某些通用键组合的行为,这些行为将覆盖其默认浏览器行为(例如control-r),但对此却一无所知。

鼠标事件

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

监听鼠标事件

使用e.pageXe.pageY属性很容易获得鼠标相对于整个浏览器窗口的e.pageX 。 在这种情况下,(0,0)的原点将位于整个浏览器窗口的左上方。

当用户的光标不在“画布”区域内时,您通常不会太在意用户的输入。 因此,最好考虑(0,0)的原点位于Canvas元素的左上方。 理想情况下,您希望在相对于Canvas区域的局部坐标系内工作,而不是相对于整个浏览器窗口的全局坐标系内工作。

鼠标事件策略

使用以下步骤将全局窗口坐标转换为本地Canvas坐标。

  1. 计算Canvas DOM元素在页面上的(x,y)位置。
  2. 确定鼠标相对于整个文档的全局位置。
  3. 若要将原点(0,0)定位在Canvas元素的左上方,并有效地将全局坐标转换为相对坐标,请获取步骤2中计算的全局鼠标位置与步骤1中计算的Canvas位置之间的差。

图1显示了您需要根据全局坐标系捕获的信息的示例。

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

图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。 例如,如果您需要捕获click和mousemove事件,只需将一个click事件监听器和一个mousemove事件监听器附加到Canvas。 这些事件侦听器仅需要附加一次,因此通常在应用程序初始化期间附加这些事件。

如果您需要事件侦听器捕获的任何有用信息来传播到Canvas上呈现的对象,则必须为系统构建自己的逻辑。 在此示例中,这样的系统将负责向与处理那些事件之一有关的所有游戏对象广播单击或鼠标移动事件。

当每个游戏对象获悉这些事件之一时,游戏对象将首先需要确定click或mousemove事件是否与它们有关。 如果是这样,则游戏对象将需要确定鼠标坐标是否位于其自己的边界内。

广播策略

您的确切策略会因游戏类型而异。 例如,2D切片集可能具有与3D世界不同的策略。

以下步骤概述了一个简单的实施,该实施对于简单的2D应用程序可以很好地工作。

  1. 在“画布”区域中检测用户的鼠标单击的坐标。
  2. 通知所有游戏对象在给定的坐标集处发生了单击事件。
  3. 对于每个游戏对象,在鼠标坐标和游戏对象的边界框之间执行一次命中测试,以确定鼠标坐标是否与该对象碰撞。

简单广播示例

click事件处理程序可能类似于清单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坐标为120,150时单击x,y坐标50,60

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

图5.边界点击击中测试成功
屏幕显示单击x,y坐标120,150内的x,y坐标160,170

您可以为游戏对象定义一个类,如清单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. 任何与处理click事件有关的游戏对象都将设置侦听器以监听合成事件。

鼠标单击事件处理程序已修改为仅触发自定义事件。 可以给自定义事件指定任意名称。 在清单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!

        }
    });
}

高级命中测试

重要的是要考虑将多个游戏对象彼此叠加时会发生什么。 如果用户单击将多个游戏对象分层的点,则需要确定如何处理该行为。 例如,您通常希望仅触发最近的对象的事件处理程序,而忽略其下的其他对象。

要处理这种分层,您需要知道每个分层游戏对象的顺序或深度。 画布不会显示任何深度的逻辑表示,因此您再次需要掌权并产生必要的逻辑来处理这种情况。

为了引入深度的概念,必须为所有游戏对象分配一个z索引以表示其深度。 清单11显示了一个示例。

清单11.向Game对象添加一个z-index
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-index的游戏对象首先出现在列表中。

清单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.在三角路径的边界内单击
用鼠标指针单击20,50 x,y坐标的黑色三角形

填充的路径仅用于说明目的而可视化。 因为不一定要在屏幕上实际显示路径才能使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应用程序的用户输入提供了一个起点。


翻译自: https://www.ibm.com/developerworks/web/library/wa-games/index.html

基于canvas雷霆

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值