《真菌洞窟(Fungus Cave)》四月开发日志
游戏简介
《真菌洞窟(Fungus Cave) 是一款正在开发的单人、回合制 Unity Roguelike 游戏。最新版本是 0.1.1。上个月 以来主要做了四件事情:
- 将游戏数据从代码内部迁移到 XML 文件。
- 新增第二层地下城。
- 强化第一层的敌人,让游戏难度平稳上升。
- 修复自动探索。
本文将讨论三个话题:读写 XML 文件,读写二进制文件,自动探索。
图 1:演示动画,四月。
图 2:演示动画,三月。
读写 XML 文件
游戏对象需要数据,但是我们不希望把对象和数据直接绑定起来,因为数据可能来自代码内部的字典,外部的文件,或者现场生成的随机数。我们可以在游戏对象和数据源之间搭建一条管道:
- [ 数据源 ] <--> [ 数据枢纽 ] <--> [ 游戏对象 ]
数据枢纽负责两件事情:
- 从源头读取数据,或者把数据写进源头。
- 从对象那里收集数据,或者把数据发送给对象。
游戏对象调用某个方法与数据枢纽沟通,这个方法可能属于游戏对象,也可能属于数据枢纽。
我为 XML 数据和二进制数据建立了两条略有不同的管道:
- [ XML 文件 <--> SaveLoadFile ] <--> [ XData ] <--> [ 游戏对象 X ]
- [ 二进制文件 <--> SaveLoadFile ] <--> [ SaveLoadGame ] <--> [ 游戏对象 Y ]
SaveLoadFile 有四个公用方法:
图 3:公用方法。
这个对象直接读写文件,输出一个完整的数据对象。
XData 代表了一系列数据枢纽对象,它们从 LoadXML() 的返回值里抽取出一部分数据,供特定游戏对象使用。有些数据枢纽实现了 ISaveLoadXML 和/或 IGetData。
ISaveLoadXML 封装了 LoadXML(string path) 和 SaveXML(string path),这个接口提供的两个方法只能读写特定文件。IGetData 负责抽取数据。
图 4:ISaveLoadXML 和 IGetData。
我的 XML 文件通常包含两个节点(见图 2),可以使用 IGetData.GetIntData("ActorTag", "HP") 获得数据。
图 5:XML 文件结构。
ActorData 是一个 XData 对象,负责读取 Data/actorData.xml。
游戏对象 X 是 XML 数据管道的最后一站。它调用数据枢纽的方法获得数据,比方说:
图 6:游戏对象获取数据。
所以说,看到游戏对象和数据源,我们的下一个问题肯定是:数据枢纽在哪里?万事皆三,巴佬们不会懂的。
读写二进制文件
我们来看第二条数据管道。
- [ 二进制文件 <--> SaveLoadFile ] <--> [ SaveLoadGame ] <--> [ 游戏对象 Y ]
SaveLoadFile.SaveBinary() 把一个数组 IDataTemplate[] 写入二进制文件。SaveLoadGame 收集游戏对象的数据,把类型转换成 IDataTemplate,传送给 SaveLoadFile。这里有一个关键点。我们不必序列化整个 Person 对象,因为它的 MaxHP,Name 等数据保存在 XML 文件里。不妨创建一个小对象 DTPerson,单独存放这个对象的 Salary。
图 7:IDataTemplate 和 DTPerson。
SaveLoadGame 类似 XData,它有两个职责:
- 收集游戏对象的数据,然后调用 SaveLoadFile.SaveBinary()。
- 调用 SaveLoadFile.LoadBinary(),然后把数据发送给游戏对象。
那么怎样收集和发送数据呢?SaveLoadGame 包含一个私有数组 ISaveLoadBinary[] slb。游戏对象 Person 实现了接口 ISaveLoadBinary,把指向自己的引用保存在 slb 里面。接口定义见图 8。
图 8:ISaveLoadBinary。
保存游戏的时候,我们遍历 slb 的每一个元素,调用 Save(out IDataTemplate data) 收集数据。Person 的 Save() 定义如下:
图 9:Person.Save() 和 Person.Load()。
读取游戏存档的第一步是调用 SaveLoadFile.LoadBinary(),把返回值放入临时变量 IDataTemplate[] load。接下来,我们遍历 load 的每一个元素,根据 IDataTemplate.DTTag 调用不同对象的 ISaveLoadBinary.Load()。请看一个简单的例子:
图 10:读取二进制文件。
RandomNumber 实现了 ISaveLoadBinary,它的一部分数据被保存为 DTSeed 对象,详见 DataTemplate。
自动探索
自动探索的原理,Roguebasin 讲得很清楚了,但是代码写起来挺容易出错的。一个月前我发现自动探索有点问题,上周花了一个晚上重写了一遍。这个模块包含三个组件:
- AutoExplore 提供了一个(并且只有一个)公用方法,输出下一步移动的坐标。
- AutoExplore 需要来自 PCAutoExplore 和 NPCAutoExplore 的数据,这两个对象都实现了 IAutoExplore。
首先来看一下 AutoExplore 的方法 public int[] GetDestination()
。
图 11:GetDestination()。
在 ResetBoard() 内部,我们首先生成一个和地下城一样大的二维数组;然后定义三个特殊的距离;接下来遍历二维数组的每个元素,设置初始距离,记录起始位置;最后返回这个二维数组。
图 12:ResetBoard()。
三个判断条件顺序不能弄错。起始位置未必是一方通行的。比方说,白色相簿试图接近米④达,把后者所在位置标记为起始点,但这个位置是被占据的,因此无法通过。
GetDestination() 的第二步很简单,我们直接看第三步。SetDistance(int[,] dungeon, Stack
图 13:SetDistance()。
上述代码里出现了两个方法:GetNeighbor(int[] center) 和 GetDistance(int[] center)。前者返回 center 周围八个格子的坐标,后者做了三件事情:
- 调用 GetNeighbor(),获取相邻位置。
- 找出上述位置中的最小距离。
- 让最小距离增加固定值,然后返回这个数值。
GetDestination() 的最后一步是找到与当前演员相邻、并且距离最小的格子。如果有多个距离相等的格子,随机选择一个。
IAutoExplore 这个接口不是必需的,但是利用这个接口,我们可以让一套代码满足多种用途:让 PC 自动探索,让 NPC 追踪 PC。稍微改一下 SetDistance(),我们还能够让 NPC 逃离 PC,我之前说过怎样实现 逃离算法。PC 应该在发现敌人时停止自动探索;有时候 PC 会在两个格子之间来回移动,最好避免这种情况。这些功能都可以添加进 PCAutoExplore。
以上是本月总结。最后留一道思考题。请结合创作时间(1995,2006 和 2017),分析以下三幅画面的镜头语言。
图 14:是 [时间删除] 的味道!
作者暂无likerid, 赞赏暂由本网站代持,当作者有likerid后会全部转账给作者(我们会尽力而为)。Tips: Until now, everytime you want to store your article, we will help you store it in Filecoin network. In the future, you can store it in Filecoin network using your own filecoin.
Support author:
Author's Filecoin address:
Or you can use Likecoin to support author: