如何在Haskell中建模一个二维世界

5
我正在制作一款游戏。游戏由一个无限平面组成。单位必须处于离散的方格上,因此它们可以用简单的 Location { x :: Int, y :: Int } 来定位。

可能会有许多种 Unit。有些可能是生物,有些只是物品,比如一块石头或木头(想象一下2D版的Minecraft)。许多都是空的(只是草或其他东西)。

在Haskell中,你会如何建模?我考虑了以下方案,但是对于对象和生物怎么办?它们可能具有不同的字段?将它们全部规范化为Unit吗?
data Unit = Unit { x :: Int, y :: Int, type :: String, ... many shared properties... }

我也考虑了添加一个“位置类型”

data Location = Location { x :: Int, y :: Int, unit :: Unit } 
-- or this
data Location = Location { x :: Int, y :: Int }
data Unit = Unit { unitFields... , location :: Location }

你有什么想法吗?在面向对象的语言中,我可能会让LocationUnit继承另一个,并使每个具体类型的Unit相互继承。

另一个考虑因素是将发送大量这些对象到网络上,所以我需要将它们序列化为JSON以供客户端使用,并且不想编写大量的解析样板。


1
请使用 Data.Map.Map 作为您的网格。 - Thomas Eding
那么x和y只需成为Map中的一个键,而Units则具有它们各自的字段?当将它们发送到客户端时,是否也应该发送一个map/hash呢? - Sean Clark Hess
1个回答

7

位置只是一个简单的二维类型。

我建议不要将单位与它们所在的位置捆绑在一起;只需使用一个地图位置单位来处理网格上的位置和那里是否存在任何东西。

至于特定类型的单位,我至少建议将常见字段分解为数据类型:

data UnitInfo = UnitInfo { ... }

data BlockType = Grass | Wood | ...

data Unit
  = NPC UnitInfo AgentID
  | Player UnitInfo PlayerID
  | Block UnitInfo BlockType

或类似的。

通常情况下,将共同点因素提炼出来,形成自己的数据类型,并尽可能使数据尽可能简单和“孤立”(即将诸如“这个单位在哪里?”之类的事情移动到单独的结构中,以便将两者关联起来,使各个数据类型尽可能“永恒”,可重用和抽象)。

在Haskell中,将Unit的“类型”定义为String是一种不良风格;它通常表示您正在尝试使用数据类型实现动态类型或OOP结构,这是一种不良的匹配。

您的JSON要求使事情变得复杂,但是此FAQ条目展示了如何在Haskell中惯用地实现此种泛化,不需要String类型或花哨的类型类hack,仅使用函数和数据类型作为主要的抽象单元。当然,前者在这里会给您带来问题;将函数序列化为JSON很困难。但是,您可以维护从代表“类型”(生物、方块等)的ADT到其实际实现的映射:

-- records containing functions to describe arbitrary behaviour; see FAQ entry
data BlockOps = BlockOps { ... }
data CreatureOps = CreatureOps { ... }
data Block = Block { ... }
data Creature = Creature { ... }
data Unit = BlockUnit Block | CreatureUnit Creature
newtype GameField = GameField (Map Point Unit)

-- these types are sent over the network, and mapped *back* to the "rich" but
-- non-transferable structures describing their behaviour; of course, this means
-- that BlockOps and CreatureOps must contain a BlockType/CreatureType to map
-- them back to this representation
data BlockType = Grass | Wood | ...
data CreatureType = ...
blockTypes :: Map BlockType BlockOps
creatureTypes :: Map CreatureType CreatureOps

这让您具备了典型面向对象编程结构的所有可扩展性和不重复自己的特性,同时保持功能简单性并允许游戏状态的简单网络传输。
一般来说,应避免以继承和其他面向对象编程概念思考;相反,尝试以函数和较简单结构的组合形式思考动态行为。函数是函数式编程中最强大的工具,因此得名,并且可以表示任何复杂的行为模式。最好不要让网络播放等需求影响基本设计;与上述示例一样,几乎总是可以在表达力和简单性构建的设计之上添加这些内容,而不是像通信格式这样的约束。

还在消化中,但非常有帮助。谢谢! - Sean Clark Hess
有一件让我困惑的事情是一个id字段。我计划在redis中存储状态,并且JSON api将需要一些关于ids的通信。我应该向我的Units添加一个id字段,还是以某种方式进行关联?例如,新客户端可能想说“我是新的!给我返回我的玩家对象!”他们会想要自己的id,但是他们将发送一些字段来指定他们的玩家。服务器生成它并返回它。因此,他们的对象没有一个,我返回的那个有。 - Sean Clark Hess
@SeanClarkHess:是的,在那里使用IntMap Unit或类似东西将标识符与单位关联起来可能是最佳选择;在Unit中包含标识符本身也是合理的。我会考虑将客户指定的字段拆分为它们自己的结构,并将其包含在Creature中,以便客户可以隔离决定的“无害”位,这些位应与他们不能接触的事物(例如,生命值)分开;称为CreatureTemplate - ehird
@SeanClarkHess:然后你只需要像 makeCreature :: CreatureTemplate -> CreaturecreateCreatureUnit :: Creature -> M Unit 这样的代码(对于一些单子 M). - ehird

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接