用内容清单来实现地图的刷新,根据开发需求的细化来优化代码结构。
——茂叔
地图上来了只小狗
上一篇里,我们成功让游戏世界有了心跳,然而地图上任然空空如也。现在我们要在地图上加入内容GameObject
了。
最开始,还是要先把GameObject
的基本需求整理一下。由于GameObject
继承自Existence
,所以标识、名称这些可以不考虑了。除此之外,GameObject
还需要下面这些信息:
- 位置,地图标识、X、Y
- 介绍描述,这是一个程序猿……
- 对应清单的
ObjectClassId
,有了这个,就知道是具体个什么东西或者不是个东西了。例如,这是一把剑,还是一把刀,是关云长的冷艳锯还是是吕奉先的方天戟。 - 类型,这是一种武器、一个角色还是一种药物……同一类型的东西具有某些共性,例如武器就都可以装备,而药品都可以服用。
- 图像,在客户端显示出来是个什么样子。我们把图像信息分解为图像文件编号和图像编号,这样可以把不同类型的东西放在不同的图像文件里面,方便维护。
- 状态,这个东西目前是什么状态,是已经腐烂,还是正在移动,或者是遭到了攻击。
- 动作阶段,一个动作可能需要不同的阶段,即便是静止的,也需要有云舒云展或者花开花落,我们需要根据不同的动作阶段在客户端显示不同的图像。
- 能不能穿过,一堵墙和一颗小草的区别。
- 主人,这是谁扔的?
- 物品……怀璧其罪。
……
别的以后再补充吧。 给GameObject
添加以下成员:
public string Description;
public string MapEID;
public sbyte X;
public sbyte Y;
public ushort Category;//类型
public ushort ClassID;
public byte Status;
public byte StatusStep;
public byte ImgFileID;
public byte ImgID;
public bool Passable;
public GameObject[] Inventory;
public string OwnerEID;
由于我们需要从地图内容清单来生成内容,这种工作其他地方也会用到,因此,我们定义一个ObjectFactory
来完成这个工作。
在GameLib
项目中添加一个ObjectFactory
类。
class ObjectFactory
{
public static GameObject Make(ushort ClassID, Action<string> LOGFUN = null)
{
GameObject ret = null;
switch (ClassID)
{
case 0://生成一只小狗
{
ret = new GameObject(LOGFUN)
{
Name = "小狗旺财",
Description = "一只小狗,就是你",
CanPass = false,
ClassID = ClassID,
Category = ObjectCategory.ANIMAL,
ImgFileID = 0,
ImgID = 0,
Status = ObjectStatus.IDEL,
StatusStep = 0,
OwnerEID = null,
Inventory = new GameObject[8]//可以带8个物品
};
}
break;
case 1://生成一只小猫
break;
case 3://生成一块石头
break;
case 4://生成一块草地
break;
default:
break;
}
return ret;
}
}
然后修改GameMap
的Refresh
方法如下:
public void Refresh()//刷新
{
LOG("重新生成地图(" + EID + ")内容");
lock (this)
{
gObjects.Clear();
foreach (MapItem item in MapItems)
{
GameObject obj = ObjectFactory.Make(item.ClassID, LOG);
if (obj != null)
{
obj.X = item.X;
obj.Y = item.Y;
gObjects.Add(obj);
LOG("加入 " + obj.Name + " 成功");
}
}
}
LastRefreshTime = DateTime.Now;
}
在GameWorld
类的构造函数里面去配置第一张地图的内容清单。
public GameWorld(Action<string> LOGFUN = null) : base(LOGFUN)
{
gMaps = new List<GameMap>();
LOG("GameWorld(" + EID + ")创建成功");
GameMap Map = new GameMap(LOG);
Map.MapItems.Add(new GameMap.MapItem()
{
ClassID= 0,
X = 1,
Y = 1
}
);
Map.Refresh();
gMaps.Add(Map);
}
修改GameObject
的心跳:
public override void HeartBeat()
{
LOG("GameObject[" + Name + "](" + EID + ")正在心跳 " + G.GlobeTime);
}
调试看看效果:
嗯,完全如我们的预期,因为我在写这篇文章的时候已经把坑都填了……
优化代码
在完成上面的工作的时候,我们发现了一些问题。
- 无论是类型
Category
还是ClassID
都是数字,以后多了记不住咋办?这一问题在各个状态值上也存在。 MapItem
被定义在GameMap
内部,使用上不方便。- EID被定义成字符串,又是MD5码,将来使用起来不直观,容易出错。
为了解决上述问题,并有利于之后的开发,我们将所有需要制定的类型都放在一个叫Type.cs
的文件里面,让代码看起来更有逼格。在GameLib
项目新建一个Type
类,删除缺省的class Type
将文件内容修改如下:
public enum ObjectCategory : ushort
{
NONE = 0,
ANIMAL = 1,
TERRAIN = 2
}
public enum ObjectClassID : ushort
{
NONE = 0,
DOG = 1,
CAT = 2,
GRASS = 3,
ROCK = 4
}
public enum ObjectStatus : byte
{
IDEL = 0,
MOVING = 1,
FIGHTING = 2
}
public struct MapItem
{
public ObjectClassID ClassID;
public sbyte X;
public sbyte Y;
}
public struct ExistenceID
{
private string value;
public ExistenceID(string v)
{
if (v == null)
value = "";
else if (Regex.Match(v, @"^[A-Z0-9]{64}$").Success)
value = v;
else
throw new InvalidCastException("ExistenceID只能为64位大写字母和数字构成的字符串");
}
public struct ExistenceID
{
private string value;
public ExistenceID(string v)
{
if (v == null || v == "")
value = "";
else if (Regex.Match(v, @"^[A-Z0-9]{64}$").Success)
value = v;
else
throw new InvalidCastException("ExistenceID只能为64位大写字母和数字构成的字符串");
}
public static ExistenceID NewValue
{
get
{
return G.MakeMD5(DateTime.Now.ToString("yyyyMMddHHmmssffff") + G.RND(10000, 99999).ToString() + G.RND(10000, 99999).ToString())
+ G.MakeMD5(G.RND(10000, 99999).ToString() + G.RND(10000, 99999).ToString());
}
}
public static ExistenceID Null
{
get
{
return "";
}
}
public override bool Equals(object obj)=> value == obj.ToString();
public override int GetHashCode()
{
return base.GetHashCode();
}
public override string ToString()
{
return value;
}
public static implicit operator ExistenceID(string v)
{
return new ExistenceID(v);
}
}
这样我们就可以把GameObject
的成员写成这样:
public string Description;
public ExistenceID MapEID;
public sbyte X;
public sbyte Y;
public ObjectCategory Category;
public ObjectClassID ClassID;
public ObjectStatus Status;
public byte StatusStep;
public byte ImgFileID;
public byte ImgID;
public bool CanPass;
public GameObject[] Inventory;
public ExistenceID OwnerEID;
ObjectFactory
的Make
方法写成这样:
……
switch (ClassID)
{
case ObjectClassID.NONE:
break;
case ObjectClassID.DOG:
{
ret = new GameObject(LOGFUN)
{
Name = "小狗旺财",
Description = "一只小狗,就是你",
CanPass = false,
ClassID = ClassID,
Category = ObjectCategory.ANIMAL,
ImgFileID = 0,
ImgID = 0,
Status = ObjectStatus.IDEL,
StatusStep = 0,
OwnerEID = null,
Inventory = new GameObject[8]//可以带8个物品
};
}
break;
case ObjectClassID.CAT:
break;
case ObjectClassID.GRASS:
break;
case ObjectClassID.ROCK:
break;
default:
break;
}
……
在GameWorld
的构造函数里添加地图内容清单也不用引用GameMap
了:
Map.MapItems.Add(new MapItem()
{
ClassID= ObjectClassID.DOG,
X = 1,
Y = 1
}
别忘了还有GameMap
哦:
public ExistenceID NorthMap = ExistenceID.Null;
public ExistenceID SouthMap = ExistenceID.Null;
public ExistenceID WestMap = ExistenceID.Null;
public ExistenceID EastMap = ExistenceID.Null;
改完调试一下,调试结果一模一样,但是这样的代码,是不是好看多了,而且将来维护扩充都更方便,也不容易出错。
好了,下一篇我们将实现更多的内容细节,并进一步讨论各种内容的组织架构。
上一篇:C#服务端的微信小游戏——多人在线角色扮演(五)
下一篇:C#服务端的微信小游戏——多人在线角色扮演(七)
请用微信扫描查看游戏效果演示