以前我们介绍了我们的逃脱风格的赛车游戏,但我们如何建立一个伪3D赛车游戏上手?
好吧,我们将需要
- 修改一些三角
- 修改基本的3D投影
- 建立一个游戏循环
- 加载一些精灵的图像
- 兴建一些道路几何
- 渲染的背景
- 呈现在路上
- 使汽车
- 启用键盘的支持,来驱动汽车
之前,我们做任何,让熄灭楼的伪3D页面 -其主要信息源(我能找到)如何建立一个伪3D赛车游戏,在线读。
注:娄的页面不呈现在谷歌浏览器-所以它的最佳浏览器Firefox或IE浏览器
读完娄的文章?太棒了!我们要建立一个变化,对他的做法“使用三维投影段的现实山”。我们将逐步做到,当然了,在接下来的4篇文章,但是,我们将在这里开始了与V1,建设很简单,笔直的公路几何和投射到我们的HTML5 canvas元素。
它在这里看到在行动
一些三角
我们坐下来实施前,可使用一些基本的三角提醒自己如何投射到二维屏幕上的一个点的三维世界。
在其最基本的,没有进入向量和矩阵,3D投影使用的法律相似三角形。如果我们的标签:
- h = camera height
- d = distance from camera to screen
- z = distance from camera to car
- y = screen y coordinate
然后,我们可以利用相似三角形法计算
1 | y = h*d/z |
在下面的图表所示:
从自上而下的观点,而不是我们也可以得出类似的图边对视图和派生类似的公式计算屏幕的x坐标
1 | x = w*d/z |
瓦特 = 道路宽度的一半(从摄像头到马路边上)
由一个因素,你可以看到,x和Ÿ,我们真正做的是缩放
1 | d/z |
坐标系
这听起来不错,在图简单的形式,但一旦你开始编码,容易得到有点困惑,因为我们已经命名的变量和其不明确表示3D世界坐标和代表2D屏幕坐标有点松。我们还假设该相机在我们这个世界的起源是在现实中,将我们的车后。
更正式的,我们应该:
- 翻译从世界坐标到相机坐标
- 投影相机坐标到规范化投影平面
- 缩放坐标投影坐标到物理屏幕(在我们的例子画布)
注:一个真正的3D系统中A *旋转*步骤将步骤1和2之间,但因为我们将要伪造曲线,我们不需要担心旋转
投影
所以我们可以提出正式的投影方程如下:
- 翻译方程计算点相对相机
- 该项目的方程是我们的’法相似三角形“以上的变化
- 规模方程考虑的区别:
- 数学 -其中0,0是在中心和Y轴的上升和
- 电脑 -其中0,0是在左上角的y轴出现故障,如下所示:
注:在一个完全成熟的3D系统,我们会更正式的定义
向量
和矩阵
类来执行更强大的3D数学,如果我们打算这样做,那么我们可能也只是使用WebGL的(或同等学历)……但是那不是这个项目的真正点。我真的很想坚持老派“刚刚够”伪3D建立一个逃脱风格的游戏。
一些更多的三角
最后一块拼图之一是如何计算ð -从相机到投影平面的距离。
而不是硬编码为ð价值,更获得所需的垂直领域。这样我们就可以选择“放大”如果需要的相机。
假设我们到一个规范化的投影平面上的投影,从-1到+1的坐标,我们可以计算出ð如下:
1 | d = 1/tan(fov/2) |
设置了FOV作为一个变量(许多),我们将能够调整以微调渲染算法。
Javascript代码结构
我提到在介绍这个代码并不完全遵循JavaScript的最佳实践-一个快速和肮脏的演示用简单的全局变量和函数。然而,因为我要建立不同版本4(直道,曲线,丘陵和精灵),我会保持里面的一些可重复使用的方法common.js
在以下模块:
- DOM – DOM的几个小帮手。
- UTIL -通用公用事业,大多数学佣工。
- 游戏 -图像装载机和游戏循环的通用游戏,如佣工。
- 渲染 -画布渲染佣工。
我将只详细方法common.js
如果他们是洪鸿,杨凤池,以实际的游戏,而不是只是简单的DOM或数学佣工。希望你可以从所应该做的什么方法的名称和内容。
像往常一样,源代码的最后文件。
一个简单的游戏循环
之前,我们可以提供什么,我们需要一个游戏循环。如果你按照我以前的游戏文章(乒乓球,突破, 俄罗斯方块,蛇或 boulderdash),那么你就已经看到了我最喜欢的例子固定时间步长 的游戏循环。
我不会去到很多细节,在这里,我简单地重新使用的一些代码从我以前的游戏拿出一个固定的时间步长的游戏循环使用 requestAnimationFrame
的想法是,我的4例,每年可致电Game.run(...),
并提供他们自己的版本
更新
-有一个固定的时间步长的游戏世界。渲染
-游戏世界时,浏览器允许。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | run: function(options) { Game.loadImages(options.images, function(images) { var update = options.update, // method to update game logic is provided by caller render = options.render, // method to render the game is provided by caller step = options.step, // fixed frame step (1/fps) is specified by caller now = null, last = Util.timestamp(), dt = 0, gdt = 0; function frame() { now = Util.timestamp(); dt = Math.min(1, (now - last) / 1000); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab gdt = gdt + dt; while (gdt > step) { gdt = gdt - step; update(step); } render(); last = now; requestAnimationFrame(frame); } frame(); // lets get this party started }); } |
再次,这是从我以前的画布游戏思想的翻版,所以如果你需要澄清如何回到游戏循环工程和阅读这些早期的文章(或在下面发表评论!)。
图像和子
我们的游戏循环开始之前,我们加载了2个独立的精灵表:
- 背景 – 3视差层天空,山丘和树木
- 精灵 -汽车精灵(加上树木和广告牌,添加到最后的版本)
使用一个小的Rake任务精灵工厂红宝石宝石产生的spritesheet 。这个任务生成统一的精灵表以及在X,Y,W,H坐标存储在背景
和精灵
常数。
注:背景家庭使用Inkscape中,而大部分精灵占位符图形借来的成因从旧版本逃脱,这里作为教学例子。如果有任何像素艺术家要提供原始艺术,变成一个真正的游戏,请与我们联系!
游戏变量
除了我们的背景和雪碧图像,我们需要的游戏变量,包括:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | var fps = 60; // how many 'update' frames per second var step = 1/fps; // how long is each frame (in seconds) var width = 1024; // logical canvas width var height = 768; // logical canvas height var segments = []; // array of road segments var canvas = Dom.get('canvas'); // our canvas... var ctx = canvas.getContext('2d'); // ...and its drawing context var background = null; // our background image (loaded below) var sprites = null; // our spritesheet (loaded below) var resolution = null; // scaling factor to provide resolution independence (computed) var roadWidth = 2000; // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth var segmentLength = 200; // length of a single segment var rumbleLength = 3; // number of segments per red/white rumble strip var trackLength = null; // z length of entire track (computed) var lanes = 3; // number of lanes var fieldOfView = 100; // angle (degrees) for field of view var cameraHeight = 1000; // z height of camera var cameraDepth = null; // z distance camera is from screen (computed) var drawDistance = 300; // number of segments to draw var playerX = 0; // player x offset from left of road (-1 to 1 to stay independent of roadWidth) var playerZ = null; // player relative z distance from camera (computed) var fogDensity = 5; // exponential fog density var position = 0; // current camera Z position (add playerZ to get player's absolute Z position) var speed = 0; // current speed var maxSpeed = segmentLength/step; // top speed (ensure we can't move more than 1 segment in a single frame to make collision detection easier) var accel = maxSpeed/5; // acceleration rate - tuned until it 'felt' right var breaking = -maxSpeed; // deceleration rate when braking var decel = -maxSpeed/5; // 'natural' deceleration rate when neither accelerating, nor braking var offRoadDecel = -maxSpeed/2; // off road deceleration is somewhere in between var offRoadLimit = maxSpeed/4; // limit when off road deceleration no longer applies (e.g. you can always go at least this speed even when off road) |
其中一些可以调整使用的调整UI控件,允许你在运行时的临界值的变化,看到他们所呈现的道路上有什么样的影响。其他来自的tweakable UI值和复位()
方法期间重新计算。
驾驶法拉利
我们提供了一个关键的映射Game.run
,允许简单的键盘输入,设置或清除变量来表示球员目前正在采取的任何行动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Game.run({ ... keys: [ { keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } }, { keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } }, { keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } }, { keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } }, { keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } }, { keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } }, { keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } }, { keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } } ], ... } |
管理球员的状态变量是:
- 速度 -目前的速度。
- 位置 -当前Z走下赛场的位置。请注意,这实际上是相机的位置,而不是法拉利。
- PlayerX的 -目前在马路对面的X位置。正常化从-1到+1是独立的实际
roadWidth
。
这些变量被设置在更新
方法,这将:
- 基于对当前
的速度
更新位置
。 - 如果向左或向右箭头键被按下更新
PlayerX的
。 - 加快
速度,
如果按下向上箭头。 - 减慢
速度,
如果按向下箭头。 - 减慢
速度,
如果没有按下向上或向下箭头。 - 减速
速度
PlayerX的
小康的道路两侧,将草。
笔直的道路,更新
的方法是很干净和简单的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | function update(dt) { position = Util.increase(position, dt * speed, trackLength); var dx = dt * 2 * (speed/maxSpeed); // at top speed, should be able to cross from left to right (-1 to 1) in 1 second if (keyLeft) playerX = playerX - dx; else if (keyRight) playerX = playerX + dx; if (keyFaster) speed = Util.accelerate(speed, accel, dt); else if (keySlower) speed = Util.accelerate(speed, breaking, dt); else speed = Util.accelerate(speed, decel, dt); if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit)) speed = Util.accelerate(speed, offRoadDecel, dt); playerX = Util.limit(playerX, -2, 2); // dont ever let player go too far out of bounds speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed } |
不要担心,它会得到更加复杂,当我们添加精灵和碰撞检测的最终版本:-)
道路几何
之前,我们可以使我们的游戏世界,我们需要建立我们的道路段的
阵列内resetRoad()
方法。
从他们的世界坐标成为一个在屏幕坐标的二维多边形,这些路段都将最终预测。我们存储每段2分,P1 是最接近相机的边缘的中心,而P2是从相机最远的边缘中心。
从技术上讲,每个段P2 P1的前面部分是相同的,但我们会发现更容易维护它们作为单独的点和改造各分部独立。
我们保持单独rumbleLength,
原因是这样,我们可以有良好的详细的曲线和丘陵,但仍然有很长的振动带。如果每个交替分段是一个不同的颜色,它会创建一个坏的频闪效果。因此,我们希望大量的小片段,但他们一起组形成每个隆隆地带。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function resetRoad() { segments = []; for(var n = 0 ; n < 500 ; n++) { // arbitrary road length segments.push({ index: n, p1: { world: { z: n *segmentLength }, camera: {}, screen: {} }, p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} }, color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT }); } trackLength = segments.length * segmentLength; } |
我们初始化P1和P2只有ž世界坐标,因为我们只需要直路。Ÿ坐标将始终为0,而x坐标将始终基于一个规模 + / - roadWidth
。这将改变后,当我们添加曲线和丘陵。
我们还可以设置空对象来存储这些点的摄像头和屏幕交涉,以避免在每次创建大量的临时对象渲染
-试图让我们的垃圾收集到最低限度,我们希望在我们的游戏循环分配对象尽可能避免。
当汽车到达的道路,我们将简单的回圈开始的结束。为了使这更容易一些,我们提供了一种方法找到任何Z值段,即使它超出了道路的长度延伸:
1 2 3 | function findSegment(z) { return segments[Math.floor(z/segmentLength) % segments.length]; } |
撕裂的背景
我们的render()
方法开始绘制背景图像。当我们在以后的文章中添加曲线和丘陵,我们将要视差滚动的背景,所以我们开始在这里,方向,作为单独的3层渲染的背景:
1 2 3 4 5 6 7 8 9 | function render() { ctx.clearRect(0, 0, width, height); Render.background(ctx, background, width, height, BACKGROUND.SKY); Render.background(ctx, background, width, height, BACKGROUND.HILLS); Render.background(ctx, background, width, height, BACKGROUND.TREES); ... |
分离车道
渲染功能,然后遍历段,每个项目段的 P1和P2从世界坐标到屏幕坐标,剪切段,如果有必要,否则渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | var baseSegment = findSegment(position); var maxy = height; var n, segment; for(n = 0 ; n < drawDistance ; n++) { segment = segments[(baseSegment.index + n) % segments.length]; Util.project(segment.p1, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth); Util.project(segment.p2, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth); if ((segment.p1.camera.z <= cameraDepth) || // behind us (segment.p2.screen.y >= maxy)) // clip by (already rendered) segment continue; Render.segment(ctx, width, lanes, segment.p1.screen.x, segment.p1.screen.y, segment.p1.screen.w, segment.p2.screen.x, segment.p2.screen.y, segment.p2.screen.w, segment.color); maxy = segment.p2.screen.y; } |
我们看到了项目点所需的数学,JavaScript版本的翻译,投影和缩放方程结合成一个单一的方法:
1 2 3 4 5 6 7 8 9 | project: function(p, cameraX, cameraY, cameraZ, cameraDepth, width, height, roadWidth) { p.camera.x = (p.world.x || 0) - cameraX; p.camera.y = (p.world.y || 0) - cameraY; p.camera.z = (p.world.z || 0) - cameraZ; p.screen.scale = cameraDepth/p.camera.z; p.screen.x = Math.round((width/2) + (p.screen.scale * p.camera.x * width/2)); p.screen.y = Math.round((height/2) - (p.screen.scale * p.camera.y * height/2)); p.screen.w = Math.round( (p.screen.scale * roadWidth * width/2)); } |
除了 计算屏幕的x和Ÿ我们每个P1和P2 点,我们也使用相同的投影数学计算预计段的宽度(瓦特)。
屏幕x和Ÿ坐标为P1和P2,随着道路宽度预计,瓦特,它变得相当直截了当的 Render.segment
帮手来计算所有的多边形,它需要渲染的草地,道路,隆隆带和车道分离器使用一个通用的Render.polygon
帮手(见common.js
)
渲染汽车
最后,渲染
方法所要求的最后一件事是使法拉利:
1 2 3 4 | Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed, cameraDepth/playerZ, width/2, height); |
这种方法被命名为
球员
,而不是对车的
原因是因为我们在比赛的最后版本有其他车在道路上,我们要明确区分球员的法拉利其他车型。
Render.player
帮手最终使用画布的DrawImage
方法,使缩放后,基于相同的投影缩放,刚才我们看到一个精灵:
数/张
在这种情况下是相对距离的车从相机存储在playerz变量。
它还‘反弹’车有点在更高的速度通过添加一个小random-ness的尺度方程的基础上/最大速度。
和繁荣,有你有它:
结论
这实际上是一个相当大的工作,已经给我们设置直的道路。我们说…
一个共同的多辅助模块
一个常见的利用数学辅助模块
一个共同的渲染画布辅助模块…
包括render.segment,render.polygon和render.sprite……
固定一步游戏循环
图像加载器
键盘处理程序
视差分层背景
一个spritesheet全车,树木和广告牌
一些基本的道路几何
一个update()方法驾驶汽车
一个render()方法渲染背景,和球员的车路
5<音频>标签与一些赛车音乐(一个隐藏的奖金!)
……但它为我们提供了一个良好的基础建设。接下来的2篇文章,描述曲线和丘陵应该轻松一点,才更加复杂,在文章的最后,我们添加了精灵和碰撞检测。
原文:http://codeincomplete.com/posts/2012/6/23/javascript_racer_v1_straight
游戏网址:http://codeincomplete.com/projects/racer/v1.straight.html
下载地址:https://github.com/jakesgordon/javascript-racer