一个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
#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++剧情脚本指令系统的更多相关文章
- [Android Pro] Android以root起一个process[shell脚本的方法]
reference to : http://***/Article/11768 有时候我们写的app要用uid=0的方式启动一个process,framework层和app层是做不到的,只有通过写脚 ...
- [shell编程]一个简单的脚本
首先,为什么要学习shell呢?哈哈,当然不是shell能够怎样怎样然后100字. 最近看到一篇博文<开阔自己的视野,勇敢的接触新知识>,读完反思良久.常常感慨自己所会不多,对新知识又有畏 ...
- 查看当前支持的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 ...
- 手把手用Monkey写一个压测脚本
版权声明: 本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有. 允许有条件转载,转载请附带底部二维码. 一.为什么需要一个测试脚本? 昨天讲解了Android Monkey命 ...
- 测试网站页面网速的一个简单Python脚本
无聊之余,下面分享一个Python小脚本:测试网站页面访问速度 [root@huanqiu ~]# vim pywww.py #!/usr/bin/python # coding: UTF-8 imp ...
- Android以root起一个process[shell脚本的方法]
有时候我们写的app要用uid=0的方式启动一个process,framework层和app层是做不到的,只有通过写脚本,利用am来实现.下面是具体步骤: 1.创建一个包含Main()方法Java p ...
- Lua 是一个小巧的脚本语言
Redis进阶实践之七Redis和Lua初步整合使用 一.引言 Redis学了一段时间了,基本的东西都没问题了.从今天开始讲写一些redis和lua脚本的相关的东西,lua这个脚本是一个好东西,可以运 ...
- [Python] 用python做一个游戏辅助脚本,完整思路
一.说明 简述:本文将以4399小游戏<宠物连连看经典版2>作为测试案例,通过识别小图标,模拟鼠标点击,快速完成配对.对于有兴趣学习游戏脚本的同学有一定的帮助. 运行环境:Win10/Py ...
- 【Selenium】4.创建你的第一个Selenium IDE脚本
http://newtours.demoaut.com/ 这个网站将会用来作为我们测试的网址. 通过录制来创建一个脚本 让我们来用最普遍的方法——录制来创建一个脚本.然后,我们将会用回放的功能来执行录 ...
随机推荐
- 【python爬虫】根据查询词爬取网站返回结果
最近在做语义方面的问题,需要反义词.就在网上找反义词大全之类的,但是大多不全,没有我想要的.然后就找相关的网站,发现了http://fanyici.xpcha.com/5f7x868lizu.html ...
- python操作json
概念 序列化(Serialization):将对象的状态信息转换为可以存储或可以通过网络传输的过程,传输的格式可以是JSON.XML等.反序列化就是从存储区域(JSON,XML)读取反序列化对象的状态 ...
- Netty那点事
一.Netty是什么 Netty,无论新手还是老手,都知道它是一个“网络通讯框架”. 所谓框架,基本上都是一个作用:基于底层API,提供更便捷的编程模型. 那么”通讯框架”到底做了什么事情呢?回答这个 ...
- 笔记二、本地git命令
参考书籍: <Pro Git>中文版.pdf git init // 建立一个git仓库, 本地目录为工作目录, .git目录是中央数据目录 git ini ...
- Jalopy 之 HelloWorld —— Jalopy 在 MyEclipse 下的安装与使用
如果你要问我Jalopy是什么.我只能告诉你“它是一个格式化代码的工具”.因为我也是一个初学者. 如果你也是初次接触,那一起来学习下吧! ·安装 1.首先,下载资源 下载地址:http://sourc ...
- 两个学生OJ差集
这个程序非常简单,因为用了最笨的办法,不过运行一点儿也不慢... 在我们学校OJ平台每个人的个人信息中都有Solved Problems List,我们可以用这个简单的程序输入两个人解决问题的所有题号 ...
- Android gingerbread eMMC booting
Android gingerbread eMMC booting This page is currently under construction. The content of this page ...
- 《OD学hadoop》第一周0625
一.实用网站 1. linux内核版本 www.kernel.org 2. 查看网站服务器使用的系统 www.netcraft.com 二.推荐书籍 1. <Hadoop权威指南> 1- ...
- C# 为WebBrowser设置代理,打开网页
WebBrowser控件是基于IE浏览器的,所以它的内核功能是依赖于IE的,相信做.NET的人都知道. 今天的主题,和上一篇文章应该是差不多的,都是通过代理来实现功能的. 请看下面的代码: //1.定 ...
- 编写jquery插件的分享
一.类级别($.extend) 类级别你可以理解为拓展jquery类,最明显的例子是$.ajax(...),相当于静态方法. 开发扩展其方法时使用$.extend方法,即jQuery.extend(o ...