我们这里的Shimeji是种可以在电脑桌面上四处走动,玩耍,分裂以及卖萌捣乱的桌面程序。
这种桌面程序具有高度可配置的特点。其运行方式是依靠xml文件来控制吉祥物的动作及动作频度。而吉祥物的形象和特殊动作可以通过替换图片来达到定制的效果。
Shimeji 程序由日本的Yuki Yamada开发制作,其官方网页为:
www.group-finity.com/Shimeji/
现在我们所见到的各式各样不同的shimeji形象,也均是由各个作者利用官方网站提供的原始程序修改而成的。
由于现在该网站似乎已经停止维护,所以这里使用的是一个Github上的作者开发的Shimeji4mac的项目作为本博客的学习内容
地址为:
https://github.com/nonowarn/shimeji4mac
我的fork地址为
https://github.com/AlanJager/shimeji4mac
原本考虑到作者太久没有上Github所以不好进行pull request,抱着试一试的心态给作者写了一封邮件,没想到收到了回复
所以有意向进行开发的朋友也可以放心了。接下来就进入正题了,
首先打开Main.java
private static final Logger log = Logger.getLogger(Main.class.getName());
static final String BEHAVIOR_GATHER = "マウスの周りに集まる";
static {
try {
LogManager.getLogManager().readConfiguration(Main.class.getResourceAsStream("/logging.properties"));
} catch (final SecurityException e) {
e.printStackTrace();
} catch (final IOExceptn e) {
e.printStackTrace();
}
}
private static Main instance = new Main();
public static Main getInstance() {
return instance;
}
private final Manager manager = new Manager();
private final Configuration configuration = new Configuration();
首先是util.logging.Logger类,大多我们都使用log4j进行日志记录,对这个难免陌生,下面一篇介绍的比较详细的文章
我也简短的说明一下,Logger类最大的特点就是对日志的级别分的非常详细,所有级别都定义在util.logging.Level类中,
分别是:Severe,Warning,Info,Config,Fine,Finer,Finest,
此外还有OFF级别用于关闭日志,All则是启动所有日志记录
基本的使用方式如下:
log.log(Level.INFO, "設定ファイルを読み込み({0})", "/動作.xml");
BEHAVIOR_GATHER = "マウスの周りに集まる";
这个变量用于响应聚集事件时,创建所有shimeji的动作。
之后的LogManager用于管理日志,详细可以参考
对基本使用进行了说明。
然后创建了静态的Main类和获取Main类的方法,这样做的原因非常明显,这也是何时使用static的习惯
这里翻译一段Stack Overflow上面一个回答:有一个这样的法则,问自己不构建Obj而去直接调用方法是否有意义,
如果有意义那么就用使它为static。比方说你的汽车类Car里有一个方法Car::convertMPpgToKpl(double mpg),可能
有人需要使用转换的方法,但是并不需要构建一个Car实例。
这大致就是个人认同的使用static的原则。
然后继续创建了Manger和Configuration的实例。
紧接着就是主函数的执行
public static void main(final String[] args) {
getInstance().run();
}
public void run() {
// 設定を読み込む
loadConfiguration();
// トレイアイコンを作成する
createTrayIcon();
// しめじを一匹作成する
createMascot();
getManager().start();
}
通过之前的static method获取main实例,然后调用定义的方法Main::run()
首先是调用Main::loadConfiguration()
private void loadConfiguration() {
try {
log.log(Level.INFO, "設定ファイルを読み込み({0})", "/動作.xml");
final Document actions = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
Main.class.getResourceAsStream("/動作.xml"));
log.log(Level.INFO, "設定ファイルを読み込み({0})", "/行動.xml");
this.getConfiguration().load(new Entry(actions.getDocumentElement()));
final Document behaviors = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
Main.class.getResourceAsStream("/行動.xml"));
this.getConfiguration().load(new Entry(behaviors.getDocumentElement()));
this.getConfiguration().validate();
} catch (final IOException e) {
log.log(Level.SEVERE, "設定ファイルの読み込みに失敗", e);
exit();
} catch (final SAXException e) {
log.log(Level.SEVERE, "設定ファイルの読み込みに失敗", e);
exit();
} catch (final ParserConfigurationException e) {
log.log(Level.SEVERE, "設定ファイルの読み込みに失敗", e);
exit();
} catch (final ConfigurationException e) {
log.log(Level.SEVERE, "設定ファイルの記述に誤りがあります", e);
exit();
}
}
首先使用DocumentBuilderFactory来parse一个xml文件,如官方文档描述,DocumentBuilderFactory:
Defines a factory API that enables applications to obtain a parser that produces DOM object trees from XML documents.
这里使用的是w3c的DOM来解析xml文件,关于如何用 DocumentBuilderFactory解析xml这篇文章里以Shimeji的部分代码为例子进行了说明。当然除了使用DOM之外,Java还有许多用于解析xml文件的方法, java中四种操作(DOM、SAX、JDOM、DOM4J)xml方式详解与比较中对这几种方法进行了详细说明,并且给出比较其效率的代码。由于这个桌宠的xml文件本身并不是很大,所以使用DOM处理绰绰有余。
接下来我们来看Configuration::load()这个方法,类似DocumentBuilderFactory解析xml,Shimeji中定义了Entry类作为几本节点,我们先来看Entry类的定义:
public class Entry {
private Element element;
private Map<String, String> attributes;
private List<Entry> children;
private Map<String, List<Entry> > selected = new HashMap<String, List<Entry>>();
public Entry(final Element element){
this.element = element;
}
public String getName() {
return this.element.getTagName();
}
}
定义了基本的属性和get方法,这里的Element实际上是Dom元素中的一个接口,它的super interface就是node,然后对属性,子节点进行了定义。然后需要给这个类添加方法,根据解析xml管用思路可以知道,我们需要获取属性和获取节点的方法这里就不多赘述了。
我们看下面实现的一个方法:
public Map<String, String> getAttributes() {
if ( this.attributes!=null) {
return this.attributes;
}
this.attributes = new LinkedHashMap<String, String>();
final NamedNodeMap attrs = this.element.getAttributes();
for(int i = 0; i<attrs.getLength(); ++i ) {
final Attr attr = (Attr)attrs.item(i);
this.attributes.put(attr.getName(), attr.getValue());
}
return this.attributes;
}
作者使用了LinkedHashMap用于储存xml中节点的多个属性,通过
LinkedHashMap, HashMap以及TreeHashMap的比较实际上使用LinkedHashMap的好处是能够保证放入元素的顺序,我们先记录这个特点,在这里似乎还没体现出使用LinkedHashMap的优势。
在Configuration::load()方法里,对每一个xml文件中定义的动作进行了创建,
for (final Entry node : list.selectChildren("動作")) {
final ActionBuilder action = new ActionBuilder(this, node);
if ( this.getActionBuilders().containsKey(action.getName())) {
throw new ConfigurationException("動作の名前が重複しています:"+action.getName());
}
System.out.println("action name is: " + action.getName());
this.getActionBuilders().put(action.getName(), action);
}
同时附上一个动作的xml内容:
<動作 名前="歩く" 種類="移動" 枠="地面">
<アニメーション>
<ポーズ 画像="/shime1.png" 基準座標="64,128" 移動速度="-2,0" 長さ="6" />
<ポーズ 画像="/shime2.png" 基準座標="64,128" 移動速度="-2,0" 長さ="6" />
<ポーズ 画像="/shime1.png" 基準座標="64,128" 移動速度="-2,0" 長さ="6" />
<ポーズ 画像="/shime3.png" 基準座標="64,128" 移動速度="-2,0" 長さ="6" />
</アニメーション>
</動作>
传入一个动作节点,然后交给ActionBuilder处理,ActionBuilder的constructor()如下:
public ActionBuilder(final Configuration configuration, final Entry actionNode) throws IOException {
this.name = actionNode.getAttribute("名前");
this.type = actionNode.getAttribute("種類");
this.className = actionNode.getAttribute("クラス");
log.log(Level.INFO, "動作読み込み開始({0})", this);
this.getParams().putAll(actionNode.getAttributes());
for (final Entry node : actionNode.selectChildren("アニメーション")) {
this.getAnimationBuilders().add(new AnimationBuilder(node));
}
for (final Entry node : actionNode.getChildren()) {
if (node.getName().equals("動作参照")) {
this.getActionRefs().add(new ActionRef(configuration, node));
} else if (node.getName().equals("動作")) {
this.getActionRefs().add(new ActionBuilder(configuration, node));
}
}
log.log(Level.INFO, "動作読み込み完了");
}
通过读取一个动作下的アニメーション,并将其传递给AnimationBuilder,
接下来我们看AnimationBuilder的constructor()
public AnimationBuilder(final Entry animationNode) throws IOException {
this.condition = animationNode.getAttribute("条件") == null ? "true" : animationNode.getAttribute("条件");
log.log(Level.INFO, "アニメーション読み込み開始");
for (final Entry frameNode : animationNode.getChildren()) {
this.getPoses().add(loadPose(frameNode));
}
log.log(Level.INFO, "アニメーション読み込み完了");
}
对于读入的一个节点判断其是否满足发生条件,如果满足则对Pose进行读取,每一个Pose对应一张png图片
private Pose loadPose(final Entry frameNode) throws IOException {
final String imageText = frameNode.getAttribute("画像");
final String anchorText = frameNode.getAttribute("基準座標");
final String moveText = frameNode.getAttribute("移動速度");
final String durationText = frameNode.getAttribute("長さ");
final String[] anchorCoordinates = anchorText.split(",");
final Point anchor = new Point(Integer.parseInt(anchorCoordinates[0]), Integer.parseInt(anchorCoordinates[1]));
final ImagePair image = ImagePairLoader.load(imageText, anchor);
final String[] moveCoordinates = moveText.split(",");
final Point move = new Point(Integer.parseInt(moveCoordinates[0]), Integer.parseInt(moveCoordinates[1]));
final int duration = Integer.parseInt(durationText);
final Pose pose = new Pose(image, move.x, move.y, duration);
log.log(Level.INFO, "姿勢読み込み({0})", pose);
return pose;
}
同时对图像的锚点,持续时间,移动长度进行了设置,最后返回一个Pose实例。整个流程结束后,一整个アニメーション将会存储在poses这个ArrayList内,同时ActionBuilder里则使用一个ArrayList来存储一系列这样的AnimationBuilder。即通过ActionBuilder管理所有的AnimationBuilder,然后每一个AnimationBuilder管理一组Poses。
继续看ActionBuilder的constructor,这里还使用了ActionRef这个类,这里主要处理的是复合动作的实现,此处仅仅将该对应的复合动作节点放在了ActionRef的ArrayList内,到此ActionBuilder则完成了工作。简单动作和复合动作分别对应了一个ActionBuilder。
然后Configuration::load()进行了Behavior的加载
for (final Entry list : configurationNode.selectChildren("行動リスト")) {
log.log(Level.INFO, "行動リスト...");
loadBehaviors(list, new ArrayList<String>());
}
类似对动作的读取,通过定义BehaviorBuilder来描述一个Behavior,
<行動 名前="マウスの周りに集まる" 頻度="0">
<次の行動リスト 追加="false">
<行動参照 名前="座ってマウスのほうを見る" 頻度="1" />
</次の行動リスト>
</行動>
主要负责对发生的频度等进行读取。
最后Main::loadConfiguraion()在加载完两个配置文件后调用了如下方法:
this.getConfiguration().validate();
这个确认有效的方法分别去检查ActionBuilder和BehaviorBuilder是否存在问题,即重新确认当前存储的节点是否确实存在于配置文件中的安全检测。到此Main::loadConfiguration()结束。