当前位置:首页>>攻略文章>>正文
魔兽科普:模拟器组成原理(一)
2013-12-27 23:50:41 作者:fhsvengetta 来源: 浏览次数:0
摘要:我会把我所知道的重点尽可能地写出来。由于这个系统非常庞杂,不可能事无巨细全都写出来,但在你读懂这篇内容之后,加上少许编程功底,应该就能够达到自制DPS模拟器的水平。
导航:

 蒙特卡洛法重识SimC反向工程时间驱动和事件驱动表达式解析面向对象的模拟器设计机器不掷骰子|


表达式解析
既然从CLI的参数开始说起,那么我们就来看看一个DPS模拟器如何解析输入的参数,“解析”说得玄乎,其实大体意思就是“读懂”。
我们只讲实现原理,与具体实现无关。
这一部分其实很像代码编译器/解释器。SimC参数本身就是一种代码,我们在程序运行的初始阶段要对它予以解释。

输入参数的解析
首先是输入参数的解析。其实这很简单,举一例:
Code (c):
warrior=Aean

我们逐字扫描。一开始,我们一定会扫描到一个关键字,所以我们的程序期望得到一个关键字。
扫描到'w'
w是一个字符,把它暂时存起来。现在我们存了一个“w”,并且期望它是一个还未写完的关键字。
扫描到'a'
a也是一个字符,把它和w存在一起。现在我们存了一个“wa”,并且期望它是一个还未写完的关键字。
扫描到'r'
r也是一个字符。现在我们存了“war”
……
扫描到'r'
现在我们存了“warrior”。虽然它已经是一个关键字了,但我们仍要继续扫描。因为某些关键字会是另一些关键字的前缀,比如“actions”和“actions.pre_combat”。
扫描到'='
扫描到等号是一个转折点。期望得到一个关键字并扫描到一个等号,这意味着关键字已经全部录入进来。现在我们可以检查我们存起来的“warrior”是不是一个关键字了。
很显然“warrior”是一个关键字,它声明了一个战士玩家,其值为玩家的名字。
现在我们可以调用warrior_t类的构造函数,等待输入表达式中的值部分,为warrior_t实例的名字属性赋值。
现在程序期望得到一个“战士的名字”类型的值。
依次扫描到'A' 'e' 'a' 'n'
它们都是字符,所以把它们存起来。
扫描到'\0',也就是字串结尾的标志位。
这说明了此条表达式已经结束,所以也意味着值已经输入完毕了。
检查已经存起来的“Aean”是否可以转化为一个战士的名字?战士的名字应该是个字串,很显然“Aean”是个字串。OK,现在给刚刚构造的warrior_t的名字属性赋值。
最后,我们把这个战士加入到需要模拟的团队里来。
由于这条表达式刚刚声明了一个战士,所以接下来,除非再声明一个其他玩家或者敌人,所有的表达式都应该是描述战士Aean或者描述模拟设置的。
假设我们将参数错写成:
Code (c):
warrioraa=Aean

当程序扫描到'='时,存储的字串是“warrioraa”。程序期望它是一个关键字,但它不是关键字。这时程序报错:“无法识别关键字warrioraa”。

copy关键字就是将调用player_t派生类构造函数改为调用其复制构造函数,这样新角色将获得上一个角色已经获得的全部属性。
main_hand关键字的参数可能看起来复杂一些,但无非也就是按这个顺序进行的,只不过是一个weapon_t类的参数(上限、下限、速度、类型、名字……)全都在这一行表达式里赋了值,一次赋了很多的值。

追加符的实现
某些关键字(例如“actions”)支持用等号“=”赋值,也支持用追加符“+=”进行追加。
其实也很好实现,只要将“actions”和“actions+”视作不同的关键字就可以了。
如果使用追加符,第一次扫描到'='时程序所保存的字串是“actions+”,这时对动作优先级列表字串进行追加。使用等号时,程序第一次扫描到'='所保存的字串会是“actions”,这时抛弃动作优先级列表字串现存的内容,予以重新赋值。

条件逻辑的实现
输入的动作优先级列表必须要搭配一定的条件逻辑,才能构成优先级。
对这种逻辑表达式,我们举一例:
Code (c):
if=(blood=2|death=2)&!time<5

这种表达式采用二叉树来表示是绝佳的。设想定义一个二叉树结点类型expr_t,具有左值lvalue指向另一个结点、

 \

 右值rvalue指向另一个结点,和自身求值函数eval

expr_t.eval = expr_t.lvalue->eval (某种运算) expr_t.rvalue->eval
\
就可以模拟整个逻辑表达式。例如上述表达式可以构建出这样一棵二叉树:
单目运算符(variable)可以让其存储一个函数指针,每次调用eval的时候访问指定函数来求值。单目运算符(constant)就很简单了,存储一个double类型的数据,然后每次调用eval都返回它。
这样我们每次扫描动作优先级列表的时候,只要访问根节点的eval就可以得到整个逻辑表达式的真假。

我们简单看一下SimC的代码片段,然后就结束。
基类:
Code (c):
class expr_t  //这是树中所有节点类型的基类
{
  std::string name_;  //保存名字。

protected:  //下面这两句函数重载,负责将类型转换正确。其他类型只要转型成double即可。
  template <typename T> static double coerce( T t ) { return static_cast<double>( t ); }  
  static double coerce( timespan_t t ) { return t.total_seconds(); }  //对timespan_t的特殊处理,这是SimC的时间类型。用秒为单位返回。

  virtual double evaluate() = 0;  //evaluate是一个纯虚函数,具体实现由派生类去定义。

public:
  expr_t( const std::string& name ) : name_( name ) {}  //构造函数,只要名字。
  virtual ~expr_t() {}  //析构函数是虚的,因为基类中还没有定义子节点,只有单目和双目类型才有子节点。

  const std::string& name() { return name_; } //取名字。

  double eval() { return evaluate(); }  //派生类覆写了evaluate,eval总是返回evaluate的值。
  bool success() { return eval() != 0; }  //根节点需要这个东西。如果根节点的eval不为零,说明式子成立。否则不成立。

  static expr_t* parse( action_t*, const std::string& expr_str ); //这个函数是字串解析的入口,它负责构建整棵树。
};
 

变量节点(引用型):
Code (c):
template <typename T>  //模板。
class ref_expr_t : public expr_t //这是引用型的变量节点类型,继承自基类expr_t
{
  const T& t;  //存储一个类型为T的引用,T由模板给出。通过引用取到的值是可以随着被引用量一起变化的,所以是变量。
  virtual double evaluate() { return coerce( t ); } //强制转型后就直接输出。

public:
  ref_expr_t( const std::string& name, const T& t_ ) : expr_t( name ), t( t_ ) {}  //构造函数,给基类命名,然后将引用存起来。
};

变量节点(函数型):
Code (c):
template <typename F>  //模板。这里F应该是一种函数指针。
class fn_expr_t : public expr_t  //函数型的变量节点类型,继承自基类expr_t
{
  F f;  //存储一个函数指针。

  virtual double evaluate() { return coerce( f() ); }  //通过函数指针调用指定的函数来取值,强制转型后输出。

public:
  fn_expr_t( const std::string& name, F f_ ) :  //构造函数。
    expr_t( name ), f( f_ ) {}  //成员初值列:给基类命名,存储函数指针f。
};
//===============================================
//这个函数型变量节点类型,看不明白就看下面这段代码。
//这是用于构造这类节点的两个函数。
//函数1:调用普通函数的。
template <typename F>  //F就是函数签名。
inline expr_t* make_fn_expr( const std::string& name, F f )  //两个参数:名字,和符合F签名的函数指针f。
{ return new fn_expr_t<F>( name, f ); }  //对于普通的函数,直接构建就可以了。
//函数2:调用一个类成员函数的。
template <typename F, typename T>   //F仍然是函数的签名,T是该函数所在类的类型。
inline expr_t* make_mem_fn_expr( const std::string& name, T& t, F f )  //三个参数:多输入一个指向类实例的引用。
//要知道,所有的“类成员函数”都有一个隐含的参数,就是this指针。
//所以类成员函数表面上没有参数的,其实有一个参数。表面上有两个参数的,实际有三个参数。
//进行调用的时候,只提供表面上签名所要求的那些参数是不够的。你必须提供这个隐含的this指针才能成功调用。
//否则一个类的实例千千万,鬼知道你要调用的是哪一个实例的?
{ return make_fn_expr( name, std::bind( std::mem_fn( f ), &t ) ); } //把隐含的参数this补上,使用std::bind来完成。
//===============================================

常量节点:
Code (c):
class const_expr_t : public expr_t //常数结点类型,继承自基类expr_t
{
  double value;  //在其中存储一个double类型的数据作为常量。

public:
  const_expr_t( const std::string& name, double value_ ) :  //构造函数
    expr_t( name ), value( value_ ) {}  //成员初值列:给基类命名(名字是用来在出错时报错的),给存储的double赋值。

  double evaluate() // override 重写基类的evaluate函数,
  { return value; }  //使其直接返回存储的double就行了。
};

单目运算符:
Code (c):
template <double ( *F )( double )>  //使用模板可以减少代码量。接受一个参数为double、返回值为double的函数指针。
class expr_unary_t : public expr_t  //这些是单目运算符。继承自基类expr_t
{
  expr_t* input;  //单目运算符没有左值和右值,只有单个输入。

public:
  expr_unary_t( const std::string& n, expr_t* i ) :  //构造函数
    expr_t( n ), input( i )  //给基类命名,给指向子节点的指针赋值。
  { assert( input ); }  //断言指针不应该是空的。这是个Debug语句,没有实际效果。

  ~expr_unary_t() { delete input; }  //自己析构的时候,顺带将子节点也析构掉。
                                                   //这样只要对根节点进行析构,就可以析构整棵逻辑树。
  double evaluate() // override 重写基类的evaluate函数,
  { return F( input -> eval() ); } //将子节点的eval返回值,经由模板函数F做某种运算(逻辑非、相反数、绝对值、取整等等),然后送给父节点。
};

双目运算符:
Code (c):
class binary_base_t : public expr_t  //这是所有双目运算符的基类,继承自expr_t。
{
protected:  //Protected型的变量,自己和自己的派生类可以调用,而其他人不行。
  expr_t* left;  //左值指针
  expr_t* right;  //右值指针

public:
  binary_base_t( const std::string& n, expr_t* l, expr_t* r ) :  //构造函数,除了名字之外,当然还需要指向左值和右值的指针。
    expr_t( n ), left( l ), right( r )  //初值列
  {
    assert( left );  //断言左值指针非空
    assert( right );  //断言右值指针非空
  }

  ~binary_base_t() { delete left; delete right; }  //析构时,将左值节点和右值节点一起析构。
};                                                                  //这样只要对根节点进行析构,就可以析构整棵逻辑树。


template <template<typename> class F> //套了两层模板。实际上,“template<typename> class F”是一些STL的签名,后边列出来。
class expr_binary_t : public binary_base_t  //这是真正的双目表达式类型,继承上面那个类。
{                                          //由于上面那个类声明了protected类型的左值和右值指针,这里就无需再声明private类型的左值和右值指针了。
public:
  expr_binary_t( const std::string& n, expr_t* l, expr_t* r ) :  //构造函数
    binary_base_t( n, l, r )                                                     //用上面那个类的构造函数。
  {}

  double evaluate() // override  覆写evaluate,将左值节点的eval和右值节点的eval做F运算,
  { return F<double>()( left -> eval(), right -> eval() ); } //输入和输出都是double。
};
//这里F其实代表了一些STL:
//std::plus
//std::minus
//std::multiplies
//std::divides
//std::equal_to
//std::not_equal_to
//std::less
//std::less_equal
//std::greater
//std::greater_equal
//这些都是数值运算。所以做成了一个模板类expr_binary_t。
//如果我想创建一个加法节点,就写 new expr_binary_t<std::plus>( name, left, right ) 。
//减法就把plus换成minus。模板大大减少了代码量,为每种计算创建一个形式相同的类,这种无聊工作由编译器承担。
//逻辑运算的数值的类型不是double,所以为逻辑运算“与”和“或”专门写了类。
class logical_and_t : public binary_base_t //与类
{
public:
  logical_and_t( const std::string& n, expr_t* l, expr_t* r ) :
    binary_base_t( n, l, r )
  {}

  double evaluate() // override
  { return left -> eval() && right -> eval(); } //左右相与
};

class logical_or_t : public binary_base_t  //或类
{
public:
  logical_or_t( const std::string& n, expr_t* l, expr_t* r ) :
    binary_base_t( n, l, r )
  {}

  double evaluate() // override
  { return left -> eval() || right -> eval(); }  //左右相或
};



相关报道:

[关闭] [返回顶部]


  返回首页 | 最新资讯 | 资源下载 | 魔兽图片 | 单机文档 | 技术攻略 | 玩家视频
备案号:蜀ICP备2024062380号-1
免责声明:本网站为热爱怀旧WOW的玩家们建立的魔兽世界资料网站,仅供交流和学习使用,非盈利和商用.如有侵权之处,请联系我们,我们会在24小时内确认删除侵权内容,谢谢合作。
Copyright © 2024 - 2024 WOWAII.COM Corporation, All Rights Reserved

机器人国度