在前面的章节里,我定义了一个抽象类VObject,作为虚拟实验室项目的“祖先”,接下来,我将实现两个具体类:VPlace和VItem,分别表示“场景类”和“物品类”。
VPlace
场景
所谓场景,实际上相当于背景,像地板这些永远不会覆盖在人物之上的东西统统可以归咎于场景。引擎中的场景应该具有以下几个特点:
- 不会轻易改变位置;
- 永远位于物品下方,不存在空间排序问题及遮挡问题;
- 在同一时刻,场景只能有一个;
- 场景一般是静止不动的,因此可以用一副图片来表示场景;
- 场景里有很多物品,这些物品按照一定的空间顺序排列。
根据场景的分类,还可以进一步抽象成VStreet(街道)、VRoom(房间)等具体信息,因此,在设计VPlace时,尽量把相关的属性放在一起。
实现VPlace类
VPlace类继承自VObject:
public class VPlace : VObject { //………… }
在里面添加一个属性,用来记录该场景中有哪些物品(包括人物,在这里我们把物品和人物同一对待,在下一级类,VRoom,才把这些区分开来):
public ObservableCollection<VItem> Items { get; set; }
按照父类中构造函数Build的顺序,重载Initialize方法和CreateUI方法(因为VPlace和VOjbect相比,没有多少“特殊”的属性需要添加,因此可以不需要重载VObject的Parse方法):
protected override void CreateUI() { UI = new UIPlace(this); } protected override void Initialize() { Items = new ObservableCollection<VItem>(); }
最后,添加一个AddItem和一个RemoveItem的方法,用来添加及删除该场景的物品:
public virtual void AddItem(VItem item) { Items.Add(item); } public virtual void RemoveItem(VItem item) { Items.Remove(item); }
由于Items属性是ObservableCollection<T>类型的,因此在改变场景中物品时,不需要写额外的事件通知机制,这点是我喜欢的。
接下来,就可以这样构造VPlace了:
String placeString = "<place name='NBA' width='1800' height='1200' centerX='0' centerY='0' x='-800' y='-500' file='engine1/place1/place.xml' />"; VPlace vp = new VPlace(placeString);
VItem
物品
物品,大概相当于前景。例如桌子、柜子等等。这些东西在渲染的时候,可能需要按照它们所处的空间位置排列,造就遮挡的效果。根据唯物主义理论,人物也是一种物品,不过和桌子比,人物会动,需要特别处理,我会专门用一个VPerson类来表示,和场景不同,虚拟使显示引起中涉及的物品可能会有以下特征:
- 有可能改变位置,永远位于背景上方;
- 同一个场景中,有多个物品,这些物品根据特定的规则,相互遮挡;
- 一个物品在一个时刻只能位于一个场景中;
- 在不同的条件下(例如时刻,鼠标悬浮等),物品有可能呈现不同的效果于用户交互。
在虚拟实验室引擎的设计过程中,我用VItem来表示“物品类”。
实现VItem
和VPlace一样,VItem也是继承自VObject类的,按照以上特点,我在VObject的属性中,添加了几个新属性。
public class VItem : VObject { //………… }
增加一个属性,表示该物品位于哪个场景:
protected VPlace m_Place; public VPlace Place { get { return m_Place; } set { if (m_Place != value) { m_Place = value; OnPropertyChanged("Place"); } } }
根据上面总结出来的特点的第四条:在不同的条件下(例如时刻,鼠标悬浮等),物品有可能呈现不同的效果于用户交互,我把这些不同的效果称作“动作(VAction)”,用两个集合类来表示动作集合。:
public Dictionary<String, VAction> Actions { get; set; } public ObservableCollection<String> ActionNames { get; set; }
此外,还有一个ActionID的属性,表示当前正在做的动作:
protected String m_ActionID; public String ActionID { get { return m_ActionID; } set { if (m_ActionID != value) { m_ActionID = value; OnPropertyChanged("ActionID"); } } }
最后,根据VObject的Build顺序,重写以下函数:
protected override void Parse(XElement xElement) { base.Parse(xElement); ActionID = xElement.Attribute("actionid", String.Empty); } protected override void CreateUI() { UI = new UIItem(this); } protected override void Initialize() { Actions = new Dictionary<string, VAction>(); ActionNames = new ObservableCollection<string>(); }
关于VAction
VAction是VItem中的动作,定义如下:
public class VAction { public String Name { get; set; } //动作名 public VRectangle[] Rectangle { get; set; } //动画组成 public String Next { get; set; } //播放完后的动画 public int FPS { get; set; } //帧率 public VAction(String name, String next, int fps, params VRectangle[] rectangle) { Name = name; Next = next; Rectangle = rectangle; FPS = fps; } } public struct VRectangle { public int X; public int Y; public int Width; public int Height; }
用一个Xml文件来描述这些VAction:
<?xml version="1.0" encoding="utf-8"?> <item image="girl.xml.png" width="147" height="144"> <action name="facerightdown" fps="10" next="facerightdown"> <rectangle x="1176" y="0" /> <rectangle x="1323" y="0" /> <rectangle x="1470" y="0" /> <rectangle x="1617" y="0" /> <rectangle x="1764" y="0" /> <rectangle x="1911" y="0" /> <rectangle x="2058" y="0" /> <rectangle x="2205" y="0" /> </actions> <action name="faceright" fps="10" next="faceright"> <rectangle x="0" y="0" /> <rectangle x="147" y="0" /> <rectangle x="294" y="0" /> <rectangle x="441" y="0" /> <rectangle x="588" y="0" /> <rectangle x="735" y="0" /> <rectangle x="882" y="0" /> <rectangle x="1029" y="0" /> </actions> </item>
关于Next属性
这里特别说明一下Next属性,所谓Next,就是当该Action执行完后,下一个需要执行的Action,如果想不停地执行某个Action,很简单,把Next设置成自身即可。
开工了,大家作业完成的如何?周老师已经准备好了皮鞭:
看谁敢不听话!