我一直在寻求学习C ++的学习,并且已经开始构建一个小的“角色扮演游戏”骨架来帮助自己学习该语言。我试图确保我正确地遵循了C ++样式/标准。

这是我开发骨架的第一个“部分”,它包括一个可用的类CharacterCharacter类具有以下属性:



characterName-角色的名称。

healthPoints-玩家的健康状况。 (停留在\ $ 0 \ rightarrow \ infty \ $范围内)。


experiencePoints-玩家有多少经验。 (保持在\ $ 0 \ rightarrow \ infty \ $范围内)。


它还具有以下方法:




applyRandomDamage-在一定范围内对玩家造成随机伤害。

applyDamage-施加一定数量的玩家伤害。

addRandomExperience-增加随机数

addExperience-向播放器添加一定量的体验。

toString-显示播放器的统计信息。 (名称/健康/经验)

我想知道以下事情:


是否存在替代项,因此我只需std::cout实例化的Character对象而无需必须创建自己的自定义toString方法?
这是否设计适当?
我遵循正确的C ++样式/标准吗?
这是惯用的C ++吗?

character.h

#pragma once
#include <iostream>

/// <summary>
/// Represents a character, with certain attributes, like
/// health points, or the character's name.
/// </summary>
class Character
{
public:
    std::string characterName;
    int         healthPoints;
    int         experiencePoints;

    Character(std::string c_characterName, int c_healthPoints, int c_experiencePoints);

    void applyRandomDamage(int minimumDamage, int maximumDamage);
    void applyDamage(int damage);
    void addRandomExperience(int minimumExperience, int maximumExperience);
    void addExperience(int experience);
    void toString();
};


character.cpp

#include <iostream>
#include <string>
#include <random>
#include "character.h"

/// <summary>
/// The constructor for our character.
/// </summary>
/// <param name="characterName">The name of our character.</param>
Character::Character(std::string c_characterName, int c_healthPoints, int c_experiencePoints)
{
    characterName    = c_characterName;
    healthPoints     = c_healthPoints;
    experiencePoints = c_experiencePoints;
}

/// <summary>
/// Apply a random amount of damage to the player, based on
/// a low and a high value.
/// </summary>
/// <param name="minimumDamage">The minimum damage to apply.</param>
/// <param name="maximumDamage">The maximum damage to apply.</param>
void Character::applyRandomDamage(int minimumDamage, int maximumDamage)
{
    std::random_device                 randomDevice;
    std::mt19937                       engine(randomDevice());
    std::uniform_int_distribution<int> distribution(minimumDamage, maximumDamage);

    int damage = distribution(engine);
    healthPoints = healthPoints - damage >= 0 ? healthPoints - damage : 0;
}

/// <summary>
/// Apply a set damage to the character, rather than a
/// random amount in a certain range.
/// </summary>
void Character::applyDamage(int damage)
{
    healthPoints = healthPoints - damage >= 0 ? healthPoints - damage : 0;
}

/// <summary>
/// Add a random amount of experience to the player.
/// </summary>
/// <param name="minimumExperience">The minimum experience to add.</param>
/// <param name="maximumExperience">The maximum experience to add.</param>
void Character::addRandomExperience(int minimumExperience, int maximumExperience)
{
    std::random_device                 randomDevice;
    std::mt19937                       engine(randomDevice());
    std::uniform_int_distribution<int> distribution(minimumExperience, maximumExperience);

    int experience = distribution(engine);
    experiencePoints = experiencePoints + experience >= 0 ? experience + experience : 0;
}

/// <summary>
/// Add a set amount of experience to the player.
/// </summary>
/// <param name="experience">The experience to add to the player.</param>
void Character::addExperience(int experience)
{
    experiencePoints = experiencePoints + experience >= 0 ? experience + experience : 0;
}

/// <summary>
/// Returns a string value representing the character, e.g
/// health points, or the character's name.
/// </summary>
void Character::toString()
{
    std::cout << "Name: "       << characterName    << "\n";
    std::cout << "Health: "     << healthPoints     << "\n";
    std::cout << "Experience: " << experiencePoints << "\n";
}


main.cpp(测试)

#include <iostream>
#include "character.h"

int main()
{
    Character character = Character("Billy Bob Jenkins", 100, 50);
    character.toString();

    character.applyRandomDamage(5, 10);
    character.addRandomExperience(5, 10);
    character.toString();

    character.applyDamage(50);
    character.addExperience(50);
    character.toString();

    std::cin.get();
}


评论

我在回答中未解决的问题:字符character = Character(“ Billy Bob Jenkins”,100,50);可以重写为Character character {“ Billy Bob Jenkins”,100,50};

#1 楼

不要每次都重新创建随机引擎

此处:

std::random_device                 randomDevice;
std::mt19937                       engine(randomDevice());
std::uniform_int_distribution<int> distribution(minimumExperience, maximumExperience);


这不会使其变得更加随机,并且会影响您的数字质量得到。随机引擎具有某种状态。通常,您只需要一个引擎,尽管您可以拥有更多,但是您不希望每次调用该函数时都重新创建它。这个答案很好地解释了它:


“ ...想一想一个随机数生成器,其中必须在每个线程的基础上维护种子。
线程局部种子意味着
每个线程都具有自己的随机数序列,与其他
线程无关。

如果种子是随机函数中的局部变量,则它
每次调用时都会被初始化,每次都给您相同的号码
。如果它是全局的,线程将干扰每个
其他人的序列。...“

— paxdiablo


所以你可以做这样的事情:

// credit: http://cpp.indi.frih.net/blog/2014/12/the-bell-has-tolled-for-rand/
auto& prng_engine()
{
  thread_local static std::random_device rd{};
  thread_local static std::mt19937 engine{rd()};

  // Or you can replace the two previous lines with:
  //thread_local static std::mt19937
  //  prng{std::random_device{}()};

  return engine;
}


不要试图成为聪明

我觉得这条线特别不可读:

experiencePoints = experiencePoints + experience >= 0 ? experience + experience : 0;


它可以替换为:

if (experience >= 0)
    experiencePoints += experience * 2;


有一条注释,解释您为什么这样做(例如,我不明白您为什么这样做experience + experience。)生成文档的注释可以,b ut并不能很好地解释原因而不是原因。
代码重复

我很讽刺的是,您在下面的位置有一个addExperience函数在上述功能中使用了它的主体。尝试以下操作:

void Character::addRandomExperience(int minimumExperience, int maximumExperience)
{
    // ...
    int experience = distribution(engine);
    addExperience(experience); // <---
}


您的toString不是惯用语

您的评论说:“返回表示字符的字符串值,例如
健康点或角色名称。”但是您的方法实际上具有void的返回值。这对您的用户不利。

就是这个如果您希望toString()的行为像用户期望的那样,请使其成为一种序列化方法,并使用std::cout取消打印。如果您希望将其输出到文件中怎么办?如果您只需要以序列化的形式弄乱对象而根本不输出它怎么办?这很简单,但是需要根据您的用例进行更改:

// Serializes for OUTPUT purposes. Don't use this for internal
// data handling
std::string Character::toString() const
{
    // to_string is C++11. Use stringstream in C++03
    return "Name: "      + characterName +
           "Health:"     + std::to_string(healthPoints) +
           "Experience:" + std::to_string(experiencePoints);
}

// return hash of properties, maybe for
// save file purposes
std::string Character::serialize() const
{
    // ...
}


注意:我添加了const,因为您没有进行任何修改(这是一个“获取”方法。)

重载operator<<

阅读关于stackoverflow的运算符重载常见问题解答。特别是,您的存根应该是这样的:

// https://stackoverflow.com/questions/4421706/operator-overloading/4421719#4421719

std::ostream& operator<<(std::ostream& os, const Character& obj)
{
    os << obj.toString();
    return os;
}


评论


\ $ \ begingroup \ $
感谢您的好评!不过,我确实有一个问题,我该如何使用您的函数正确生成随机数?您能否在帖子中提供示例用法?
\ $ \ endgroup \ $
– Ethan Bierlein
15年8月28日在3:23

\ $ \ begingroup \ $
@EthanBierlein诸如收益分配之类的东西(prng_engine());
\ $ \ endgroup \ $
–user82194
15年8月28日在3:31

\ $ \ begingroup \ $
这是示例代码回顾。
\ $ \ endgroup \ $
– lonstar
15年8月28日在5:15

\ $ \ begingroup \ $
我同意您引用的“特别不可读”行确实像您描述的那样不可读,但是我认为您实际上对它的解析不正确:+绑定比> =和?更紧密。 :,按照书面形式,实际上等同于if(experiencePoints + experience> = 0){experiencePoints = 2 * experience; } else {experiencePoints = 0; }但是,我认为原始代码在这里有一个错误,第二个经验就是“ experiencePoints”,OP想要实现的只是“ experiencePoints + = experience;”。如果(experiencePoints <0)experiencePoints = 0;。
\ $ \ endgroup \ $
–伊尔马里·卡洛宁(Ilmari Karonen)
2015年8月28日在11:22



\ $ \ begingroup \ $
如果我能多次投票赞成“不要试图变得聪明”,我会。欢迎来到华润。感谢您的好评。
\ $ \ endgroup \ $
–丹
15年8月28日在14:46

#2 楼

我只想添加一些有关您的“ healthPoints”和“ experiencePoints”的想法。我发现(以维护方式)进行以下操作很危险:


对每个应用损害/经验/任何功能的功能进行相同的检查:由于逻辑相同,因此应全部定义为一个逻辑放!
让此类中的任何函数更改这些参数(什么也不阻止任何函数直接修改您的“ healthPoints”,因为它甚至不是私有的,但即使如此,您类中的每个函数都可以更改它) 。
使用这种设计,您可能会错误地增加或减少健康状况,反之亦然,编译器应禁止这样做。

因此,我要做的就是创建一个简单的模板,其中包含值以及加法和减法,以检查行为是否良好。结果,“ AddDamage”和co将不再需要检查边界!该类应保持POD并内联所有内容,以免影响性能。

enum SpecType {HEALTH, MANA, EXPERIENCE};

template <SpecType T> class Spec
{
private:
    /* Could be unsigned int as only subtraction can
       be a problem (and we define it in the class),
       but beware exposing this to the outside xP */ 
    int points;
public:
    Spec<T>(int value) : points(value) {}
    // ...

    int Points() const
    {
      return points;
    }

    Spec<T> operator-=(int value)
    {
      if (value >= points)
        points = 0; // maybe do something about death too...
      else
        points -= value;

       return *this;
    }
};

// ...

class Character
{
public:
    std::string characterName;
    Spec<HEALTH>      healthPoints;
    Spec<EXPERIENCE>  experiencePoints;
    Spec<MANA>        manaPoints;
    // ...
}

// ...

Character::Character(std::string c_characterName, int c_healthPoints, int c_experiencePoints) : characterName(c_characterName), heathPoints(c_healthPoints), experiencePoints(c_experiencePoints) // just as easy
{}

void Character::applyRandomDamage(int minimumDamage, int maximumDamage)
{
    // ...

    int damage = distribution(engine);
    healthPoints -= damage; // <= so much easier, and no risk to forget checking for below zero or ...
}


如果要添加级别,可以将其添加到规格中类,因此您的损坏功能也不必自己关心。或派生healthPoint并添加更多操作员,这些操作员将获得类型化的伤害点(正常,钝器,加重...),因此计算结果会因这些而有所不同...?

小小的好处:也许创建用户这些类型的文字,请参见。 http://www.stroustrup.com/C++11FAQ.html#UD-literals

评论


\ $ \ begingroup \ $
Boost.Units安全时尚。不错:)
\ $ \ endgroup \ $
–莫文
15年8月28日在21:50

\ $ \ begingroup \ $
首先,还有一个很流行的max(health-Damage,0),根据编译器的不同,max(health-Damage,0)的效率也可能更高。
\ $ \ endgroup \ $
– Voo
15年8月29日在13:18

\ $ \ begingroup \ $
您能否提供Spec类的示例用法?我已经使用了一些模板,但是我不确定如何在这里使用该类。
\ $ \ endgroup \ $
– Ethan Bierlein
15年8月29日在15:31

\ $ \ begingroup \ $
@Shautieh您在争论的是“只要没有人做错任何事,就可以正常工作”,这永远都是正确的。您刚刚确定范围足够小以至于可以(因此,在几行之后不再是真的吗?)。但是真正的问题是,未签名的泄漏会在您的界面上泄漏,因为它显然不仅仅是内部细节。您的示例已经说明:public unsigned int Points()const-现在,每个调用者也必须处理unsigned。迟早(确实很快)有人会将其传递给采用int的效用函数。
\ $ \ endgroup \ $
– Voo
15年8月31日在19:20



\ $ \ begingroup \ $
@EthanBierlein:我添加了一些来自您代码的示例(希望我没有做任何错误^^)。零测试将通过模板中定义的-=函数完成(与正常类完全相同),因此,当对任何规格造成损害时,“用户代码”甚至不需要关心。如果只想在健康点案例中而不是在体验案例中做一些特殊的事情,则模板可以是专门的。或者,您可以给它一个函子,告诉类在达到零时该做什么(健康=>死亡,经验或法力=>什么都没有?,...)。
\ $ \ endgroup \ $
– Shautieh
15年8月31日在21:23



#3 楼

关于您的代码,已经有很多事情了(我很喜欢它的样子:p)。但是我仍然想加两分钱:




#pragma once不是标准的,由于标准委员会正在研究模块系统,因此极不可能将其标准化。另一方面,它应该适用于大多数编译器,有时会缩短编译时间。如果您不想让皮棉工具抱怨它,则应替换它或使用包含保护器对其进行完善:

#ifndef RPGGAME_CHARACTER_H_
#define RPGGAME_CHARACTER_H_

// Your code here...

#endif // RPGGAME_CHARACTER_H_


尝试仅包含所需的标头。如果决定按照建议的方式重载operator<<,character.h只需要知道std::ostream存在,因此可以在其中包含<iosfwd>。它是一个头,其中包含IO流库中类的声明。另一方面,character.cpp可能需要知道std::ostream是什么,因此您需要包括<ostream>。只要您不使用std::cout及其朋友,就不需要包括<iostream>

当您不修改函数中的值时,可以通过const引用来传递它通过复制传递它,以便不执行不必要的复制:

Character(const std::string& c_characterName, int c_healthPoints, int c_experiencePoints);
          ^^^^^            ^


规则实际上比这复杂。例如,小型类型(数字或指针之类的内置类型)倾向于通过复制传递。另外,还有更多涉及复制省略和移动操作的细微规则,但是大多数情况下,当您不修改参数时,您会使用const引用。


尽可能使用构造函数初始化列表。使用它和简单地初始化构造函数主体中的成员变量之间的区别在于,使用初始化列表,可以使用正确的值构造对象。另一方面,在构造函数的主体中初始化成员变量时,将构造对象,然后为成员变量分配值,这意味着更多潜在的工作。

Character::Character(const std::string& characterName, int healthPoints, int experiencePoints):
    characterName(characterName),
    healthPoints(healthPoints),
    experiencePoints(experiencePoints)
{}


请注意,我故意更改了您传递的变量的名称:括号前的名称将与成员变量的名称相对应,而括号中的名称将在成员变量名称之前选择参数名称。取决于您如何命名变量:p

我看到您正在使用MSVC文档注释。如果您想要更多可移植的注释,则可以改用Doxygen注释。它们通常被更多的项目使用,并被更多的人所理解。


#4 楼

我注意到这是一件小事。我可以只提供两个可能的函数重载,而不是拥有两个单独的体验修改函数和两个不同的运行状况修改函数。这意味着Character类的公共部分将改为具有以下签名:

void applyDamage(int damage);
void applyDamage(int minimumDamage, int maximumDamage);

void applyExperience(int experience);
void applyExperience(int minimumExperience, int maximumExperience);


然后将在character.cpp中实现这些重载的后续实现。我认为这种实现方式可能对用户更友好,要求用户知道的名称更少。

我的一些变量名很不确定。我找到了一些,以及它们的后续替换:



characterName-> name


healthPoints-> health


experiencePoints-> experience


我还决定,如果我需要创建实体,则使用字符等方法定义基本Entity类将非常有用类生物/ NPC。

class Entity
{
public:
    std::string name;
    int health;

    Entity(const std::string& name, int health);
    ...
}