第3部分。丘陵
在上一部分中,我们创建了一个简单的伪三维赛车游戏,在其中实现了直线道路和弯道。这次我们要照顾山丘;幸运的是,这比创建弯曲的道路要容易得多。在第一部分中,我们使用相似三角形的定律创建了三维透视投影:...导致我们获得了将3d世界的坐标投影到2d屏幕的坐标的方程式。...但是从那时起,我们只使用直行道路,因此世界坐标只需要z分量,因为x和y都等于零。这很适合我们,因为添加山足以为路段赋予对应的非零坐标y,然后现有功能将render()
神奇地起作用。是的,这足以使人上山。只需将y分量添加到每个路段的世界坐标。道路几何形状的变化
我们将修改现有方法,addSegment
以使调用它的函数可以传递p2.world.y,而p1.world.y将与上一段的p2.world.y相对应:function addSegment(curve, y) {
var n = segments.length;
segments.push({
index: n,
p1: { world: { y: lastY(), z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { y: y, z: (n+1)*segmentLength }, camera: {}, screen: {} },
curve: curve,
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
function lastY() {
return (segments.length == 0) ? 0 : segments[segments.length-1].p2.world.y;
}
添加常数以表示低(LOW
),中(MEDIUM
)和高(HIGH
)坡度:var ROAD = {
LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
HILL: { NONE: 0, LOW: 20, MEDIUM: 40, HIGH: 60 },
CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
};
更改现有方法addRoad()
,使其接收参数y,该参数将与平滑函数一起用于从山上逐渐上升和下降的情况:function addRoad(enter, hold, leave, curve, y) {
var startY = lastY();
var endY = startY + (Util.toInt(y, 0) * segmentLength);
var n, total = enter + hold + leave;
for(n = 0 ; n < enter ; n++)
addSegment(Util.easeIn(0, curve, n/enter), Util.easeInOut(startY, endY, n/total));
for(n = 0 ; n < hold ; n++)
addSegment(curve, Util.easeInOut(startY, endY, (enter+n)/total));
for(n = 0 ; n < leave ; n++)
addSegment(Util.easeInOut(curve, 0, n/leave), Util.easeInOut(startY, endY, (enter+hold+n)/total));
}
此外,类似于我们在第2部分中所做的addSCurves()
,我们可以强加我们构造几何所需的任何方法,例如:function addLowRollingHills(num, height) {
num = num || ROAD.LENGTH.SHORT;
height = height || ROAD.HILL.LOW;
addRoad(num, num, num, 0, height/2);
addRoad(num, num, num, 0, -height);
addRoad(num, num, num, 0, height);
addRoad(num, num, num, 0, 0);
addRoad(num, num, num, 0, height/2);
addRoad(num, num, num, 0, 0);
}
更改更新方法
在我们创建的街机游戏中,我们不会尝试模拟现实,因此丘陵不会以任何方式影响玩家或游戏世界,这意味着该方法update()
不需要更改。希尔渲染
该方法render()
也不需要更改,因为投影方程最初是为了正确投影具有非零y坐标的路段而编写的。视差滚动背景
除了将y坐标添加到所有路段外,唯一的变化将是背景图层与丘陵的垂直位移的实现(就像它们随曲线水平移动一样)。我们使用辅助函数的另一个参数来实现这一点Render.background
。最简单的机制是相对于位置的通常背景位移playerY
(应从当前玩家细分的世界位置y内插)。这不是最现实的行为,因为可能值得考虑玩家当前路段的坡度,但是这种效果很简单,并且对于简单的演示来说效果很好。结论
就是这样,现在我们可以用真实的山丘来补充假曲线了:我们在第一部分中所做的工作,包括添加真实的3D丘陵的基础设施,我之前没有告诉过您。在本文的最后一部分,我们将在道路边缘添加精灵,树木和广告牌。我们还将添加其他可以与之竞争的赛车,识别碰撞并修复玩家的“圈数记录”。第4部分。就绪版本
在这一部分,我们将添加:- 广告牌和树木
- 其他车
- 碰撞识别
- 汽车的基本AI
- 与单圈计时器和单圈记录接口
……这将为我们提供足够的交互性,最终将我们的项目称为“游戏”。有关代码结构的注意事项
, /, Javascript.
. () , ...
… , , , , .
在第1部分中,在游戏周期开始之前,我们上传了一个精灵表,其中包含所有的汽车,树木和广告牌。您可以在任何图像编辑器中手动创建一个精灵表,但是最好将图像的存储和坐标的计算委托给自动化工具。在我的情况下,精灵表是由一个小的Rake任务使用Ruby Gem sprite-factory生成的。该任务从单独的图像文件生成组合的Spritesheets,并计算坐标x,y,w,h,这些坐标将存储在常量中SPRITES
:var SPRITES = {
PALM_TREE: { x: 5, y: 5, w: 215, h: 540 },
BILLBOARD08: { x: 230, y: 5, w: 385, h: 265 },
CAR04: { x: 1383, y: 894, w: 80, h: 57 },
CAR01: { x: 1205, y: 1018, w: 80, h: 56 },
};
添加广告牌和树木
在道路的每个路段上添加一个数组,其中将包含沿道路边缘的对象精灵。每个子图由source
从集合中获取的SPRITES
以及水平偏移量组成offset
,水平偏移量已归一化,因此-1表示道路的左边缘,而+1表示右边缘,这使我们不必依赖于值roadWidth
。有些精灵是有意放置的,有些则是随机的。function addSegment() {
segments.push({
...
sprites: [],
...
});
}
function addSprite(n, sprite, offset) {
segments[n].sprites.push({ source: sprite, offset: offset });
}
function resetSprites() {
addSprite(20, SPRITES.BILLBOARD07, -1);
addSprite(40, SPRITES.BILLBOARD06, -1);
addSprite(60, SPRITES.BILLBOARD08, -1);
addSprite(80, SPRITES.BILLBOARD09, -1);
addSprite(100, SPRITES.BILLBOARD01, -1);
addSprite(120, SPRITES.BILLBOARD02, -1);
addSprite(140, SPRITES.BILLBOARD03, -1);
addSprite(160, SPRITES.BILLBOARD04, -1);
addSprite(180, SPRITES.BILLBOARD05, -1);
addSprite(240, SPRITES.BILLBOARD07, -1.2);
addSprite(240, SPRITES.BILLBOARD06, 1.2);
for(n = 250 ; n < 1000 ; n += 5) {
addSprite(n, SPRITES.COLUMN, 1.1);
addSprite(n + Util.randomInt(0,5), SPRITES.TREE1, -1 - (Math.random() * 2));
addSprite(n + Util.randomInt(0,5), SPRITES.TREE2, -1 - (Math.random() * 2));
}
...
}
注意:如果要创建真实的游戏,我们可以编写道路编辑器以可视化方式创建具有丘陵和曲线的地图,并添加沿道路布置精灵的机制...但是对于我们的任务,我们可以通过编程方式进行addSprite()
。加料机
除了道路边缘的对象精灵外,我们还将添加一个将占据每个路段的汽车集合,以及高速公路上所有汽车的单独集合。var cars = [];
var totalCars = 200;
function addSegment() {
segments.push({
...
cars: [],
...
});
}
通过存储两个汽车数据结构,我们可以轻松地以一种方法迭代地遍历所有汽车update()
,并在必要时将它们从一个区段移动到另一个区段。同时,这允许我们render()
仅在可见段上执行机器。每台机器都有一个随机的水平位移,z位置,子画面源和速度:function resetCars() {
cars = [];
var n, car, segment, offset, z, sprite, speed;
for (var n = 0 ; n < totalCars ; n++) {
offset = Math.random() * Util.randomChoice([-0.8, 0.8]);
z = Math.floor(Math.random() * segments.length) * segmentLength;
sprite = Util.randomChoice(SPRITES.CARS);
speed = maxSpeed/4 + Math.random() * maxSpeed/(sprite == SPRITES.SEMI ? 4 : 2);
car = { offset: offset, z: z, sprite: sprite, speed: speed };
segment = findSegment(car.z);
segment.cars.push(car);
cars.push(car);
}
}
希尔渲染(返回)
在前面的部分中,我讨论了渲染道路段(包括曲线和山丘)的过程,但是其中有几行我没有考虑过。他们关心的是一个变量,它maxy
从屏幕底部开始,但是在渲染每个段以确定我们已经渲染了屏幕的哪一部分时递减:for(n = 0 ; n < drawDistance ; n++) {
...
if ((segment.p1.camera.z <= cameraDepth) ||
(segment.p2.screen.y >= maxy))
continue;
...
maxy = segment.p2.screen.y;
}
这将使我们可以裁剪已被渲染的山丘覆盖的部分。在艺术家的传统算法中,渲染通常发生在背面,而较近的部分则与较远的部分重叠。但是,我们不能花时间渲染最终会被覆盖的多边形,因此,如果投影坐标较小,则从前向后渲染和裁剪已被渲染的近段覆盖的遥远段变得容易maxy
。渲染广告牌,树木和汽车
但是,渲染子画面时,从前到后的道路段迭代遍历将不起作用,因为它们经常相互重叠,因此必须使用艺术家的算法进行渲染。这使我们的方法变得复杂,render()
并迫使我们分两个阶段绕过路段:- 从前到后进行道路渲染
- 后退以渲染精灵
除了部分重叠的精灵外,我们还需要处理由于山顶的地平线而“略微突出”的精灵。如果子画面足够高,则即使它所在的路段位于山坡的后部,因此也不会呈现,因此我们应该看到其上部。我们可以通过保存的值解决这个最后的问题maxy
每个段为线clip
在步骤1。然后,我们可以裁剪沿线本段的精灵clip
在步骤2中,呈现逻辑的其余部分确定如何比例和位置基于系数精灵scale
和坐标screen
的道路段的(计算阶段1),因此在该方法的第二阶段,render()
我们需要进行以下操作:
for(n = (drawDistance-1) ; n > 0 ; n--) {
segment = segments[(baseSegment.index + n) % segments.length];
for(i = 0 ; i < segment.sprites.length ; i++) {
sprite = segment.sprites[i];
spriteScale = segment.p1.screen.scale;
spriteX = segment.p1.screen.x + (spriteScale * sprite.offset * roadWidth * width/2);
spriteY = segment.p1.screen.y;
Render.sprite(ctx, width, height, resolution, roadWidth, sprites, sprite.source, spriteScale, spriteX, spriteY, (sprite.offset < 0 ? -1 : 0), -1, segment.clip);
}
for(i = 0 ; i < segment.cars.length ; i++) {
car = segment.cars[i];
sprite = car.sprite;
spriteScale = Util.interpolate(segment.p1.screen.scale, segment.p2.screen.scale, car.percent);
spriteX = Util.interpolate(segment.p1.screen.x, segment.p2.screen.x, car.percent) + (spriteScale * car.offset * roadWidth * width/2);
spriteY = Util.interpolate(segment.p1.screen.y, segment.p2.screen.y, car.percent);
Render.sprite(ctx, width, height, resolution, roadWidth, sprites, car.sprite, spriteScale, spriteX, spriteY, -0.5, -1, segment.clip);
}
}
与广告牌和树木的碰撞
现在我们可以沿道路边缘添加和渲染对象精灵,我们需要更改方法update()
以确定玩家在当前段中是否遇到了这些精灵中的任何一个:我们使用辅助方法Util.overlap()
来实现对矩形相交的广义识别。如果检测到交叉路口,我们将停车:if ((playerX < -1) || (playerX > 1)) {
for(n = 0 ; n < playerSegment.sprites.length ; n++) {
sprite = playerSegment.sprites[n];
spriteW = sprite.source.w * SPRITES.SCALE;
if (Util.overlap(playerX, playerW, sprite.offset + spriteW/2 * (sprite.offset > 0 ? 1 : -1), spriteW)) {
break;
}
}
}
注意:如果您学习真实的代码,您会发现实际上我们不是在停车,因为那样一来,它将无法侧向移动以避开障碍物;作为一个简单的技巧,我们固定它们的位置,并允许汽车“滑动”到精灵周围。与汽车相撞
除了沿道路边缘与小精灵碰撞外,我们还需要识别与其他汽车的碰撞,如果检测到交叉路口,我们会通过将玩家“推”回与之碰撞的机器后面来减慢玩家的速度:for(n = 0 ; n < playerSegment.cars.length ; n++) {
car = playerSegment.cars[n];
carW = car.sprite.w * SPRITES.SCALE;
if (speed > car.speed) {
if (Util.overlap(playerX, playerW, car.offset, carW, 0.8)) {
break;
}
}
}
机器更新
为了让其他汽车沿着道路行驶,我们将为他们提供最简单的AI:- 以恒定的速度骑
- 超车时自动绕过玩家
- 超车时自动绕过其他汽车
注意:我们不必担心在道路上沿着弯道转弯其他汽车,因为这些弯道不是真实的。如果我们使汽车仅沿道路段移动,它们将自动沿曲线通过。
所有这一切都发生在update()
通话期间的游戏周期中,在此通话updateCars()
中,我们将每辆汽车以恒定速度向前移动,如果在此帧中它们移动了足够的距离,则从一个段切换到下一个段。function updateCars(dt, playerSegment, playerW) {
var n, car, oldSegment, newSegment;
for(n = 0 ; n < cars.length ; n++) {
car = cars[n];
oldSegment = findSegment(car.z);
car.offset = car.offset + updateCarOffset(car, oldSegment, playerSegment, playerW);
car.z = Util.increase(car.z, dt * car.speed, trackLength);
car.percent = Util.percentRemaining(car.z, segmentLength);
newSegment = findSegment(car.z);
if (oldSegment != newSegment) {
index = oldSegment.cars.indexOf(car);
oldSegment.cars.splice(index, 1);
newSegment.cars.push(car);
}
}
}
该方法updateCarOffset()
提供了“人工智能”的实现,使机器可以绕过播放器或其他机器。这是代码库中最复杂的方法之一,在实际游戏中,它应该更加复杂,以使机器看上去比简单的演示更为真实。在我们的项目中,我们使用幼稚的AI蛮力,迫使每台机器:- 期待20个细分市场
- 如果她发现前方有较慢的汽车横穿自己的小路,则绕过她
- 从道路左侧的障碍物向右转
- 在道路的右侧向左拐弯
- 转弯足以避免在剩余距离内前方的障碍物
我们还可以用那些玩家看不见的汽车作弊,从而使它们不能相互绕过并通过。他们应该只在玩家的视野范围内才显得“聪明”。function updateCarOffset(car, carSegment, playerSegment, playerW) {
var i, j, dir, segment, otherCar, otherCarW, lookahead = 20, carW = car.sprite.w * SPRITES.SCALE;
if ((carSegment.index - playerSegment.index) > drawDistance)
return 0;
for(i = 1 ; i < lookahead ; i++) {
segment = segments[(carSegment.index+i)%segments.length];
if ((segment === playerSegment) && (car.speed > speed) && (Util.overlap(playerX, playerW, car.offset, carW, 1.2))) {
if (playerX > 0.5)
dir = -1;
else if (playerX < -0.5)
dir = 1;
else
dir = (car.offset > playerX) ? 1 : -1;
return dir * 1/i * (car.speed-speed)/maxSpeed;
}
for(j = 0 ; j < segment.cars.length ; j++) {
otherCar = segment.cars[j];
otherCarW = otherCar.sprite.w * SPRITES.SCALE;
if ((car.speed > otherCar.speed) && Util.overlap(car.offset, carW, otherCar.offset, otherCarW, 1.2)) {
if (otherCar.offset > 0.5)
dir = -1;
else if (otherCar.offset < -0.5)
dir = 1;
else
dir = (car.offset > otherCar.offset) ? 1 : -1;
return dir * 1/i * (car.speed-otherCar.speed)/maxSpeed;
}
}
}
}
在大多数情况下,该算法效果很好,但是在前面有大量汽车的情况下,我们可以注意到汽车在从左向右和向后移动,试图挤入其他两台机器之间的缝隙。有许多方法可以提高AI的可靠性,例如,如果汽车发现没有足够的空间来避开障碍物,则可以让汽车减速。接口
最后,我们将创建一个基本的HTML界面:<div id =“ hud”>
<span id =“ speed” class =“ hud”> <span id =“ speed_value” class =“ value”> 0 </ span> mph </ span>
<span id =“ current_lap_time” class =“ hud”>时间:<span id =“ current_lap_time_value” class =“ value”> 0.0 </ span> </ span>
<span id =“ last_lap_time” class =“ hud”>最后一圈:<span id =“ last_lap_time_value” class =“ value”> 0.0 </ span> </ span>
<span id =“ fast_lap_time” class =“ hud”>最快圈:<span id =“ fast_lap_time_value” class =“ value”> 0.0 </ span> </ span>
</ div>
...并为其添加CSS样式#hud { position: absolute; z-index: 1; width: 640px; padding: 5px 0; font-family: Verdana, Geneva, sans-serif; font-size: 0.8em; background-color: rgba(255,0,0,0.4); color: black; border-bottom: 2px solid black; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; }
#hud .hud { background-color: rgba(255,255,255,0.6); padding: 5px; border: 1px solid black; margin: 0 5px; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }
#hud #speed { float: right; }
#hud #current_lap_time { float: left; }
#hud #last_lap_time { float: left; display: none; }
#hud #fast_lap_time { display: block; width: 12em; margin: 0 auto; text-align: center; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }
#hud .value { color: black; font-weight: bold; }
#hud .fastest { background-color: rgba(255,215,0,0.5); }
...,我们将在游戏周期内执行其update():if (position > playerZ) {
if (currentLapTime && (startPosition < playerZ)) {
lastLapTime = currentLapTime;
currentLapTime = 0;
if (lastLapTime <= Util.toFloat(Dom.storage.fast_lap_time)) {
Dom.storage.fast_lap_time = lastLapTime;
updateHud('fast_lap_time', formatTime(lastLapTime));
Dom.addClassName('fast_lap_time', 'fastest');
Dom.addClassName('last_lap_time', 'fastest');
}
else {
Dom.removeClassName('fast_lap_time', 'fastest');
Dom.removeClassName('last_lap_time', 'fastest');
}
updateHud('last_lap_time', formatTime(lastLapTime));
Dom.show('last_lap_time');
}
else {
currentLapTime += dt;
}
}
updateHud('speed', 5 * Math.round(speed/500));
updateHud('current_lap_time', formatTime(currentLapTime));
辅助方法updateHud()
允许我们仅在值更改时更新DOM元素,因为这样的更新可能是一个缓慢的过程,并且如果值本身不更改,我们不应该以60fps的速度执行它。function updateHud(key, value) {
if (hud[key].value !== value) {
hud[key].value = value;
Dom.set(hud[key].dom, value);
}
}
结论
!最后一部分很长,但是我们仍然完成了,完成的版本到达了可以称为游戏的阶段。她距离完成的比赛还很遥远,但这仍然是一场比赛。尽管如此简单,但我们确实设法创建了一个游戏,这真是令人惊讶。我不打算将这个项目带入一个完整的状态。它仅应视为对伪三维赛车游戏主题的介绍。该代码由github发布,您可以尝试将其转变为更高级的赛车游戏。您也可以尝试:- 为汽车增加音效
- 改善音乐同步
- 全屏实施
- ( , , , ..)
- (, ..)
- ,
- , -
- ,
- ( , ..)
- drawDistance
- x,y
- ( , )
- 叉子和公路连接
- 昼夜的变化
- 天气状况
- 隧道,桥梁,云层,墙壁,建筑物
- 城市,沙漠,海洋
- 将西雅图和太空针塔添加到背景中
- “恶棍”-增加竞争对手与之竞争
- 游戏模式-最快圈速,一对一比赛(捡硬币?向小人射击?)
- 大量的游戏自定义选项
- 等等
- ...
至此就完成了。另一个“周末项目”花费的时间比预期的要长得多,但最终结果还是不错的。参考文献
可播放演示的链接: