线索游戏(C# 2010)






4.93/5 (55投票s)
你是嫌疑人!
妙探寻凶游戏
根据 Idea Finder 的记载,ClueDo 游戏由 Anthony Ernest Pratt 发明,并于1947年在英国获得专利,后被 Waddington Games 收购,1949年,美国版权被 Parker Brothers 买下。这款“谁是凶手?”游戏从此成为世界各地雨天和乡村壁橱里的常客。这款谋杀谜题游戏曾推出过多个PC版本,在我错过上次在商店货架上看到的最后一版后,又找不到另一版,于是我去了最近的旧货店,花2美元买了一份二手棋盘游戏,大约一个月前开始了这个项目,现在看起来相当不错。
玩游戏
如果你只想玩你最喜欢的旧游戏,并且已经安装并运行了 Microsoft 的 C#2010,那么你所需要做的就是下载上面的源代码,然后在谋杀案发生当天进入博迪先生的豪宅。你可以通过提示选择你喜欢的任何嫌疑人来玩。
这是一个单人游戏,所以其他玩家都由电脑的人工智能控制,你可以通过菜单“选项->设置-AI”来设置。
一旦打开此菜单,您可以通过单击角色姓名来循环选择不同的 AI 级别。如果您也能对自己的“智能”做同样的事情,那会很棒,但即使您扮演芥末上校并将芥末上校的 AI 设置为“天才”,您仍然需要自己完成所有工作。
还有一个叫做“自动进行”的功能,它是“速度”控制的一部分,你可以通过菜单“选项->速度”来访问。
在默认设置下,**自动进行**将以与您与旁人围桌玩游戏时类似的方式推进游戏,使得难以跟上并仔细做笔记。您可能希望在第一次玩时将其关闭,并手动点击“确定”按钮以推动游戏进行,同时您进行侦查。这就引出了游戏板右侧的**笔记本**。
记笔记
在上面的图片中,你会注意到一些事情。首先,左边的名字是游戏牌的名称,分为嫌疑人牌、武器牌或房间牌。看看图片的左边缘,你会看到你的嫌疑人持有的三张牌;在这个特定的例子中:左轮手枪、温室和台球室。名字的右边,你可以看到六个彩色方框。这些方框由用户控制。你可以右键或左键点击它们,以循环切换每个彩色方框可能持有的四个值:**X**、**勾选**、**?**或**空白**。每个嫌疑人都有一个颜色,你的嫌疑人的方框在游戏开始时会自动设置,以帮助你。X表示该嫌疑人绝对**没有**那张牌,勾选表示他有,而空白表示你一无所知,?表示你认为他可能有。这应该足以帮助你解决谜题。
我们来看看棋盘。
你可以清楚地看到9个房间和大理石瓷砖。在上面的图片中,斯嘉丽小姐和芥末上校两位角色已经完成了他们的开局,并离开了各自的起始位置走向房间。游戏的目标是从一个房间移动到另一个房间,提出“建议”,并让其他玩家(和你一样的嫌疑人)向你展示他们的牌。当你弄清楚他们都持有什么牌时,你应该就知道哪三张牌被从其余牌中分离出来并现在放在棋盘中央,然后赢得游戏。
控制面板位于笔记本的右侧。
它由几个脉冲按钮组成,供您“掷骰子”、“提出建议”或“结束回合”,这些都一目了然。这些按钮下方是骰子盒,骰子(那个上面有点的方形东西!)在这里掷出。骰子盒下方是您的终极按钮“指控”,这是让某人真正麻烦的全部所需。
提出建议
在你的回合开始时,你可能会立即有机会提出建议(如果其他玩家将你召唤到一个新房间以提出他们的建议),或者你将不得不离开一个房间。无论哪种情况,只要“建议”按钮启用,你就有权提出新的建议,每次进入一个房间都会发生这种情况。抓住每一次机会提出建议可能是一个好主意。
一旦你点击了“建议”按钮,你的游戏板将如上图所示。你在提出建议时必须建议你所在的房间,所以你唯一的选择是“谁”和“如何”关于它。要做出选择,你只需点击嫌疑犯的脸部和你想要建议的武器图像,当你决定完毕后,图像将旋转到位;当你准备好时,点击出现在“气泡”底部的“确定”按钮。
此时,“建议”需要被“证明”。所以你“左边”的玩家(或者至少如果你坐在桌子旁,他会在那里)将不得不通过向你展示你所要求的任何牌来尝试反驳你的建议。如果他们没有牌,他们会告诉你
如果她确实有一张牌,她会向你展示她有什么,你的建议就到此为止了,但每当其他玩家提出建议时,只有他们和反驳他们建议的人知道展示的是哪张牌。所有其他玩家都必须通过推理和逻辑来猜测那张牌是什么。
在游戏过程中,你会多次遇到别人提出建议,而轮到你试图反驳。当你无法反驳建议时,会出现一个横幅告诉你“你没有牌”,或者如果你能反驳,同样的横幅会显示你有一张牌,并向你展示你可以展示哪些牌。在这里,如果勾选了自动进行设置,那么如果你没有牌或只有一张牌,游戏将继续进行而无需你干预,但即使你没有关闭自动进行选项,当你有不止一张被建议的牌时,游戏也会停止并等待你选择你想向提出建议的玩家展示哪张牌。
秘密通道
在掷骰子之前,首先考虑是否要使用“秘密通道”。秘密通道位于角落房间,让你无需掷骰子即可穿过棋盘。因此,如果你在一个房间里,想去对角线上的房间,那么你就会想使用秘密通道。要做到这一点,你需要在掷骰子**之前**点击你所在房间中出现的彩色方块。下图显示了芥末上校回合开始时的棋盘,他还没有决定是掷骰子还是使用秘密通道。你仍然可以看到角落里的黄色方块,表明芥末上校可以选择使用秘密通道,从休息室前往温室,而不是掷骰子。
我们来掷骰子!
要掷骰子,请点击“掷骰”按钮。当掷骰动画停止时,游戏板将通过在所有大理石瓷砖和门上放置彩色方块来显示你可以移动的位置。
上图显示了芥末上校在游戏开始时掷出6点时的移动选项。只需点击游戏板,芥末上校就会移动到你告诉他的任何位置。
我指控!
当你提出足够多的建议并完全弄清楚后,就可以提出你的指控并结束游戏了,但要小心……如果你指控错误,你就会被淘汰,不得不看着人工智能,直到其他侦探破获博迪先生的神秘谋杀案。要指控,你只需点击右侧骰子盒下方的“指控”按钮即可。
这里有点不同的是,你可以随意指控任何地点、任何武器的任何人。所以你实际上不需要在任何特定的房间,这意味着你可以选择你认为的犯罪现场。上图显示了指控选项,你可以像提出建议时一样点击相应的图片进行选择。要取消指控,当它显示“取消指控”时再次点击“指控”按钮,这样你就可以退缩,让别人当英雄。或者,如果你有把握,点击屏幕中央那个巨大的红色脉冲按钮,看看你是不是高手!
代码
大理石化过程
这个项目写起来很有趣,而且一路都进行得很顺利,但第一个问题是棋盘本身。这个游戏中出现的所有图像都是从互联网下载的(我唯一真正自己画的是那块奶酪!)。但是,我找不到任何棋盘本身的图片可用于游戏,因为图形动画要求每块瓷砖都完美对齐(如果我想走捷径的话),以便于将嫌疑人的图像放置在正确的位置。因此,我不得不自己画棋盘……所以,好吧,棋盘**和**那块奶酪。这实际上比你想象的要难。对于大理石瓷砖,我把所有空白瓷砖涂成黄色,然后一次性将几块不同的瓷砖涂白,同时将整个透明地图(白色瓷砖)剪切并粘贴到一块巨大的大理石板图像上,每次涂白几块新瓷砖,并在粘贴最新图像时拾取大理石,直到所有瓷砖都大理石化。如果将它们全部涂白并将棋盘粘贴到一块大理石板上,那就不会那么酷了,因为那样整个地板就会只有一张大理石图像。现在这样好得多。
移动棋子
为了跟踪嫌疑人可以移动的方块以及每个房间的尺寸,我使用MS-Paint制作了一张微型地图,描绘了游戏的楼层,其中每个像素都相当于一个方块。这是图片
可能还有更好的方法可以做到这一点,但这样做让我能够更好地可视化游戏数据,而不是直接以数据库格式将其写入代码。白色像素是游戏板上的“大理石方块”,红色方块是房间,黑色方块是不可进入的,蓝色方块是门。这里困难的地方在于“编程”图像中的门像素,使其包含“RoomID”和“dirDoor”等信息。要做到这一点
blue is not quite so blue, but neither should you
a poem by Christ Kennedy
(我已向CodeProject申请成为他们的在线桂冠诗人,但尚未收到回复;如果我的所有粉丝联名请愿,将大大增加该网站最终拥有其真正需要的合适诗人的机会。)
迷你地图上的蓝色像素确实包含这些信息。蓝色像素中的蓝色,像所有像素一样,由四个字节大小的变量定义:R(红色)、G(绿色)、B(蓝色)和A(Alpha)。虽然蓝色门在B分量上都具有相同的255,但R和G值分别存储了出口方向和房间ID值。你可以在下面显示的classSuspect函数中看到这一点
/// <summary>
/// uses the 24x24 pixel bitmap bmpClueFloor to set up 2d array describing the game map.
/// each pixel represents a square on the board.
/// the pixel color components (Red, Green, Blue) determines the type of tile.
/// Marble Tile (corridors) : R=255,
/// G=255,
/// B=255 (white).
/// Illegal square : R=0,
/// G=0,
/// B=0 (black).
/// Room : R=255,
/// G = ROOM ID NUMBER (study =0,
/// hall =1, ... billiard room = 7, library =8),
/// B=0 (variant of Red).
/// Door : R=egress direction (0=north, 1=east,
// 2=south, 3=west, 4=secretpassage),
/// G=Room ID,
/// B=255 (variant of Blue).
/// e.g. the marble tile south of the door
/// (6,4 in 2d array) to the study is white (0,0,0).
/// the first tile into the study(roomId=0) is considered a door-tile
/// facing south(dir=2) therefore tile (6,3 in 2d array) is colored (2,0,255).
/// the top-left-most tile (0,0) is the study's secret passage
/// to the kitchen(roomID=4) therefore its tile is colored(4,4,255).
/// the right-bottom-most tile (23,23) is the kitchen's secret passage
/// to the Study(roomID=0) therefore its tile is colored (4,0,255).
/// </summary>
void initFloor()
{ // static floor tiles
Bitmap bmpFloorTiles = new Bitmap(Clue_CS2010.Properties.Resources.ClueFloor);
cFloor = new classFloor[bmpFloorTiles.Width, bmpFloorTiles.Height];
for (int intX = 0; intX < bmpFloorTiles.Width; intX++)
for (int intY = 0; intY < bmpFloorTiles.Height; intY++)
{
//if (intX == 0 && intY == 23)
// MessageBox.Show("stop initFloor()");
cFloor[intX, intY] = new classFloor();
Color clrTile = bmpFloorTiles.GetPixel(intX, intY);
if (clrTile.R == 255 && clrTile.G == 255 && clrTile.B == 255)
{ // white = tile
cFloor[intX, intY].eType = enuFloor.tile;
}
else if (clrTile.R == 0 && clrTile.G == 0 && clrTile.B == 0)
{ // black = invalid
cFloor[intX, intY].eType = enuFloor.invalid;
}
else if (clrTile.B == 255)
{ // blue = door : R = direction door exits room, G = roomID#
cFloor[intX, intY].eType = enuFloor.door;
cFloor[intX, intY].dirDoor = (enuDir)clrTile.R;
cFloor[intX, intY].eRoom = (enuRooms)clrTile.G;
}
else if (clrTile.R == 255)
{ // red = room : G = roomID#
cFloor[intX, intY].eType = enuFloor.room;
cFloor[intX, intY].eRoom = (enuRooms)clrTile.G;
}
else
MessageBox.Show("this should not happen");
}
}
房间(红色像素)也有颜色代码,但后来发现这是不必要的,因为嫌疑人只沿着方块移动,然后进入房间,当他们在房间里时,他们会被绘制在围绕中心点的指定位置,所以红色方块也可以是黑色(尽管那样会使地图的可视化更加困难)。无论如何,一旦这个`cFloor`数组设置好,并且我们的楼层以代码可以轻松读取的数据格式设置好,其余部分就相当简单了。
嗯,实际上,还有一些其他问题。
玩家控制的嫌疑人和AI控制的嫌疑人使用大致相同的代码,同样的情况发生在AI玩家回合移动并决定掷骰子时,或者人类玩家按下掷骰按钮时:骰子掷出!当动画停止时,程序必须决定特定嫌疑人可以走到哪些方格。为此,它使用了一种名为“广度优先搜索”的算法,我在之前的文章《战场模拟器》中描述过。本质上,它从嫌疑人当前位置一步步移动,记录下每轮移动中可以去哪里,每轮再向前迈一步,直到达到限制(骰子结果),同时记录下嫌疑人可以去哪些地方,并存储在一个数组中,这个数组属于
public classSearchElement[,] cSEAllowableMoves;
如下所示的类
public class classSearchElement
{
public Point pt;
public int intSteps;
public int intTurns;
public int intCost;
public enuDir SrcDir;
public classSearchElement next;
public void set(Point PT, int Cost, enuDir sourceDirection)
{ set(PT, sourceDirection, Cost, 0); }
public void set(Point PT, enuDir sourceDirection, int Steps, int turns)
{
pt = PT;
SrcDir = sourceDirection;
intSteps = Steps;
intTurns = turns;
intCost = intTurns * 3 + intSteps;
}
}
由于数组初始化为只包含`null`值(请注意,所有先前元素都已插入到链表中,以便下次搜索时可以回收),当玩家(或AI嫌疑人)点击屏幕时,`pictureBox`的点击事件处理程序会测试与屏幕点击区域对应的图块是否为`null`;如果是,则忽略该点击;如果不是,则表示该图块是该嫌疑人可以移动到的有效位置。
然后,当它知道玩家想去哪里时,它会在`classSuspect`函数`shortestPath()`中进行另一次类似的广度优先搜索,使用上面所示的相同`classSearchElement`,从目的地一步步走向当前位置,同时记录下它所经过的路径(通过`classSearchElement`中的`SrcDir`变量)。然后动画开始。
模式
本项目中最重要的变量之一是`enuMode`类型的`eMode`,如下所示
public enum enuMode
{
idle,
dealCards,
Accuse,
Accuse_Animation,
chooseCharacter,
warnPlayerNextTurn,
beginTurn,
gameOver,
rollDie_begin,
rollDie_animate,
rollDie_end,
showAllowableMoves,
animateMove,
Suggest,
AISuggestionComplete,
animateIntroduction,
animateSummonSuspect,
animateSummonWeapon,
animateSecretPassage,
respondToSuggestion,
playerChoosesCardToShow,
playerTurnIdle,
endTurn,
any
};
每当`tmrDelay`启用并计时时,其事件处理程序都会检查游戏处于何种模式,然后决定下一步需要做什么。当嫌疑人从一个方块移动到另一个方块,或通过门进出房间时,`eMode`变量被设置为`animateMove`,程序流程进入`moveStep()`函数。
`moveStep()`函数根据`shortestPath()`函数计算并存储在全局`udrMoveAnimationInfo`结构变量中的路径,将嫌疑人的图标沿棋盘移动,每次移动7个像素(每个图块面积为28 x 28像素),沿图标从一个图块移动到下一个图块的方向,直到到达目的地。`classSuspect`的位置由两个变量描述:`eRoom`和`ptTile`。由于`eRoom`变量告诉程序该嫌疑人是“在房间内”(指定哪个房间)、“在起始位置”还是“在图块上”,因此只有当嫌疑人的`eRoom`值设置为`tiles`时才需要`ptTile`变量。
人工智能
当我制作了一个六人版的妙探寻凶游戏(还有许多细节未完成)后,我开始着手制作我能想象到的最聪明、最完美、最天才的人工智能。模拟这个最简单的方法是计算每个嫌疑人玩的回合数,然后决定在这么多回合后,这个或那个嫌疑人有可能“猜出”解决方案,然后直接让他们看牌。换句话说,就是作弊。
但这有什么乐趣可言呢?!
不,这个完全不是这样做的。天才设置利用了我赋予它的每一个技巧。虽然我不是天才,但这个AI已经非常接近了。为了让我的电脑“天才化”,我让它仔细记录每个嫌疑人声明他们**没有**的牌,以及他们在展示牌时所说的每组牌。这样,在游戏“证明建议”阶段的每个循环中,它都可以检查之前的未知牌组是否包含任何嫌疑人最近声明没有的牌,直到它淘汰掉三张牌中的两张,然后确切地知道多少回合前展示的是哪张牌,即使它们自己没有看到。
例如
怀特夫人建议
芥末上校用刀在厨房。
孔雀夫人说她没有牌。
但之前,斯嘉丽小姐建议
芥末上校用绳子在厨房。
而孔雀夫人,在那一回合,确实给斯嘉丽小姐展示了一张牌。一张神秘的牌,我们现在可以推断出实际上是**绳子**,因为我们现在知道孔雀夫人没有**芥末上校**,孔雀夫人也没有**厨房**,所以她一定是向斯嘉丽小姐展示了**绳子**。
跟上了吗?
天才人工智能会记住所有未知卡牌的组合,并勤奋地记录谁否认拥有什么,这使得它能够相当快地找出谜题的解决方案。它还知道每个嫌疑人手中持有多少张牌(截至目前,在六人游戏中,每个嫌疑人各持有三张牌),因此一旦这三张牌被识别出来,天才人工智能就知道该嫌疑人没有其他牌。为了解决谜题,它可以从列表中排除每一种可能性,或者它会注意到没有嫌疑人拥有这张或那张牌,从而使那张牌成为显而易见的选择。每当它的嫌疑人列表、武器列表和房间列表缩小到各剩一个时,它就会提出指控。而且,电脑从不犯错(我懒得让较笨的级别出错,只是让它们不那么勤奋)。
但是,做好笔记并不是赢得一场精彩的《妙探寻凶》游戏的全部。你仍然需要问对问题。`classSuspect` 在下面的函数中做到了这一点
public void makeSuggestion()
{
prepareSuggestionAI();
pickSuspectForSuggestion();
pickWeaponForSuggestion();
MainForm.udrSuggestion.eRoom = eRoom;
...
通过首先选择一名嫌疑人,然后选择一种武器。为了选择嫌疑人,它会查看自己的笔记,试图排除“未知”,即其他嫌疑人向第三方展示的牌组。如果它知道这个侦探游戏中“谁”是凶手,那么它可能会选择那张牌,并确信它不会重新学习它已经知道的东西,或者它可能会选择它自己手中的牌(如果电脑真的有手的话,我猜)。但在它弄清楚谁杀了博迪先生之前,它必须通过查看“未知”来排除可能性。如果嫌疑人类别中没有神秘的未知牌可供排除,它就会随机选择列表中剩余的任何嫌疑人。但如果它确实有一张或多张这些未知嫌疑人牌需要排除,它就会按照建议被反驳的相反顺序绕桌子,然后选择它找到的第一张嫌疑人-未知牌。它以相反的顺序进行,因为其理念是,在你的笔记本上收集尽可能多的X是最好的。如果你要求反驳你的建议的第一个嫌疑人向你展示一张牌,你只是将一个领域(嫌疑人、武器或房间)缩小了一张牌,但你没有学到太多其他东西。通过在笔记本上收集所有这些X,推断出向任何第三方展示的是哪些牌就变得容易得多,谜题也能更快地解决。
然后人工智能选择一种武器来建议,基本上与它选择嫌疑人时做的事情相同,但要记住它不想询问与它已经选择的神秘未知嫌疑人卡牌在同一嫌疑人列中的神秘未知武器卡牌。例如,如果它知道梅子教授在被问及厨房、孔雀和烛台时展示了一张卡牌,并且它已经决定建议孔雀,那么它就不想建议烛台,因为如果它排除了其中一个,它就已经解决了另一个。通过询问不同的武器,它能学到更多,询问它不怀疑同一个嫌疑人拥有的武器(请原谅混淆)。收集更多的X,解决更多的未知,减少更多的可能性,直到对谁、在哪里以及如何做出明确的推断来结束游戏。明白了吗?简单。
简洁明了。
AI 的内部笔记记录经常使用两个重要函数
void checkNote(enuCards eCard, enuSuspects eSuspect)
和
public void XNote(enuCards eCard, enuSuspects eSuspect)
这里的“笔记”指的是单个单元格,它们在内部代表用户在笔记本上看到的图形,即彩色方块。每个嫌疑人(6个)和每张牌(21张)都有一个6x21的二维数组笔记。当AI推断出或被告知某个嫌疑人持有某张牌时,它会检查一个笔记。
检查笔记()
`checkNote(eSuspect, eCard)` 函数将数组中的特定笔记设置为“勾选”,这意味着卡牌 `eCard` 的持有者已被识别为嫌疑人 `eSuspect`,并且如果卡牌的持有者已被识别,那么所有其他嫌疑人就已知**没有**相同的卡牌(每张卡牌只有一张),因此所有其他嫌疑人对那张特定卡牌的笔记都会被 X 掉(使用下面的 `Xnote()`)。此外,当一张新卡牌的持有者被识别时,会计算出该嫌疑人已知持有的卡牌总数,如果该嫌疑人的所有卡牌都已被识别,那么它就知道该嫌疑人没有其他卡牌,因此所有其他卡牌都会被 X 掉。
X笔记()
使用 `XNote(eSuspect, eCard)` 函数将一张卡片标记为 X,与上面的 `checkNote()` 类似,不同之处在于它让 AI 记录下该嫌疑人 `eSuspect` **没有**卡片 `eCard` 的信息。当这种情况发生时,它会遍历其所有神秘未知卡片组,查找包含该嫌疑人最近被发现没有的卡片 `eCard` 的卡片组,并消除这些未知项。通过消除未知项,它可以将未知卡片组中的卡片数量减少到一张,然后可以在其笔记中检查(使用上面的 `checkNote()`)该卡片。如果所有嫌疑人都已知**没有**某张卡片,那么 AI 就知道该卡片是解决方案的一部分,并相应地调整其建议。
在X掉和勾选,勾选和X掉之间,X掉和勾选很快就会演变成大量的计算,没过多久,整个混乱的谜团就 nicely 迎刃而解了。
让天才AI变笨
脑叶切除术!是的,你猜对了。如果桌边的所有嫌疑人都是天才,你很快就会因为玩这个游戏而感到沮丧。如果你对抗一群天才,赢得游戏需要运气(关闭自动进行会有帮助!)。首先,天才们记住一切,从不犯错,并且非常勤奋地记录游戏中发生的一切。你仍然可以调整你的建议,并很快得到答案,因为有办法打败它,但是……大多数人只是喜欢和我们普通人一起玩。这意味着人工智能有时需要有点笨。幸运的是,这并不是什么大麻烦。人工智能有五个级别,从“笨”到“天才”。要让一台天才电脑变笨,只需要一个恶意的病毒,或者几行涉及使用某种随机数生成器的代码。
这是一个例子,人工智能决定是否要计算该嫌疑人已识别出的卡牌数量,如果已识别出该嫌疑人持有的所有卡牌,则将其余卡牌标记为 X。
// ai may notice that all of this suspect's cards have been identified
if ((eAILevel == enuAILevel.stupid && (int)(MainForm.rnd.NextDouble() * 1000) % 400 == 0)
|| (eAILevel == enuAILevel.dull && (int)(MainForm.rnd.NextDouble() * 1000) % 50 == 0)
|| (eAILevel == enuAILevel.average && (int)(MainForm.rnd.NextDouble() * 1000) % 10 == 0)
|| (eAILevel == enuAILevel.bright && (int)(MainForm.rnd.NextDouble() * 1000) % 2 == 0)
|| (eAILevel == enuAILevel.genius ))
{
int intCountNumCardsKnown = 0;
for (enuCards eCardCounter = (enuCards)0;
eCardCounter < enuCards._numCards; eCardCounter++)
{
if (udrAILogic.eNotes[(int)eSuspect, (int)eCardCounter] == enuNote.check)
intCountNumCardsKnown++;
}
if (intCountNumCardsKnown == MainForm.cSuspects[(int)eSuspect].cCards.Length)
{
// all of this suspect's cards have been identified
// -> X the ones that are unknown for this suspect
for (enuCards eCardCounter =
(enuCards)0;eCardCounter < enuCards._numCards; eCardCounter++)
{
if (udrAILogic.eNotes[(int)eSuspect,(int)eCardCounter] != enuNote.check)
XNote(eCardCounter, eSuspect);
}
}
}
图形与大张旗鼓
你可能已经注意到让这款游戏有点酷的漂亮效果。其中一些你以前见过(如果你读过或下载过我以前的一些文章),比如标题动画中的杰西卡兔。她使用我在之前文章《.NET 的精灵编辑器》中讨论的 `classSprite` 进行动画制作。对于使用过这个类的任何人,你们会很高兴知道我做了一些升级,其中最重要的是加载时间的加速。这个类的主要缺点是加载时间太长,以至于使用起来很繁琐。但这个新升级是向后兼容的,并且会自动将精灵文件从 `.spr` 升级到 `.sp2`,这样它就能将加载时间减少10倍(至少!)。只需下载这个 Clue 代码,从中复制 `classSprite`,然后把它放在任何地方。
至于其他漂亮的东西,没有什么比杰西卡·辛普森更漂亮的了。呃……呃……原谅我的弗洛伊德式口误(我自己是艾希莉的粉丝!),没有什么比杰西卡兔更漂亮的了,卡牌本身也挺酷的。就像我之前提到的,这个项目中的所有图像都是从互联网下载并修改的。当我决定要动画化卡牌分类、洗牌(三种不同类型)和在分发剩余卡牌之前选择神秘卡牌的过程时,我必须为每张卡牌生成“精灵”。这似乎不必要,所以我创建了一个名为`classRotatedImage`的新类型。这个类接收一张输入图像,将其旋转三次,并将四张基础图像存储在硬盘上的`.rbmp`文件中。这样做需要时间,这就是为什么你第一次加载游戏时,需要长达两分钟才能看到介绍动画,但一旦这些卡牌生成完毕,加载时间就会快得多。这四张基础图像在零到π/2弧度(或接近)之间均匀旋转,这样就可以使用.NET原生的`Bitmap`的`FlipRotate`函数来生成该类的特定实例总共16张旋转图像中的其余12张。
别慌
这个项目中最困难的部分是让牌在发牌动画中翻转。这部分是牌被分类然后面朝上散开。想法是创建一个效果,产生这些二维图像像瀑布般翻转的错觉,就像魔术师把牌散开在桌上,然后翻开第一张牌,看着其余的牌跟着翻转一样。创建翻一张牌的错觉只是在动画过程中将图像缩小到适当大小,然后将其加宽直到在反面再次恢复正常大小。但是让一串牌同时做到这一点就有点困难了,它是在主窗体的
AnimateDealCards_FlipCardsUtoD_DirR(enuCardType eTypeCard)
函数中完成的,该函数由以下函数调用:
animateDealCards()
当`tmrDelay`的tick事件处理程序触发时,`eMode`处于`dealCards`模式,并且变量`eDealMode`被设置为`flipWeaponcards`、`flipSuspectcards`或`flipRoomcards`。
这个想法是为了模拟魔术师在桌面上展开一副扑克牌,然后翻开第一张,看着其余的牌跟着翻转的效果。为此,程序会展开牌并保留它们在桌面上的位置。然后,它会移动一根无形的手指,穿过整个展开的牌。每张牌都有一条边与桌面保持接触;这条边与桌面接触的点称为该牌的支点。在翻牌动画的任何时刻,最左边且支点仍在无形手指右侧的牌被指定为支点牌。这是最左边仍然面朝上的牌。它首先被绘制,然后是它右边的下一张,然后是再下一张,依此类推,直到最右边的最后一张牌被绘制,然后动画继续绘制支点左边的牌,一张接一张,从最靠近支点牌的牌开始,直到完成最左边的牌。
但在绘制这些牌之前,需要计算它们的相对图像宽度。为此,算法在侧视图下循环查看牌。
要计算B牌的宽度,它
- 知道A牌浮动边缘(x,y)的位置,在上方
- A牌支点与B牌支点之间的距离
- 它计算A牌和B牌的接触点
- 然后它计算B牌与桌子形成的夹角
- 然后就知道B牌输出图像所需的宽度
由于一张牌的接触点被该算法用作下一张牌计算中的“浮动边缘”,因此它可以轻松地一张接一张地循环处理所有牌。所有面朝下的牌(位于支点牌左侧的牌)的尺寸计算方式大致相同,但方向相反,x 值取负以简化操作。然而,支点牌左侧紧邻的牌,其边缘可能紧贴支点牌的侧面,或者其侧面可能紧贴支点牌的边缘,在对此抓狂一周(以一种非常酷、冷静、庄重的方式)之后,我决定将两张牌浮动边缘的 X 值设置为无形手指的位置,这样近似就足够了(毕竟,这又不是热追踪导弹的制导系统)。事情就此结束。
透过魔镜
片头动画很酷。是的,很酷。
为此,我使用了放大三倍的棋盘和笔记本的副本,并将其保存在内存中。这个图像太大了,无法平滑地运行动画,所以它被切割成一个由更小的图像组成的数组,整个地图片段都存储在这个数组中,使用的内存大约是以前的两倍,但每个更小的图像都可以被绘制放大镜和其背后的放大图像到屏幕上的算法更轻松地访问和使用。这些整块的宽度是通过计算能均匀地整除整个放大图像宽度的最小值来确定的,并且该最小值也要大于放大镜镜片宽度的两倍。高度也以类似的方式完成。
放大镜镜片图像由
- 镜片边缘,
- 镜片边缘周围的红色区域
- 以及镜片内部的白色区域
这个放大镜在**放大图像**上移动。当它的位置确定后,移动完成后,它的表面积会被描述(镜头的实际位置在内存中存储为镜头中心的位置,因此需要计算镜头覆盖的区域)。然后,这个表面积会从一个图块图像中复制出来,存储在上一段描述的区域中,到一个与镜头大小相同的新位图中。镜头边缘内部的区域被设置为透明,然后镜头叠加到我们刚刚创建的放大棋盘的新位图上,这样放大图像看起来就像“在镜头后面”。然后,镜头边缘周围的红色区域被设置为透明,这个新图像、边缘和镜头后面的放大棋盘在显示到屏幕之前,会被绘制到一个常规大小的棋盘和笔记本副本上。
如果没有抗议的老鼠和蜘蛛网,事情就会这样。还有,这个沃尔多家伙在这里干嘛?但事情总是可以改进的,所以,这就是我们需要加入一点动画的原因。所有动画角色都使用新的 `classSprite` 完成,无论是否有人看着它们,它们都会执行各自的操作(它们的 `intConfiguration` 和 `intConfigurationStep` 值在介绍动画的每次迭代中都会更新)。当放大镜足够接近需要它们时,它们的图像就会生成并显示在屏幕上。这些精灵图像在绘制放大镜的镜头之前放置在巨型棋盘图像的副本上,然后放置在常规大小棋盘的位图上。
当所有这些都完成后,放大镜的把手被绘制到屏幕上,整个动画只是循环播放,直到用户开始新游戏。
带出被告
当用户(或电脑AI,无论是天才化、愚钝化还是白痴化)最终提出指控时,我们进入“我指控”模式。这是一段简洁的代码,它创造了一个相当酷的效果。所有东西都有两张图片:一张彩色的,一张黑白的。除了探照灯,移动的部分都在前景,背景只是背景。然而,黑白背景确实有一个彩色的“我指控”指控嫌疑人和对话气泡,但除此之外,其他一切都是黑白的。
使用蓝幕(我不用绿幕了,绿色和平的抗议者在我家门外大声喧哗,我害怕他们会做什么),我通过在我的蓝幕上绘制一个透明的圆形来移动我的“探照灯”,然后将其叠加到彩色图像上(前景动画的彩色部分绘制在彩色背景上),并从中裁剪出一个彩色的圆形片段;然后,将所得图像的蓝色部分设为透明,我将其绘制到黑白图像上,并重复该过程,移动探照灯,直到我们最终准备好翻牌,看清“谁”和“如何”!
但可悲的是,我们可能永远不会知道博迪先生为何如此悲惨地被谋杀。