65.9K
CodeProject 正在变化。 阅读更多。
Home

“成为小偷”体验是如何创造的

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2013 年 4 月 5 日

CPOL

10分钟阅读

viewsIcon

19233

“成为小偷”体验是如何创造的

30 天开发一个 Windows 8 应用

引言

《神偷》 是由 Skybound Comics 出版的一个新的漫画系列,也是《行尸走肉》的出版商。 《神偷》讲述了世界上最伟大的神偷康拉德·保尔森的故事,他为了救赎开始从其他神偷那里偷窃,以纠正他生活中的错误。 《行尸走肉》电视剧的播出平台 AMC TV 目前正在开发一部基于《神偷》的电视剧。

IE10:化身神偷 是 Skybound 和 Internet Explorer 团队的合作项目,通过一套尖端的、跨浏览器的体验来推广已经备受好评的漫画系列,这些体验突出了现代 Web 的最佳特性。

我们通过 Internet Explorer 10 打造“神偷”体验的目标是提供一种身临其境的体验,这种体验堪比您通常在原生应用程序中找到的体验。我们通过使用最新的 HTML5、CSS3、触摸及相关技术来实现这一点。

充分利用 Internet Explorer 10 的硬件加速 SVG 处理和行业领先的触摸支持,我们将用户置于 Robert Kirkman 的《神偷》的漫画世界中,让他们可以跟随这个系列中艺术品盗窃主角的脚步。

完美契合触摸操作

Internet Explorer 在浏览器对指针事件的支持方面设定了新的标准,而我们充分利用了这一点来打造“神偷”体验。例如,用户可以通过多点触摸旋转一个表盘来练习他们的开保险箱技巧。Internet Explorer 对使用新提交的 Pointer Events API 的触摸控件的支持使这项工作变得异常轻松,使我们能够构建出色的基于触摸的旋转体验。

让我们来看一下(包括 CoffeeScript 和渲染后的 Javascript)

CoffeeScript

@wheel = $('#Dial')
@angle = 0
 
if window.MSGesture
    gesture = new MSGesture()
    gesture.target = @wheel[0]
    @wheel.bind("MSGestureChange", @didRotate).bind("MSPointerDown", @addGesturePointer)
else
    @wheel.mousedown(@startRotate)
    $(window).mousemove(@didRotate).mouseup(@stopRotate)
 
addGesturePointer:(e)=>
    #Handle Touch v. Mouse in IE10
    if e.originalEvent.pointerType != 2 #Not Touch Input
        e.target.msSetPointerCapture(e.originalEvent.pointerId)
        @startRotate(e.originalEvent)
        @gesture = false
        $(window).bind("MSPointerMove", @didRotate).bind("MSPointerUp", @stopRotate)
    else
    @gesture && @gesture.addPointer(e.originalEvent.pointerId)
 
startRotate: (e)=>
    document.onselectstart = (e)-> false
    @rotating = true
    currentPoint = screenPointToSvg(e.clientX, e.clientY, @wheel[0])
    angle = Math.atan2(currentPoint[1] - @center_y + 55, currentPoint[0] - @center_x + 90)
    angle = angle * LockpickGame.rad2degree
    @mouseStartAngle = angle
    @dialStartAngle = @angle
 
didRotate: (e)=>
    return unless @rotating
    if @gesture
        @angle += e.originalEvent.rotation * LockpickGame.rad2degree
    else
    if e.originalEvent.pointerType
        e = e.originalEvent
    currentPoint = screenPointToSvg(e.clientX, e.clientY, @wheel[0])
    angle = Math.atan2(currentPoint[1] - @center_y - 20, currentPoint[0] - @center_x + 0)
    angle = angle * LockpickGame.rad2degree
    @angle = @normalizeAngle(@dialStartAngle)+(@normalizeAngle(angle) - @normalizeAngle(@mouseStartAngle))
 
    @center_x = 374.3249938
    @center_y = 354.7909851
    rotate_transform = "rotate(#{@angle} #{@center_x} #{@center_y})"
    requestAnimationFrame(()=>
        @wheel.attr("transform", rotate_transform)
    )
 
stopRotate: (e)=>
    document.onselectstart = (e)-> true
    @rotating = false

JavaScript

var gesture,
    _this = this;
 
this.wheel = $('#Dial');
 
this.angle = 0;
 
if (window.MSGesture) {
    gesture = new MSGesture();
    gesture.target = this.wheel[0];
    this.wheel.bind("MSGestureChange", this.didRotate).bind("MSPointerDown", this.addGesturePointer);
} else {
    this.wheel.mousedown(this.startRotate);
    $(window).mousemove(this.didRotate).mouseup(this.stopRotate);
}
 
({
    addGesturePointer: function(e) {
        if (e.originalEvent.pointerType !== 2) {
            e.target.msSetPointerCapture(e.originalEvent.pointerId);
            _this.startRotate(e.originalEvent);
            _this.gesture = false;
            return $(window).bind("MSPointerMove", _this.didRotate).bind("MSPointerUp", _this.stopRotate);
        } else {
            return _this.gesture && _this.gesture.addPointer(e.originalEvent.pointerId);
        }
    },
    startRotate: function(e) {
        var angle, currentPoint;
        document.onselectstart = function(e) {
        return false;
        };
        _this.rotating = true;
        currentPoint = screenPointToSvg(e.clientX, e.clientY, _this.wheel[0]);
        angle = Math.atan2(currentPoint[1] - _this.center_y + 55, currentPoint[0] - _this.center_x + 90);
        angle = angle * LockpickGame.rad2degree;
        _this.mouseStartAngle = angle;
        return _this.dialStartAngle = _this.angle;
    },
    didRotate: function(e) {
        var angle, currentPoint, rotate_transform;
        if (!_this.rotating) {
            return;
        }
        if (_this.gesture) {
            _this.angle += e.originalEvent.rotation * LockpickGame.rad2degree;
        } else {
        if (e.originalEvent.pointerType) {
            e = e.originalEvent;
        }
        currentPoint = screenPointToSvg(e.clientX, e.clientY, _this.wheel[0]);
        angle = Math.atan2(currentPoint[1] - _this.center_y - 20, currentPoint[0] - _this.center_x + 0);
        angle = angle * LockpickGame.rad2degree;
        _this.angle = _this.normalizeAngle(_this.dialStartAngle) + (_this.normalizeAngle(angle) - _this.normalizeAngle(_this.mouseStartAngle));
    }
    _this.center_x = 374.3249938;
    _this.center_y = 354.7909851;
    rotate_transform = "rotate(" + _this.angle + " " + _this.center_x + " " + _this.center_y + ")";
    return requestAnimationFrame(function() {
        return _this.wheel.attr("transform", rotate_transform);
    });
    },
    stopRotate: function(e) {
        document.onselectstart = function(e) {
            return true;
    };
    return _this.rotating = false;
    }
});

本质上,我们将 MSGestureChange 事件绑定到我们的 didRotate 函数。如果玩家使用的是触摸而不是鼠标,我们只需要三行代码就可以计算并移动表盘,而不是传统鼠标指针所需的八行。IE 会处理其余的工作,包括惯性运动。Gesture API 同时返回角度和惯性值,使得所有功能集成变得轻而易举。

触摸渗透到几乎所有的体验中,从绘制玩家角色到扒窃,再到导航最终的“劫案”。结合 Internet Explorer 在 Windows 8 中的无边框、沉浸式、全屏模式,它能让用户更全面地投入到行动中,就像原生应用程序一样。

矢量图形的魅力

计算,特别是 Web,正在朝着一个分辨率无关的世界发展。您的网站或应用程序需要在从低分辨率到高分辨率的各种屏幕上看起来都很棒;矢量图形,尤其是 SVG,将成为 Web 开发者工具箱中的关键工具。

SVG 允许您直接在代码中表达图像,这使得图像可以无限缩放,而不会损失任何质量或保真度。并且通过 Internet Explorer 中的一项新功能,您可以对 SVG 应用滤镜,从而轻松地以编程方式添加复杂的特效,如阴影和模糊。

下面的代码是右侧星形图像的 SVG 代码。

元素绘制实际的星形,然后将其 filter 属性设置为

元素的 id,就可以使滤镜实际显示出来。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
    <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="227px" height="197px" viewBox="0 0 227 197" enable-background="new 0 0 227 197" xml:space="preserve">
    <defs> <!-- Placed just after your opening svg element -->
        <filter width="200%" height="200%" x="0" y="0" id="blurrydropshadow">
            <feOffset in="SourceAlpha" result="offOut" dy="10" dx="10"></feOffset>
            <feGaussianBlur in="offOut" result="blurOut" stdDeviation="7"></feGaussianBlur>
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend>
        </filter>
        <filter width="200%" height="200%" x="0" y="0" id="soliddropshadow">
            <feOffset in="SourceAlpha" result="offOut" dy="10" dx="10"></feOffset>
            <feBlend in="SourceGraphic" in2="offOut" mode="normal"></feBlend>
        </filter>
    </defs>
    <polygon fill="#4C5BA9" filter="url(#soliddropshadow)" stroke="#000000" stroke-width="4" stroke-miterlimit="10" points="113.161,24.486 135.907,70.576
186.77,77.966 149.965,113.843 158.654,164.5 113.161,140.583 67.667,164.5 76.356,113.843 39.552,77.966 90.414,70.576 "/>
</svg>

我们还发现,Internet Explorer 在对 HTML(包括顶级 SVG 元素)应用动画的速度和流畅性方面比我们现有的其他浏览器表现更好,这在很大程度上归功于 Internet Explorer 为所有 HTML 元素提供的硬件加速。

图像即代码

使用 SVG 的一大好处是它们易于移动、编辑和保存,因为它们本身就以文本形式表示,并且可以通过 JavaScript 动态修改。这在别名创建器中得到了很好的利用,在该工具中,我们为每个可能的角色以及所有可用的面部特征和配饰导入外部 SVG。然后我们将所有这些内容显示为一个可编辑的绘图表面,允许用户添加特征和配饰,以及使用他们的光标或手指直接在屏幕上绘制。

用户创建完他们的别名后,SVG 数据将使用 HTML5 的 localStorage API 保存。此时,用户的别名可以在网站的任何地方使用。别名只需从本地存储中提取并像任何其他图像一样使用(只是这个别名可以以任何尺寸使用),而无需进行任何图像编码等。另一个很棒的功能是,当用户想要编辑他们的别名时,同样也很容易:加载已创建的角色,加载编辑器,然后进行创建!

优化

在 Web 上使用 SVG 时,需要考虑的一个重要事项是,在您从矢量图形应用程序导出艺术作品后,可以对其进行大量优化。大多数程序(如 Adobe Illustrator 和 Inkscape)往往会在文件中留下许多不需要的元素,并留下非常复杂的路径,这些路径可以在不损失任何图像定义的情况下进行简化。该网站是我们早期一项大型 SVG 项目的实践之一,我们在摸索出一个合适的 SVG 工作流程时学到了很多。由于我们混合使用了内联 SVG 和作为 标签的 SVG,我们需要采取几种不同的优化方法。

通常,对于任何不需要内部动画的艺术作品,我们都会通过各种 SVG 清理软件(如 CleanSVGScour)进行处理。对于一些内联 SVG,我们需要采取更细致的方法,删除多余的组,并在矢量编辑器本身内进行路径简化,以确保我们不会丢失脚本所需的 ID 属性。

前端技术

正如我们在上面提到的,这个项目的大部分是用 CoffeeScript 和普通 JavaScript 的组合编写的。

正如您在前面的代码片段中所看到的,CoffeeScript 允许我们以更简洁的方式编写 JavaScript,但在它能够在您的浏览器中运行时,所有内容都会编译成等效的 JavaScript。

我们将 CoffeeScript 作为一项新兴 Web 技术实验,它能够与基于 Web 标准的开发很好地结合,因为归根结底(引用 CoffeeScript 文档的话来说),它就是 JavaScript。

在 CSS 方面,我们也尝试了 Less。与 CoffeeScript 类似,Less 是一种样式表预处理器,它使我们能够构建更灵活、更易于维护的 CSS。它添加的变量、混合器和操作证明非常有价值。

 

技巧集

在“劫案”中,神偷必须在不被发现的情况下通过关卡。在后面的关卡中,有巡逻的守卫需要神偷(通过在关卡地图上的任意位置双击)来分散注意力,以便玩家能够安全地离开关卡。我们需要一个算法来为守卫绘制一条绕过关卡的路径,以便他们能够跟随并调查神偷触发的干扰事件。

A* (A Star) – 寻路算法

A*,也写作 A*,是当今图中最常见的寻路算法。在某些情况下,A* 保证只要路径存在,它总能找到两点之间的最短路径,前提是没有障碍物使得从点 A 移动到点 B 不可能。它常用于游戏、机器人等领域。

这是维基百科上的 A* 条目,其中包含其伪代码:http://en.wikipedia.org/wiki/A_star

为什么选择 A*?

在这种情况下,使用 A* 非常简单,因为地图已经是网格状的,网格只是图的一种类型,而且地图不包含电梯、传送门等特殊对象,这些对象可能会破坏 A*。为了处理这些类型的对象,我们就必须使用另一种算法,比如 Dijkstra 算法。

实现中的挑战

在使用 A* 算法为我们的人工智能保安创建路径时,我们在 JavaScript 对数据结构的支持方面遇到了一些挑战。A* 实现通常需要几种 JavaScript 中不存在的数据结构。例如,通常会将它的内部列表表示为哈希表,虽然 JS 有对象形式的字典,但它们只能使用字符串作为键,因此我们需要确保每次操作它们时都对我们的对象进行哈希处理。

另一个问题是需要一个优先队列。为了提高效率,A* 需要一个可能访问的节点队列,该队列按“最佳”候选节点排序。这个概念由优先队列表示,它通常实现为二叉堆。JS 中不存在这种数据结构,因此我们根据《算法导论》一书中的经典伪代码对其进行了实现。这本书是这类主题的圣经。

仅 200 行代码

我们为“神偷”实现的 JavaScript A* 算法大约有 200 行代码,包括注释!我们邀请有兴趣了解更多关于寻路和人工智能的人参考 astar.js 并阅读上面提到的维基百科文章。

* A* pathfinding algorithm implementation based on the the pseudo-code
* from the Wikipedia article (http://en.wikipedia.org/wiki/A_star)
* Date: 02/05/2013
*/
 
"use strict";
 
// Node of the search graph
function GraphNode(x, y) {
    this.x = x;
    this.y = y;
    this.hash = x + "" + y;
    this.f = Number.POSITIVE_INFINITY;
}
 
// Constructor for the pathfinder. It takes a grid which is an array of arrays
// indicating whether a position is passable (0) or it's blocked (1)
function AStar(grid) {
    // Compares to nodes by their f value
    function nodeComparer(left, right) {
        if (left.f > right.f) {
            return 1;
        }
 
        if (left.f < right.f) {
            return -1;
        }
 
        return 0;
    }
 
    // Manhattan heuristic for estimating the cost from a node to the goal
    function manhattan(node, goal) {
    return Math.abs(node.x - goal.x) + Math.abs(node.y - goal.y);
}
 
// Gets all the valid neighbour nodes of a given node (diagonals are not
// neighbours)
function getNeighbours(node) {
    var neighbours = [];
 
    for (var i = -1; i < 2; i++) {
        for (var j = -1; j < 2; j++) {
            // Ignore diagonals
            if (Math.abs(i) === Math.abs(j)) {
                continue;
            }
 
            // Ignore positions outside the grid
            if (node.x + i < 0 || node.y + j < 0 ||
                node.x + i >= grid[0].length || node.y + j >= grid.length) {
                continue;
            }
 
            if (grid[node.y + j][node.x + i] === 0) {
                neighbours.push(new GraphNode(node.x + i, node.y + j));
            }
        }
    }
 
    return neighbours;
}
 
// Builds the path needed to reach a target node from the pathfinding information
function calculatePath(parents, target) {
    var path = [];
 
    var node = target;
    while (typeof node !== "undefined") {
        path.push([node.x, node.y]);
        node = parents[node.hash];
    }
 
    return path.reverse();
}
 
// Searches for a path between two positions in a grid
this.search = function(start, end) {
    var fCosts = new BinaryHeap([], nodeComparer);
    var gCosts = {};
    var colors = {};
    var parents = {};
 
    // Initialization
    var node = new GraphNode(start[0], start[1]);
    var endNode = new GraphNode(end[0], end[1]);
    node.f = manhattan(node, endNode);
 
    fCosts.push(node);
    gCosts[node.hash] = 0;
 
    while (!fCosts.isEmpty) {
        var current = fCosts.pop();
 
        // Have we reached our goal?
        if (current.x === endNode.x && current.y === endNode.y)
        {
            return calculatePath(parents, endNode);
        }
 
        // Mark the node as visited (Black), and get check it's neighbours
        colors[current.hash] = "Black";
 
        var neighbours = getNeighbours(current);
        for (var i = 0; i < neighbours.length; i++)
        {
            var neighbour = neighbours[i];
 
            if (colors[neighbour.hash] === "Black") {
                continue;
        }
 
        // If we had not visited the neighbour before, or we have found a faster way to visit the neighbour, then update g and calculate f
        if (typeof gCosts[neighbour.hash] !== "Undefined" || gCosts[current.hash] + 1 < gCosts[neighbour.hash]) {
            parents[neighbour.hash] = current;
            gCosts[neighbour.hash] = gCosts[current.hash] + 1;
            neighbour.f = gCosts[neighbour.hash] + manhattan(neighbour, endNode);
 
            // Neighbour not visited before, mark it as a potential candidate ("Gray")
            if (typeof colors[neighbour.hash] !== "Undefined") {
                colors[neighbour.hash] = "Gray";
                fCosts.push(neighbour);
            }
            else { // We have found a better way to reach this node
                fCosts.decreaseItem(neighbour);
            }
        }
    }
}
 
return [];
};
}

Internet Explorer 10 中的新功能

Internet Explorer 10 充满了新的 API,它们提供了更接近原生应用的体验。下面是我们在此体验中使用的 API 列表以及它们如何使我们受益。

  • 指针事件 (Pointer Events) – 体验中的所有交互都使用 Pointer 对象来检测用户输入,处理鼠标、笔和触摸事件。
  • 手势 API (Gesture API) - 基于 Pointer API,Gesture API 用于处理站点中更高级的指针交互,例如用两根手指旋转保险箱表盘,以及在“劫案”中绘制路径。
  • CSS3 动画 (CSS3 Animations) - CSS3 动画用于站点中一些较大的过渡动画,包括页面之间以及幕后部分之间的过渡。这使得浏览器能够处理动画计算,而不是在 JavaScript 中进行编程。
  • requestAnimationFrame - requestAnimationFrame 允许我们在浏览器认为最合适的时间获取动画帧,而不是固定的帧率,如果浏览器变得卡顿,可能会完全丢帧。整个体验都使用它来提供尽可能流畅的动画。
  • 页面可见性 API (pageVisibility API) - 使用页面可见性 API,我们可以检测站点何时不再是当前可见的浏览器标签页。这使我们能够管理不需要在用户不主动使用站点时运行的应用程序部分。我们主要使用此功能在站点不可见时将其音频静音。
  • SVG 滤镜 (SVG Filters) - SVG 滤镜用于在站点的一些动态创建的部分上创建效果。这些主要用于为代码创建的项目(如炸弹游戏中绘制的电线)添加与站点整体风格相匹配的阴影。
  • setImmediate API - setImmediate API 在网站的各个地方使用,以提高网站性能和功耗。它作为计时器使用,就像 setInterval 或 setTimeout 一样,并且具有一个额外的好处,即在 CPU 准备好处理时立即被调用。这平衡了速度和功耗。它被用于保存个人资料数据或其他需要尽快完成的项目,以保持用户体验的流畅。

本文是 Internet Explorer 团队 HTML5 技术系列的一部分。通过 @ http://modern.IE 免费获得 3 个月的 BrowserStack 跨浏览器测试,亲身体验本文中的概念。

© . All rights reserved.