# 7.增大距离 一个典型的冒险包含许多谜题。众所周知,Infocom的冒险很难完成。解决每个难题可能需要数周甚至数月的反复试验。 当玩家操纵角色失败后,如果只是返回“你不能这么操作”来回应玩家,会很 nt,很没意思 它忽略了电脑游戏的一个重要方面,而这也是生活本身的一部分:玩家必须从错误中吸取教训。 当你的游戏反复输入东西都是,你不能这样做的时候,会显得很无聊的。 冒险游戏至少应该做的是解释为什么玩家的命令无法完成:“你不能这样做,因为......”这有助于使虚拟世界更具说服力,故事更可信,游戏更有趣。 我们已经付出了相当大的努力让游戏解释为什么某些命令是无效的。只需看看名词.c,inventory.c,location.cmove.c中的许多printf调用。 但随着游戏变得越来越复杂,这正成为一个相当大的负担。我们需要一种更结构化的方法来检测和处理错误情况。这就是我们在本章中将要讨论的内容。 大多数命令对一个或多个对象进行操作,例如: - 玩家拿起一件物品,然后把它交给另一个 NPC。 - 玩家沿着一条通道到另一个位置。 首先要检查的(在[解析器](http://en.wikipedia.org/wiki/Parsing)捕获检测是否会有明显[拼写错误](http://en.wikipedia.org/wiki/Typographical_error)之后)是这些对象是否存在; 失败应该导致类似“这里没有...“或”你看不到任何东西...”等文字出现。在本章中,我们将构建一个通用函数,每个命令都可以使用它来找出玩家是否在可及的范围内。 你可能认为我们只需要区分两种情况:对象在这里,或者它不在这里。 但是许多命令需要更多的渐变,而不仅仅是“这里”和“不在这里”。例子: - 要使用武器或工具,玩家必须持有它;仅仅在现场存在是不够的。 - 你不能放下一个你没拿起来的道具,也不能拿起一个已经有的东西 - 如果你跟商人买东西的时候,他店里东西你不能随便拿 | distSelf | 对象是玩家 | object == player | | ----------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------ | | distHeld | 玩家持有物体 | object->location == player | | distHeldContained | 玩家拿着另一个包含该物体的物体(例如袋子) | object->location != NULL &&
object->location->location == player | | distLocation | 对象是玩家的位置 | object == player->location | | distHere | 对象位于玩家的位置 | object->location == player->location | | distHereContained | 一个物体(NPC 或“容器”)存在于玩家的位置,正在拿着另一个物体 | object->location != NULL &&
object->location->location == player->location | | distOverthere | 对象是附近的位置 | getPassage(player->location, object) != NULL | 第一种情况(对象是玩家)可能看起来微不足道,但它仍然很重要。例如,命令“examine yourself”不应该返回“这里没有你自己”。 我试图遵循一个逻辑顺序:附近的事物最高优先级,随后优先级会变低。 | distNotHere | 对象不在这里(或看起来)不在这里 | | | ----------------- | -------------------------------- | -------------- | | distUnknownObject | 解析器无法识别输入的名词 | object == NULL | 请注意,我们有七种不同的“这里”案例,但只有一种是“不在这里”。这是因为通常,游戏只需要提供有关玩家可以感知的事物的信息。如果它不在这里,那么就没什么可说的了。 在最左边的列中,我们为每个案例提出了一个符号名称。我们将在名为 DISTANCE 的[枚举](http://en.wikipedia.org/wiki/Enumerated_type)中收集这些名称。 ```c typedef enum { distSelf, distHeld, distHeldContained, distLocation, distHere, distHereContained, distOverthere, distNotHere, distUnknownObject } DISTANCE; ``` typedef 以及枚举类 enum 之前有接触过吗?没有接触过的话就去学习一下吧。 在最右边的列中,我们为每个情况提出了一个满足条件。通过一些重新洗牌,我们可以很容易地将其转换为计算对象“距离”的函数(从玩家的角度来看): ```c DISTANCE getDistance(OBJECT *from, OBJECT *to) { return to == NULL ? distUnknownObject : to == from ? distSelf : to->location == from ? distHeld : to == from->location ? distLocation : to->location == from->location ? distHere : getPassage(from->location, to) != NULL ? distOverthere : to->location == NULL ? distNotHere : to->location->location == from ? distHeldContained : to->location->location == from->location ? distHereContained : distNotHere; } ``` 思考题:你是否有其他方法实现这个功能? 注:自行实验即可 就这样!我们可以调用此函数并对其返回值进行比较。例如,我们在 noun.c中有以下代码: ```c else if (!(obj == player || obj == player->location || obj->location == player || obj->location == player->location || getPassage(player->location, obj) != NULL || (obj->location != NULL && (obj->location->location == player || obj->location->location == player->location)))) ``` 现在,我们可以用适当的距离检查替换每个子条件: ```c else if (!(getDistance(player, obj) == distSelf || getDistance(player, obj) == distLocation || getDistance(player, obj) == distHeld || getDistance(player, obj) == distHere || getDistance(player, obj) == distOverthere || getDistance(player, obj) == distHeldContained || getDistance(player, obj) == distHereContained) ``` 这可以简化为: ```c else if (getDistance(player, obj) >= distNotHere) ``` 尝试理解一下这样做的意义 这只是一个例子,让你对这个概念有所了解;您将在下面找到noun.c的实际实现,看起来略有不同。 是时候把事情落实到位了。枚举 DISTANCE 和函数 getDistance 的定义被添加到 misc.hmisc.c 中,因为我们将在多个模块中使用它们。 # misc.h ```c typedef enum { distSelf, distHeld, distHeldContained, distLocation, distHere, distHereContained, distOverthere, distNotHere, distUnknownObject } DISTANCE; extern bool isHolding(OBJECT *container, OBJECT *obj); //是否持有物体 extern OBJECT *getPassage(OBJECT *from, OBJECT *to); //获取通道 extern DISTANCE getDistance(OBJECT *from, OBJECT *to); //计算距离 extern OBJECT *actorHere(void); extern int listObjectsAtLocation(OBJECT *location); ``` # misc.c ```c #include #include #include "object.h" #include "misc.h" bool isHolding(OBJECT *container, OBJECT *obj) { return obj != NULL && obj->location == container; } OBJECT *getPassage(OBJECT *from, OBJECT *to) { if (from != NULL && to != NULL) { OBJECT *obj; for (obj = objs; obj < endOfObjs; obj++) { if (isHolding(from, obj) && obj->destination == to) { return obj; } } } return NULL; } DISTANCE getDistance(OBJECT *from, OBJECT *to) { return to == NULL ? distUnknownObject : to == from ? distSelf : isHolding(from, to) ? distHeld : isHolding(to, from) ? distLocation : isHolding(from->location, to) ? distHere : isHolding(from, to->location) ? distHeldContained : isHolding(from->location, to->location) ? distHereContained : getPassage(from->location, to) != NULL ? distOverthere : distNotHere; } OBJECT *actorHere(void) { OBJECT *obj; for (obj = objs; obj < endOfObjs; obj++) { if (isHolding(player->location, obj) && obj == guard) { return obj; } } return NULL; } int listObjectsAtLocation(OBJECT *location) { int count = 0; OBJECT *obj; for (obj = objs; obj < endOfObjs; obj++) { if (obj != player && isHolding(location, obj)) { if (count++ == 0) { printf("You see:\n"); } printf("%s\n", obj->description); } } return count; } ``` 注意:isHolding 这个函数之后我们将在各个地方使用 # location.h ```c extern void executeLook(const char *noun); extern void executeGo(const char *noun); ``` 在函数 executeGo 中,我们可以用检查距离来替换大多数 if 条件。 # location.c ```c #include #include #include #include "object.h" #include "misc.h" #include "noun.h" void executeLook(const char *noun) { if (noun != NULL && strcmp(noun, "around") == 0) { printf("You are in %s.\n", player->location->description); listObjectsAtLocation(player->location); } else { printf("I don't understand what you want to see.\n"); } } void executeGo(const char *noun) { OBJECT *obj = getVisible("where you want to go", noun); switch (getDistance(player, obj)) { case distOverthere: printf("OK.\n"); player->location = obj; executeLook("around"); break; case distNotHere: printf("You don't see any %s here.\n", noun); break; case distUnknownObject: // already handled by getVisible break; default: //上述情况均为出现 if (obj->destination != NULL) { printf("OK.\n"); player->location = obj->destination; executeLook("around"); } else { printf("You can't get much closer than this.\n"); } } } ``` 思考题:你能否为 switch 函数增加更多 case 来完善判断条件? 函数 executeGet 也是如此。 # inventory.h ```c extern void executeGet(const char *noun); extern void executeDrop(const char *noun); extern void executeAsk(const char *noun); extern void executeGive(const char *noun); extern void executeInventory(void); ``` # inventory.c ```c #include #include #include "object.h" #include "misc.h" #include "noun.h" #include "move.h" void executeGet(const char *noun) { OBJECT *obj = getVisible("what you want to get", noun); switch (getDistance(player, obj)) { case distSelf: printf("You should not be doing that to yourself.\n"); break; case distHeld: printf("You already have %s.\n", obj->description); break; case distOverthere: printf("Too far away, move closer please.\n"); break; case distUnknownObject: // already handled by getVisible break; default: if (obj->location == guard) { printf("You should ask %s nicely.\n", obj->location->description); } else { moveObject(obj, player); } } } void executeDrop(const char *noun) { moveObject(getPossession(player, "drop", noun), player->location); } void executeAsk(const char *noun) { moveObject(getPossession(actorHere(), "ask", noun), player); } void executeGive(const char *noun) { moveObject(getPossession(player, "give", noun), actorHere()); } void executeInventory(void) { if (listObjectsAtLocation(player) == 0) { printf("You are empty-handed.\n"); } } ``` 最后,我们将调整 noun.c中的约束。我们正在向函数getObject添加两个参数,以便找到特定名词的匹配项,同时忽略任何被认为不存在的对象。这将在下一章中得到真正的回报,我们将在下一章中介绍具有相同标签的不同对象。 # noun.h ```c extern OBJECT *getVisible(const char *intention, const char *noun); extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun); ``` # noun.c ```c #include #include #include #include "object.h" #include "misc.h" static bool objectHasTag(OBJECT *obj, const char *noun) { return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0; } static OBJECT *getObject(const char *noun, OBJECT *from, DISTANCE maxDistance) { OBJECT *obj, *res = NULL; for (obj = objs; obj < endOfObjs; obj++) { if (objectHasTag(obj, noun) && getDistance(from, obj) <= maxDistance) //只考虑与对象距离小于或等于最大距离的物体 { res = obj; } } return res; } OBJECT *getVisible(const char *intention, const char *noun) { OBJECT *obj = getObject(noun, player, distOverthere); //玩家看不到的自动忽略 if (obj == NULL) { if (getObject(noun, player, distNotHere) == NULL) { printf("I don't understand %s.\n", intention); } else { printf("You don't see any %s here.\n", noun); } } return obj; } OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun) { OBJECT *obj = NULL; if (from == NULL) { printf("I don't understand who you want to %s.\n", verb); } else if ((obj = getObject(noun, from, distHeldContained)) == NULL) //限制范围 { if (getObject(noun, player, distNotHere) == NULL) { printf("I don't understand what you want to %s.\n", verb); } else if (from == player) { printf("You are not holding any %s.\n", noun); } else { printf("There appears to be no %s you can get from %s.\n", noun, from->description); } } else if (obj == from) { printf("You should not be doing that to %s.\n", obj->description); obj = NULL; } return obj; } ``` 思考题:你能理解什么时候加 const,什么时候不用吗? 在本章中,距离的概念主要用于在游戏可以给用户的不同响应之间进行选择。但是,距离的好处并不局限于输出端;它可以同样很好地用于在输入端进行改进。在下一章中,我们将使用距离来提高对用户输入的名词的识别。 输出样例 Welcome to Little Cave Adventure. You are in an open field. You see: a silver coin a burly guard a cave entrance --> go guard You can't get much closer than this. --> give silver You are not holding any silver. --> ask silver There appears to be no silver you can get from a burly guard. --> get silver You pick up a silver coin. --> get gold You don't see any gold here. --> give silver You give a silver coin to a burly guard. --> go cave OK. You are in a little cave. You see: a gold coin an exit --> get gold You pick up a gold coin. --> give gold There is nobody here to give that to. --> quit Bye!