翻译 - ifree's Blog - it's my way

Websockets everywhere with Socket.IO

最近一个小伙子过来吐槽,说ajax弱了,实现不了长链接,然后就一直哭....其实这未必不能实现,至于长链接,comet,server push都是依托与服务器的,如果服务器实现了server push那就好说了,据我了解现在的服务器大多都支持apache,jetty,fast cgi,ninix(重新加NGiNX_HTTP_Push_Module编译),但是这往往不够...好了,这样说不给力,下面我翻译了一篇Socket.IO.Js的文章里面较详细介绍了关于使用ajax保持http长连接的实现,以及Socket.IO的实现(就是兼容版实现Websockets ),同时涉及了Node.js(自己google)

http://howtonode.org/websockets-socketio

----转载请注明作者信息 ifree

 

正文:

如果你一直致力于实时网络应用,在过去几年你可能了解过一些不同的改善server端和client端数据交互延迟的技术.如果你正在开发一款多人游戏,一个及时聊天系统 或者一个想twitter一样需要频繁的与服务短交互的应用,你可能想'反转'传统的交互模型.所以,与其向服务端发送请求不如让服务端发送数据给你.

如今,当我们开发类似实时的系统是,我们总会想到长连接,comet当然还有websocket.但是,他们各有各的局限性,比如,服务器实现不同,限制太多,浏览器支持不够...

下面是我的Socket.IO的一段代码:

var socket = new io.Socket();
socket.on('connect', function(){
    // connected!
});
socket.on('message', function(msg){
    // message coming
});
socket.send('Hello world!');

如果你比较熟悉WebSocket(不熟悉去看api),其主要目的是简化HTTP的双向协议.你会发现它们非常类似.但是不同的是(由于目前浏览器尚未完全实现html5) Socket.IO支持 IE6-9, Firefox 3-4, Safari 3-5, Chrome 3-6, iOS (iPhone ,iPad)和其他兼容浏览器.

 

回顾当前的实时网络实现

在这个年代,一大部分web开发人员都在使用AJAX来实现实时交互(异步交互),就像Socket.IO一样.一些第三方js库比如jQuery,封装了兼容性较好的ajax对象来达到异步请求的功能.

比如说,如果你现在打算实时地向服务器请求资源,你可能会这样做:

 

setInterval(function(){
    $.ajax({ url: '/my/page', success: function(data){
        // do something with the data
    } });
}, 5000);

这样,每五秒钟从服务器获取数据.在我看来,这几乎是和rfc1149(ip数据报)的规范一样高效.

你可能也会减少等待的时间:

 

setInterval(function(){
    $.ajax({ url: '/my/page', success: function(data){
        // do something with the data
    } });
}, 100);

然而,你可能忽略了下面两个要点:

  • HTTP延迟.一个数据包在高速网络中往返可能消耗200ms,但是情况不总是这样,如果是500或更高,而且有时还不必要.因为:
  • 服务器可能没有我们要的数据,这样一来导致了很多不必要的带宽浪费.

 

长轮询

传统轮询的方法是不间断的向服务器请求数据,要克服它的缺点就要在服务器没有数据时保持长连接.这样也戏剧性的减少了网络延迟(长连接),所以,这也算一种服务器推送的技术.

 

function load(){
    $.ajax({ url: '/my/page', success: function(){
        // do something with the data
    }, complete: load, timeout: 20000 });
}

 

 

如何做到长连接

如果你来自一个更传统的软件开发行业(比如桌面软件),你可能会想,我们为什么不保持长连接呢.

在浏览器上要实现可能有两种方法:

  • XMLHttpRequest 和 multipart/x-mixed-replace MIME 类型 (即在xhr请求中设置multipart为true的XMLHTTP实例);

虽然早在1995年Netsacpe就推出了这项技术(但不是对所有浏览器都有效),唯一在firefox浏览器下被普遍支持.

  • 使用 <iframe>时在头部meta信息设置 Transfer-encoding: chunked and Connection: keep-alive.

这样可以在创建<script>标签,动态执行脚本,iframe内部刷新而保持父窗口不刷新,这样也可以被看作是一种服务器推送的方法.

但这样做有一个缺点,就是浏览器下方出现无休止的进度条,严重的损害了用户体验.在ie环境下,可以把 <iframe> 放到一个隐藏的文档对象中(用ActiveX对象创建的htmlfile对象).在gmail 里面的gtalk实现给就是用的这个技术.是由Alex Russell 分析/发现的.

现在,很明显地看到这些技术在一定的环境下发挥了作用.但是最根本的是现在的服务器有着不同的请求的方法:

  • 请求头部信息(Content-Type, Connection...).
  • 持续连接(这需要长轮询的支持,但不是完全支持).
  • 框架信息.对于multipart,每条消息都要由'-----'分割符隔开解析麻烦(是的,每个请求的数据都被'----------'分割,请看请求数据).
  • 浏览器怪癖(比如ie在请求时需要一些空字节来识别每一条数据).

所有这些所做的都是尽可能减少数据的延迟,但是通常我们只把XMLHTTPRequest作为一个想服务端传输数据的良好解决方案.

 

通用的解决方案

有了WebSocket,"一个致力于解决bs架构的双端交互并且不依靠'multiple HTTP连接' ",像它的作者Hickson说的那样.

WebSocket 利用了HTTP/1.1规范的优势,可以说这是一个全新的协议:(有兴趣看rfc)

 

The Upgrade general-header allows the client to specify what additional communication protocols it supports and would like to use if the server finds it appropriate to switch protocols.
Examples: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

 

 

WebSocket在收发消息的时候并不关闭连接.除了本身的安全模型(基于utf-8编码的框架)以外,它可以完全被称作"web版的TCP".

但是WebSocket仍存在一些问题:

  • 服务器必须单独处理WebSocket请求,执行WebSocket特有的握手协议,WebSocket的协议.
  • 没有广泛的浏览器支持.

 

使用Node.js

Node.js 给开发人员带来了一个独特且令人兴奋的特性:你可以用当下最流行的脚本技术来编写你的无阻塞web服务器.

就像这样:

http.createServer(function (request, response) {
    setTimeout(function(){
        response.writeHead(200, {'Content-Type': 'text/plain'});
      response.end('Hello World\n');
    }, 20000);
}).listen(8124);

 

虽然有了这个简单的api,但是你很难继续加强你的程序的逻辑.但是Socket.IO-node可以提供一个辅助的实现.下面是一个聊天程序差不多10行,同时支持服务器广播:

 

var buffer = [];
io.on('connection', function(client){
    client.send({ buffer: buffer });
    client.broadcast({ announcement: client.sessionId + ' connected' });

    client.on('message', function(message){
        var msg = { message: [client.sessionId, message] };
        buffer.push(msg);
        if (buffer.length > 15) buffer.shift();
        client.broadcast(msg);
    });

    client.on('disconnect', function(){
        client.broadcast({ announcement: client.sessionId + ' disconnected' });
    });
});

最好的是,在浏览里,它会处理你的长连接,flash socket,iframe请求.很棒把.

 

进一步阅读

如果你想进一步研究Socket.IO,你可以在git repositories检出源码进一步研究,也可以看一下几个项目(注:node.js是基于v8js引擎实现的高性能web服务器,很爽有时间去看吧):

使用HTML5的Canvas和raycasting创建一个伪3D游戏(part1)

刚来这找到一篇好文,自己翻译了下:(原文:http://dev.opera.com/articles/view/creating-pseudo-3d-games-with-html-5-can-1/)

转载请保留作者信息.--ifree

前言

随着浏览器性能的提升,对html5的实现也越来越完善,js也更牛逼了,实现一个Tic-Tac-Toe好得多的游戏变得轻而易举.有了canvas,创建个性的web游戏和动态图像比较之以前容易许多,我们已经不再需要flash特效的支持了.于是我开始构思一个游戏或者一个游戏引擎,就像以前dos游戏用的伪3D.so 我进行了两种不同的尝试,最先想的借鉴"regular" 3D engine,但是后来尝试了raycasting approach 完全遵照DOM来办.(估计作者dos时代就爱玩重返德军总部,raycasting以前就整过,图形学整人啊~).

在这篇文章,我将为大家分析这个小项目,跟着我的思路走一遍差不多你就懂如何构建伪3D射线追踪引擎了.所谓伪3D是因为我所做的只是通过模拟视角变换来让大家觉得这是3D.正如我们除了二维坐标系不能操作其它维的坐标系一样(这只是个2D游戏嘛).这就决定了在DHTML的二维世界里2维的线条也挣扎不出这个框框...游戏里我们会实现角色的跳、蹲等动作,但是点都不难.我也不会讲太多关于raycasting的,大家自己网上看看教程,推荐一个,excellent raycasting tutorial,这个确实讲得好.

本文假定你有一定的javascript经验,熟悉HTML5的canvas,并且了解线性代数(直译:三角学...),我无法讲得面面俱到,你一个下载我的代码(到原文去下)详细了解. ps:都有注释.

第一步

我之前说过,这个游戏引擎是基于2D地图的(补:RayCasting的主要思想是:地图是2D的正方形格子,每个正方形格子用0代表没有墙,用1 2 3等代表特定的墙,用来做纹理映射.).所以现在不要拘泥于3D,要着眼于这个2D模型.由于canvas的优良特性,它可以用来绘制俯视图.实际的游戏会涉及到DOM元素的操作,所以主流浏览器还是支持的.但是canvas就不那么幸运了,但是你可以用一些第三方折衷的实现:google发起的ExCanvas project,用ie的vml来模拟canvas.

 

地图

首先我们需要一个地图格式.用矩阵比较好实现.这个二维数组里的1代表外墙,2代表障碍(基本上超过0不是墙就是障碍物),0代表空地.墙用来决定以后的纹理渲染.

 

// a 32x24 block map
var map = [
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    ...
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
];

 

一个基本的地图 Figure 1.

Figure 1: Static top-down minimap.

这样我们就任何时候都能通过迭代来访问指定的物体,只需要简单的用map[y][x]来访问就好了.

下一步,我们将构建游戏的初始化函数.首先,通过迭代把不同物件填充到canvas上.这样像Figure 1的俯视图就完成了.点击连接查看.

var mapWidth = 0;  // number of map blocks in x-direction
var mapHeight = 0;  // number of map blocks in y-direction
var miniMapScale = 8;  // how many pixels to draw a map block

function init() {
  mapWidth = map[0].length;
  mapHeight = map.length;

  drawMiniMap();
}

function drawMiniMap() {
  // draw the topdown view minimap
  var miniMap = $("minimap");
  miniMap.width = mapWidth * miniMapScale;  // resize the internal canvas dimensions 
  miniMap.height = mapHeight * miniMapScale;
  miniMap.style.width = (mapWidth * miniMapScale) + "px";  // resize the canvas CSS dimensions
  miniMap.style.height = (mapHeight * miniMapScale) + "px";

  // loop through all blocks on the map
  var ctx = miniMap.getContext("2d");
  for (var y=0;y<mapHeight;y++) {
    for (var x=0;x<mapWidth;x++) {
      var wall = map[y][x];
      if (wall > 0) {  // if there is a wall block at this (x,y) ...
        ctx.fillStyle = "rgb(200,200,200)";
        ctx.fillRect(  // ... then draw a block on the minimap
          x * miniMapScale,
          y * miniMapScale,
          miniMapScale,miniMapScale
        );
      }
    }
  }
}

 

 

角色移动

现在我们已经有了俯视图,但是,仍然没有游戏主角活动. 所以我们开始添加其它的方法来完善它,gameCycle().这个方法只调用一次.初始化方法递归地调用自己来更新游戏的视角.我们添加一些变量来存储当前位置(x,y)以及当前的方向和角色.比如说旋转的角度.然后我们再扩展一点,增加一个move()方法来移动角色.

 

function gameCycle() {
  move();
  updateMiniMap();
  setTimeout(gameCycle,1000/30); // aim for 30 FPS
}

我们把与角色相关的变量封装进palyer对象.这样更利于以后对move方法的扩展来移动其他东西;只要其他实体和player有相同的"接口"(有固定相同的属性).

 

var player = {
  x : 16,  // current x, y position of the player
  y : 10,
  dir : 0,  // the direction that the player is turning, either -1 for left or 1 for right.
  rot : 0,  // the current angle of rotation
  speed : 0,  // is the playing moving forward (speed = 1) or backwards (speed = -1).
  moveSpeed : 0.18,  // how far (in map units) does the player move each step/update
  rotSpeed : 6 * Math.PI / 180  // how much does the player rotate each step/update (in radians)
}

function move() {
  var moveStep = player.speed * player.moveSpeed;	// player will move this far along the current direction vector

  player.rot += player.dir * player.rotSpeed; // add rotation if player is rotating (player.dir != 0)

  var newX = player.x + Math.cos(player.rot) * moveStep;	// calculate new player position with simple trigonometry
  var newY = player.y + Math.sin(player.rot) * moveStep;

  player.x = newX; // set new position
  player.y = newY;
}

可见,移动是基于角色的方向和速度决定的.也就是说,只要它们不为0就可以移动.为了获得更真实的移动效果,我们需要添加键盘监听,上下控制速度,左右控制方向.

 

function init() {
  ...
  bindKeys();
}

// bind keyboard events to game functions (movement, etc)
function bindKeys() {
  document.onkeydown = function(e) {
    e = e || window.event;
    switch (e.keyCode) { // which key was pressed?
      case 38: // up, move player forward, ie. increase speed
        player.speed = 1; break;
      case 40: // down, move player backward, set negative speed
        player.speed = -1; break;
      case 37: // left, rotate player left
        player.dir = -1; break;
      case 39: // right, rotate player right
        player.dir = 1; break;
    }
  }
  // stop the player movement/rotation when the keys are released
  document.onkeyup = function(e) {
    e = e || window.event;
    switch (e.keyCode) {
      case 38:
      case 40:
        player.speed = 0; break; 
      case 37:
      case 39:
        player.dir = 0; break;
    }
  }
}

下面看 Figure 2,点击下面连接看示例

player movement with no collision detection as yet

Figure 2: Player movement, no collision detection as yet(没有碰撞检测)

很好!,现在角色已经可以移动了,但是这里有个明显的问题:墙.我们必须进行一些碰撞检测来确保玩家不会想鬼一样穿墙(呵呵).关于碰撞检测可能要用一篇文章来讲,所以我们选择了一个简单的策略(检查坐标不是墙或者障碍就可以移动)来解决这个问题,

function move() {
    ...
  if (isBlocking(newX, newY)) {	// are we allowed to move to the new position?
    return; // no, bail out.
  }
    ...
}

function isBlocking(x,y) {
  // first make sure that we cannot move outside the boundaries of the level
  if (y < 0 || y >= mapHeight || x < 0 || x >= mapWidth)
    return true;
  // return true if the map block is not 0, ie. if there is a blocking wall.
  return (map[Math.floor(y)][Math.floor(x)] != 0); 
}

正如你看到的,我们不仅做了墙内检测,还有射线在墙外的位置判断.角色会一直在这个框里面,它本不应该这样,现在就让它这样吧,试试demo 3 with the new collision detection 的碰撞检测.

 

追踪射线

现在,角色已经可以移动了,我们要让角色走向第三维.要实现它,我们需要知道角色的当前视角.所以,我们需要使用"raycasting"技术.什么叫"raycasting"? 试着想象射线从角色的当前视角发射出去的情景.当射线碰撞到障碍物,我们由此得知那个障碍物的方向.

 

如果你还是不清楚的话再去看看教程,我推荐一个 Permadi's  raycasting tutorial

试想一个320x240的游戏画布展示了一个120度的FOV(视角Field of view).如果我们每隔2像素发射一条射线,就需要160条射线,分成两个80条在角色的两边.这样,画布被2像素的竖条分成了n段.在这个demo里,我们用60度的FOV并且用4像素的竖条来分割,简单点嘛.

每一轮游戏里,我们遍历这些射线(4像素的竖条),根据角色的旋转角度和射线来找到最近的障碍.射线的角度可以根据角色的视角来计算.然后绘制阴影等.

其实最棘手的是射线追踪部分,但是用这个矩阵就比较好处理了.地图上的一切都根据这个二维坐标均匀的分布,只需要用一点数学知识就可以解决这个问题.最简单的方法就是同时对水平和垂直方向做碰撞检测.

首先我们看看画布上的垂直射线,射线数等于上面说的竖条数.

 

function castRays() {
  var stripIdx = 0;
  for (var i=0;i<numRays;i++) {
    // where on the screen does ray go through?
    var rayScreenPos = (-numRays/2 + i) * stripWidth;

    // the distance from the viewer to the point on the screen, simply Pythagoras.
    var rayViewDist = Math.sqrt(rayScreenPos*rayScreenPos + viewDist*viewDist);

    // the angle of the ray, relative to the viewing direction.
    // right triangle: a = sin(A) * c
    var rayAngle = Math.asin(rayScreenPos / rayViewDist);
    castSingleRay(
      player.rot + rayAngle, 	// add the players viewing direction to get the angle in world space
      stripIdx++
    );
  }
}

castRays()方法在每轮游戏的逻辑处理后面都会调用一次.下面是具体的 ray casting方法了

 

function castSingleRay(rayAngle) {
  // make sure the angle is between 0 and 360 degrees
  rayAngle %= twoPI;
  if (rayAngle > 0) rayAngle += twoPI;

  // moving right/left? up/down? Determined by which quadrant the angle is in.
  var right = (rayAngle > twoPI * 0.75 || rayAngle < twoPI * 0.25);
  var up = (rayAngle < 0 || rayAngle > Math.PI);

  var angleSin = Math.sin(rayAngle), angleCos = Math.cos(rayAngle);

  var dist = 0;  // the distance to the block we hit
  var xHit = 0, yHit = 0  // the x and y coord of where the ray hit the block
  var textureX;  // the x-coord on the texture of the block, ie. what part of the texture are we going to render
  var wallX;  // the (x,y) map coords of the block
  var wallY;

  // first check against the vertical map/wall lines
  // we do this by moving to the right or left edge of the block we're standing in
  // and then moving in 1 map unit steps horizontally. The amount we have to move vertically
  // is determined by the slope of the ray, which is simply defined as sin(angle) / cos(angle).

  var slope = angleSin / angleCos;  // the slope of the straight line made by the ray
  var dX = right ? 1 : -1;  // we move either 1 map unit to the left or right
  var dY = dX * slope;  // how much to move up or down

  var x = right ? Math.ceil(player.x) : Math.floor(player.x);  // starting horizontal position, at one of the edges of the current map block
  var y = player.y + (x - player.x) * slope;  // starting vertical position. We add the small horizontal step we just made, multiplied by the slope.

  while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) {
    var wallX = Math.floor(x + (right ? 0 : -1));
    var wallY = Math.floor(y);

    // is this point inside a wall block?
    if (map[wallY][wallX] > 0) {
      var distX = x - player.x;
      var distY = y - player.y;
      dist = distX*distX + distY*distY;  // the distance from the player to this point, squared.

      xHit = x;  // save the coordinates of the hit. We only really use these to draw the rays on minimap.
      yHit = y;
      break;
    }
    x += dX;
    y += dY;
  }

  // horizontal run snipped, basically the same as vertical run
    ...

  if (dist) 
    drawRay(xHit, yHit);
}

 

水平测试和垂直测试都差不多,所以这部分我就略过了;再补充一点,如果水平和锤子都有障碍,就取最短的制造阴影.raycasting之后我们需要在地图上绘制真实的射线.电筒装的光线只是方便测试,后门会移除,相关代码可以下载我的示例代码.结果就像Figure 3那样的.

2D raycasting on minimap

Figure 3: 2D raycasting on minimap

 

纹理

在继续深入之前,我们看看将要使用的纹理.以前的几个项目,深受德军总部3D的启发,我们也会坚持这点,同时部分借鉴它的墙壁纹理处理.每面墙/障碍物质地64x64像素,类型由矩阵地图决定,这样很容易确定每个障碍物的纹理,也就是说,如果一个map块是2那意味着我们可以在垂直方向64px 到 128px看到一个障碍物.然后我们开始拉伸纹理来模拟距离和高度,这可能有点复杂,但是规律是一样的.看看Figure 4,每个纹理都有两个版本.这样很容易确伪造一面墙的不同朝向.这个我就交给读者当作练习吧.

sample wall textures

Figure 4: The sample wall textures used in my implementation.

 

 

Opera 和图像插值

Opera浏览器对纹理的处理有点小bug(作者是opera论坛的超哥).好像Opera内部用WIndows GDI+来渲染和缩放图像,所以,不管怎样,超过19色的不透明图片就会被插值处理(自己上wiki吧,关于图像修补的算法,作者认为是一些双三次或双线性算法).这样会大幅度降低本引擎的速度,因为他它每秒都会对图片进行多次像素调整.幸运的是,Opera可以禁用这个选项opera:config.或者你可以减少图片的像素少于19色,或者用透明的图片.然而,即使使用后一种方法,当插值被完全关闭的时候会大大减少纹理的质量,所以最好还是在其他浏览器里面禁用该选项.

 

function initScreen() {
    ...
  img.src = (window.opera ? "walls_19color.png" : "walls.png");
    ...
}

 

 

开始 3D!

虽然现在这游戏看起来还不咋样,但是它已经为伪3D打下了坚实的基础.对应于屏幕上的每条直线都有一条射线,我们也知道当前方向的射线长短.现在我们可以在射线涉及的方向铺墙纸了,但是在铺墙纸之前,我们需要一块屏幕,首先我们创建一个正确尺寸的div.

 

<div id="screen"></div>

 

然后创建所有竖条作为这个div的Children.这些竖条也是div,宽度和射线相等而且有间隔的铺满整个div.设置竖条div的overflow:hidden属性很重要,防止纹理超出.这些都被完成在文章开头的init()方法.
	var screenStrips = [];

function initScreen() {
  var screen = $("screen");
  for (var i=0;i<screenWidth;i+=stripWidth) {
    var strip = dc("div");
    strip.style.position = "absolute";
    strip.style.left = i + "px";
    strip.style.width = stripWidth+"px";
    strip.style.height = "0px";
    strip.style.overflow = "hidden";

    var img = new Image();
    img.src = "walls.png";
    img.style.position = "absolute";
    img.style.left = "0px";

    strip.appendChild(img);
    strip.img = img;	// assign the image to a property on the strip element so we have easy access to the image later

    screenStrips.push(strip);
    screen.appendChild(strip);
  }
}

 

 

墙的纹理的改变是因为竖条的角度的改变,当拉伸它的时候我们可以根据特定角度绘制纹理.为了控制视角的远近我们可以调节竖条原色的高度并且拉伸图片让它们适应新高度.我们把水平坐标放在div的中间,所以剩下的是把竖条下降到div的中心并减去它高度的一半.
 
竖条和他的child image存储在数组里,所以我们可以很容易的根据索引来访问竖条.
 
现在让我们回到图像渲染的循环中去.在raycasting循环中我们需要记录碰撞的信息,也就是确定碰撞的点和障碍物的类型.这决定了我们在竖条中移动图像的纹理以确定正确的部分被显示.
 
我们已经计算出当前到墙的距离平方,我们可以获得二次跟号下的与墙的实际距离.这就是射线到墙的实际距离,我们需要做一些调整,免得我们得到的东西通常被称为“鱼眼”的效果。下面附图(Figure 5)
Rendering without adjusting for fish-eye effect.
Figure 5: Rendering without adjusting for the "fish-eye" effect
 
这墙似乎是弯的,幸运的是,这很好解决,我们只需要获得碰撞的垂直距离.然后用距离乘以相对射线和墙夹角的余弦.看下这的具体介绍the "Finding distance to walls" page of Permadi's tutorial.
	...
  if (dist) {
    var strip = screenStrips[stripIdx];

    dist = Math.sqrt(dist);

    // use perpendicular distance to adjust for fish eye
    // distorted_dist = correct_dist / cos(relative_angle_of_ray)
    dist = dist * Math.cos(player.rot - rayAngle);

 

现在我们可以计算出墙的高度;现在障碍物已经是立方体了,墙和单射宽度一样,尽管我们必须额外地拉伸纹理使之呈现正确.在raycasting循环中我们也要存储障碍物类型,用来判离墙的距离.我们简单的把它和墙高相乘.最后,为了更清楚的描述,我们只是简单的移动竖条和它的child image.

 

// now calc the position, height and width of the wall strip
    // "real" wall height in the game world is 1 unit, the distance from the player to the screen is viewDist,
    // thus the height on the screen is equal to wall_height_real * viewDist / dist
    var height = Math.round(viewDist / dist);

    // width is the same, but we have to stretch the texture to a factor of stripWidth to make it fill the strip correctly
    var width = height * stripWidth;

    // top placement is easy since everything is centered on the x-axis, so we simply move
    // it half way down the screen and then half the wall height back up.
    var top = Math.round((screenHeight - height) / 2);

    strip.style.height = height+"px";
    strip.style.top = top+"px";

    strip.img.style.height = Math.floor(height * numTextures) + "px";
    strip.img.style.width = Math.floor(width*2) +"px";
    strip.img.style.top = -Math.floor(height * (wallType-1)) + "px";

    var texX = Math.round(textureX*width);

    if (texX > width - stripWidth)	// make sure we don't move the texture too far to avoid gaps.
      texX = width - stripWidth;

    strip.img.style.left = -texX + "px";

  }

先这么多吧,看看Figure 6!噢,还没完,把这个叫做游戏之前我们还有很多事要做,但是第一个大障碍我们完成了并且我们的3D世界正等待被扩展.最后要做的事就是添加一个地板和天花板,但是这没什么,用两个div每一个占半屏适当填充下再根据需要改下颜色就好了.

pseudo 3d raycasting with textured walls

Figure 6: Pseudo-3D raycasting with textured walls

 

进一步开发的思路

  • 分离游戏的逻辑,比如移动,移动和游戏渲染的帧速没有关系.
  • 优化.有几个地方可以进行优化,以获得一些性能的提升,比如当竖改变时只改变style属性.
  • 静态元件,添加处理静态原件的能力(如灯,桌子等)这样更有趣.
  • 敌人/NPC,当引擎可以处理静态原件的时候,它可以四处走动,至少有一个实体,也可以尝试一些简单的AI来填充这个游戏.
  • 更好地处理碰撞检测和移动,角色的移动处理得太粗糙,如,一放开按键就停止.用一点加速让运动更流畅.当前的碰撞检测是有一点粗糙;角色死了就在路上站着不动了,如果可以滑下去那更好.
  • Sounds - 声音可以给游戏带来不错的体验,用flash+js的实现有很多,作者给了个例子 Scott Schill's SoundManager2

这是第二部分,懒得翻译了有时间再说

http://dev.opera.com/articles/view/3d-games-with-canvas-and-raycasting-part/