Shimeji开源桌宠代码学习(1)

本文主要探讨了Shimeji桌宠程序的源码学习,特别是基于Github上的Shimeji4mac项目。文章从Main.java开始,介绍了java.util.logging.Logger的使用,讨论了静态类和方法的应用原则,并详细解释了XML配置文件的加载过程,涉及DOM解析XML的方法。此外,还提到了ActionBuilder和ActionRef在处理复合动作中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Shimeji在日语中本意为“蘑菇”。

我们这里的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进行日志记录,对这个难免陌生,下面一篇介绍的比较详细的文章

java.util.logging.Logger使用详解

我也简短的说明一下,Logger类最大的特点就是对日志的级别分的非常详细,所有级别都定义在util.logging.Level类中,

分别是:Severe,Warning,Info,Config,Fine,Finer,Finest,

此外还有OFF级别用于关闭日志,All则是启动所有日志记录

基本的使用方式如下:

log.log(Level.INFO, "設定ファイルを読み込み({0})", "/動作.xml");


然后声明了一个静态常量,

BEHAVIOR_GATHER = "マウスの周りに集まる";

这个变量用于响应聚集事件时,创建所有shimeji的动作。

之后的LogManager用于管理日志,详细可以参考

java.util.logging.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()结束。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值