Welcome Guest ( Log In | Register )

欢迎访问本站。游客仅能浏览首页新闻、版块主题、维基条目与资源信息,需登录后方可获得内容发布、话题讨论、维基编辑与资源下载等权限。若无账号请先完成注册流程。
 
Reply to this topicStart new topic
> 《真菌洞窟(Fungus Cave)》四月开发日志
Bozar
2019-04-23, 11:27
Post #1


GEEKs will Eventually Evolve into Kryptonians. | All my jokes are cries for help.
Group Icon
 1267
   73

Group: Avatar
Posts: 1111
Joined: 2008-05-18
Member No.: 21346




QUOTE
一个宇宙人,一个未来人,一个超能力者。

## 游戏简介

[《真菌洞窟(Fungus Cave)》](https://github.com/Bozar/FungusCave)是一款正在开发的单人、回合制 Unity Roguelike 游戏。最新版本是 [0.1.1](https://github.com/Bozar/FungusCave/releases)。[上个月](https://trow.cc/board/showtopic=48413) 以来主要做了四件事情:

* 将游戏数据从代码内部迁移到 XML 文件。
* 新增第二层地下城。
* 强化第一层的敌人,让游戏难度平稳上升。
* 修复自动探索。

本文将讨论三个话题:读写 XML 文件,读写二进制文件,自动探索。

» Click to show Spoiler - click again to hide... «

图 1:演示动画,四月。

» Click to show Spoiler - click again to hide... «

图 2:演示动画,三月。


## 读写 XML 文件

游戏对象需要数据,但是我们不希望把对象和数据直接绑定起来,因为数据可能来自代码内部的字典,外部的文件,或者现场生成的随机数。我们可以在游戏对象和数据源之间搭建一条管道:

* [ 数据源 ] <--> [ 数据枢纽 ] <--> [ 游戏对象 ]

数据枢纽负责两件事情:

* 从源头读取数据,或者把数据写进源头。
* 从对象那里收集数据,或者把数据发送给对象。

游戏对象调用某个方法与数据枢纽沟通,这个方法可能属于游戏对象,也可能属于数据枢纽。

我为 XML 数据和二进制数据建立了两条略有不同的管道:

* [ XML 文件 <--> SaveLoadFile ] <--> [ XData ] <--> [ 游戏对象 X ]
* [ 二进制文件 <--> SaveLoadFile ] <--> [ SaveLoadGame ] <--> [ 游戏对象 Y ]

[SaveLoadFile](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/SaveLoadFile.cs) 有四个公用方法:

» Click to show Spoiler - click again to hide... «

图 3:公用方法。

这个对象直接读写文件,输出一个完整的数据对象。

XData 代表了一系列数据枢纽对象,它们从 LoadXML() 的返回值里抽取出一部分数据,供特定游戏对象使用。有些数据枢纽实现了 ISaveLoadXML 和/或 IGetData。

ISaveLoadXML 封装了 LoadXML(string path) 和 SaveXML(string path),这个接口提供的两个方法只能读写特定文件。IGetData 负责抽取数据。

» Click to show Spoiler - click again to hide... «

图 4:ISaveLoadXML 和 IGetData。

我的 XML 文件通常包含两个节点(见图 2),可以使用 IGetData.GetIntData("ActorTag", "HP") 获得数据。

» Click to show Spoiler - click again to hide... «

图 5:XML 文件结构。

[ActorData](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/ActorData.cs) 是一个 XData 对象,负责读取 Data/actorData.xml。

游戏对象 X 是 XML 数据管道的最后一站。它调用数据枢纽的方法获得数据,比方说:

» Click to show Spoiler - click again to hide... «

图 6:游戏对象获取数据。

所以说,看到游戏对象和数据源,我们的下一个问题肯定是:数据枢纽在哪里?万事皆三,巴佬们不会懂的。

## 读写二进制文件

我们来看第二条数据管道。

* [ 二进制文件 <--> SaveLoadFile ] <--> [ SaveLoadGame ] <--> [ 游戏对象 Y ]

SaveLoadFile.SaveBinary() 把一个数组 IDataTemplate[] 写入二进制文件。SaveLoadGame 收集游戏对象的数据,把类型转换成 IDataTemplate,传送给 SaveLoadFile。这里有一个关键点。我们不必序列化整个 Person 对象,因为它的 MaxHP,Name 等数据保存在 XML 文件里。不妨创建一个小对象 DTPerson,单独存放这个对象的 Salary。

» Click to show Spoiler - click again to hide... «

图 7:IDataTemplate 和 DTPerson。

[SaveLoadGame](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/SaveLoadGame.cs) 类似 XData,它有两个职责:

* 收集游戏对象的数据,然后调用 SaveLoadFile.SaveBinary()。
* 调用 SaveLoadFile.LoadBinary(),然后把数据发送给游戏对象。

那么怎样收集和发送数据呢?SaveLoadGame 包含一个私有数组 ISaveLoadBinary[] slb。游戏对象 Person 实现了接口 ISaveLoadBinary,把指向自己的引用保存在 slb 里面。接口定义见图 8。

» Click to show Spoiler - click again to hide... «

图 8:ISaveLoadBinary。

保存游戏的时候,我们遍历 slb 的每一个元素,调用 Save(out IDataTemplate data) 收集数据。Person 的 Save() 定义如下:

» Click to show Spoiler - click again to hide... «

图 9:Person.Save() 和 Person.Load()。

读取游戏存档的第一步是调用 SaveLoadFile.LoadBinary(),把返回值放入临时变量 IDataTemplate[] load。接下来,我们遍历 load 的每一个元素,根据 IDataTemplate.DTTag 调用不同对象的 ISaveLoadBinary.Load()。请看一个简单的例子:

» Click to show Spoiler - click again to hide... «

图 10:读取二进制文件。

[RandomNumber](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/RandomNumber.cs) 实现了 ISaveLoadBinary,它的一部分数据被保存为 DTSeed 对象,详见 [DataTemplate](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/DataTemplate.cs)。

## 自动探索

自动探索的原理,[Roguebasin](http://www.roguebasin.com/index.php?title=Dijkstra_Maps_Visualized) 讲得很清楚了,但是代码写起来挺容易出错的。一个月前我发现自动探索有点问题,上周花了一个晚上重写了一遍。这个模块包含三个组件:

* [AutoExplore](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/AutoExplore.cs) 提供了一个(并且只有一个)公用方法,输出下一步移动的坐标。
* AutoExplore 需要来自 [PCAutoExplore](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/PCAutoExplore.cs) 和 [NPCAutoExplore](https://github.com/Bozar/FungusCave/blob/master/Assets/Scripts/NPCAutoExplore.cs) 的数据,这两个对象都实现了 IAutoExplore。

首先来看一下 AutoExplore 的方法 `public int[] GetDestination()`。

» Click to show Spoiler - click again to hide... «

图 11:GetDestination()。

在 ResetBoard() 内部,我们首先生成一个和地下城一样大的二维数组;然后定义三个特殊的距离;接下来遍历二维数组的每个元素,设置初始距离,记录起始位置;最后返回这个二维数组。

» Click to show Spoiler - click again to hide... «

图 12:ResetBoard()。

三个判断条件顺序不能弄错。起始位置未必是一方通行的。比方说,白色相簿试图接近米④达,把后者所在位置标记为起始点,但这个位置是被占据的,因此无法通过。

GetDestination() 的第二步很简单,我们直接看第三步。SetDistance(int[,] dungeon, Stack<int[]> start) 递归地标记出所有未探索(unexplored)格子的距离。

» Click to show Spoiler - click again to hide... «

图 13:SetDistance()。

上述代码里出现了两个方法:GetNeighbor(int[] center) 和 GetDistance(int[] center)。前者返回 center 周围八个格子的坐标,后者做了三件事情:

* 调用 GetNeighbor(),获取相邻位置。
* 找出上述位置中的最小距离。
* 让最小距离增加固定值,然后返回这个数值。

GetDestination() 的最后一步是找到与当前演员相邻、并且距离最小的格子。如果有多个距离相等的格子,随机选择一个。

IAutoExplore 这个接口不是必需的,但是利用这个接口,我们可以让一套代码满足多种用途:让 PC 自动探索,让 NPC 追踪 PC。稍微改一下 SetDistance(),我们还能够让 NPC 逃离 PC,我之前说过怎样实现 [逃离算法](https://trow.cc/board/showtopic=48285)。PC 应该在发现敌人时停止自动探索;有时候 PC 会在两个格子之间来回移动,最好避免这种情况。这些功能都可以添加进 PCAutoExplore。

以上是本月总结。最后留一道思考题。请结合创作时间(1995,2006 和 2017),分析以下三幅画面的镜头语言。

» Click to show Spoiler - click again to hide... «

图 14:是 [时间删除] 的味道!



This post has been edited by Bozar: 2019-04-23, 11:46
TOP
Fast ReplyReply to this topicStart new topic
 


Time is now: 2020-03-29, 19:26