对于一个学校项目,我正在用C ++创建一个基于文本的冒险游戏。我已经向老师展示了我的代码,他说我的代码在顺序上并不完全逻辑,我应该在函数中编写if语句,因为有很多。我需要帮助来了解如何进行这些更改。

请注意,这是代码的功能之一,而不是全部功能。

/>

评论

(1)不要编写函数,因为有太多的代码行,而是在函数中分组了功能。 (2)不要在代码中使用数字文字,而应使用具有描述性名称的常量/枚举。

重构。首先尝试将每个选择中的代码移动到一个函数中。无需进一步重构,即可将通用功能移至单独的较小功能中。就像雕刻一样。

您应该有一个包含切换逻辑的决策类/方法,决策中逻辑的实际实现应采用自己的方法(甚至类,具体取决于您希望在类中对域语言建模的程度)。我不了解c ++,但在c#中,很多营地中的if语句都被认为是一种代码味道。阅读有关状态机的信息,您可能会发现它们是解决问题的好方法;)

不是您的问题的答案,但是,如果您有兴趣编写自己的文字冒险,可以考虑学习Inform7编程语言。

从侧面讲,粘性和恶意之间存在巨大差异。抱歉,宠物怒气冲冲。

#1 楼

我发现了一些可以帮助您改善代码的方法。

不要滥用using namespace std


using namespace std放在每个程序的顶部是您应该避免的坏习惯。我不知道您实际上已经做到了,但是对于新的C ++程序员来说,这是一件令人震惊的普遍事情。

避免使用全局变量

我看到使用了name,但它既未在函数内声明,也未传递给函数,这意味着存在全局变量。通常最好显式传递函数需要的变量,而不要使用模糊的全局变量隐式链接。

消除未使用的变量

此代码声明了一个变量event1,但随后却不执行任何操作。您的编译器足够聪明,可以帮助您找到解决此类问题的方法。

不要使用system("cls")


那里不使用system("cls")system("pause")的两个原因。首先是它不能移植到您可能现在或可能不关心的其他操作系统上。第二个问题是这是一个安全漏洞,您绝对必须关注它。具体来说,如果定义了某个程序并将其命名为clspause,则您的程序将执行该程序,而不是您想要的程序,而其他程序可以是任何程序。首先,将它们隔离到单独的函数cls()pause()中,然后修改代码以调用这些函数而不是system。然后重写这些函数的内容,以使用C ++进行所需的操作。

固定换行符

以“出于某些特殊原因”开头的字符串包含/n,它是两个字符,但从上下文中可以清楚地看出,您打算使用单个换行符。

使用菜单对象或至少一个常用菜单功能

在代码中的许多地方,都有类似菜单的内容。您的代码提供了两个选项,然后要求用户根据输入数字选择一个。与其在许多地方重复该代码,不如将其通用。实际上只有提示字符串会发生变化,但是呈现选择和要求输入的基本逻辑都是相同的。看来您是一个入门程序员,所以也许您还没有了解对象,但是这种带有关联数据的重复任务确实非常适合面向对象的编程,而C ++非常擅长表达。

具体来说,这就是我可能的处理方法。我要创建一个Menu对象:

class Menu
{
public:
    Menu(const string &name, const string &prompt, 
        const std::vector<std::pair<string, string> > &choices 
        = std::vector<std::pair<string, string> >{});
    virtual ~Menu();
    const string& getChoice() const;
    bool operator==(const string &name) const;
private:
    static const string menuend;
    string _name, _prompt;
    std::vector<std::pair<string, string> > _choices;
};


实现在这里:

Menu::Menu(const string &name, const string &prompt, 
        const std::vector<std::pair<string, string> > &choices) 
    : _name(name), _prompt(prompt), _choices(choices) 
{}

bool Menu::operator==(const string &name) const
{
    return name==_name;
}

const string& Menu::getChoice() const
{ 
    if (_choices.size() == 0) {
        cout << _prompt;
        return menuend;
    }
    unsigned choice; 
    int i;
    do { 
        cout << _prompt;
        i = 1;
        for (auto ch : _choices)
            cout << i++ << ": " << ch.first << '\n';
        cin >> choice; 
        --choice;
    } while (choice >= _choices.size()); 
    return _choices[choice].second; 
}

Menu::~Menu() 
{}

const string Menu::menuend{"END"};


最后,我们可以将游戏本身构造为以下\n对象的std::vector

std::vector<Menu> game{  
    Menu("mainroad", 
            "You are on a road that heads west and east of your position.\n" 
            "Which way will you go?\n", std::vector<std::pair<string,string> >{
                {"Go West", "spider"}, 
                {"Go East", "brickhouse"}, 
                {"Wait for something to happen", "dragon"}}),
    Menu("spider", 
            "You travel down the road, about only 100 metres and you encounter \n" 
            "a giant spider with vicious poison coated fangs.\n" 
            "its hideous appearance causes your throat to dry and your knees to shake!\n"
            "What on earth will you do?\n\n", std::vector<std::pair<string, string> >{
                {"Attempt to attack the spider with your sword.","spiderattack"},
                {"Throw your sword in the off chance it might kill it.","throwsword"},
                {"RUN FOR YOUR LIFE!", "running"}}),
    Menu("spiderattack",
            "You viscously swing your sword at the spiders general direction.\n" 
            "The swing was so great, your arms jolts out of place,\n"
            "creating a surge of pain.\n" 
            "Your arm is now broken, and you fall to the ground in pain....\n" 
            "The spider launches 3 metres straight into your body...\n"
            "What on earth is it doing?\n" 
            "Oh My God! The spider is devouring everything....\n" 
            "All that remained was bones of the once mobile adventurer.\n"), 
    Menu("brickhouse",
            "After a mile walk, you arrive at an old brick house.\n" 
            "You walk slowly inside.\n" 
            "The door slams behind you and the room lightens up.\n" 
            "What on earth is going on...?\n\n" 
            "Unable to open the door, you look around for anything of use.\n" 
            "Nothing, not a single piece of furniture.\n" 
            "What will you do?\n", std::vector<std::pair<string, string> >{
                {"Wait for someone to save you.", "trapdoor"}, 
                {"Or Wait for someone to save you.", "library"}})
};


游戏本身完全由数据驱动:

void road() {
    auto menu = std::find(game.begin(), game.end(), "mainroad"); 
    while (menu != game.end())
        menu = std::find(game.begin(), game.end(), menu->getChoice());
}


如果您需要的不仅仅是Menu类提供的内容,则可以简单地派生一种新的类并将其放入Menu中。还应该显而易见的是,只需为vector类定义一个ostream提取器,就可以很容易地将所有这些作为脚本从文件中读取。

考虑使用更好的随机数生成器

您当前正在使用

random1 = rand() % 2;


这种方法有两个问题。一个是随机数发生器的低阶位不是特别随机,所以Menu都不是。在我的机器上,有一个相对于0的轻微但可测量的偏差。第二个问题是它不是线程安全的,因为random1存储隐藏状态。如果编译器和库支持,更好的解决方案是使用C ++ 11 rand。它看起来很复杂,但实际上非常易于使用。一种方法(来自Stroustrup)是这样的:

int rand_int(int low, int high)
{
    static std::default_random_engine re {};
    using Dist = std::uniform_int_distribution<int>;
    static Dist uid {};
    return uid(re, Dist::param_type{low,high});
}


请注意,它仍然存储状态,但是至少分布是正确的。

评论


\ $ \ begingroup \ $
因此,请举例说明std :: uniform_int_distribution的用法
\ $ \ endgroup \ $
–马丁·约克
2014年5月13日14:54

\ $ \ begingroup \ $
@LokiAstari:好点。我已经编辑了答案,以包含来自可靠来源的std :: uniform_int_distribution的示例用法。
\ $ \ endgroup \ $
–爱德华
14年5月13日在15:48

\ $ \ begingroup \ $
我觉得这个答案确实很挑剔,却没有解决程序设计的核心问题。
\ $ \ endgroup \ $
–古斯塔夫·贝特拉姆(Gustav Bertram)
2014年5月14日晚上8:27

\ $ \ begingroup \ $
@GustavBertram:我添加了菜单对象概念的详细示例,以尝试解决您的问题。
\ $ \ endgroup \ $
–爱德华
2014年5月15日18:44

\ $ \ begingroup \ $
看起来好多了!
\ $ \ endgroup \ $
–古斯塔夫·贝特拉姆(Gustav Bertram)
2014年5月16日14:35

#2 楼

我想你的老师是什么意思:

if (choice == 1) {
    spider();


但是有一种更好的方法可以写这个。您的冒险游戏实际上是有限状态机。您可以通过一个简单的循环来实现它:

#include <iostream>

struct  state;
struct  transition;

struct transition {
    char *text;
    struct state *next_state;
};

struct state {
    char *text;
    struct transition transitions[8];
};

extern struct state start;
extern struct transition start_transitions[];
extern struct state spider;
extern struct transition spider_transitions[];

struct state start = {
    "You are on a road that heads west and east of your position.\n"
        "Which way will you go?",
    {
        {"Go West", &spider},
        {"Go East", NULL},
        {"Wait for something to happen", NULL},
        { NULL }
    }
};

struct state spider = {
    "You travel down the road, about only 100 metres and you encounter\n"
        "a giant spider with vicious poison coated fangs.\n"
        "Its hideous appearance causes your throat to dry and your knees to shake!\n"
        "What on earth will you do?",
    {
        { "Attempt to attack the spider with your sword.", NULL },
        { "Throw your sword in the off chance it might kill it.", NULL },
        { "RUN FOR YOUR LIFE!", NULL },
        { NULL }
    }
};


int main(void)
{
    state *cur = &start;
    while (cur) {
        // Print text
        std::cout << cur->text << std::endl;

        // List available choices
        unsigned trans = 0;
        while (cur->transitions[trans].text) {
            std::cout << trans << ". " << cur->transitions[trans].text << std::endl;
            trans += 1;
        }

        // Read input
        unsigned choice;
        std::cin >> choice;
        std::cin.ignore();

        // Check input is valid
        if (choice < trans) {
            // Move to new state
            cur = cur->transitions[choice].next_state;
        }    
    }
    return 0;
}


当然,更成熟的游戏版本将从数据文件读取状态和转换,而不是包括它们直接在代码中。

评论


\ $ \ begingroup \ $
pff您刚刚删除了有限状态机的发现。自己发明这些东西然后看到实际的图案很有趣
\ $ \ endgroup \ $
–约翰·尼古拉斯
14年5月13日在13:34

\ $ \ begingroup \ $
有指定的初始化程序,它们是C99,而不是C ++。您是否正在使用编译器扩展(c)?
\ $ \ endgroup \ $
–莫文
14年5月13日在13:47



\ $ \ begingroup \ $
@Morwenn你是对的。我大多数时候都写C代码。 15年前,我大部分时间都停止使用C ++。我将删除C99指定的初始化程序。我使用支持此功能的编译器(CLang和GCC)。
\ $ \ endgroup \ $
– Erwan Legrand
14年5月13日在16:16

\ $ \ begingroup \ $
@John Nicholas也许我做到了。 OP更有可能在课程中或通过阅读他人的代码来了解它。
\ $ \ endgroup \ $
– Erwan Legrand
14年5月13日在16:25

#3 楼

我想说的是,我们需要研究您游戏的设计。

对我来说,一款冒险游戏正在穿越一系列地点。在每个位置,您可以移动到另一个位置(链接到当前位置)或与该位置的对象进行交互。所以在我看来,您想要构建位置图。

所以首先我们有一个游戏对象。

 class Game
 {
      std::string  currentLocation;                // Assuming a one player game
                                                   // You only need to store that
                                                   // players location.

      std::map<std::string, Location>   locations; // All locations in the game
                                                   // You can look them up by name.
 };


所以我们有一个有位置的游戏。但是,它们如何配合在一起。

 class Location
 {
     std::string                           description;
     std::map<std::string, std::string>    linked;       // Places linked to from
                                                         // here. The action is the key
                                                         // the location is the value
                                                         // and you can get the location
                                                         // details by looking up the 
                                                         // location in the map.
     std::vector<std::string>              thingsLyingHere;
 };

 Example:
 ========
   description: "You are on a road that heads west and east of your position."
   linked["Go West"]  = "TheCreepyCrossroads";
   linked["Go East"]  = "TheLongRoad";
   linked["Wait for something to happen"] = "Waiting At Start";


现在,您的主程序将查看您当前的位置。在Game对象中查找该位置,并可以打印出该位置的描述和选项。当用户输入可用操作之一时,您会在链接中查找并更新当前位置。然后您再次开始循环。

#4 楼

您的代码在将代码与数据分离方面做得很差。我建议您将项目分为两个部分:一个数据文件,用于定义您的冒险经历和代码,该文件解析该数据文件并让您玩游戏。您可以为数据文件提出自己更复杂的“语法”。还有一些工具可以为您进行解析,但是由于该项目是用于学习的,我建议您编写自己的解析代码。
如果要运行一些自定义代码,例如打架或翻滚,随机选择,您也可以将其添加到语法中,并将标记链接到要执行的函数(例如,使用if语句选择正确的代码段)。
很有趣!

数据文件
普通文本文件,例如“ the_dragon_tale.txt”,其中包含您所有的短信和可能的选择。带有注释的文本文件指示程序流程。
示例:
[road]
You are on a road that heads west and east of your position.
Which way will you go?
#west Go West
#east Go East
#roadwait Wait for something to happen

[west]
A spider!!
#fightspider

[east]
A dragon!
#endofgame

[roadwait]
Very boring.
#road


代码
您的程序现在看起来完全不同。首先编写一个读取和解析您的冒险文本文件的函数。对于每个[tag]部分,它将创建一个类的实例,并链接一个由#部分标记的可能的选择。
示例:
struct Choice {
    std::string text;
    std::string target;
}

struct Encounter {
    std::string tag;
    std::string message;
    std::vector<Choice> choices;
}

std::string ReadFile(const std::string& filename) {
    /** check online resources */
}

Encounter ParseEncounter(const std::string& text) { ... }

std::map<std::string,Encounter> ParseEncounters(const std::string& text) {
    std::map<std::string,Encounter> encounters;
    size_t pos = 0;
    while(true) {
        // find next [
        size_t next = text.find("[");
        if(next == std::string::npos) break;
        // get the encounter text
        std::string enc_str = text.substr(pos,next);
        // parse encounter
        Encounter enc = ParseEncounter(enc_str);
        // store encounter
        encounters[enc.tag] = enc;
        // next 
        pos = next;
    }
    return encounters;
}

std::string RunEncounter(const std::map<std::string,Encounter>& encounters, const std::string& tag) {
    system("cls");
    // get encounter
    Encounter node = encounters[tag];
    // print message and choices
    std::cout << node.message << std::endl;
    for(size_t i=0; i<node.choices.size(); i++) {
        std::cout << i << ". " << node.choices[i].text << std::endl;
    }
    // get correct choice from user
    int choice = -1;
    while(!(0 <= choice && choice < node.choices.size())) {
        std::cin >> choice;
    }
    cin.ignore();
    // return choice tag
    return node.choices[choice].target;
}

int main() {
    std::string str = ReadFile("adventure.txt");
    std::map<std::string,Encounter> encounters = ParseEncounters(str);
    std::string tag = "road";
    while(tag != "endofgame") {
        tag = RunEncounter(tag);
    }
}


评论


\ $ \ begingroup \ $
+1是完整的,尽管鉴于问题的性质,我倾向于为OP留一些工作,因为他们似乎正在做某种家庭作业;)
\ $ \ endgroup \ $
–浏览
2014年5月13日晚上8:35

\ $ \ begingroup \ $
还有很多事情要做,如果我没有在某个地方犯错,我会感到惊讶;)
\ $ \ endgroup \ $
– Danvil
2014年5月13日14:23

#5 楼

我会开始将您的代码分解成大块。您可以通过以下几种方法来实现此目的,例如,为每个房间赋予自己的功能(我记得我的第一个文本冒险型游戏可以做到这一点,可以追溯到何时!),或者将房间定义为数据结构,例如:

struct Choice
{
  int nextroom;
  std::string text;

  Choice(int n, std::string t) : nextroom(n), text(t) {}
};

struct Room
{
  const std::string text;
  std::vector<Choice> choices;
  bool deathroom;

  Room(std::string t, std::vector<Choice> c, bool d = false)
   : text(t), choices(c), deathroom(d) {}
};


现在,您可以定义房间的集合:

std::vector<Room> rooms;
Room r1(
    "You are in room 1.\n",
    std::vector<Choice>{ Choice(2, "Option 1"), Choice(3, "Option 2") });
rooms.push_back(r1);


通过这可以将您的游戏变成一个循环:

int room = 0;
while (rooms[room].deathroom == false)
{
    std::cout << rooms[room].text;
    room = get_choice(room);
}
std::cout << "Game Over!\n";


您当然需要自己实现get_choice;)这使得添加和删除房间,甚至将来从文本文件中读取房间定义等都变得更加简单。

评论


\ $ \ begingroup \ $
啊,您想出了类似的解决方案。我认为这是一个好方法!在我的帖子中,我还建议使用数据文件来定义房间。
\ $ \ endgroup \ $
– Danvil
2014年5月13日晚上8:33

#6 楼

我同意您的老师的看法,将代码分解为功能将有助于代码的可读性。

例如,将每个决策结果的代码放入其自己的函数中。

cout << "1. Go West" << endl;
...

if (choice2 == 1) {
    go_west();
} else if (choice2 == 2) {
    go_east();
} else if (choice2 == 3) {
    wait();
}


如果我正在阅读代码这使得找出正在发生的事情变得容易得多。如果输入2会怎样?我不需要跳过所有工作,也不必等待代码发生变化,因为它们在其他地方的函数中。如果我对该代码感兴趣,那么我看一下该函数。

如果两条路线导致相同的结果,这也将有所帮助,您无需复制和粘贴仅调用同一函数的代码。

如果您了解了switch语句,那么使用switch代替if / else可能会为您提供更多的信息。 >这在现实世界中不是很重要。使用您认为更好的方法。

处理决策点时也可以使用enums或常量值。如果我看一下if / else语句,我需要记住:1表示东方还是西方?

switch (choice2) {

case 1:
    go_west();
    break;
case 2:
    go_east();
    break;
case 3:
    wait();
    break;
}


更高级的策略是使用跳转表。

#define WEST 1
#define EAST 2
#define WAIT 3

if (choice2 == WEST) {
    go_west();
} else if (choice2 == EAST) {
    go_east();
} else if (choice2 == WAIT) {
    wait();
}


在当前阶段,这可能对您来说不是一个可行的选择,但可能会在以后的教育中对其他人或您自己有所帮助。

评论


\ $ \ begingroup \ $
以这种方式使用定义可能不是一个好主意。每个位置都有不同的答案集,并且定义的范围很广。
\ $ \ endgroup \ $
–古斯塔夫·贝特拉姆(Gustav Bertram)
2014年5月14日晚上8:31

#7 楼

一个好的通用规则是,如果代码占用两个以上的文本屏幕,则应将其拆分为多个函数。当然,您可以阅读意大利面条代码,但请始终记住,您正在编码供其他人阅读。这些人可能是您的老师(在学校),您的同事(在您离开学校时)或您在6个月内的时间(当您看着它,然后转到“ WTF ?!”)。

具有功能的最大之处在于,您可能有多种到达某处的方法。如果您处在平坦无特征的平原上,然后向北然后向西走,那么通常希望到达的位置与从西开始然后向北走一样。但是,您的代码根本不允许这种情况发生。您是否要复制并粘贴代码以执行两次相同的操作? (如果您的答案不是“ hell no!”主题的变种,那么您刚刚通过了编程能力测试。;)函数将使您非常轻松地执行此操作,因此请使用它们。

代码中要修复的一件事是重入函数调用。我的意思是,您正在从road()内部调用road()。这几乎总是一件坏事。如果函数执行过程中全局资源发生任何变化(提示:cls()更改了全局资源的状态...),那么重新输入可能会执行您不希望执行的操作。您需要跟踪每个重新输入如何影响之前的输入,这很快就会变得非常复杂。更糟糕的是,每次重新输入时,都会用掉新的堆栈空间。在非常小的系统(例如Arduino)上,您可以轻易地消耗比系统可用空间更多的内存,这一切都会崩溃。即使在大型系统(如PC)上,如果您不小心设置了某些东西,这些东西会重新进入循环并持续一段时间,那么您也可以用这种方法杀死它们。您确实需要解决此问题,因为这是非常不好的做法。

哦,玩一些经典的80年代文字冒险游戏。 “ Planetfall”或“ Zork”系列是最重要的提示。

#8 楼

编程时的一个重要经验法则是使功能块小到足以完全容纳在屏幕上,也就是说,最多不超过20行代码。否则,在编写非常大的程序时,您很容易失去概述。

因此,将您的代码分成多个块,您可以指定一个有意义的名称,将每个块放入一个仅提供该名称的子函数中,然后替换其中的代码块只需调用该子功能即可获得原始功能。如果您的代码块中需要任何局部变量,请将其作为子函数的参数传递,并返回进一步需要的任何值。

评论


\ $ \ begingroup \ $
这种方法不利于性能(在高端性能应用程序中)。最好使用#pragma或代码块{}来组织函数实现,而不是将它们拆分为无意义的函数。在代码中具有许多功能可能会导致“意大利面条”代码-仔细阅读它会变得很乏味
\ $ \ endgroup \ $
– NirMH
2014年5月13日在8:14

\ $ \ begingroup \ $
的确,您应该确保函数可以做一件事,而不是限制函数的长度。通常,通过添加空格和良好的格式来增加行数会提高可读性,并将所有内容拆分为太多的功能会降低读者对代码的理解。
\ $ \ endgroup \ $
– rubenvb
2014年5月13日在8:16

#9 楼

将功能中的功能分组是第一步。您可以添加如下函数:决策应使用决策函数完成,该函数接受包含决策描述的字符串和带有可能选择的字符串向量。它返回所选元素的索引。为砖房,巨龙等提供单独的功能。

随着冒险游戏的不断发展,这种策略当然是不够的。我建议将游戏逻辑抽象为适合该任务的数据结构。我认为一棵树是最好的选择。下面的代码示例应该说明这种想法,它们并不完整(最重要的是,它们缺少大多数公共接口):

在通过创建具有顶级故事和以下决定的根节点来实际启动游戏逻辑之前。决策的每个选择都有一个选择文本和一个下一个节点,选择后将被选择。 cout函数返回下一个节点。这会将很多if / else语句吸收到树结构中。

评论


\ $ \ begingroup \ $
对于next_node,最好使用shared_ptr而不是原始指针。
\ $ \ endgroup \ $
–浏览
14年5月13日在8:57

\ $ \ begingroup \ $
是的,我同意。我更新了样本。
\ $ \ endgroup \ $
–爵士
2014年5月13日9:00

#10 楼

只看一眼,我不得不说:这看起来像是一场引人入胜的游戏。我特别喜欢“粘剑挥舞”的视觉形象。我设想一些非常粘的东西,例如由糖蜜制成的肢体,或其他具有这种粘性的东西。 ;-]

我认为if语句是正常的。功能有很多限制。在游戏中最终可能涉及诸如清单或健康栏等功能的情况下,可能会陷入僵局,除非您编写了大量复杂的代码以使其正常工作。

另一方面,您拥有不同的手指。像您所做的那样依靠决策树将分支表示为嵌套的条件语句,可能会更加令人头疼。话虽如此,只有您才能解释自己的生物学记忆。局外人看着它,付钱来教育您,可能是很矫情的,但是他或她的困惑并不一定会阻碍您自己个人风格的发展。最重要的问题是:如果您是独立开发游戏,独自编写代码,您是否能够记住编写游戏时的想法?一个月后有意义吗?一年?十年?

您的函数(如果您选择使用它们)应该打印结果,然后返回某种变量,一旦返回,它将以限制或扩展了玩家的自由度。例如,您可以将游戏分成几个房间,每个函数都可能返回一个数字,该数字会将您定向到一个房间而不是另一个房间。对于情况,事件和健康条之类的事情也可以这样做。只要它可以用整数(或其他某种形式的通用数据)表示。

真的,这一切都取决于您。在您的位置,我只会对库存之类的东西使用功能。但这就是我。我之所以找到这个论坛,是因为我自己在C语言中进行此类编程的经验。