这几天年终绩效考核,琐事比较多所以更新得没有以前快了,绝对不是因为网上说的什么微软关闭Silverlight网站的影响啦,就算微软说现在马上抛弃SL,我也要把这个游戏写完,对于那些一边玩着Windows系统,一边大骂.net framework是微软的骷髅的人我一般也没有什么可说的。
说到游戏,数据存取这个功能是必须的,不管是游戏设置还是状态什么的,都是可以存成数据文件,然后可以从文件加载。本节就来实现将我们画的图形存成Xml文件,然后可以从Xml文件中加载图形,也只有这样,我们的过关游戏才能实现。
要实现数据保存就要现有文件的内容格式,对于我们的坐标系上各个图形元素而言,就是要把他们按照依赖顺序写到Xml中,这样才能保存加载的时候才能不出意外,因此需要做的事情有以下几点:
1.一个坐标系中的每个图形都必须有唯一的ID或者Name
2.必须按照图形被添加到坐标系的顺序保存。
3.必须记录每个图形的上层依赖(比如线AB依赖于点A和点B)
4.图形的属性也需要保存(例如交点索引Index以及样式等)
综合以上,我们可以给出一个Xml文件格式:
<?xml version="1.0" encoding="utf-16"?> <CS> <Shapes> <FreePoint Name="A" Style="00,11,1.00,1.00,False" P="1.00,2.00"/> <FreePoint Name="B" Style="00,11,1.00,1.00,False" P="-2.00,3.00"/> <FreePoint Name="C" Style="00,11,1.00,1.00,False" P="0.00,0.00"/> <LineShape Name="AB" Style="08,00,4.00,1.00,False" D="A,B"/> <LineShape Name="BC" Style="08,00,4.00,1.00,False" D="B,C"/> <LineShape Name="CA" Style="08,00,4.00,1.00,False" D="C,A"/> <CircleShape Name="OA" Style="00,02,0.50,1.00,True" D="A,A,B"/> <CircleShape Name="OB" Style="00,02,0.50,1.00,True" D="B,A,B"/> <IntersectionPointOfCircles Name="D" Style="00,08,0.50,1.00,True" D="OA,OB" I="1"/> <IntersectionPointOfCircles Name="E" Style="00,08,0.50,1.00,True" D="OA,OB" I="2"/> <LineShape Name="DE" Style="08,00,0.50,1.00,True" D="D,E"/> <CircleShape Name="OB1" Style="00,02,0.50,1.00,True" D="B,B,C"/> <CircleShape Name="OC" Style="00,02,0.50,1.00,True" D="C,B,C"/> <IntersectionPointOfCircles Name="F" Style="00,08,0.50,1.00,True" D="OB1,OC" I="1"/> <IntersectionPointOfCircles Name="G" Style="00,08,0.50,1.00,True" D="OB1,OC" I="2"/> <LineShape Name="FG" Style="08,00,0.50,1.00,True" D="F,G"/> <IntersectionPointOfLines Name="H" Style="00,08,1.00,1.00,False" D="DE,FG"/> <LineShape Name="HA" Style="08,00,0.50,1.00,True" D="H,A"/> <CircleShape Name="OH" Style="00,02,2.00,1.00,False" D="H,H,A"/> </Shapes> </CS>
Xml的好处是有目共睹的,看到上面的Xml格式,你是否对数据保存的功能实现胸有成竹了呢?当然,你可以已经发现了还有些细节工作需要做的:
第一,我们要格式化样式:
public static string GetStyleString(Shape shape) { var result = string.Format("{0},{1},{2},{3},{4}", BrushPicker.GetBrushId(shape.Stroke as SolidColorBrush).ToString("00"), BrushPicker.GetBrushId(shape.Fill as SolidColorBrush).ToString("00"), shape.StrokeThickness.ToString("0.00"), shape.Opacity.ToString("0.00"), shape.StrokeDashArray.Count > 0); return result; }
第二我们要添加图形的上层依赖列表(Parents):
public List<ICoordinate> Parents { get; set; } public void RegisterDependencies(params ICoordinate[] parents) { if (Parents == null) Parents = new List<ICoordinate>(); foreach (var parent in parents) { Parents.Add(parent); if (parent.Children == null) parent.Children = new List<ICoordinate>(); parent.Children.Add(this); } }
第三我们要在ICoordinate中添加新的接口WriteXml和ReadXml,这两个方法的功能恰好相反,因此放在一起实现比较直观,例如FreePoint中时这样实现的这两个方法的:
public override void WriteXml(XmlWriter writer) { base.WriteXml(writer); writer.WriteAttributeString("P", Center.X.ToString("0.00") + "," + Center.Y.ToString("0.00")); } public override void ReadXml(XElement element) { base.ReadXml(element); var ps = element.ReadString("P").Split(','); Center.SetPosition(double.Parse(ps[0]), double.Parse(ps[1])); }
OK,这些工作完成后,数据保存就十分简单了,如果你对XML文件比较熟悉的话:
private static void Write(CoordinateSystem cs, XmlWriter writer) { if (cs.Shapes.Count == 0) return; writer.WriteStartElement("CS"); writer.WriteStartElement("Shapes"); foreach (var shape in cs.Shapes) { writer.WriteStartElement(shape.GetType().Name); writer.WriteAttributeString("Name", shape.Name); writer.WriteAttributeString("Style", ShapeStyleEditor.GetStyleString(shape.Shape)); if (shape.Parents != null) writer.WriteAttributeString("D", shape.Parents.Select(d => d.Name).ToListString(",")); shape.WriteXml(writer); writer.WriteEndElement(); } writer.WriteEndElement(); //Shapes writer.WriteEndElement(); //CS }
数据保存成XML看来比较简单,反过来解析就不那么容易了,在此我们需要用到反射机制,而反射的实例创建是调用默认的无参构造函数创建的,如:
var shape = (ICoordinate) Activator.CreateInstance(typeof(FreePoint));
但是我们定义的所有图形都是严格按照依赖创建的,也就是说没有无参的构造函数,如果直接使用我们现有的方法把Xml解析成Shapes,只能使用无穷的if else if else了,这无疑是非常不雅的,为了实现高度自治,我们还是为每个Shape类实现无参的构造函数吧,有点麻烦,但是长痛不如短痛。
首先为了能够在构造函数中使用Xml节点等一些信息,我们需要定义一个静态的类ShapeSerializer:
public static class ShapeSerializer { private static CoordinateSystem CS { get; set; } private static XElement XmlNode { get; set; } public static int Index { get { return (int)XmlNode.ReadDouble("I"); } } public static double Ratio { get { return XmlNode.ReadDouble("R"); } } public static string Style { get { return XmlNode.ReadString("Style"); } } public static List<ICoordinate> Parents { get; private set; } }
这样就能方便的实现无参构造函数了,拿LineShape举例吧:
public sealed class LineShape : CoordinateBase { public LineShape() : this(ShapeSerializer.Parents[0] as PointShape, ShapeSerializer.Parents[1] as PointShape) { } public LineShape(PointShape p1, PointShape p2) { Line = new LogicalLine(p1.Center, p2.Center); P1 = p1; P2 = p2; RegisterDependencies(p1, p2); } }
我们把完整的解析代码页放在ShapeSerializer类中,如下:
public static void Parse(string xml, CoordinateSystem cs) { try { CS = cs; CS.Children.Clear(); var element = XElement.Parse(xml); var figuresNode = element.Element("Shapes"); var types = ReflectorUtils.DiscoverTypes<CoordinateBase>(); if (figuresNode != null) { foreach (var figureNode in figuresNode.Elements()) { var type = types.FirstOrDefault(t => t.Name == figureNode.Name); if (type == null) throw new Exception("Type not found:" + figureNode.Name); XmlNode = figureNode; Parents = GetParents(); var shape = (ICoordinate) Activator.CreateInstance(type); shape.Name = figureNode.ReadString("Name"); shape.CS = CS; shape.ReadXml(XmlNode); ShapeStyleEditor.ApplyStyle(shape.Shape, Style); } } cs.Shapes.OfType<IMovable>().ToList().ForEach(m => m.Move(((ICoordinate) m).Center)); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } }
这样,整个保存和解析的功能就完成了,如何测试呢?对了,我们不是有了右键菜单吗,我们再加两个菜单选项【复制】和【粘贴】,分别实现将内容保存到剪贴板和从剪贴板加载图形,这样就能很好地测试了,而且还省去了打开和保存文件那么麻烦。
运行效果如图:
http://www.diyuexi.com/webpages/query/ShareRes.aspx
大功告成,算是完成了里程碑的一页!前面讲到,我的一个目的就是做一个几何作图的过关游戏,根据过关的等级显示不同的技能,比如初始技能只有【笔】、【尺】、【规】,当你过了用这些初始竟能画平行线这一关,就会多一个技能【平行线】,所以以后就可以更方便地去画平行线了,下节我们就来看看如何实现【平行线】这类组合技能的实现!