我正在尝试使用haskell和SDL1.2绑定创建一个基本的2D引擎(为了好玩,我只是在学习)。 理想情况下,世界将是程序生成的,大块的,允许自由探索。
现在,我的块由200 * 200个瓷砖组成,我使用类型代表:
Mat [Tile] = Vec.Vector (Vec.Vector [Tile])
和这些功能:
fromMat :: [[a]] -> Mat a
fromMat xs = Vec.fromList [Vec.fromList xs' | xs' <- xs]
(§) :: Mat a -> (Int, Int) -> a
v § (r, c) = (v Vec.! r) Vec.! c
我正在使用循环的切片列表以允许精灵动画,以及稍后的动态行为。
游戏循环的每一帧,程序读取与当前摄像机位置相关的矢量部分,显示相应的图块并返回一个新的矢量,其中每个循环列表都被其尾部替换。
以下是负责此事的代码:
applyTileMat :: Chunk -> SDL.Surface -> SDL.Surface -> IO Chunk
applyTileMat ch src dest =
let m = chLand $! ch
(x,y) = chPos ch
wid = Vec.length (m Vec.! 0) - 1
hei = (Vec.length m) - 1
(canW,canH) = canvasSize ch in
do sequence $ [ applyTile (head (m § (i,j))) (32*(j-x), 32*(i-y)) src dest | i <- [y..(y+canH)], j <- [x..(x+canW)]]
m' <-sequence $ [sequence [(return $! tail (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]] --weird :P
return ch { chLand = fromMat m' }
第一个序列执行显示部分,第二个序列返回新的矢量m'。
起初我使用以下理解来获得m'
let !m' = [id $! [(tail $! (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]]
但这样做会导致内存使用量不断增加。我认为它与懒惰的评估有关,防止数据被正确地垃圾收集,但我真的不明白为什么。
在这种特殊情况下,由于我必须查看整个向量,因此它并不真正重要。但是,如果我只想每帧“更新”我的部分块,我不知道该怎么做,从而创建一个只包含前一部分数据的新块。
我可能没有按照预期的方式使用Data.Vector,但这是我用O(n)随机访问找到的最简单的数据结构。
整个代码在那里: https://github.com/eniac314/wizzard/blob/master/tiler.hs
答案 0 :(得分:3)
确实问题是向量在元素中是懒惰的。首先,让我们看看为什么你的例子不起作用。
let !m' = [id $! [(tail $! (m § (i,j))) | j <- [0..wid]] | i <- [0..hei]]
!m
中的爆炸模式并没有太大作用。所有!
都确保变量是构造函数或lambda,而不是函数应用程序。在不评估任何元素的情况下,可以将!m
视为[]
或(:)
。同样,įd $!
- s不会强制内部列表的任何实际元素。
return ch { chLand = fromMat m' }
fromMat
是下一个罪魁祸首。 fromMat
不强制内部向量,也不强制元素。结果,对旧矢量的引用无限期地粘在了thunk上。
通常,正确的解决方案是导入Control.DeepSeq
,并使用force
或$!!
来完全评估向量。不幸的是,我们不能这样做,因为循环列表(试图强制一个结果无限循环)。
我们真正需要的是一个将矢量的所有元素都带到弱头正常形式的函数:
whnfElements :: Vector a -> Vector a
whnfElements v = V.foldl' (flip seq) () v `seq` v
我们可以使用它为向量定义严格的map
:
vmap' :: (a -> b) -> Vector a -> Vector b
vmap' f = whnfElements . V.map f
现在更新成为:
update :: Mat [Tile] -> Mat [Tile]
update = (vmap' . vmap') tail