接上文,继续讨论UI动画的框架部分,并对不属于“解释器模式”的编译过程进行分析。
1. 词法分析
上文中提到:
如果“源代码”用一些流行的文本格式编写,比如xml,则可以借助开源的xml解析器来进行词法分析,否则如果使用自定义格式“撰写源代码”,则需要手工编写词法分析器。
手工编写词法分析器是个体力活,成本较高,因此UI动画框架的配置语言采用xml文件格式编写,xml开源库有很多:TinyXml、Xerces、Mini-Xml等。UI动画框架采用TinyXml来解析动画配置文件。TinyXml基于DOM,一次性载入整个XML文档进行解析,在内存中形成和文档对应的树形层次结构,并提供相应的接口以访问内存中的数据结构。使用TinyXml将配置文件加载到内存之后,基本就跳过词法分析过程,直接进入语法分析过程了。
2. 语法分析
语法分析就是依据语法规则将词法分析的结果转换成抽象语法树的过程。语法树上的对象就是“解释器模式”中的“表达式”,也就是一个个具体的解释器,这些解释器负责解释开发者编写的代码并产生输出,从而实现开发者的意图。
我们先定义一个语法分析的框架:
typedef std::map<String, String> AttriMap;
class AnimationParser
{
public:
void BeginElement(const String& element, const AttriMap& attributes)
{
if (element == "strategy")
BeginStrategy(attributes);
else if (element == "strategys")
BeginStrategys(attributes);
else if (element == "single")
BeginSingle(attributes);
else if (element == "group")
BeginGroup(attributes);
else if (element == "animation")
BeginAnimation(attributes);
else if (element == "scheme")
BeginScheme(attributes);
else
throw ("invalid node: " + element);
}
void EndElement(const String& element)
{
if (element == "strategy")
EndStrategy(attributes);
else if (element == "strategys")
EndStrategys(attributes);
else if (element == "single")
EndSingle(attributes);
else if (element == "group")
EndGroup(attributes);
else if (element == "animation")
EndAnimation(attributes);
else if (element == "scheme")
EndScheme(attributes);
else
throw ("invalid node: " + element);
}
private:
IStrategy* BuildStrategy(const AttriMap& attributes) const;
void BeginScheme(const AttriMap& attributes);
void BeginAnimation(const AttriMap& attributes);
void BeginGroup(const AttriMap& attributes);
void BeginSingle(const AttriMap& attributes);
void BeginStrategys(const AttriMap& attributes);
void BeginStrategy(const AttriMap& attributes);
void EndScheme();
void EndAnimation();
void EndGroup();
void EndSingle();
void EndStrategys();
void EndStrategy();
private:
int animationId; // 缓存一个动画的id,供解析过程使用
int strategyId; // 缓存一个策略的id,供解析过程使用
std::vector<Animation*> animations;
std::vector<IStrategy*> strategys;
};
外层有一个递归的函数负责逐层循环驱动:
AnimationParser parser;
TiXmlDocument document;
doc.Parse(text);
Process(parser, doc.RootElement());
// 逐层循环递归
void Process(AnimationParser& parser, const TiXmlElement* element)
{
if (!element) return;
AttriMap attrs; // 属性名字 ~ 属性值 得映射表
for (const TiXmlAttribute* attr = element->FirstAttribute(); attr; attr = attr->Next())
attrs[attr->Name()] = attr->Value();
parser.BeginElement(element->Value(), attrs);
for (const TiXmlNode* child = element->FirstChild(); child; child = child->NextSibling())
if (TiXmlNode::ELEMENT == child->Type())
Process(parser, child->ToElement()); // 递归
parser.EndElement(element->Value());
}
Process是一个典型的深度优先遍历的递归函数,逻辑极其简单。剩下要做的就是分别实现各种Element的解析了。
2.1. animation
animation配置段本身比较简单,因为它的语义很简单:
<animation id = "500" desc = "法器升级的光效" >
...
</animation>
开始一个animation的解析,对animation本身,开始只需要记录这个animation的id即可:
void AnimationParser::BeginAnimation(const AttriMap& attributes)
{
animationId = atoi(attributes["id"]);
}
因为一个animation配置段不管是配置为SingleAnimation还是GroupAnimation,最终只应该解析成一个Animation对象,因此在进行到EndAnimation时,animations容器中只能有一个元素,将这个元素(及其id)加入到一个全局的动画管理器里面即可:
void AnimationParser::EndAnimation()
{
if (animations.size() != 1)
throw ("parse animation failed !");
if (!AnimationManager::GetSingleton().AddAnimation(animationId, animations.back()))
throw ("add animation failed !"));
animations.pop_back();
}
2.2. single
single的配置稍稍复杂一些,它有多个属性:
<single image = "set:effect animation:faqi" clip = "true" from = "win:Root/FaqiBar">
<strategys type = "parallel" >
<strategy id = "frame" param = "30" />
<strategy id = "jmp_frame" param = "2" />
<strategy id = "times" param = "16" />
</strategys>
</single>
解析起来也是比较简单的(属性由SingleAnimation构造函数去提取):
void AnimationParser::BeginSingle(const AttriMap& attributes)
{
animations.push_back(new SingleAnimation(attributes));
}
结束时,animations容器的尾部就是当前的SingleAnimation对象,如果此时容器中还有另外一个对象,那么另外一个对象必定是GroupAnimation对象,因此,把尾部的SingleAnimation对象加入进末二对象中即可:
void AnimationParser::EndSingle()
{
if (animations.size() > 1)
{
Animation* animation = animations.back();
animations.pop_back();
animations.back()->AddAnimation(animation);
}
}
2.3. group
group的配置更复杂一些,它是由single组成的:
<group type = "serial" >
<single>
<strategys type = "parallel" >
<strategy id = "frame" param = "40" />
<strategy id = "move" param = "6;30" />
</strategys>
</single>
<single image = "set:effect animation:tips">
<strategys type = "serial" >
<strategy id = "jmp_frame" />
<strategy id = "times" param = "16" />
</strategys>
</single>
</group>
group的解析并不复杂:
void AnimationParser::BeginGroup(const AttriMap& attributes)
{
if (attributes["type"] == "serial")
animations.push_back(new GroupAnimationSerial());
else if (attributes["type"] == "parallel")
animations.push_back(new GroupAnimationParallel());
else
throw ("Invalid type of group!"));
}
结束时,animations容器的尾部就是当前的GroupAnimation对象,如果此时容器中还有另外一个对象,那么另外一个对象必定也是GroupAnimation对象,因此,把尾部的GroupAnimation对象加入进末二对象中即可,代码和AnimationParser::EndSingle完全相同。
2.4. strategy
strategy的配置是最复杂的,它是“终结符”,有很多种类型的“终结符”,不同类型的strategy负责不同的功能,不同的功能有不同的参数:
<strategy id = "frame" param = "30" />
<strategy id = "move" param = "vertical;10;-20" />
<strategy id = "scale" param = "1;0.8;0.8;6" />
<strategy id = "fade" param = "1;0.8" />
在开始解析strategy时,strategys容器里面肯定有一个对象,因为strategy必定归属于一个strategys对象。strategy有很多种,可想而知,会有一个很大的if-else结构:
void AnimationParser::BeginStrategy(const AttriMap& attributes)
{
if (!strategys.empty())
strategys.back()->AddStrategy(BuildStrategy(attributes));
else
throw ("strategy not in strategys.");
}
IStrategy* AnimationParser::BuildStrategy(const AttriMap& attributes) const
{
if (attributes["id"] == "frame"):
return new FrameStrategy(attributes["param"]);
else if (attributes["id"] == "move"):
return new MoveStrategy(attributes["param"]);
else if (attributes["id"] == "scale")
return new ScaleStrategy(attributes["param"]);
else if (attributes["id"] == "fade")
return new FadeStrategy(attributes["param"]);
else if (attributes["id"] == "times")
return new TimesStrategy(attributes["param"]);
...
else
throw ("invalid strategy type!") + attributes["id"]);
}
各个具体的策略在各自的构造函数中,用”param”属性字符串初始化,比如”move”策略:
class MoveStrategy : public IStrategy
{
public:
MoveStrategy(const String& param)
{
char typeStr[128];
steps = 0;
float par = 0;
scanf(param.c_str(), "%s;%d;%g", typeStr, &steps, &par);
int type = atoi(typeStr);
// 创建path对象
... ...
}
private:
int steps; // 移动步数
Locus* path; // 移动路线对象
};
strategy的结束处理处理可以什么都不用做:
void AnimationParser::EndStrategy()
{
}
2.5. strategys
strategys由strategy组成,本身的配置比较简单:
<strategys type = "parallel" >
<strategy id = "frame" param = "40" />
<strategy id = "move" param = "6;30" />
</strategys>
解析代码如下:
void AnimationParser::BeginStrategys(const AttriMap& attributes)
{
if (strategys.empty())
strategyId = atoi(attributes["id"]); // 只有独立Strategys的id才有用
if (attributes["type"] == "parallel")
strategys.push_back(new StrategysParallel(times));
else if (attributes["type"] == "serial")
strategys.push_back(new StrategysSerial(times, 0));
else
throw ("Invalid type of strategys " + attributes["type"]);
}
结束时,需要分两种情况,一种是独立的strategys,另外一种是animation中的strategys。独立的strategys是绑定到游戏中玩家指定的窗口上,使之具有某种动画效果,这种strategys的id是有意义的,对客户代码可见:
void AnimationParser::EndStrategys()
{
int ps = strategys.size();
if (ps == 0) throw ("since inner error.");
if (ps == 1)
{
if (!animations.empty()) // animation中的strategys
animations.back()->AddStrategy(strategys.back());
// 独立的strategys,加入到全局的动画管理器里面
else if (!AnimationManager::GetSingleton().AddStrategy(strategyId, strategys.back()))
throw ("add strategy failed .");
strategys.pop_back();
}
else // 这种是strategys嵌套strategys的情况
{
IStrategy* strategy = strategys.back();
strategys.pop_back();
strategys.back()->AddStrategy(strategy);
}
}
3. 执行
至此,一棵抽象语法树就建立起来了,当然,动画配置文件中会有很多个动画配置或策略配置,每一个都是一棵抽象语法树。这些抽象语法树上的部件其实就是“解释器模式”中的“解释器对象(即各种Expression)”,在运行时,这些“解释器对象”在解释的过程,也就是执行相应对象的功能的过程,你就可以愉快地看到各种各样的动画了。
一般地,MMORPG游戏客户端都有一个总的死循环(通常也叫帧循环),大概以30fps以上的帧率运行,动画框架在这个帧循环中驱动动画,并不需要客户代码自己启动定时器来操纵动画运行。实际上,动画策略是绑定在一个窗口对象上的,在CEGUI中就是Window对象,Window对象会在帧循环中update自己,我们的动画的执行入口就是在这个update里面:
void Window::updateSelf(float elapsed)
{
... ...
// 动画处理
if (d_aniStrategy && d_parent->isVisible())
{
d_aniStrategy->Run(*this);
if (d_aniStrategy->IsFinished(*this))
{
d_aniStrategy->Restore(); // 回收到策略池中
d_aniStrategy = 0;
}
}
}
动画策略控制结束之后,自动从Window对象中剥离,窗口就不再具有动画效果了。
4. 结语
现在,我们回过头来看前文中所描述的那些令你不爽的感觉是否都一一消失了?
不好的感觉:
✘ 硬编码,极其僵化,无法动态调整动画效果
✘ 需要在业务代码中为动画逻辑增加控制字段,比如定时器变量等,而这些跟业务逻辑无关
✘ 动画逻辑和业务逻辑紧密耦合,影响业务逻辑的清晰性,降低业务代码的可维护性
✘ 如果动画不再需要了,要小心翼翼的从业务代码中将其剥离
✘ 动画逻辑不能复用,每个需要动画的地方只能重新发明轮子
✘ 动画逻辑的代码需要操纵UI层对象,学习和理解成本较高
好的感觉:
✔ 我们很容易做到动态加载动画配置文件(实际上,我在项目中就是这样做的),如果发现动画效果不理想,即时修改配置代码,然后动态刷新配置,不用关闭游戏,马上就能在游戏中看到修改后的动画效果,那感觉不要太爽!
✔ 我不需要在业务代码中为动画的运行增加任何辅助变量,极端情况下,我只需要一行代码即可:
-- 给指定的窗口添加动画效果,动画策略id为123
GuiWinMgr:RunAnimation(GuiWinMgr:getWindow(self.WindowName), 123)
-- 创建一个独立的动画,动画id为456
GuiWinMgr:RunAnimation(GuiWinMgr:CreateAnimation(456))
✔ 动画逻辑和业务逻辑已经做到了最大限度的解耦了!
✔ 如果某个动画不需要了,你要做的就是删除业务代码中的那几行代码,甚至,如果你想偷懒,你仅仅只是删除掉动画配置文件的配置即可!
✔ 同一个策略或者同一个动画可以在任意有需要的地方被复用!
✔ 开发者不需要直接操作UI底层对象的属性,他在较高层次上配置动画,不会被底层对象的各种属性所烦扰!
世界又变得美好起来了!