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

火星任务 (1) : 表面着陆

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (45投票s)

2010 年 10 月 11 日

CPOL

32分钟阅读

viewsIcon

118010

downloadIcon

879

保卫太阳系的策略/动作游戏:在表面飞行时的碰撞检测

火星任务

大约八九年前,我用 Visual Basic 6.1 写了这个游戏的第一版。当时我没有互联网,我的VB经验也很有限,但尽管在没有C#引以为豪的多功能“指针”的帮助下编程存在缺点,我还是成功地完成了一个还不错的项目。请注意,当我开始的时候,我只想写一个简单的太空游戏,不要有太多花哨的东西。也许有地球和月亮,一些克林贡人,一门相位炮和一些被困在某处的殖民者。但当我每周都在写作,燃烧着一点点绿色灵感的时候,我把我的蜘蛛代码纠缠成一团由复杂想法组成的混乱补丁,最终成为一个非常酷的游戏,尽管它背后是一团糟糕的意大利面条代码。我原本打算控制一艘飞船,但你最终控制了数百名宇航员,他们拥有“工程”、“化学”、“地质”、“飞行”和“医疗”等技能。你的工程师用你的地质学家和他们的舱外活动服帮助你开采的材料建造了许多飞船。化学家压缩大气气体生产燃料,他们利用他们的化学实验室为你的工程师提供原材料,你的飞行员驾驶飞船四处飞行。经过大约8到9个月的调试、测试和重新思考,扭转整个混乱局面并玩弄它,我从未设法编写“保存游戏”和“加载游戏”功能,这是一个主要的缺点,因为从地球上的Terran-One基地前往火星可能需要长达12个小时!天哪!游戏没有什么真正的故事情节,没有外星人,直到最后几周才发生战斗或射击,那时我把一个炸弹藏在Terran-One里面,你的英雄必须从某个小行星上救出他的女孩米兰达,带她回家,并通过拆除炸弹来拯救地球。诚然,故事情节很薄弱,但整个概念是“探索太阳系”,所以行星和卫星的旋转都根据NASA网站上的最新信息,每个天体的引力也足够真实,非常有趣。有洞穴和资源,坠毁和爆炸。这是一个既有趣又充实的重大项目。

这一次,我事先已经在脑子里把整个事情都规划好了。它将与原作非常相似,但这次是战争!邪恶的克隆人,或者来自火星的蜘蛛外星人,或者大卫·鲍伊穿着罐头套装用他血腥的爪牙射击小行星,我还不确定,因为到目前为止我已经为此工作了一个半月,而且地形碰撞检测工作得相当好。你会注意到标题“火星任务 (1) : 表面着陆”中的 (1),这是一个正在进行的项目,而且到目前为止我所做的碰撞检测有很多可以谈论的地方,所以我想我会把这个项目分成几篇不同的文章,我们从这一篇开始。大约一年前,CodeProject上发表了一个名为一个简单的月球着陆器克隆的小项目,它允许你在一帧景观中飞行并尝试着陆你的飞船。

本文提供的源代码是一个可执行文件,我将其组合起来,以帮助我测试和调试游戏的着陆阶段。它有一堆功能,你可以使用F1键调出图像中显示的groupbox来查看,恕我直言,这些功能不会出现在最终版本中。

debugging_controls.PNG

这里的水平滚动条和所有其他控件允许您生成所需的景观。

  1. 平均海拔:本质上是“海平面”,尽管没有实际的“海洋”。它是从大气层顶部(0)到下方地面(>0)测量的平均地面高度。
  2. 海拔偏差因子:对于“崎岖”或“起伏”类型地形(通过右下角的组合框设置),海拔偏差因子指定构成景观的丘陵和岩石的高度。
  3. 宽度:构成景观的景观单元格数量。
  4. 重力:你驾驶飞船时可以随时改变重力大小。
  5. 洞穴数量:将在您的景观上生成的洞穴数量。
  6. 洞穴平均深度:每个洞穴生成后将拥有的洞穴单元格的平均数量。

这些复选框是我用来帮助查找和解决沿途遇到的各种错误的额外功能。

  1. 显示折痕:景观蜿蜒环绕,因此最后一个单元格连接到第一个单元格。此选项在这两个单元格之间绘制一条垂直线。
  2. 显示飞船半径:这会在飞船周围绘制一个圆形,定义碰撞检测对象的边界。碰撞检测功能在确定飞船是否与景观碰撞时,不考虑飞船图像,而是用一个点和半径近似飞船的大小和位置,这定义了飞船占据的圆形区域。简单来说,就像一个球在外面弹跳。
  3. 绘制点:将绘制用于连接景观单元格的点,使其更容易看到你在哪里以及你正在撞击什么。
  4. 显示洞穴法线:洞穴墙壁的法线角度是每面洞穴墙壁将移动物体推开的角度。这些角度用于确定碰撞与墙壁形成的偏转角度,并随后计算飞船撞到物体后将采取的角度。勾选此项将在洞穴墙壁上绘制一束发状线条。
  5. 显示洞穴单元格:地表和洞穴之间最大的区别在于,在地表上,每个 `(x, y)` 笛卡尔点都直接位于一个景观单元格上方,没有混淆。然而,由于洞穴蜿蜒盘旋并相互重叠,笛卡尔平面上的任何点都可以逻辑上对应于任意数量洞穴中的任意数量洞穴单元格。因此,至关重要的是要记住您的飞船在每次 `move()` 结束时在哪里,否则它会无缘无故地传送,或者更糟的是,无法检测到碰撞,您最终会身处虚无的冥界,寻找回到游戏地图的路!而这不能发生。
  6. 绘制调试:方便!这是一个非常强大的工具。我使用的碰撞检测方案涉及围绕移动物体旋转游戏世界,并创建一个与屏幕成方形的矩形路径,然后再测试物体是否与任何东西发生碰撞。当处于调试模式时,此功能的作用是向您显示一个带有旋转碰撞检测系统的第二个窗体,以便您可以查看哪里出了问题以及需要修复什么。在我将其组合起来之前,我使用塑料赛璐珞片,将其贴在屏幕上,绘制景观,在我的桌子上旋转整个东西,并记下六位数的点和线位置,试图弄清楚发生了什么。我就是这样让景观工作的,但当我遇到洞穴时,我决定走图形路线,这真的很有回报! Debugging_Screen_Captures.png

除了那些HScrollBarsCheckBoxes,你还有一个comboBox,可以让你在三种不同类型的景观之间进行选择:RockyRollingCratered。你有一个按钮可以生成一个新的景观,还有两个radioButtons让你“飞行”或“调试”。classControls有一个标签按钮,当鼠标移到上面时会改变颜色。它有一个boolean Flag,点击时可以切换,你的程序可以像一个普通的布尔变量一样使用它。你可以在“颜色背景”复选框下方看到这些按钮的例子,它让你指定景观、天空和洞穴的颜色。

下面您将看到三种不同类型地形的屏幕截图

screen_capture_-_Rocky.PNG

screen_capture_-_Rolling_hills.PNG

screen_capture_-_Cratered.PNG

这是典型的洞穴景观

screen_capture_-_Cave.PNG

驾驶你的航天飞机

所有的飞行都用鼠标操作,除了推进器选择(喷气式飞机术语中的“档位”),它们通过数字1-5选择。F2会弹出你的飞船信息,如下所示。

ship_s_info.PNG

你在这里看到并呈现给玩家的值在调试游戏时有点误导。例如,如果你在地面上(或在上面飞行),你可以读取你的经度,那是子午线以东或以西的距离,但在内部,0东(零度东)是景观单元格数组中间的单元格,再往东一点就是数组再往下一点,直到你到达数组的末尾,然后再次循环回到`cell[0]`,或180西。海拔,虽然在内部在大气层顶部测量为零,但它是海平面以上的高度,所以玩家看到的海拔例如是1000米,但在内部,飞船的Y位置是`Y = intAverageAltitude - 1000`。速度在这里显示为大小和`circleControl`中的红色箭头,但在内部,速度存储在一个名为`ptVel`的双精度点中。左侧的`circleControl`有两个箭头,我刚刚提到的速度,以及飞船的角度。红色箭头指向运动方向的反方向,黑色箭头告诉你飞船朝向哪个方向。最后,引擎指的是你按下油门时推进器施加的力的大小。

当你飞行时,你会想把引擎设置成一个足够强劲的值,以便提供升力,但又不要太强劲,以免飞得比你想要的更远更快。目前,你的飞船只会弹跳,永远不会真正“坠毁”,就像真实碰撞会产生的火球那样,所以如果你坠毁了也没有真正的危险,但你可能会在尝试不坠毁时获得更多乐趣。在游戏的第一阶段,飞船会像你移动鼠标一样快速地原地旋转,稍后我会添加一个阻尼功能,让一些飞船旋转得比其他飞船快,从而使驾驶一些重型飞船变得更加困难,并保持隐形战斗机更具机动性。但目前,你的飞船会面向屏幕上的鼠标,除非你已经安全着陆。然后鼠标左键会启动引擎,你就可以出发了。记住,你可以将重力调整到你想要的任何值,还有我没告诉你的最后一个按钮“Halt ship”,它会立即停止飞船的运动。

你可以使用鼠标滚轮随意放大

screen_capture_-_zoom-in.PNG

或者你可以缩小(限制尚未定义,所以如果你走得太远,它只会看起来很奇怪)

screen_capture_-_zoom-out.PNG

除此之外,你不需要知道太多其他信息就可以开始,所以就带她出去兜兜风,然后把她带回经销商处,我们会为你准备好保险文件以供签署。

代码

但是如果我不解释我做了什么或如何让它工作,那么这篇文章就不会有什么意义,所以,既然你已经服用了晕车药并穿好了衣服,我们现在来谈谈代码。

表面

地表景观由名为classLandscapeCells的元素组成,这些元素在一个名为classLandscape的父类中以数组形式连接在一起。构成景观的每个单元格元素都包含一个classDoublePoint类型的点,它不过是一个使用双精度变量而非浮点变量的精巧版PointF。每个单元格的X分量在表面上均匀分布,只有从一个单元格到下一个单元格的高度变化因单元格而异,这使得确定任何物体在任何时候所处的单元格变得微不足道。它们的值以向下增长的正Y轴和向右增长的正X轴进行测量,因此,我在此项目(以及大多数涉及图形的其他项目)中的所有计算都使用我们高中三角学所学一切的变体来完成。通过简单地将所有计算中的单位圆顺时针旋转而不是逆时针旋转,我计划在纸上的所有内容都无需转换即可直接显示在屏幕上。正Y轴向下,我们没有任何麻烦。

在下图中,您可以看到几个景观单元格的示例,请注意,该类指向两个点,尽管该单元格实际上只有一个属于它自己的点,即它左侧的点。它还指向右侧邻居的左侧点。这样,当我决定制造地震和火山喷发时,每个单元格的点可以变化,而景观的凝聚力保持不变。我一直在使用的“荣耀版”classDoublePoint有一个锁定机制,这实际上派上了用场。通过结合使用一个静态布尔变量来锁定/解锁整个类,以及每个实例中的公共布尔变量,我可以锁定任何classDoublePoint的值,这样如果我的代码稍后尝试更改这些值之一,它会警告我,我就会意识到我在此过程中搞砸了什么(这被证明是一个非常有效的调试工具!)。

Landscape.png

classLandscapeCell还具有一个类型,以便程序知道它是一个简单的景观单元格,还是一个深邃、黑暗、不祥的恐怖洞穴的入口。在洞穴类型景观单元格的情况下,classLandscapeCell中的classCave指针不是null,而是实际指向在整个景观构建时生成的单元格。

洞穴

在最初的《火星任务》游戏中,洞穴与景观单元格相似,你可以通过观察飞船位置的Y分量来判断你所在的洞穴单元格,因为每个单元格都是笔直向下测量的,然后两侧向左移动一定量,向右移动另一个值。这有其缺点,因为它无法循环,但比我这里使用的方案更容易管理。

classCave也包含一个classCaveCell类型的数组,但是洞穴单元格并不垂直向下。相反,每个新单元格都沿与前一个单元格两侧角度法线方向向下移动,移动距离等于classLandscape中的常量K。然后,当找到下一个单元格的中心点时,新单元格会稍微向一个方向倾斜,并且两侧的点位于距中心点等距的位置。然后,下一个中心点根据上一个单元格的中心点和角度确定。参见下图,它并不像上面这段话听起来那么复杂。

CaveScape.png

在上图中,您可以看到第0个单元格通向景观,宽度恰好是K个游戏单位,并且垂直向下,因为每个洞口都将其ptRightptLeft点设置为相同的高度或Y值,而它们的X值相隔K个单位。我在这里使用这些变量是为了解释,但K的真实名称是

static public int intLandscapeCellWidth = 1000;

(在这个项目中我将继续设置为1024,因为这个漂亮的十进制数字与我的计算机不符,它更喜欢进行二进制位移而不是长除法) 并且不是拥有`ptRight`和`ptLeft`,单元格有三个`classCaveCellWall`类型的单元格墙,一个右墙,一个左墙,和一个底墙,底墙只在最后一个单元格中实际实例化,否则留作指向`null`。每个单元格墙要么有一个`ptMine`和一个`ptNext`,要么在底墙的情况下有一个实际的`ptLeft`和一个`ptRight`。此外,每个单元格墙都有一个`double`变量,指示与该墙垂直的角度。类似于景观单元格的`ptRight`指针指向其右邻居的`ptLeft`点,如上所述,洞穴单元格墙的`ptNext`实际上指向下一个洞穴单元格的`ptMine`,因此没有重复,我们可以安心地知道我们可以“重复利用,减少浪费,回收利用!”而且成本低廉。

碰撞检测

但是,如果程序无法判断您的飞船应该弹跳还是飞行,那么这一切都毫无意义。任何合格的太空学员都会告诉您,如果您不吃一顿丰盛的早餐,在学校努力学习并梦想着唐纳滋,您就无法成为一名宇航员。除此之外,您可能会惊讶地发现,要理解碰撞检测方案背后的数学原理,您真正需要的只是高中学到的相同基本几何、三角学和代数。尽管我花了数周时间才使其正常工作,但数学并不那么糟糕。让我稍微向您介绍一下,您就会明白我的意思。这涉及大量的修修补补,我不得不设计一些方案来观察实际发生了什么,因为我处理的数字又大又丑,所以深入研究数字既慢又乏味,但这里为您呈现的是简洁明了的版本。

在这个项目中,碰撞检测最重要的一点是,任何移动并可能与任何其他物体发生碰撞的东西,其所有的碰撞、移动、坠毁和飞行信息都集中在一个类中,这个类可以添加到宇宙飞船、宇航员、导弹或Bob在矿井里的午餐盒中。我这里说的是通用、便携且乐于被投掷、发射、投送或丢弃的应用类,它被称为

public class classCollisionDetectionObject

这里是我们存放classDoublePoint变量的地方,比如速度、当前位置、预定位置,以及其他一些东西,比如质量和半径,以及一个枚举类型,它跟踪物体“在”哪里,比如“洞穴”、“表面”、“基地”或“星际空间”。质量暂时不重要,因为我们只是假设撞击山坡的任何东西都比山坡小得多,以至于它们之间的质量差异是无限的,山坡根本不会动。但稍后,当鲍勃的午餐盒从悬崖上掉下来,坠向下面的珍妮时,我们不得不看看鲍勃阿姨的磅蛋糕是否足以在珍妮最喜欢的车上留下凹痕,或者她的月球漫游车是否将其弹开,她是否能得到一顿免费午餐。然而,目前我们只是看看我们的航天飞机是应该飞行还是弹跳(我们暂时不跟踪损坏,我们只是先弹跳一段时间,以后在本项目后续章节中再讨论)。

因此,我们有了一个强大的类,伟大而无与伦比的classCollisionDetectionObject,它通过引用传递给函数

public classCollisionResult moveLandscape(ref classCollisionDetectionObject cCDO)
classLandscape类中
public classCollisionResult moveCaveScape(ref classCollisionDetectionObject cCDO)

classCave类中。

目前,在这个测试场中,可执行文件中的计时器调用classLandscapemove()函数,该函数反过来确定控制权是否真正落入classCavemove()函数。有时这会立即发生,例如当所讨论的物体已经在一个洞穴内部时,正如为此目的定义的枚举类型所指示的那样。在其他时候,碰撞检测方案在classLandscapemove()函数中执行,并在那里计算出物体正在从地表移动到洞穴中,此时调用classCavemove()函数,并从该函数中获取产生的碰撞(或没有碰撞),保留在手边,以查看是否有其他东西阻碍物体到达洞穴口,并且在所有测试完成后,首先发生的任何碰撞就是物体停止的地方。我之所以说“首先发生的任何碰撞”,是因为在物体没有从一个环境过渡到下一个环境,而只是在空中飞行没有撞到任何东西的情况下,无限远处没有碰撞的默认“碰撞”会将物体放置在其预定位置。但是当物体从一个位置(例如地表)开始然后过渡到另一个位置(例如洞穴)时,洞穴必须返回一个“碰撞”报告,告诉程序物体不再在地表,而是在洞穴内部,无论它是否实际与洞穴墙壁发生碰撞。

当你飞入一个洞穴时,过渡非常平滑,因为在地表,你看到地表和几个洞穴单元格,你飞过它们,直到进入一个洞穴,然后你看到洞穴深处狭窄通道所允许的范围,以及在你潜入并穿过最初几个单元格之前,你再也看不到地表,而是沉浸在下面的洞穴深处。

我们开始正事吧。碰撞检测使用物体在笛卡尔平面上的当前位置,以及该物体在同一笛卡尔平面上的预定位置或“终点”。然而,在洞穴内部,洞穴景观的`move()`函数还需要知道物体在开始时正在哪个单元格中飞行,因此,当所讨论的物体已经位于某个洞穴内部时,这会包含在`classCollisionDetectionObject`中。为了计算物体预定轨迹是否会导致它与墙壁或景观地板发生碰撞,整个系统会围绕物体的终点旋转,使得所得的测试系统看起来完全相同,但起点和终点位于相同的Y坐标上,我们可以定义物体正在移动的矩形路径。然后,我们知道起点和终点在表面上的X坐标,将每个旋转后的景观单元格与这个矩形进行测试,以查看其中是否有任何阻碍物体运动。因为移动物体的位置是用一个点和一个半径来近似的,所以运动矩形不能完全测试物体的最终位置,所以我们还需要将最终的圆形位置与这些相同的墙壁进行测试,稍后会详细介绍。

在洞穴内部,程序首先尝试在距离起始单元格最近的单元格内找到终点,方法是沿任一方向远离已知起始单元格,并且不超出等于总行进距离(从起点到终点的直线)除以常数K(这不是它的真名,但K的经理坚持我们避免使用它的真实身份!)的单元格数量。如果未找到终点,则假定物体不仅与洞穴壁发生碰撞,而且实际上穿过洞穴,因此位于洞穴外部。发生这种情况时,从已知起始单元格向上和向下计数最大单元格数量,使用起始单元格到下一个单元格的“下降”角度作为比较标准来评估行进方向,然后从距已知起始单元格第n个单元格开始测试,并沿相反方向(沿运动方向)行进2n个单元格,并假设会有东西阻止物体到达它知道不在洞穴内部的点。当我写这篇文章时,我意识到如果我知道它行进的方向,我可能不需要返回n步,并且可以安全地假设它会从那里前进时被阻挡。我将在以后测试这个理论,并在下一篇文章中告诉你更多。

一旦我们定义了测试系统的限制,我们通过围绕终点旋转这个测试系统来创建运动矩形。然后,沿着物体正在行进的路径,遍历每个单元格,我们将它们旋转到我们的测试系统中,并查看它们是否穿过运动矩形。当它们穿过时,我们就开始计算!

接下来发生的就是所有的魔法。首先,我们(形象地)在起点和终点之间的中心水平线上,以与我们发生碰撞的墙壁/地板垂直的角度画一条线。在距离这条中心线R(移动物体的半径)的距离处,我们查看Y值,并确定该点是否在测试线上。如果是,那么我们就得到了在计算机太空游戏和太阳系模拟领域中所谓的“切向碰撞点”。因为,如果你还记得你十年级的几何、三角学或其他什么,一条接触圆周的线垂直于从圆心到这条线与圆周接触点的线。一条刚好接触圆周边缘的线被称为切线。从圆心画一条线,然后在其圆周上画一条垂直于它的线,你就得到了一个切线。因此,通过进行这个测试,我们可以看到“球”或移动物体是否“正好”撞到这条线(海绵宝宝方块裤已经失去了他对“方块”这个词的专属权利,在他律师重新提交给最高法院之前,我们保留在这里使用它的特权!),这样这个球就会整齐地从墙壁上弹开。稍后会详细介绍...

下面是一个巨大的超大尺寸复制品,显示了我们的球在切向碰撞中从墙壁上弹开。

Deflection_Angle_-_TANGENTIAL_Collision.png

在上图中,您可以看到蓝色的运动矩形和一条名为“测试线”的黑色线,它与红色圆形成切线。这个红色圆圈不是预期的终点位置,而是球被阻碍无法进一步移动的位置。您可以通过蓝色矩形延伸到图像边缘,即预期终点中心位置的事实来看到这一点。在此示例中,测试线与物体碰撞,穿过中心线的切线将碰撞的切点置于线上,因此物体正好撞击到线。知道测试线的法线角度和物体的近似半径,我们可以计算切向碰撞点,然后我们用它来将物体放置到其最终位置。程序实际做的是计算旋转测试系统中终点位置之间的距离,并跟踪最近的碰撞,直到所有测试都完成后,然后将物体放置在距游戏世界中原始未旋转位置相同距离处,并沿物体的实际运动方向。

在下面的图像中,您会看到一种不同类型的碰撞,即非切向碰撞。这些碰撞稍微复杂一些,但并非完全难以理解。我相信您会明白其中的逻辑,尽管我花了一个月才使其正常工作,但您无疑会欣赏解决方案的简洁性。就像我上面提到的,切向碰撞和非切向碰撞之间的区别在于线的切线实际上没有与测试线接触。让我们看看下面的图表。

Deflection_Angle_-_NON-TANGENTIAL_Collision.png

如果你看图片左下角的黑线,你会发现将其向上延伸到无穷远会穿过物体的终点位置。那是因为它没有足够深入矩形,无法在简单的切向碰撞中将其完全停止。我们需要做的第一件事是确定碰撞是否是切向的。为此,我们画一条远离圆心并(步骤1)找到它到达物体半径周长的点。你可以在右边的绿圈上看到这一点。从延伸的测试线延伸并穿过该圆心的灰线在图像中标记为“点法线交点”的点处与绿圈相交。这个点的Y坐标告诉我们它实际上在测试线上方,并且测试线没有与它接触。这就是我们知道发生非切向碰撞的原因。接下来我们需要做的是计算水平中心线和测试线的顶角之间的差值(步骤2)。步骤3:知道dyCollision(如上图所示)和物体的半径,我们就可以计算出碰撞角,并加上Pi来得到偏转角(偏转角与切向碰撞中的法线角相同),然后我们将用它来偏转碰撞后物体的轨迹。目前,我们只想将物体放置在最左侧、运动矩形内部、最近障碍物右侧的位置。知道dy和R以及碰撞角,计算dx是一个简单的问题,从测试线终点的X坐标中减去该值,并计算与起始位置的距离,然后看这是否是最近的碰撞。

太棒了!这些你在高中也学过!

圆形碰撞

上述计算用于当物体的运动矩形与测试线碰撞时。但在某些情况下,终点位置超出游戏世界的边界,距离小于圆的半径。当这种情况发生在运动矩形之外时,我们称之为“圆形碰撞”或“终点碰撞”,国际工程标准和碰撞命名委员会仍在讨论这个问题,但为了本文的方便,我们将简单地称之为“圆形碰撞”。`classMath`有一个静态函数,用于计算一条线是否与一个圆碰撞,如果是,则在哪个交点。

public static bool LineIntersectsCircle(classDoubleCartesian ptCircleCenter, double dblRadiusCircle, classDoubleCartesian pt1, classDoubleCartesian pt2, ref classDoubleCartesian ptIntersection)

它需要通过引用传递`classRadian`或`classDoublePoint`,并返回一个布尔值,仅当直线与圆相交时才设置为true。因此,测试与物体终点位置的碰撞只需发送圆的描述以及直线的两个端点,它就会告诉我们是否存在碰撞以及碰撞发生在哪里。这里感兴趣的是圆/线交点,因为它被此函数定义为一条垂直于测试线并穿过圆心的线与测试线的交点。无论圆和线是否实际碰撞,此值都会被设置,并被此程序用于计算偏转角。

说到这里……

偏转角

为了计算偏转角,我们需要知道两件事:

  1. 原始轨迹角度
  2. 与我们物体接触的平面法线角度

但你会说“非切向碰撞”不会接触平面!我相信这是你们所有人首先想到的,而且你是对的。但对于非切向碰撞,我们只需将pi加到碰撞角上,然后当我们把信息传递给我们可靠的朋友Mr.DeflectedCollisionAngle()时,就把它忘掉。

public static void DeflectedCollisionAngle(double dblNormalToLineOfCollision, ref classMath.classRadialCoor cRadVel)
{
///rotate the system about the point of collision until the Normal is vertical pointing upwards
double dblNeededRotation = classMath.cleanAngle(1.5 * Math.PI - dblNormalToLineOfCollision);
double dblRotatedTrajectory =classMath.cleanAngle( cRadVel.angle + dblNeededRotation);
double dblResultantAngle = 0;
        
dblResultantAngle = classMath.cleanAngle(-dblRotatedTrajectory);
        
double dblMagnitudeFactor = Math.Abs(Math.Cos(dblResultantAngle)) * .7;

cRadVel.angle = classMath.cleanAngle(dblResultantAngle - dblNeededRotation);
cRadVel.radius *= dblMagnitudeFactor;
}

这实际上是一个静态辅助函数,其中有几个函数负责大量的旋转和数组操作。我们在这里所做的是旋转这个系统,使`NormalToLineOfCollision`值垂直,并且我们的球必然向下反弹,朝着一条需要弹起的假想水平线。然后就像计算高尔夫球从你十三岁时制作的迷你高尔夫模拟器边缘弹起一样简单(或者我是唯一一个在苹果][电脑上修修补补图形度过夏天的人吗?),计算旋转系统中角度的变化,并将其添加到描述角度和大小的参考径向坐标中。这里值得注意的是,球不会像你在沃尔玛找不到的`Flubber`球或你当地药店的任何`SuperBall`那样弹跳。这里的想法是,一个以锐角撞击墙壁的物体会掠过表面并继续前进,被偏转但其预定方向未受扰。然而,如果你正面撞向山坡,那么你的飞船就不会那么有弹性,撞击会将其压扁并显著降低其速度。这里的计算是一个简单的`Cos()`函数。因为系统是旋转的,法线朝上,所以正面碰撞看起来就像垂直下落。接近+或-pi/2的角度的`Cos()`接近零,而接近零或pi的锐角的`Cos()`接近一。因此,加上一个0.7的下降,并将其乘以`Cos()`,你就得到了一种巧妙的方法来降低那些糟糕碰撞的速度,同时保持了意外弹跳的强劲方向。

糟糕的弹跳

但事情并非总是完美。我仍在想办法修复这个问题,但时不时地你会撞到墙壁并说“那次弹跳很奇怪”。我相信这发生在球直接弹到某个点上,碰到边缘,然后以错误的方式做出奇怪的古怪事情。高尔夫中会发生,冰球中会发生,天哪,这里也会发生……这种情况不常发生,我有一个计划,在两条法线几乎平行的线之间的边缘发生非切向碰撞时,将使用线的法线作为偏转角。我会对此进行调整,但你们必须知道,此代码随附的保修条款保护其设计者免受任何此类糟糕弹跳的影响。仔细阅读细则,伙计!

何时需要推动

有时候你只想让东西滑动。所以你推它们一把。如果你的飞船停在山坡上,它不应该只是呆在那里。它应该滑下山坡到底部。我之前提到过的偏转幅度对此很有帮助,但如果飞船停滞不前,重力会把它拉下来,向远离墙壁的法线方向推一下,它会稍微侧向移动,然后掉下来,被推,掉下来,被推,如此循环,直到它到达底部。碰撞应该把物体精确地放在它所属的位置。理论上是这样。但是,不幸的是,情况并非如此,有时你必须确保你的游戏世界不会因为一些未检测到的奇怪碰撞而崩溃,导致物体卡在不属于它们的地方。无论如何,`Push()` 函数是为了确保任何穿过地形的飞船最终都能再次回到地表,尽管我已经修复了这些问题,足以愉快地说它不再是一个真正的问题,但我保留它只是为了怀旧,很高兴它帮助我度过了调试一个棘手问题时的难关。然而,有时一堵墙的推力会导致飞船穿过相邻的墙壁一半。这可能是一个问题,因此在构建时会检测沿地形和沿洞穴墙壁的每个此类情况,以便每堵墙和每个地形单元格都指向它们推动的单元格。当这两堵墙来回推动物体几次后,程序就会将移动的物体`楔入`它们之间。这很容易通过在两堵墙之间划一条分隔线,并进行一些三角函数计算来确定楔入物体需要与这个分隔线上的公共点相距多远来完成。

您可以在classLandscapeWedge()函数中看到这一点。我原打算在classCave中实现同样的功能,但发现没有必要。

接下来呢?

我已经让太阳系模拟器运行起来了。我从美国宇航局在互联网上发布的各种无人任务中下载了大量的图像,并将它们插入到一个classRotatedImage中,该图像每转四分之一圈旋转90次,这样你就可以看到行星和卫星在围绕其主星运行时自身也在旋转。太阳系中有数百颗卫星,只有少数有自己的图像,其余的都使用相同的几个默认图像,但看起来已经很不错了。接下来我将把景观添加到太阳系模拟器中。然后我必须立即着手处理游戏保存/加载功能,如果这个项目以后值得玩,这些功能至关重要。所以我想这就是下一步。

待稍后处理的是资源、宇航员及其专长,以及他们利用这些专长可以进行的操作。例如“压缩大气”或“挖掘资源”,或“建造无线电”或其他任何东西。当我建立并运行一个和平时期系统时,我们将看到一个真实的战争游戏,其中有你在**Defender**类型的游戏设置中驾驶的战斗机,或者在洞穴中进行**Vanguard**,有炮塔火力、外星飞船、导弹、炸弹以及所有其他需要你建造武器、研究技术、开采资源和训练你的飞行员和工程师的东西。我估计我将为此工作一年。每两个月或不定期发布一篇文章,然后我们看看进展如何。

致所有太空学员,“瞄准月亮,你也许能射中星星!”

火星任务 (2) : 探索太阳系

© . All rights reserved.