zoukankan      html  css  js  c++  java
  • 一个c++剧情脚本指令系统

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

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

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

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

    role_walk	LEFT		100;
    role_dialog	"stop!!!"	4;
    role_jump	FORWARD;
    role_walk	RIGHT		200;
    monster_dialog	"byebye!!!"	2;
    monster_run	RIGHT		400    
    role_walk	RIGHT		500;
    role_jump	BACK;   
    

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

    role_walk	LEFT		100;
    role_dialog	"stop!!!"	4;
    role_jump	FORWARD;
    role_walk	RIGHT		200;
    monster_dialog	"byebye!!!"	2;
    [
    monster_run	RIGHT		400    
    role_walk	RIGHT		500;
    ]
    role_jump	BACK;    

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

     这是脚本解析代码.

    #ifndef __MovieCommandAST_H__
    #define __MovieCommandAST_H__
    
    #include <boost/fusion/include/adapt_struct.hpp>
    #include <boost/variant/variant.hpp>
    #include <boost/variant/recursive_variant.hpp>
    #include <boost/fusion/include/std_pair.hpp>
    
    namespace MovieScript
    {
        typedef boost::variant<std::string, int, float>        ArgType;
        typedef std::vector<ArgType>                        ArgList;
    
        namespace Parser
        {
            struct command_atom
            {
                std::string        cmd;
                ArgList            args;
                command_atom():cmd("") {}
            };
    
            struct command_flow;
            typedef boost::variant<boost::recursive_wrapper<command_flow>, command_atom> command_unit;
    
            typedef std::list<command_unit> CommandUnitList;
    
            struct command_flow
            {
                CommandUnitList cmd_flow;
            };
    
        }
    }
    
    BOOST_FUSION_ADAPT_STRUCT
    (
    MovieScript::Parser::command_atom,
    (std::string, cmd)
    (MovieScript::ArgList, args)
    )
    
    BOOST_FUSION_ADAPT_STRUCT
    (
    MovieScript::Parser::command_flow,
    (MovieScript::Parser::CommandUnitList, cmd_flow)
    )
    
    
    #endif
    View Code
    #ifndef __MovieCommandEnumParser_H__
    #define __MovieCommandEnumParser_H__
    
    #include <boost/spirit/include/phoenix_operator.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <boost/config/warning_disable.hpp>
    
    namespace MovieScript
    {
        namespace fusion = boost::fusion;
        namespace qi = boost::spirit::qi;
        namespace phoenix = boost::phoenix;
        namespace ascii = boost::spirit::ascii;
    
        namespace Parser
        {
            struct Enum_ : qi::symbols<char, int>
            {
                Enum_()
                {
                    add
                        ("LEFT"    , 1)
                        ("RIGHT"   , 2)
                        ("FORWARD"  ,3)
                        ("BACK"   , 4)
                        ("STAY"   , 5)
                        ;
                }
    
            } Enum;        
        
    
            template <typename Iterator>
            struct EnumParser : qi::grammar<Iterator, int()>
            {
                EnumParser() : EnumParser::base_type(start)
                {
                    using qi::eps;
                    using qi::lit;
                    using qi::_val;
                    using qi::_1;
                    using ascii::char_;
    
                    start = eps [_val = 0] >>
                        ( Enum    [_val += _1] ) 
                        ;
                }
    
                qi::rule<Iterator, int()> start;
            };
        }
    }
    
    
    #endif
    View Code
    #ifndef __MovieCommandParser_H__
    #define __MovieCommandParser_H__
    
    #include <boost/spirit/include/qi.hpp>
    #include <boost/config/warning_disable.hpp>
    #include <boost/fusion/include/std_pair.hpp>
    #include <boost/spirit/include/phoenix_object.hpp>
    #include <boost/spirit/include/phoenix_core.hpp>
    #include <boost/spirit/include/phoenix_operator.hpp>
    #include <boost/spirit/include/phoenix_fusion.hpp>
    #include "MovieCommandAST.h"
    
    namespace MovieScript
    {
        namespace fusion = boost::fusion;
        namespace qi = boost::spirit::qi;
        namespace phoenix = boost::phoenix;
        namespace ascii = boost::spirit::ascii;
    
        namespace Parser
        {
            template<typename Iter>
            struct commnent_grammar : qi::grammar<Iter>
            {
                qi::rule<Iter> _skipper;
    
                commnent_grammar():base_type(_skipper)
                {
                    using qi::eol;
                    using qi::omit;
                    using ascii::char_;
                    using ascii::blank;
                    using qi::lit;
    
                    _skipper = omit[lit("//") >> *(char_ - eol)] | blank;
                }
            };
    
            template <typename Iterator>
            struct cmd_grammar : qi::grammar<Iterator, command_flow(), commnent_grammar<Iterator>>
            {
                typedef commnent_grammar<Iterator> skipper;
                qi::rule<Iterator, command_flow(), skipper> cmd_flow;
                qi::rule<Iterator, command_unit(), skipper> cmd_unit;
                qi::rule<Iterator, command_atom(), skipper> cmd_atom;
                qi::rule<Iterator, std::string(), skipper>  cmd_name, enum_name;
                qi::rule<Iterator, ArgType(), skipper>      argtype;
                qi::rule<Iterator, ArgList(), skipper>      arglist;
    
                cmd_grammar() : cmd_grammar::base_type(cmd_flow)
                {
                    using qi::lit;
                    using qi::lexeme;
                    using qi::int_;
                    using qi::float_;
                    using qi::eps;
                    using qi::eol;
                    using qi::bool_;
                    using ascii::char_;
                    using ascii::alpha;
                    using ascii::alnum;
                    using ascii::string;
                    using namespace qi::labels;
    
                    using phoenix::construct;
                    using qi::on_error;
                    using qi::fail;
                    using qi::debug;
    
                    cmd_name =    lexeme[ +(alpha | alnum | char_('_')) ];
                    enum_name = lexeme[ +(alpha | alnum | char_('_')) ];
                    argtype = float_ | bool_ | enum_name ;
                    cmd_atom =    cmd_name >> *(argtype) ;
                    cmd_unit =    (lit('[') >> +eol >> cmd_flow >> +eol >> lit(']')) | (cmd_atom);
                    cmd_flow =    eps >> *eol >> cmd_unit % (+eol);
    
                }
            };
    
        }
    
    }
    
    
    #endif
    View Code

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

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

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

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

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

      5  回到2.

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

      

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

    bool Processer::pump()
    {
        static CommandUnitList::const_iterator it;
        reenter(&coro_stream)
        {
            for(it = g_cmd_glows.cmd_flow.begin(); it != g_cmd_glows.cmd_flow.end();)
            {
                if( ! is_block() ) {
                    boost::apply_visitor(command_flow_handler(this), *it);
                    block();
                    yield return true;
                }
    
                execute();
                yield return true;
            }
            shutdown();
            yield return false;
        }
        return false;
    }

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

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

            class ICommandExecutor
            {
                command_atom cmd_atom;
                Private::coroutine coro_executor;
                
            public:
                ICommandExecutor();
                ICommandExecutor(const command_atom& cmd_atom_);
                ICommandExecutor(const ICommandExecutor& cmd);
    
                bool execute();
                void setdowned() { _downed = true; }
    
                template<class ReturnType>
                ReturnType getValue(int pos)
                {
                    return boost::get<ReturnType>(cmd_atom.args.at(pos));
                }
    
            protected:
                virtual bool run_exec();
                virtual bool enter_exec();
                virtual bool leave_exec();
                virtual bool downed();
    
                bool _downed;
            };

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

    bool ICommandExecutor::execute()
    {
        reenter(&coro_executor)
        {
            yield return enter_exec();
            while(!downed())
            {
                yield return run_exec();
            }
            yield return leave_exec();
        }
        return false;
    }

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

    一个简单的指令工厂.

            class CommandFactory
            {
            public:
                typedef boost::function< ICommandExecutor*(const command_atom&) >    CreateCommandFunction;
                typedef Loki::SingletonHolder<CommandFactory>                        MySingleton;
    
                inline static CommandFactory& Instance()
                { return MySingleton::Instance(); }
    
                ICommandExecutor* create(const command_atom& cmd_atom);
                void register_commnad(const std::string& cmdname, CreateCommandFunction creator);
    
            private:
                typedef std::map<std::string, CreateCommandFunction>    IdToCommandMap;
                IdToCommandMap    id_to_command_map;
            };
    
            template<class CommandExecutorType>
            class CommandExecutorNew
            {
            public:
                static ICommandExecutor* create(const command_atom& cmd_atom)
                {
                    return new CommandExecutorType(cmd_atom);
                }
            };
    View Code
  • 相关阅读:
    QQ空间爬虫--获取好友信息
    分层最短路-2018南京网赛L
    安装SSH,配置SSH无密码登陆
    树形DP--求树上任意两点间距离和
    JTS基本概念和使用
    odps编写UDF的实现
    oozie安装总结
    同步工具的选择
    转:hive面试题
    转:hive-列转行和行转列
  • 原文地址:https://www.cnblogs.com/flytrace/p/3262888.html
Copyright © 2011-2022 走看看