项目希望能够实现一些剧情动画,类似角色移动,镜头变化,台词展现等.剧情动画这东西随时需要修改调整,不能写死在代码里.考虑之后认为需要做一个简单的DSL来定制剧情脚本,策划在脚本里按顺序写入命令,然后我们解释命令执行即可.

  项目的很多功能系统并没有能够实现导入lua中,非我所能决定,若可以则使用lua方便不少.因此我决定使用C++来制作这个剧情脚本DSL.

  使用boost的spirit来负责脚本的解析,使用asio的coroutine简化了指令处理逻辑.

  DSL当然不能太复杂,第一个版本看起来类似:

  1. role_walk LEFT 100;
  2. role_dialog "stop!!!" 4;
  3. role_jump FORWARD;
  4. role_walk RIGHT 200;
  5. monster_dialog "byebye!!!" 2;
  6. monster_run RIGHT 400
  7. role_walk RIGHT 500;
  8. role_jump BACK;

  稍加按上边指令流程走下来,会发现一些指令是有延时性的.比如走,跑等,都需要移动到目标地点才算结束.当遇到这个指令时,我们是继续往下解析指令,还是在当前指令阻塞呢?遇到指令立即解析执行,那很可能在一帧里就把脚本的所有指令都执行完毕了,本来30秒的剧情在不到1/60秒里结束了.如果遇到延时性指令立即阻塞呢,会遇到可能有几条延时性指令同时开始的场景.因此决定再加上一个规则,使用方括号括起来的脚本指令,将强制同时执行,第二版本如下:

  1. role_walk LEFT 100;
  2. role_dialog "stop!!!" 4;
  3. role_jump FORWARD;
  4. role_walk RIGHT 200;
  5. monster_dialog "byebye!!!" 2;
  6. [
  7. monster_run RIGHT 400
  8. role_walk RIGHT 500;
  9. ]
  10. role_jump BACK;  

 至此我认为脚本的规则能适应足够多场景了.该脚本暂不需要控制结构,控制条件在脚本进行时都预先知道了.

 这是脚本解析代码.

  1. #ifndef __MovieCommandAST_H__
  2. #define __MovieCommandAST_H__
  3.  
  4. #include <boost/fusion/include/adapt_struct.hpp>
  5. #include <boost/variant/variant.hpp>
  6. #include <boost/variant/recursive_variant.hpp>
  7. #include <boost/fusion/include/std_pair.hpp>
  8.  
  9. namespace MovieScript
  10. {
  11. typedef boost::variant<std::string, int, float> ArgType;
  12. typedef std::vector<ArgType> ArgList;
  13.  
  14. namespace Parser
  15. {
  16. struct command_atom
  17. {
  18. std::string cmd;
  19. ArgList args;
  20. command_atom():cmd("") {}
  21. };
  22.  
  23. struct command_flow;
  24. typedef boost::variant<boost::recursive_wrapper<command_flow>, command_atom> command_unit;
  25.  
  26. typedef std::list<command_unit> CommandUnitList;
  27.  
  28. struct command_flow
  29. {
  30. CommandUnitList cmd_flow;
  31. };
  32.  
  33. }
  34. }
  35.  
  36. BOOST_FUSION_ADAPT_STRUCT
  37. (
  38. MovieScript::Parser::command_atom,
  39. (std::string, cmd)
  40. (MovieScript::ArgList, args)
  41. )
  42.  
  43. BOOST_FUSION_ADAPT_STRUCT
  44. (
  45. MovieScript::Parser::command_flow,
  46. (MovieScript::Parser::CommandUnitList, cmd_flow)
  47. )
  48.  
  49. #endif
  1. #ifndef __MovieCommandEnumParser_H__
  2. #define __MovieCommandEnumParser_H__
  3.  
  4. #include <boost/spirit/include/phoenix_operator.hpp>
  5. #include <boost/spirit/include/qi.hpp>
  6. #include <boost/config/warning_disable.hpp>
  7.  
  8. namespace MovieScript
  9. {
  10. namespace fusion = boost::fusion;
  11. namespace qi = boost::spirit::qi;
  12. namespace phoenix = boost::phoenix;
  13. namespace ascii = boost::spirit::ascii;
  14.  
  15. namespace Parser
  16. {
  17. struct Enum_ : qi::symbols<char, int>
  18. {
  19. Enum_()
  20. {
  21. add
  22. ("LEFT" , )
  23. ("RIGHT" , )
  24. ("FORWARD" ,)
  25. ("BACK" , )
  26. ("STAY" , )
  27. ;
  28. }
  29.  
  30. } Enum;
  31.  
  32. template <typename Iterator>
  33. struct EnumParser : qi::grammar<Iterator, int()>
  34. {
  35. EnumParser() : EnumParser::base_type(start)
  36. {
  37. using qi::eps;
  38. using qi::lit;
  39. using qi::_val;
  40. using qi::_1;
  41. using ascii::char_;
  42.  
  43. start = eps [_val = ] >>
  44. ( Enum [_val += _1] )
  45. ;
  46. }
  47.  
  48. qi::rule<Iterator, int()> start;
  49. };
  50. }
  51. }
  52.  
  53. #endif
  1. #ifndef __MovieCommandParser_H__
  2. #define __MovieCommandParser_H__
  3.  
  4. #include <boost/spirit/include/qi.hpp>
  5. #include <boost/config/warning_disable.hpp>
  6. #include <boost/fusion/include/std_pair.hpp>
  7. #include <boost/spirit/include/phoenix_object.hpp>
  8. #include <boost/spirit/include/phoenix_core.hpp>
  9. #include <boost/spirit/include/phoenix_operator.hpp>
  10. #include <boost/spirit/include/phoenix_fusion.hpp>
  11. #include "MovieCommandAST.h"
  12.  
  13. namespace MovieScript
  14. {
  15. namespace fusion = boost::fusion;
  16. namespace qi = boost::spirit::qi;
  17. namespace phoenix = boost::phoenix;
  18. namespace ascii = boost::spirit::ascii;
  19.  
  20. namespace Parser
  21. {
  22. template<typename Iter>
  23. struct commnent_grammar : qi::grammar<Iter>
  24. {
  25. qi::rule<Iter> _skipper;
  26.  
  27. commnent_grammar():base_type(_skipper)
  28. {
  29. using qi::eol;
  30. using qi::omit;
  31. using ascii::char_;
  32. using ascii::blank;
  33. using qi::lit;
  34.  
  35. _skipper = omit[lit("//") >> *(char_ - eol)] | blank;
  36. }
  37. };
  38.  
  39. template <typename Iterator>
  40. struct cmd_grammar : qi::grammar<Iterator, command_flow(), commnent_grammar<Iterator>>
  41. {
  42. typedef commnent_grammar<Iterator> skipper;
  43. qi::rule<Iterator, command_flow(), skipper> cmd_flow;
  44. qi::rule<Iterator, command_unit(), skipper> cmd_unit;
  45. qi::rule<Iterator, command_atom(), skipper> cmd_atom;
  46. qi::rule<Iterator, std::string(), skipper> cmd_name, enum_name;
  47. qi::rule<Iterator, ArgType(), skipper> argtype;
  48. qi::rule<Iterator, ArgList(), skipper> arglist;
  49.  
  50. cmd_grammar() : cmd_grammar::base_type(cmd_flow)
  51. {
  52. using qi::lit;
  53. using qi::lexeme;
  54. using qi::int_;
  55. using qi::float_;
  56. using qi::eps;
  57. using qi::eol;
  58. using qi::bool_;
  59. using ascii::char_;
  60. using ascii::alpha;
  61. using ascii::alnum;
  62. using ascii::string;
  63. using namespace qi::labels;
  64.  
  65. using phoenix::construct;
  66. using qi::on_error;
  67. using qi::fail;
  68. using qi::debug;
  69.  
  70. cmd_name = lexeme[ +(alpha | alnum | char_('_')) ];
  71. enum_name = lexeme[ +(alpha | alnum | char_('_')) ];
  72. argtype = float_ | bool_ | enum_name ;
  73. cmd_atom = cmd_name >> *(argtype) ;
  74. cmd_unit = (lit('[') >> +eol >> cmd_flow >> +eol >> lit(']')) | (cmd_atom);
  75. cmd_flow = eps >> *eol >> cmd_unit % (+eol);
  76.  
  77. }
  78. };
  79.  
  80. }
  81.  
  82. }
  83.  
  84. #endif

上边代码将指令流看做是可递归的.方括号内的指令集仍可包含方括号.虽然暂时用不上,但这个概念是有用的.今后可修改规则令指令流可递归解析及执行.没有解析双引号,为了本地化方便,台词使用序号索引.这个脚本称不上语言,若想添加与游戏内联系的变量,控制结构等,还需要一个中间数据结构来与游戏传递消息,保存状态.这已经超出了该脚本的设定功能.但若真要深入做下去,显然需要实现这些.那就相当于做一个类似lua的语言了,这不只是单靠spirit所能解决的问题.

  现在来看下脚本处理流程.

  1  扫描脚本文件,按顺序解析出一个指令链表.

  2  读取指令链表,每遇到指令则推送,如果遇到方括号,则推送方括号内的所有指令.

  4  接收推送的指令,如果是即时性的指令,立即执行.如果是延时性的指令,需要一个判断条件,未达成则一直执行.

  5  回到2.

  6   读到链表结尾,剧情脚本结束.

  

  推送指令然后执行类似一个管道流操作,或者可以看做生产者和消费者的关系.处理这种场景使用协程能将程序逻辑写的很自然.如下是我的代码片段.使用协程,在一个循环里处理了推送指令和执行2个动作.

  1. bool Processer::pump()
  2. {
  3. static CommandUnitList::const_iterator it;
  4. reenter(&coro_stream)
  5. {
  6. for(it = g_cmd_glows.cmd_flow.begin(); it != g_cmd_glows.cmd_flow.end();)
  7. {
  8. if( ! is_block() ) {
  9. boost::apply_visitor(command_flow_handler(this), *it);
  10. block();
  11. yield return true;
  12. }
  13.  
  14. execute();
  15. yield return true;
  16. }
  17. shutdown();
  18. yield return false;
  19. }
  20. return false;
  21. }

pump每帧都被调用.但是reenter(&coro_stream){ ... } 内的for循环每次只执行一步,而非全部执行.首先执行boost::apply_vistor读取指令,下一个循环将执行execute(),若block标志被改变,则继续读取指令.在一个循环里实现了异步顺序处理.没有协程不是说做不了,但使用协程,就可以在短短的这个循环里写出清晰简单的逻辑.

不满意的地方是对指令的抽象.当等到推送指令后(实际上只是一个包含指令名字和参数的结构),我们需要把它构建为一个游戏能真正执行的指令,就是转化为对游戏功能执行函数的调用.我的本意是将游戏功能执行函数绑定到指令上,令指令与具体的游戏功能解耦.实际遇到一个参数传递的问题.从脚本解析出来的参数,放在一个vector里.除非游戏功能执行函数直接以这个vector作为输入参数,否则必须将vector逐个元素解开再传入.问题来了,每条指令参数的类型,数量都是不同的,于是每条指令不得不也是"特定"的.如果你有一个指令基类,也许就意味着每条指令就是一个子类.若c++参数能在类似lua在调用处展开(lua参数实际是table),无疑很有用.没找到好的办法.仍用传统的类结构实现指令.

  1. class ICommandExecutor
  2. {
  3. command_atom cmd_atom;
  4. Private::coroutine coro_executor;
  5.  
  6. public:
  7. ICommandExecutor();
  8. ICommandExecutor(const command_atom& cmd_atom_);
  9. ICommandExecutor(const ICommandExecutor& cmd);
  10.  
  11. bool execute();
  12. void setdowned() { _downed = true; }
  13.  
  14. template<class ReturnType>
  15. ReturnType getValue(int pos)
  16. {
  17. return boost::get<ReturnType>(cmd_atom.args.at(pos));
  18. }
  19.  
  20. protected:
  21. virtual bool run_exec();
  22. virtual bool enter_exec();
  23. virtual bool leave_exec();
  24. virtual bool downed();
  25.  
  26. bool _downed;
  27. };

指令的执行仍可利用协程改善逻辑.execute()的实现:

  1. bool ICommandExecutor::execute()
  2. {
  3. reenter(&coro_executor)
  4. {
  5. yield return enter_exec();
  6. while(!downed())
  7. {
  8. yield return run_exec();
  9. }
  10. yield return leave_exec();
  11. }
  12. return false;
  13. }

这里我把指令运行分为了进入,运行,离开三个阶段.实现这三个阶段的顺序实现需要某种状态机制.而使用协程,逻辑看起来就清爽了.

一个简单的指令工厂.

  1. class CommandFactory
  2. {
  3. public:
  4. typedef boost::function< ICommandExecutor*(const command_atom&) > CreateCommandFunction;
  5. typedef Loki::SingletonHolder<CommandFactory> MySingleton;
  6.  
  7. inline static CommandFactory& Instance()
  8. { return MySingleton::Instance(); }
  9.  
  10. ICommandExecutor* create(const command_atom& cmd_atom);
  11. void register_commnad(const std::string& cmdname, CreateCommandFunction creator);
  12.  
  13. private:
  14. typedef std::map<std::string, CreateCommandFunction> IdToCommandMap;
  15. IdToCommandMap id_to_command_map;
  16. };
  17.  
  18. template<class CommandExecutorType>
  19. class CommandExecutorNew
  20. {
  21. public:
  22. static ICommandExecutor* create(const command_atom& cmd_atom)
  23. {
  24. return new CommandExecutorType(cmd_atom);
  25. }
  26. };

一个c++剧情脚本指令系统的更多相关文章

  1. [Android Pro] Android以root起一个process[shell脚本的方法]

    reference to :  http://***/Article/11768 有时候我们写的app要用uid=0的方式启动一个process,framework层和app层是做不到的,只有通过写脚 ...

  2. [shell编程]一个简单的脚本

    首先,为什么要学习shell呢?哈哈,当然不是shell能够怎样怎样然后100字. 最近看到一篇博文<开阔自己的视野,勇敢的接触新知识>,读完反思良久.常常感慨自己所会不多,对新知识又有畏 ...

  3. 查看当前支持的shell,echo -e相关转义符,一个简单shell脚本,dos2unix命令把windows格式转为Linux格式

    /etc/shells [root@localhost ~]# more /etc/shells /bin/sh /bin/bash /sbin/nologin /usr/bin/sh /usr/bi ...

  4. 手把手用Monkey写一个压测脚本

    版权声明: 本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有. 允许有条件转载,转载请附带底部二维码. 一.为什么需要一个测试脚本? 昨天讲解了Android Monkey命 ...

  5. 测试网站页面网速的一个简单Python脚本

    无聊之余,下面分享一个Python小脚本:测试网站页面访问速度 [root@huanqiu ~]# vim pywww.py #!/usr/bin/python # coding: UTF-8 imp ...

  6. Android以root起一个process[shell脚本的方法]

    有时候我们写的app要用uid=0的方式启动一个process,framework层和app层是做不到的,只有通过写脚本,利用am来实现.下面是具体步骤: 1.创建一个包含Main()方法Java p ...

  7. Lua 是一个小巧的脚本语言

    Redis进阶实践之七Redis和Lua初步整合使用 一.引言 Redis学了一段时间了,基本的东西都没问题了.从今天开始讲写一些redis和lua脚本的相关的东西,lua这个脚本是一个好东西,可以运 ...

  8. [Python] 用python做一个游戏辅助脚本,完整思路

    一.说明 简述:本文将以4399小游戏<宠物连连看经典版2>作为测试案例,通过识别小图标,模拟鼠标点击,快速完成配对.对于有兴趣学习游戏脚本的同学有一定的帮助. 运行环境:Win10/Py ...

  9. 【Selenium】4.创建你的第一个Selenium IDE脚本

    http://newtours.demoaut.com/ 这个网站将会用来作为我们测试的网址. 通过录制来创建一个脚本 让我们来用最普遍的方法——录制来创建一个脚本.然后,我们将会用回放的功能来执行录 ...

随机推荐

  1. Java SpringMVC实现国际化整合案例分析(i18n)

    所谓国际化就是支持多种语言,web应用在不同的浏览环境中可以显示出不同的语言,比如说汉语.英语等.下面我将以具体的实例来举例说明: (1)新建动态Javaweb项目,并导入几个SpringMVC必需的 ...

  2. IOS系统中使用zepto的live事件绑定不了的一个办法

    IOS系统中使用zepto的live事件绑定不了的一个办法: 对事件对象添加样式:cursor:pointer

  3. HDU 4643 GSM 算术几何

    当火车处在换基站的临界点时,它到某两基站的距离相等.因此换基站的位置一定在某两个基站的中垂线上, 我们预处理出任意两基站之间的中垂线,对于每次询问,求询问线段与所有中垂线的交点. 检验这些交点是否满足 ...

  4. c# 计算1-100之间的所有质数(素数)的和

    求1-100之间的所有质数(素数)的和: 所以先必须知道什么是质数? 一个数是不是素数   “素数”是指除了能被1和自身整除外,不能被任何其它整数整除的自然数. 然后知道有哪些是质数: 记忆的方法: ...

  5. 【原创】通俗易懂地解决中文乱码问题(2) --- 分析解决Mysql插入移动端表情符报错 ‘incorrect string value: '\xF0...

    这篇blog重点在解决问题,如果你对字符编码并不是特别了解,建议先看看 < [原创]通俗易懂地解决中文乱码问题(1) --- 跨平台乱码 >. 当然,如果只是针对解决这个Mysql插入报错 ...

  6. c#操作txt

    C#追加文件 StreamWriter sw = File.AppendText(Server.MapPath(".")+"\\myText.txt"); sw ...

  7. Java 异常 —— java.io.InvalidClassException: javax.xml.namespace.QName; local class incompatible

    项目中有个 WebService 接口,调试时使用 Main 方法运行,别人的机器上都能运行,就笔者的机器出问题.他们说是RP的问题…… 异常信息: java.io.InvalidClassExcep ...

  8. 使用public key来做SSH authentication

    public key authentication(公钥认证)是对通过敲用户名.密码方式登录服务器的一种替代办法.这种方法更加安全更具有适应性,但是更难以配置. 传统的密码认证方式中,你通过证明你你知 ...

  9. 在eclipse如何删除无效的maven build

    在Eclipse的maven项目中,点击一次“maven build...”明明没有配置,它也就会产生一个maven build,那么如何删除这些无效的配置呢?

  10. Bitset位图

    位图(bitmap)就是用每一位来存放某种状态,适合于大规模数据但是数据状态又不是很多的情况下,通常来判断数据是否存在.位图的常见应用有两种: 1.存放大规模数据,例如腾讯的面试题,给40亿个unsi ...