【转】265行代码实现第一人称游戏引擎

原文:html5gamedev.org/?p=2383

总在有个自以为很先进的理念后不久就在网上找到别人的实现!

今天,让我们进入一个可以伸手触摸的世界吧。在这篇文章里,我们将从零开始快速完成一次第一人称探索。本文没有涉及复杂的数学计算,只用到了光线投射技术。你可能已经见识过这种技术了,比如《上古卷轴2 : 匕首雨》、《毁灭公爵3D》还有 Notch Persson 最近在 ludum dare 上的参赛作品。Notch 认为它够好,我就认为它够好!

用了光线投射就像开挂一样,作为一名懒得出油的程序员,我表示非常喜欢。你可以舒畅地浸入到3D环境中而不受“真3D”复杂性的束缚。举例来说,光线投射算法消耗线性时间,所以不用优化也可以加载一个巨大的世界,它执行的速度跟小型世界一样快。水平面被定义成简单的网格而不是多边形网面树,所以即使没有 3D 建模基础或数学博士学位也可以直接投入进去学习。

利用这些技巧很容易就可以做一些让人嗨爆的事情。15分钟之后,你会到处拍下你办公室的墙壁,然后检查你的 HR 文档看有没有规则禁止“工作场所枪战建模”

玩家



我们从何处投射光线?这就是玩家对象(Player)的作用,只需要三个属性 x,y,direction。


1

2

3

4

5

function Player(x, y, direction) {

  this.x = x;

  this.y = y;

  this.direction = direction;

}

地图



我们将地图存作简单的二维数组。数组中,0代表没墙,1代表有墙。你还可以做得更复杂些,比如给墙设任意高度,或者将多个墙数据的“楼层(stories)”打包进数组。但作为我们的第一次尝试,用0-1就足够了。

?


1

2

3

4

function Map(size) {

  this.size = size;

  this.wallGrid = new Uint8Array(size * size);

}

投射一束光线



这里就是窍门:光线投射引擎不会一次性绘制出整个场景。相反,它把场景分成独立的列然后一条一条地渲染。每一列都代表从玩家特定角度投射出的一条光线。如果光线碰到墙壁,引擎会计算玩家到墙的距离然后在该列中画出一个矩形。矩形的高度取决于光线的长度——越远则越短。

绘画的光线越多,显示效果就会越平滑。

1. 找到每条光线的角度


我们首先找出每条光线投射的角度。角度取决于三点:玩家面向的方向,摄像机的视野,还有正在绘画的列。

?


1

2

var angle = this.fov * (column / this.resolution - 0.5);

var ray = map.cast(player, player.direction + angle, this.range);

2. 通过网格跟踪每条光线



接下来,我们要检查每条光线经过的墙。这里的目标是最终得出一个数组,列出了光线离开玩家后经过的每面墙。

从玩家开始,我们找出最接近的横向(stepX)和纵向(stepY)网格坐标线。移到最近的地方然后检查是否有墙(inspect)。一直重复检查直到跟踪完每条线的所有长度。

?


1

2

3

4

5

6

7

8

9

10

function ray(origin) {

  var stepX = step(sin, cos, origin.x, origin.y);

  var stepY = step(cos, sin, origin.y, origin.x, true);

  var nextStep = stepX.length2 < stepY.length2

    ? inspect(stepX, 1, 0, origin.distance, stepX.y)

    : inspect(stepY, 0, 1, origin.distance, stepY.x);

  if (nextStep.distance > range) return [origin];

  return [origin].concat(ray(nextStep));

}

寻找网格交点很简单:只需要对 x 向下取整(1,2,3…),然后乘以光线的斜率(rise/run)得出 y。

?


1

2

var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;

var dy = dx * (rise / run);

现在看出了这个算法的亮点没有?我们不用关心地图有多大!只需要关注网格上特定的点——与每帧的点数大致相同。样例中的地图是32×32,而32,000×32,000的地图一样跑得这么快!

3. 绘制一列


跟踪完一条光线后,我们就要画出它在路径上经过的所有墙。

?


1

2

var z = distance * Math.cos(angle);

var wallHeight = this.height * height / z;

我们通过墙高度的最大除以 z 来觉得它的高度。越远的墙,就画得越短。

额,这里用 cos 是怎么回事?如果直接使用原来的距离,就会产生一种超广角的效果(鱼眼镜头)。为什么?想象你正面向一面墙,墙的左右边缘离你的距离比墙中心要远。于是原本直的墙中心就会膨胀起来了!为了以我们真实所见的效果去渲染墙面,我们通过投射的每条光线一起构建了一个三角形,通过 cos 算出垂直距离。如图:

我向你保证,这里已经是本文最难的数学啦。

渲染出来


我们用摄像头对象 Camera 从玩家视角画出地图的每一帧。当我们从左往右扫过屏幕时它会负责渲染每一列。

在绘制墙壁之前,我们先渲染一个天空盒(skybox)——就是一张大的背景图,有星星和地平线,画完墙后我们还会在前景放个武器。

?


1

2

3

4

5

Camera.prototype.render = function(player, map) {

  this.drawSky(player.direction, map.skybox, map.light);

  this.drawColumns(player, map);

  this.drawWeapon(player.weapon, player.paces);

};

摄像机最重要的属性是分辨率(resolution)、视野(fov)和射程(range)。

  • 分辨率决定了每帧要画多少列,即要投射多少条光线。
  • 视野决定了我们能看的宽度,即光线的角度。
  • 射程决定了我们能看多远,即光线长度的最大值

组合起来


使用控制对象 Controls 监听方向键(和触摸事件)。使用游戏循环对象 GameLoop 调用 requestAnimationFrame 请求渲染帧。这里的 gameloop 只有三行

?


1

2

3

4

5

oop.start(function frame(seconds) {

  map.update(seconds);

  player.update(controls.states, map, seconds);

  camera.render(player, map);

});

细节


雨滴


雨滴是用大量随机放置的短墙模拟的。

?


1

2

3

4

5

6

var rainDrops = Math.pow(Math.random(), 3) * s;

var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);

ctx.fillStyle = ‘#ffffff‘;

ctx.globalAlpha = 0.15;

while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);

这里没有画出墙完全的宽度,而是画了一个像素点的宽度。

照明和闪电



照明其实就是明暗处理。所有的墙都是以完全亮度画出来,然后覆盖一个带有一定不透明度的黑色矩形。不透明度决定于距离与墙的方向(N/S/E/W)。

?


1

2

3

ctx.fillStyle = ‘#000000‘;

ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0);

ctx.fillRect(left, wall.top, width, wall.height);

要模拟闪电,map.light 随机达到2然后再快速地淡出。

碰撞检测


要防止玩家穿墙,我们只要用他要到的位置跟地图比较。分开检查 x 和 y 玩家就可以靠着墙滑行。

?


1

2

3

4

5

6

Player.prototype.walk = function(distance, map) {

  var dx = Math.cos(this.direction) * distance;

  var dy = Math.sin(this.direction) * distance;

  if (map.get(this.x + dx, this.y) <= 0) this.x += dx;

  if (map.get(this.x, this.y + dy) <= 0) this.y += dy;

};

墙壁贴图


没有贴图(texture)的墙面看起来会比较无趣。但我们怎么把贴图的某个部分对应到特定的列上?这其实很简单:取交叉点坐标的小数部分。

?


1

2

step.offset = offset - Math.floor(offset);

var textureX = Math.floor(texture.width * step.offset);

举例来说,一面墙上的交点为(10,8.2),于是取小数部分0.2。这意味着交点离墙左边缘20%远(8),离墙右边缘80%远(9)。所以我们用 0.2 * texture.width 得出贴图的 x 坐标。

试一试



恐怖废墟中逛一逛。

接下来做什么?


因为光线投射器是如此地快速、简单,你可以快速地实现许多想法。你可以做个地牢探索者(Dungeon Crawler)、第一人称射手、或者侠盗飞车式沙盒。靠!常数级的时间消耗真让我想做一个老式的大型多人在线角色扮演游戏,包含大量的、程序自动生成的世界。这里有一些带你起步的难题:

  • 浸入式体验。样例在求你为它加上全屏、鼠标定位、下雨背景和闪电时同时出现雷响。
  • 室内级别。用对称渐变取代天空盒。或者,你觉得自己很屌的话,尝试用瓷片渲染地板和天花板。(可以这么想:所有墙面画出来之后,画面剩下的空隙就是地板和天花板了)
  • 照明对象。我们已经有了一个相当健壮的照明模型。为何不将光源放到地图上,通过它们计算墙的照明?光源占了80%大气层。
  • 良好的触摸事件。我已经搞定了一些基本的触摸操作,手机和平板的小伙伴们可以尝试一样 demo。但这里还有巨大的提升空间。
  • 摄像机特效。比如放大缩小、模糊、醉汉模式等等。有了光线投射器这些都显得特别简单。先从控制台中修改 camera.fov 开始。

源码地址:https://github.com/hunterloftis/playfuljs/

原文链接: http://www.playfuljs.com/a-first-person-engine-in-265-lines

时间: 2024-12-19 14:54:11

【转】265行代码实现第一人称游戏引擎的相关文章

【转】265行JavaScript代码的第一人称3D H5游戏Demo

译文:http://blog.jobbole.com/70956/ 原文:http://www.playfuljs.com/a-first-person-engine-in-265-lines/ 这是一篇关于利用 Canvas 实现3D 游戏场景绘制的文章,看完感觉很受启发,所以自己准备总结一下.我们先看下最终效果: 很酷是不是.原作者只使用了265行代码就实现了这么炫酷的效果.代码写的也很清晰明了,并且原文也对原理也进行了解释.本篇就直接将原作者的代码全部贴上来大家观赏观赏. 1 <!doct

265行 JavaScript 代码实现第一人称引擎

原文:A first-person engine in 265 lines(2014-6-11)翻译:Jaward华仔 今天,让我们进入一个可以伸手触摸的世界吧.在这篇文章里,我们将从零开始快速完成一次第一人称探索.本文没有涉及复杂的数学计算,只用到了光线投射技术.你可能已经见识过这种技术了,比如<上古卷轴2 : 匕首雨>.<毁灭公爵3D>还有 Notch Persson 最近在 ludum dare 上的参赛作品.Notch 认为它够好,我就认为它够好! [Demo (arrow

HTML5游戏实战(3): 60行代码实现水平跑酷游戏

跑酷游戏无疑是目前最受欢迎的游戏类型之一,3D跑酷游戏可能比较难做一点(这个我不懂),2D跑酷却是非常容易的,只要有美术资源,做一个<天天酷跑>这类流行游戏也并非是什么难事. 这里通过一个松鼠跑酷的游戏介绍一下用Gamebuilder+CanTK开发游戏的方法,这个游戏的主场景仅用了60来行代码. CanTK(Canvas ToolKit)是一个开源的游戏引擎和APP框架,是开发HTML5游戏和APP的利器,如果你喜欢它,请在github上给它加星,您的支持是我们努力的动力:https://g

HTML5游戏实战(1):50行代码实现正面跑酷游戏

前段时间看到一个"熊来了"的HTML5跑酷游戏,它是一个典型的正面2D跑酷游戏,这里借用它来介绍一下用Gamebuilder+CanTK开发正面跑酷游戏的基本方法. 0.先Show一下最终成果: 在线运行:http://gamebuilder.duapp.com/apprun.php?appid=ZecSmA0UGRTh9juSEmu8RjkK-41416104149375 在线编辑:http://gamebuilder.duapp.com/gamebuilder.php?appid=

265行JavaScript代码的第一人称3D H5游戏Demo【个人总结1】

本文目的是分解前面的代码.其实,它得逻辑很清楚,只是对于我这种只是用过 Canvas 画线的人来说,这个还是很复杂的.我研究这个背景天空也是搞了一天,下面就是只加载天空的代码. 在线效果点击:http://1.codemo2.sinaapp.com/3d_demo_265line/index.html   [可以用键盘“左右”键控制][手机浏览器触控有些异常] 原理大概就是: 1. 创建主循环 一个无限循环 2. 主循环内重复调用绘制方法 3. 绘制方法: 针对 Player 的位置和方向,绘制

HTML5游戏实战(2):90行代码实现捕鱼达人

捕鱼达人是一款非常流行的游戏,几年里赚取了数以千万的收入,这里借用它来介绍一下用Gamebuilder+CanTK开发游戏的方法.其实赚钱的游戏未必技术就很难,今天我们就仅用90来行代码来实现这个游戏. CanTK(Canvas ToolKit)是一个开源的游戏引擎和APP框架,是开发HTML5游戏或者APP的利器,如果你喜欢它,请在github上给它加星,您的支持是我们努力的动力:https://github.com/drawapp8/cantk 0.先Show一下最终成果: 在线运行:htt

改造u3d第一人称控制器,使之适合Cardboard+蓝牙手柄控制

一.在u3d编辑器中删除FPSController游戏对像中自带的Camera: 二.在u3d编辑器中将CardBoardMain游戏对像添加到FPSController的子物体: 三.修改脚本: 1.删除FirstPersonController脚本中的RotateView方法调用: 2.在CardboardHead中的UpdateHead中,修改FPSController对像的transform的Y轴上的转向: 以上,即可以完成使用cardboard追踪到的头部转向数据来控制第一人称游戏对像

从4行代码看右值引用

从4行代码看右值引用 概述 右值引用的概念有些读者可能会感到陌生,其实他和C++98/03中的左值引用有些类似,例如,c++98/03中的左值引用是这样的: int i = 0; int& j = i; 这里的int&是对左值进行绑定(但是int&却不能绑定右值),相应的,对右值进行绑定的引用就是右值引用,他的语法是这样的A&&,通过双引号来表示绑定类型为A的右值.通过&&我们就可以很方便的绑定右值了,比如我们可以这样绑定一个右值: int&

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

最近开始使用Spring开发新项目了,开发新项目必定少不了折腾增删查改. 其中模糊查询,精确查询,分页查询也算是不好对付的功能,需要手写大量重复的代码来实现相关的功能,如何优雅的实现查询功能呢? 首先上两张截图.第一张截图是分页列出活动,可以根据活动名称和活动内容进行模糊查询. 举办机构,举办年份,活动分类为可选项.如指定了年份就会检索相应年份的数据. 第二张截图是列出分类. 可以根据分类名称进行模糊查询. 可以发现,两个listData 方法均只有4行代码. 第一行指定模糊查询的列名s,第二行