小话游戏脚本(三)
三.heScript的一种简单实现
在此就heSript实现过程中的一些解决方案和自己的想法陈列一番,由于自己编程水平实在拙劣,又没什么实际经验,所以导致相关的代码非常糟糕,所以竭诚欢迎大家批评指正,在下先拜谢了:D ( PS:以下代码均使用MinGW3.4.2进行编译,使用IDE为Dev-C++4.9.9.2,由于代码中使用了一些C++的新近标准,所以在VC6中不能正确编译,而在VC7.1及VC8中的编译问题则未有试验 )
.一开始我先定义了一个简单的错误处理模块,用于处理程序运行过程中的各种异常,并且据此定义了一个为方便使用的宏 THROW,其中代码相当简单,有意者可参见示例程序中的 heException.h文件 :)
.接着我编写了一个相当简单的词法分析器,用以将读入的文本字符转换成词法单元(Token),目的是为接下来的编译操作打下基础,相应的头文件粗列如下:
//预定义的词法属性枚举值
enum heToken
{
TOKEN_INCLUDE = 0,
TOKEN_VAR,
TOKEN_CONST,
TOKEN_BLOCK,
TOKEN_ENDBLOCK,
TOKEN_INT,
TOKEN_STRING,
TOKEN_IDENTIFY,
TOKEN_END,
TOKEN_FORCE32 = 0xFFFFFFFF
};
class heLexParser
{
public:
heLexParser( const char* filename );
~heLexParser();
heToken GetNextToken();
void ReadToken( heToken token );
int GetIntVal() const;
const string& GetStrVal() const;
private:
bool isNum( char lexChar );
bool isIdentify( char lexChar );
fstream m_fs;//文件流
static const int MAX_LINE_COUNT = 512;
static const int MAX_STRING_COUNT = 256;
static const int MAX_NAME_COUNT = 32;//最长的 函数及参数 名字
char m_buffer[MAX_LINE_COUNT];//用以暂存脚本内容
char m_value[MAX_NAME_COUNT];//存储真实的 Token 数据
int m_intVal;//用以记录返回的整数值
string m_strVal;//用以记录返回的字符串值
};
其中的代码实现相当简单,有意者可以参见示例程序中的heLexParser.h/cpp文件,其中对于词法属性的解析( GetNextToken() )比较杂乱,正统并且更具扩展性的做法是使用有限状态机 :)
.接着便该是编译模块了,由于heScript的设计相对简单,所以我编写了heScript这个类来执行编译工作以及运行编译后的脚本代码,当然,在编写编译执行模块之前,我必须首先定义好脚本的编译码格式,经过几番的修改,现在的情况如下(有意者请参看heScriptType.h文件):
const int BAD_PARAM_VALUE = -1;
//参数
union Param
{
int iValue;
const char* cString;
};
//参数链表
struct ParamList
{
Param param;
ParamList* pNext;
ParamList():pNext(NULL) { param.iValue = BAD_PARAM_VALUE; }
};
//指令
struct Operation
{
#ifdef HEDEBUG
friend ostream& operator << ( ostream& o, const Operation& op );
#endif
OpCode opCode;
int paramCount;
ParamList paramList;
~Operation();//处理内存的管理
};
//指令流
typedef std::list<Operation> OpStream;
接着为了便于管理脚本中出现的各类数据,我分别编写了很多表类,相关的头文件都比较简单,现分列如下(实现代码可以参见相关的cpp文件):
//为了处理Include重复文件的问题,编写了以下的类
class heIncludeTable
{
public:
void Add( const string& str );
bool Get( const string& str ) const;
#ifdef HEDEBUG
void Show( ostream& o ) const;
#endif
private:
set<string> m_table;
};
//常量表
typedef Pair< int, bool > CstValRet;//此处的Pair为自定义类型,参见这里,下同
class heConstValueTable
{
public:
void Add( const string& str, int value );
CstValRet Get( const string& str ) const;
#ifdef HEDEBUG
void Show( ostream& o ) const;
#endif
private:
map<string, int> m_table;
};
//字符串表
class heStringTable
{
public:
const char* Add( const string& str );
#ifdef HEDEBUG
void Show( ostream& o ) const;
#endif
private:
vector<string> m_table;
};
//变量表
const int BAD_VAR_INDEX = -1;
typedef Pair<int,bool> VarValRet;
class heVarTable
{
public:
void Add( const string& var, int val );
VarValRet Get( int index ) const;
int GetDir( int index ) const;
VarValRet Get( const string& var ) const;
int GetIndex( const string& var ) const;
void SetVar( int varIndex, int val );//用以设定变量的值
void AddVar( int varIndex, int delta );//用以改变变量的值
#ifdef HEDEBUG
void Show( ostream& o ) const;
#endif
private:
map<int,int> m_table;
map<string,int> m_indexTable;
static int c_varCount;
};
//模块表
const int BAD_BLOCK_INDEX = -1;
typedef Pair< OpStream*, bool > BlkRet;
class heBlockTable
{
public:
void Add( const string& blockName, const OpStream& blockOp );
BlkRet Get( int index );
const OpStream& GetDir( int index ) const;
BlkRet Get( const string& blockName );
int GetIndex( const string& blockName ) const;
#ifdef HEDEBUG
void Show( ostream& o ) const;
#endif
private:
map<int,OpStream> m_table;//用以存储block指令流
map<string,int> m_indexTable;
static int c_blockCount;
};
//以下的函数管理,感觉在数据表示上有些冗余:)
typedef void (*PFunc)( const ParamList& paramList );
class heFuncManager
{
public:
//注册自定义函数
void Register( const string& funcName, int paramCount, PFunc pFunc );
PFunc GetFunc( int funcIndex ) const;
int GetIndex( const string& funcName ) const;
int GetParamCount( int funcIndex ) const;
#ifdef HEDEBUG
void Show( ostream& o ) const;
#endif
private:
std::map<int,PFunc> m_funcs;//整数 函数指针 一一映射
std::map<string,int> m_funcsName;//函数名 整数 一一映射
std::map<int,int> m_funcsParam;//整数 参数个数 一一映射
static int c_funcCount;
};
关于Pair类型的定义说明:
template<typename T1,typename T2>
struct Pair
{
Pair( const T1& t1, const T2& t2 );
Pair():first(T1()),second(T2()) {};
T1 first;
T2 second;
};
template<typename T1,typename T2>
Pair<T1,T2>::Pair<T1,T2>( const T1& t1, const T2& t2 ):first(t1),second(t2) {};
好了,简绍完这些辅助模块,该是重头戏编译登场了:)由于相关的代码比较繁琐,这里便暂列出几个核心的函数用以说明:
void parseCode( heLexParser& lexParser, OpStream& opStream );
void parseInclude( heLexParser& lexParser, OpStream& opStream );
void parseVar( heLexParser& lexParser, OpStream& opStream );
void parseConst( heLexParser& lexParser, OpStream& opStream );
void parseBlock( heLexParser& lexParser, OpStream& opStream );
void parseCommand( heLexParser& lexParser, OpStream& opStream );
解析过程中,我使用了递归下降的方法,因为感觉这很符合人类的思维习惯,当然,更好的做法可能是使用自动化工具,如Yacc,另外进一步的信息可以点这里。
接着便只剩下运行了,经过一番摸索,还是使用堆栈最为稳定,所以我定义了如下的两个运行时堆栈:
stack<const OpStream*> m_bkStack;//当前执行的指令流
stack<OpStream::const_iterator> m_opStack;//当前执行的指令
而用于执行指令的函数总相对简单:
void Run( int begOp = 0 );//对外接口
void run();//运行所有指令
void runSingle();//运行单条指令
一切搞定之后,我们就可以简单的编写一个测试程序用以执行上面的实例脚本代码了:)
四.小小的一番总结
也算是花了不少的时间,我胡侃了一番游戏脚本,其中的内容着实一般,希望大家不要耻笑,高手直接无视便可,写这些东西的初因也是为了自己更好的学习,也没有任何传道授业解惑的意思,至于编写上面的那点程序也仅仅是完成自己的一个喜好,顺便也练练自己那双拙笨的双手,就实用性角度而言,我绝不认为白手起家重新构建一门脚本语言是一种明智之举,毕竟几经完备、备受考验的脚本语言并不匮乏,如Lua、Python、Ruby等等都是一流的脚本语言,自己实现脚本,除了纯粹用以提高自己的水平或是工程所迫以外,没有什么其他好处,所以需要实事求是的看待,总之还是那句老话:除非深思熟虑,不要重造车轮 :)