Night Stalker






4.26/5 (9投票s)
一款使用Sprite-Editor图形、用于AI控制角色寻路的路径点(way-points)以及迷你地图碰撞检测方案的C#游戏。
引言
Mattel Intellivision 主机上的原版 Night Stalker 游戏非常棒!我一直玩到手柄坏掉,这是我第二次重制这款游戏,第一次是用 Visual Basic 6.1,使用 picture box 来制作角色动画,这次是用 C# 结合我在之前一篇文章中介绍的 Sprite Editor。这次,我在创建游戏中的精灵(sprites)时做了一些自由发挥,并且修改了地图,纯粹为了好玩,但本质上还是同一个游戏。
就代码而言,程序员可能会对寻路方案(way-points scheme)感兴趣,该方案通过一个表格来告诉AI角色如何从地图的一端到达另一端,而不是每次都计算路径,以提高性能。还有一个专门用于碰撞检测的迷你地图。
所以,如果你只想怀旧一下旧的喜爱游戏,不妨看看这个,但如果你认为寻路点和迷你碰撞检测地图可以帮助你的代码,那么你就有理由同时做这两件事。
原版
Intellivision 的 Night Stalker 有一些追随者,部分原因是游戏的黑暗性质和令人毛骨悚然的心跳音效,这种音效影响着所有听到的人,但也仅仅是因为它是一款很棒的游戏。由“机器人专家”Steve Montero 设计和编程,他编程 Night Stalker 是自然而然的,因为游戏中的主要反派都是机器人。基本玩法是这样的:你被困在一个迷宫里,机器人不断地从一个机器人巢穴中出现。你杀得越多,它们出现的越多,它们也越擅长追踪你。同时,你只有一把六发左轮手枪,但你必须在迷宫中搜寻才能重新装弹,而且游戏节奏从不懈怠。还有蝙蝠和蜘蛛来补充在你身后追逐的攻击者,虽然它们只会让你昏睡,但最好还是保持清醒,哪怕再多一会儿。
隆重推出……杰西卡·兔女郎
我没有使用当时我觉得很棒的旧图形,而是决定使用上面提到的 Sprite-Editor 创建一套新角色。玩家角色正是你最喜欢的,而且一如既往地性感,杰西卡·兔女郎。我们可以编造一个故事,说罗杰·兔被陷害了,而杰西卡是唯一的办法来救他,但我当地的图书馆没有《谁陷害了罗杰·兔?》的拷贝,所以我没有得到任何音效,虽然我本可以尝试 YouTube,但我们就称之为把罗杰留在家里,带杰西卡出去的借口吧。他一直以来都是个电灯泡。
除了新角色以及玩家和更糟糕的敌人可以进行对角线射击之外,这几乎和我记忆中的一样。所以,大胆去玩吧,试试看**不要**玩得开心。
代码
什么是寻路点(Way-Point)?:*基础*
寻路点(way-points)的概念在我一篇名为《Battlefield Simulator》的文章中曾略微提及,但没有提供相关的源代码。根据维基百科,寻路点是“路线上任何可以轻松识别的地图参考点”。我们就用这个定义,因为它在这里的意思差不多。由于这款游戏完全在一个不变的地图上进行,这些寻路点可以在设计时由程序员创建。对于这款特定的游戏,寻路点的使用可能有点大材小用了,因为地图相对较小。但是,当你处理巨大的地图,并且需要实时让你的角色沿着正确的路径移动时,寻路点是实现这一目标的方法之一。
什么是寻路点(Way-Point)?:*核心*
Night Stalker 使用三种不同的地图,它们都反映了同一个迷宫。第一个是玩家在屏幕上看到的地图,它仅用于图形输出;第二个是寻路点地图,专门用于创建寻路点数据库。查看源文件中资源目录中的寻路点地图 MapsWP.png,你会发现它看起来是游戏地图的黑白版本,有一些黄线和几个红点。然后,如果你放大图像,你可能会注意到最左上角的像素与红点的颜色相同,而它旁边的像素与墙壁的颜色相同。这是因为这两种颜色被用来识别构成墙壁的红色寻路点和黑色障碍点。
此时,寻路点仅存在于图像中,必须通过首先扫描整个位图将其传输到内存中,直到找到所有红点,并获得游戏中所有寻路点的完整数组。接下来,'rebuild way-points' 算法,在开发阶段运行,它会查找每个寻路点附近的寻路点,并创建一个列表,包含在预先设定的给定范围内内的所有邻居,并将它们保存在一个如下定义的结构类型的单独数组中,并恰当地命名为 'neaghbours'。
public struct udtShortestPathToWayPoint
{
public classWayPoint wp;
public int index;
public string strPath;
}
有了这些信息,AI角色就可以找到自己当前所在的寻路点,并知道通往任何邻近寻路点的路径。而且,由于AI角色的所有行程计划都以寻路点为参考,它们真正需要知道的是,当它们想去地图的另一边时,下一步应该访问哪个邻居。这里的问题是,虽然它们知道如何从例如底特律到安娜堡,但它们不一定知道如何到达杰克逊,即使它们只需要继续开车。所以,它们不去任何地方,而是坐在车里试图决定是去温莎、安娜堡还是庞蒂亚克,因为它们不知道从底特律去杰克逊的路。
现在,如果你只有局部地图告诉你如何从一个城镇到附近的城镇,而你想横跨全国,那么你需要一张表格,你可以在表格的顶部边缘交叉引用你所在的城镇名称, along the side edge of the table with the name of the town you want to go to,然后读取你需要到达的下一个城镇的名称。这就像现在地图上的表格一样,告诉你两个城市之间的距离,但不是从A到B的里程数,而是告诉你从你现在所在的地方该去哪里。
Night Stalker 中的 nextWPTable[,]
数组中包含的整数值指的是当前寻路点邻居列表的索引。考虑取自 Night Stalker 游戏 `classRobot()` 类中的代码,其中机器人需要找到通往其最终目的地正确寻路点的路径。
if (udrMove.WPnext != udrMove.WPtarget)
{
udrMove.strPath += udrMove.WPnext
.neaghbours[
cLibWPs.nextWPTable[udrMove.WPnext.index,
udrMove.WPtarget.index]
].strPath;
classWayPoint WPNext
= udrMove.WPnext.neaghbours[
cLibWPs.nextWPTable[udrMove.WPnext.index,
udrMove.WPtarget.index]
].wp;
udrMove.WPfrom = udrMove.WPnext;
udrMove.WPnext = WPNext;
}
这段示例代码演示了对该表的两次引用。第一次是设置“N”、“E”、“S”、“W”方向的路径,这些路径列在一个名为 `strPath` 的字符串字段中。udrMove
结构体的 `WPnext` 字段是 `classWayPoint` 类型,并且使用从 nextWPTable[,]
获取的索引来引用这个寻路点的邻居列表,其中 `WPnext.index`(我们正在查看的寻路点在 `WPs[]` 数组中的索引)作为二维数组的列分量,`WPtarget.index`(目标寻路点在 `WPs[]` 数组中的索引)作为相同二维 nextWPTable
的行分量。这个字符串告诉机器人要沿着什么路径逐像素地移动,以到达通往最终目的地的下一个寻路点。
第二次引用将一个临时的 `WPNext` 设置为沿途的同一个中间寻路点,该寻路点现在将是机器人后续代码执行时将使用的寻路点。
为了帮助阐明这个概念,请看下面的图片,它显示了 6x6 示例表中路径 1-4-5 和 2-3-6 所需的条目。请注意,虽然 Night Stalker 中的条目是整数值,对应于角色前往某个最终目的地所需的下一个寻路点的索引,但这个示例使用寻路点的“名称”来进一步简化示例。
蓝色示例从 WP #1 开始,想要去 WP #5,所以 AI 查看表格条目 (1,5) 并读取 '4',这意味着在前往 #5 的途中,它必须首先经过 WP #4。然后,当在 WP#4 上时,AI 会将当前 (4) 与目标 (5) 进行交叉引用,并查看表格条目 (4,5) 读取 '5' 是下一个也是最终的寻路点。同样,对于橙色示例,从 2 开始并想要到达 6,AI 查看 (2,6) 并读取 '3',然后查看 (3,6) 并读取 '6'。
迷你碰撞检测地图
如上所述,Night Stalker 的碰撞检测方案完全运行在一个游戏地图的微缩版本上。使用一种算法将原始地图缩小,该算法对主地图的 5x5 区域的像素进行平均,然后将一个像素设置为原始地图中最重要的颜色。这是在创建输出图形地图之前使用黑白地图进行的,所以迷你地图主要由黑白像素组成。我说主要是因为还有其他碰撞颜色被用来增强游戏,但最简单的看法就是黑白,我稍后会更详细地解释其他颜色。
微缩碰撞检测地图的概念利用了较小尺寸的像素数量减少,来进行逐像素的碰撞测试,从而大大加快了碰撞测试的速度。结果是一个最多偏差五像素的近似值,对于这款游戏来说被认为是可接受的。由于所有 AI 角色都沿着寻路点之间的轨道行进,因此唯一需要检测的墙壁碰撞是子弹对象和玩家角色。为了做到这一点,会创建一个包含对象描述的结构,碰撞检测算法可以理解。
public struct udtCDInfo
{
public Size sz;
public Point ptRelPos;
}
size 字段 (sz
) 以像素为单位描述了对象在微缩碰撞检测地图中的大小,point 字段 (ptRelPos
) 描述了对象中心相对于该矩形 (sz
) 左上角的相对位置。下面是一个典型的例子
udrCDInfo.ptRelPos = new Point(4, 4);
udrCDInfo.sz = new Size(7, 14);
这将杰西卡的中心放置在一个大小为 (7,14) 的矩形上的 (4,4)。在调试器运行期间查看这些选项的设置在 formNightStalker
的
public formNightStalker()
函数中,并在下面提供
/// debug start
cLibCD.bolDrawCDInfo =
System.IO.Directory.GetCurrentDirectory().ToUpper().Contains("DEBUG");
//mnuDebug.Visible = System.IO.Directory.GetCurrentDirectory()
// .ToUpper().Contains("DEBUG");
if (false && cLibCD.bolDrawCDInfo)
cLibCD.drawCDOnMap(ref bmpMap);
/// debug end
其中 cLibCD.bolDrawCDInfo
会在所有子弹、机器人和杰西卡周围画上黄色的矩形,而调用 cLibCD.drawCDOnMap(ref Map)
会在迷你 bmpCD
地图上出现碰撞方块的地方画上小的 5x5 像素的黄色方块。
陷阱和碉堡破坏者
CDmap
包含的其他颜色用于“陷阱”,这些陷阱只会在杰西卡从蝙蝠或蜘蛛的昏睡中醒来并“晕眩”一段时间后短暂地引起碰撞。这些由 drawCDOnMap()
函数绘制成“绿色”。
这是 CD 地图的局部放大视图。
创建和使用此碰撞检测方案的唯一真正困难在于编写“碉堡破坏者”子弹,这是更高级、更致命的敌人发射给杰西卡的。我不知道如何做到这一点,然后决定使用单独的颜色来描述碉堡的碰撞像素。它们可以在游戏资源目录中的 bmpCD 文件中看到。新游戏开始时,游戏时间 CD 地图会被这张位图的新副本覆盖。查看迷你地图,你会发现碉堡的左、底、右墙壁都是深灰色,而碉堡内部是浅灰色。浅色内部阻止所有子弹穿过,但不会阻止杰西卡移动。而深灰色则阻止杰西卡从侧门逃脱,迫使她从碉堡顶部的“屋顶”离开。
然后,当更糟糕的敌人带着碉堡破坏者子弹来对付杰西卡时,碉堡必须被摧毁。为此,碰撞检测首先检测碉堡破坏者与碉堡之间的碰撞,然后调用 damageBunker()
函数。
DamageBunker()
是所有发生的地方。resources 目录有一个迷你碉堡墙图像,一个游戏大小的碉堡图像,在运行时,我们有一个 mini CD 地图的 bmpCD 副本。在 damageBunker()
开始时,就知道损坏的位置,所以唯一需要做的事情就是实际损坏碉堡。这很简单:在碰撞发生的地方用一个预设大小的白色椭圆填充,然后继续。
但如果你这样做,它就不会向玩家展示杰西卡如果留在她的碉堡里,在它遭受所有损坏后会处于多么危险的境地。所以,真正的问题在于向玩家展示碉堡正在被摧毁。为了做到这一点,我们首先将 bmpCD 地图的碉堡剪切到一个临时位图上,这个位图比原图宽 2 像素,高 2 像素。然后,因为我们需要创建一个掩码来表示剩余的损坏的碉堡(来自之前的撞击),我们必须将墙壁**和**内部变成实心的均匀颜色。但是,如果墙壁(深灰色)已经损坏,那么在一个点进行填充并不能保证填充会完全覆盖损坏的碉堡墙壁的剩余部分。因此,为了保证填充操作能够捕捉到墙壁断开的碎片,会在我们刚刚从迷你 bmpCD 游戏地图上剪切出的整个临时副本周围绘制一个一像素宽的与墙壁颜色相同的边框(深灰色)。然后,我们进行填充,将深灰色墙壁填充成浅灰色内部颜色,并将其重新复制到一个较小的位图中,该位图窄 2 像素,短 2 像素,只剩下损坏但现在颜色均匀的迷你碉堡。
现在,知道碰撞发生的位置,我们通过在该位置用白色椭圆填充来对这个复制的、颜色均匀的地图进行当前子弹的伤害,并将损坏着色为“白色”。
这个碉堡现在需要用于两个目的
- 作为创建更大掩码的源,该掩码将在损坏的碉堡处放置一个白色的“孔”。
- 作为迷你 CD 碉堡墙壁的掩码,我们需要重绘阻碍杰西卡进入碉堡的碰撞墙壁,然后将这个迷你碉堡图像粘贴回我们用于所有墙壁碰撞的迷你 CD 地图上。
让我们来看第一个问题:让玩家看到损坏。我们有一个微缩的、颜色均匀的损坏碉堡图像。为了首先将其调整到游戏大小,我们会逐像素扫描它,对于描述未损坏碉堡的每一个浅灰色像素,掩码位图的游戏大小碉堡都会获得一个新的 fillRectangle
绘制在其上。这个填充矩形可以是任何颜色,只要整个掩码位图使用相同的颜色,我在这里称之为 'clrBunkerInterior
'。然后,当这个位图完成后,它会调用 MakeTransparent()
,使用相同的 clrBunkerInterior
颜色来替换默认的“白色”,使这种浅灰色透明,这样当它随后覆盖在完整的游戏大小的未损坏碉堡图像副本之上时,只有白色会被绘制出来,我们就得到了一个游戏大小的损坏碉堡图像。
这个游戏大小的损坏碉堡图像在每个游戏周期的动画调用期间都会使用白色作为透明色进行透明化处理,这样我们只看到剩下的东西,而白色的“孔”允许杰西卡通过墙壁上的任何孔逃脱。你可以在下面的代码摘录中看到这一点
// resize the CD-bunker image to real-map size (5x bigger)
Bitmap bmpRealMask = new Bitmap(cLibHelper.bmpBunker.Width,
cLibHelper.bmpBunker.Height);
SolidBrush brsClrDoor = new SolidBrush(clrBunkerDoor);
SolidBrush brsWhite = new SolidBrush(Color.White);
using (Graphics g = Graphics.FromImage(bmpRealMask))
{
g.FillRectangle(brsWhite,
new Rectangle(0,
0,
bmpRealMask.Width,
bmpRealMask.Height));
for (int intX = 0; intX < bmpCDBunker.Width ; intX++)
for (int intY = 0; intY < bmpCDBunker.Height ; intY++)
if (bmpCDBunker.GetPixel(intX, intY) == clrBunkerDoor
|| bmpCDBunker.GetPixel (intX, intY) == clrBunkerWalls)
g.FillRectangle(brsClrDoor,
new Rectangle((intX) * 5,
(intY ) * 5,
5,
5));
else
g.FillRectangle(brsWhite,
new Rectangle((intX ) * 5,
(intY ) * 5,
5,
5));
}
// make the bunker-door color transparent
bmpRealMask.MakeTransparent(clrBunkerDoor);
using (Graphics g = Graphics.FromImage(cLibHelper.bmpBunker))
g.DrawImage(bmpRealMask, new Point(0, 0));
将损坏的墙壁绘制到迷你地图上需要用迷你损坏碉堡的透明版本(颜色均匀)来掩盖迷你墙壁,并将此副本覆盖在未损坏的迷你墙壁图像之上,只绘制碉堡的白色(损坏部分)到墙壁上。然后,将损坏的墙壁图像复制到损坏的碉堡之上,然后再粘贴到迷你 bmpCD 地图上。
加载器锁定?没问题
我不确定如何处理调试器每次运行使用 DirectX 音频的项目时都会发出警告的“LoaderLock
”问题,但第二次按 F5 启动项目后即可解决,并且可执行文件没有任何问题。显然,有一种方法可以让你的 C# 项目忽略这个 loader-lock 警告,但我还没有找到。
杰西卡坏?她说她只是长成这样!