我使用Haskell制作游戏,(这是一项任务,所以不要评判我),但我面临着有关数据类型的问题。
所以我想要的是一个数据类型实体,它具有位置,速度,角度和旋转速度。一个记录非常适合这个想法:
data Entity = Entity {
location :: Vector,
velocity :: Vector,
angle :: Float,
rotation :: Float
}
现在我想要实体的实例,即Player Rock Pickup和Bullet。但是Players Rocks和Bullets必须有一个额外的字段,即health :: Int,而Pickup必须有另外一个字段,即pickupType :: PickupType。
但我有一些方法可以处理任何实体类型。例如:
move :: Entity -> Entity
move e@(Entity {location, velocity, angle, rotation}) = e {location = location + velocity, angle = angle + rotation}
我不知道如何做到这一点,或者甚至可能做到这一点。我不明白为什么如果不可能,因为这在其他语言中肯定是可能的。
有些尝试以及为什么它们不是我想要的:
尝试1:
type Player = Player {
e :: Entity,
health :: Int
}
这很有效,但真的很难看。例如,这就是你如何移动玩家:
movePlayer :: Player -> Player
movePlayer p@(e) = p {e = move e}
这真的太丑了。
肯定: 易于创建抽象类。 易于创建实例。 简单的抽象方法。
否定: 很难获取或设置实例的实体实现字段。
尝试2:
class Entity e where
getLocation :: e -> Vector
getVelocity :: e -> Vector
...
setLocation :: Vector -> e -> e
setVelocity :: Vector -> e -> e
...
data Player = Player {
playerLocation :: Vector,
playerVelocity :: Vector,
...
playerHealth :: Int
}
instance Entity Player where
getLocation = location
getVelocity = velocity
...
setLocation l e = e {location = l}
setVelocity v e = e {playerVelocity = v}
...
move :: (Entity e) => e -> e
move e = (setLocation (getLocation e + getVelocity e) . setAngle (getAngle e + getRotation e)) e
它确实有效,但我希望我们都同意他们的定义现在真的很难看。适用于任何实体的抽象方法也变得丑陋。唯一的好处是像movePlayer这样的方法变得非常简单。
movePlayer :: Player -> Player
movePlayer = move
我甚至不需要再定义movePlayer,因为我可以使用move。
肯定: 易于获取或设置实例的实体实现字段。
否定: 很难创建抽象类。 更难创建实例。 硬抽象方法。
尝试3:
为实体提供任何实例所需的所有字段。
data Entity = Entity {
location :: Vector,
velocity :: Vector,
angle :: Float,
rotation :: Float,
health :: Int,
pickupType :: PickupType
}
这样我甚至不需要定义实例,我只能使用实体。唯一的问题是你有很多多余的数据。这是我目前使用的,IMO是我的问题的最佳解决方案,但我仍然不喜欢它。
肯定: 很容易创建抽象类,即使它不再是抽象的。 无需定义实例。 简单的抽象方法。 易于获取或设置实例的实体实现字段。
否定: 很多未使用的数据。 每次创建实体时都必须定义许多无意义的字段。
所以请帮助我,我找不到比这三个更好的方法:(
答案 0 :(得分:2)
我会第一次尝试,原因很简单:
它准确地捕捉了Player
的意图 - 它是Entity
的附加信息。
data Player = Player {
e :: Entity
health :: Int
}
虽然所有处理的函数最初都可能很麻烦,但您可能永远不必再次看到它们,这意味着您在代码中提供了一个足够抽象的接口,而不是直接访问Player
的状态。
movePlayer :: Player -> Player
movePlayer p@(e) = p {e = move e}
这个函数写一次,然后理想情况下你再也不用处理内部。
此外,现在您可以按预期使用类型类:您可以在单独的类型类中抽象movePlayer
,例如Movable
:
class Movable m where
move :: m -> m
-- Obviously, you can move entities
instance Movable Entity where
move e = -- stuff
但现在移动Players
非常容易:
instance Movable Player where
move (Player entity health) = Player (move entity) health
-- works, since `Entity` is movable
除此之外,你的类型类方法有一个重大缺陷:Player
的函数怎么样但Entity
没有呢?在这种情况下,您将Player
继承Entity
,如下所示:
class Entity e => Player e where
-- stuff ...
但是由于Haskell的类型类是开放的,任何东西都可以成为Player
,这不是它应该如何工作(当然,除非你的意图)。 / p>
答案 1 :(得分:2)
我会说你的第一次尝试是要走的路,原因与@ ThreeFx的回答相同。我会提出一个略有不同的选择。
鉴于以下类型:
data Player = Player {
playerEntity :: Entity,
health :: Int
}
data Pickup = Pickup {
pickupEntity :: Entity,
pickupType :: PickupType
}
我们可以提供通用的高阶函数,以便更轻松地对Entity
执行Entity
操作,而不是为Player
上的每个操作设置单独的类型类。 } s和Pickup
s:
overPlayerEntity :: (Entity -> Entity) -> Player -> Player
overPlayerEntity fn (Player pe h) = Player (fn pe) h
overPickupEntity :: (Entity -> Entity) -> Pickup -> Pickup
overPickupEntity fn (Pickup pe t) = Pickup (fn pe) t
现在,我们可以
movePlayer = overPlayerEntity move
movePickup = overPickupEntity move
我们也可以将它包装成一个类型类,以便更容易编写通用代码:
class HasEntity a where
overEntity :: (Entity -> Entity) -> a -> a
instance HasEntity Player where overEntity = overPlayerEntity
instance HasEntity Pickup where overEntity = overPickupEntity
这允许这样的事情:
move' :: HasEntity a => a -> a
move' = overEntity move
适用于Player
和Pickup
。这样就无需使用move
等专用版本的函数,同时我们只需要编写一次Entity
访问样板。
顺便说一句,这种over...Entity
做事方式正在逼近"镜头"在@ duplode和@Paul Johnson的答案结尾处提到的技巧。这些基本上是两个(非常)专用镜头。如果我们添加HasEntity
类型类,它会为我们提供所谓的"经典镜头" (这是lens
库中使用的术语)。您并不需要担心一般镜头概念的含义或含义,但这可以为您提供一个了解未来镜头的切入点。
答案 2 :(得分:0)
你可以使用一个总和(又名" union")类型。
data Entity =
Player {
location :: Vector,
-- etc.
health :: Int }
| Pickup {
location :: Vector,
-- etc.
pickupType :: PickupType}
只需让sum类型保存变化的数据,就可以将其解决。
这样做的好处是你可以拥有一个[实体],当实体的所有不同变体都是不同的类型时(不像OO语言),这是你无法做到的。
根据您的游戏模型,您可能还希望将位置和速度数据与其余玩家信息分开。这些数据能否更好地保存在像四叉树这样的空间数据结构中?这样,您可以将常量数据与每帧中变化的内容分开。
你应该关注的一件事是lenses,它可以解决你用大量的getter和setter描述的问题。
答案 3 :(得分:0)
这是一个冗长的评论,而不是一个答案,因为ThreeFx's answer已经涵盖了我要写的内容。
首先,避免使用OOP术语,因为它会导致混淆。在您的尝试#1中,Entity
是一种数据类型,而不是一个类,它也不是抽象的。在尝试#1中Player
和Entity
之间的关系只是一个组合:Player
有一个Entity
,它不是Entity
的实例1}}。
其次,记录更新语法 在Haskell中非常难看。这并不会让它变得复杂,只是有点麻烦。所以,当你说,例如“很难获得或设置实例的实体实现的字段”,它实际上并不难,只是不太漂亮。确定数据类型的设计并不是一个严重的问题。
第三,镜头是(在许多其他事物中)避免记录更新语法的丑陋的一种方式。你现在可能不想深入研究(至少等到你完成作业之后),但是我无法抗拒留下一个高度相关的教程链接,供你在将来的某些时候阅读:{ {3}}