项目希望能够实现一些剧情动画,类似角色移动,镜头变化,台词展现等.剧情动画这东西随时需要修改调整,不能写死在代码里.考虑之后认为需要做一个简单的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
#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" , )
("RIGHT" , )
("FORWARD" ,)
("BACK" , )
("STAY" , )
;
} } 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 = ] >>
( Enum [_val += _1] )
;
} qi::rule<Iterator, int()> start;
};
}
} #endif
#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

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

一个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. 【python爬虫】根据查询词爬取网站返回结果

    最近在做语义方面的问题,需要反义词.就在网上找反义词大全之类的,但是大多不全,没有我想要的.然后就找相关的网站,发现了http://fanyici.xpcha.com/5f7x868lizu.html ...

  2. python操作json

    概念 序列化(Serialization):将对象的状态信息转换为可以存储或可以通过网络传输的过程,传输的格式可以是JSON.XML等.反序列化就是从存储区域(JSON,XML)读取反序列化对象的状态 ...

  3. Netty那点事

    一.Netty是什么 Netty,无论新手还是老手,都知道它是一个“网络通讯框架”. 所谓框架,基本上都是一个作用:基于底层API,提供更便捷的编程模型. 那么”通讯框架”到底做了什么事情呢?回答这个 ...

  4. 笔记二、本地git命令

    参考书籍:     <Pro Git>中文版.pdf   git init           // 建立一个git仓库, 本地目录为工作目录, .git目录是中央数据目录 git ini ...

  5. Jalopy 之 HelloWorld —— Jalopy 在 MyEclipse 下的安装与使用

    如果你要问我Jalopy是什么.我只能告诉你“它是一个格式化代码的工具”.因为我也是一个初学者. 如果你也是初次接触,那一起来学习下吧! ·安装 1.首先,下载资源 下载地址:http://sourc ...

  6. 两个学生OJ差集

    这个程序非常简单,因为用了最笨的办法,不过运行一点儿也不慢... 在我们学校OJ平台每个人的个人信息中都有Solved Problems List,我们可以用这个简单的程序输入两个人解决问题的所有题号 ...

  7. Android gingerbread eMMC booting

    Android gingerbread eMMC booting This page is currently under construction. The content of this page ...

  8. 《OD学hadoop》第一周0625

    一.实用网站 1. linux内核版本 www.kernel.org 2. 查看网站服务器使用的系统  www.netcraft.com 二.推荐书籍 1. <Hadoop权威指南> 1- ...

  9. C# 为WebBrowser设置代理,打开网页

    WebBrowser控件是基于IE浏览器的,所以它的内核功能是依赖于IE的,相信做.NET的人都知道. 今天的主题,和上一篇文章应该是差不多的,都是通过代理来实现功能的. 请看下面的代码: //1.定 ...

  10. 编写jquery插件的分享

    一.类级别($.extend) 类级别你可以理解为拓展jquery类,最明显的例子是$.ajax(...),相当于静态方法. 开发扩展其方法时使用$.extend方法,即jQuery.extend(o ...