我正在尝试创建一个由几种不同类型组成的游戏引擎:
data Camera = Camera ...
data Light = SpotLight ... | DirectionalLight ...
data Object = Monster ... | Player ... | NPC ...
但是,我现在正在尝试为所有这些实体实现基本物理。这要求它们各自包含一个pos :: (Double, Double, Double)
和一个velocity :: (Double, Double, Double)
。
在面向对象的语言中,我会这样实现它:
Camera implements PhysicalObject
其中PhysicalObject
包含两个属性pos
和velocity
。
我的第一反应是将它们全部放在同一类型中:
data Object = Monster ... | Player ... | NPC ... | Camera ...
但是,我担心这可能会使实现相机特定功能、光线特定功能等变得困难。实际上,除了它们在世界上都拥有物理位置和速度之外,它们几乎没有其他共同点。
有没有比在每个类型构造函数中定义两个属性更简单的方法?
我可以想到两种方法 - 类型类和镜头。
类型类
class PhysicalObject m where
position :: m -> (Double, Double, Double)
velocity :: m -> (Double, Double, Double)
然后,您将按照以下行为对象创建实例
data Camera = Camera
{ cameraPosition :: (Double,Double,Double)
, cameraVelocity :: (Double,Double,Double)
}
instance PhysicalObject Camera where
position = cameraPosition
cameraVelocity = cameraVelocity
对于您的其他类型的类型也是如此。那么任何不需要知道对象细节的函数都可以要求它的参数是PhysicalObject
的实例,例如:
type TimeInterval = Double
newPosition :: PhysicalObject m => TimeInterval -> m -> (Double,Double,Double)
newPosition dt obj = (x + du * dt, y + dv * dt, z + dw * dt)
where
(x,y,z) = position obj
(u,v,w) = velocity obj
但是,您将很难编写使用此代码修改对象的函数 - 该类告诉Haskell它如何访问对象的位置和速度,但不知道如何修改它们。
镜头
另一种选择是转到镜头库。这有点像野兽,但它允许你编写一些非常自然的代码。首先,有一些样板
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
现在定义一些位置和速度数据类型。不要担心以下划线为前缀的奇怪字段名称 - 我们不会使用它们。
data Pos = Pos { _posX, _posY, _posZ :: Double }
data Vel = Vel { _velX, _velY, _velZ :: Double }
instance Show Pos where show (Pos x y z) = show (x,y,z)
instance Show Vel where show (Vel x y z) = show (x,y,z)
现在,您可以使用一些模板Haskell来为您的数据类型派生镜头。这将生成类型类HasPos
和HasVel
,其方法允许您访问和修改作为这些类实例的任何值。
makeClassy ''Pos
makeClassy ''Vel
现在定义相机类,其中包括位置和速度。
data Camera = Camera
{ _cameraPos :: Pos
, _cameraVel :: Vel } deriving (Show)
Template Haskell的另一部分将自动创建cameraPos
和cameraVel
的功能,使您可以访问和修改相机的位置和速度。
makeLenses ''Camera
最后,声明您的相机是HasPos
类和HasVel
类的实例,并具有其方法的默认实现。
instance HasPos Camera where pos = cameraPos
instance HasVel Camera where vel = cameraVel
现在我们已经准备好做一些真正的工作了。让我们定义一个示例相机
camera = Camera (Pos 0 0 0) (Vel 10 5 0)
修改相机的函数,返回具有更新位置的新相机,是
move :: (HasPos a, HasVel a) => TimeInterval -> a -> a
move dt obj = obj
& posX +~ dt * obj^.velX
& posY +~ dt * obj^.velY
& posZ +~ dt * obj^.velZ
请注意,这是一个完全通用的函数,用于移动任何具有位置和速度的对象 - 它根本不特定于Camera
类型。它还具有看起来很像命令式代码的优点!
如果您现在将所有这些加载到 GHCI 中,您可以看到它的实际效果
>> camera
Camera {_cameraPos = (0.0,0.0,0.0), _cameraVel = (10.0,5.0,0.0)}
>> move 0.1 camera
Camera {_cameraPos = (1.0,0.5,0.0), _cameraVel = (10.0,5.0,0.0)}
我会实现它类似于:
type Position = (Double, Double, Double)
type Velocity = (Double, Double, Double)
class PhysicalObject a where
pos :: a -> Position
velocity :: a -> Velocity
data Camera = Camera
{ camPos :: Position
, camVel :: Velocity
} deriving (Eq, Show)
instance PhysicalObject Camera where
pos = camPos
velocity = camVel
然后,您可以对定义的需要PhysicalObject
的每个类型执行类似的操作。
您需要开始依赖类型类和对象编码之类的东西。第一种方法是将公共接口编码为每个类型继承自的类型类。
class PhysicalObject o where
pos :: o -> Vector3
velocity :: o -> Vector3
二是构建一个共同的对象
data PhysicalObject = PhysicalObject { poPos :: Vector3, poVelocity :: Vector3 }
data Monster = Monster { monsterPO :: PhysicalObject
, ... monsterStuff ...
}
甚至可以用于实例化第一个类型类
instance PhysicalObject PhysicalObject where
pos = poPos
velocity = poVelocity
instance PhysicalObject Monster where
pos = pos . monsterPO
velocity = velocity . monsterPO
但是,要小心这样的类型类编码,因为过多地使用它们通常会在阅读代码时造成歧义。可能很难理解类型并知道正在使用哪个实例。