這幾天年終績效考核,瑣事比較多所以更新得沒有以前快了,絕對不是因為網上說的什么微軟關閉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
大功告成,算是完成了里程碑的一頁!前面講到,我的一個目的就是做一個幾何作圖的過關游戲,根據過關的等級顯示不同的技能,比如初始技能只有【筆】、【尺】、【規】,當你過了用這些初始竟能畫平行線這一關,就會多一個技能【平行線】,所以以后就可以更方便地去畫平行線了,下節我們就來看看如何實現【平行線】這類組合技能的實現!