7

除了纯函数的概念,我对函数式编程知之甚少。在 John Carmack 的 2013 Quakecon 演讲中,他提到了与游戏相关的函数式编程经常被问到的一个问题:如果您无法访问状态,您如何开枪并对其他玩家造成伤害?(意译)在提到事件系统的一些事情时,我不太明白,因为在我看来,事件系统仍然需要状态?

如何用一种纯粹的函数式语言来实现这一点?

4

3 回答 3

8

重复我最喜欢的一句话

... 接受世界的状态并返回一个新世界,从而保持纯净。

这是在谈论 Clean,Haskell 的表弟,但它仍然是相关的。它的要点是,你是对的,你需要某种状态,但它不必是可变的。考虑

myFun :: StateOfTheWorld -> a -> (StateOfTheWorld, b)

所以我们不修改状态,我们只是产生一个新的。这是引用透明的,因为给定相同的世界状态和相同的动作,您将得到相同的东西。

对你来说,你可能有类似的东西

 killPlayer :: Game -> Event -> Game
 killPlayer g (Kill x) = g { isDead = x : isDead g }

这只是对记录使用功能更新。这有点笨拙,所以我们可能会做类似的事情

 killPlayer :: Game -> Event -> Action
 killPlayer (PlayerDamaged x amount) = if playerHealth g x <= amount
                                       then KillPlayer x
                                       else ReduceHealth x amount

所以我们只返回差异,而不是完整的游戏状态。

这有效,但很难看。所以我们用do符号和 Control.Monad.State 来美化它。这听起来很可怕,但这正是我们在上面所做的,只是有更多的句法抽象。事实上,这也是IOGHC 上的内容。我不知道你是否了解过 Monads,但 State monad 通常是一个鼓舞人心的例子。

最后回到游戏,我见过的许多游戏框架都是这样的:一堆东西监听事件,然后建议对游戏状态进行一些小的增量更改并返回不同的,最后框架本身进行适当的 openGL 调用或任何实施这些更改的方法。

于 2013-08-03T12:46:14.183 回答
2

状态只是环境中的一组值。Haskell 让你明确地对待你的环境,所以我们可以称之为Env. 我们创造新的

letThereBeLight :: Env
letThereBeLight = Env { personHealth = 100 }

并修改它们

shootEmUp :: Env -> Env
shootEmUp oldEnv = oldEnv { personHealth = personHealth oldEnv - 30 }

像这样的类型Env -> Env被称为Endo Env端到端应用它们以对状态进行许多更改。

assassinate = shootEmUp . shootEmUp . shootEmUp . shootEmUp

如果你想做的不仅仅是修改一个状态,你需要在你的 state 旁边对其他值进行排序Endo。而不是看起来Env -> Env您开始查看其他数据流的模型并从Env -> (Env, a)那里a构建。这个东西被称为状态单子,因为有一些非常聪明的方法可以很容易地操纵这两个信息流在一起。

于 2013-08-03T20:18:44.517 回答
1

为了支持我的评论,这里有一个改编自http://www.haskellforall.com/2013/05/program-imperatively-using-haskell.html的示例:

-- -----------------------------------------------------------------------------
-- * Our homegrown state monad (use @State@ from the MTL package in production).

-- | @State@ is a function (lets call it "state-updater") which "updates" a
-- state @s@ and returns some associated result @r@.
newtype State s r = State { run :: s -> (r, s) }

-- | This state-updater function is a monad.
instance Monad (State s) where

  -- | Build a state-updater which returns @x@ and don't change the state.
  return x = State $ \st -> (x, st)

  -- | From a state-updater @m@ and a function @f@ which returns a state-updater
  -- we can build a new (lazy) state-updater by performing update actions of this two
  -- state-updaters.
  m >>= f = State $ \st -> let (x, st') = run m st in run (f x) st'

-- | Simply swap the state.
put :: s -> State s ()
put st = State $ const ((), st)

-- | Get the current state as a result of this state-updater.
get :: State r r
get = State $ \st -> (st, st)

-- -----------------------------------------------------------------------------
-- * An example.

-- | Player with its health.
newtype Player = Player { _health :: Int } deriving ( Show )

-- | Game of two players.
data Game = Game { _player1 :: !Player, _player2  :: !Player } deriving ( Show )

-- | Starting from weak and strong players.
initialState :: Game
initialState = Game (Player 10) (Player 20)

-- | First player hit second.
hit12 :: State Game ()
hit12 = do
  g@(Game _ p2@(Player health)) <- get
  put g { _player2 = p2 { _health = health - 1 } }

-- | Second player hit first.
hit21 :: State Game ()
hit21 = do
  g@(Game p1@(Player health) _) <- get
  put g { _player1 = p1 { _health = health - 1 } }

-- | Test it.
test :: ((), Game)
test = run (do { hit12; hit12; hit12; hit21 }) initialState
-- 
-- initialState
-- =>
-- Game {_player1 = Player {_health = 10}, _player2 = Player {_health = 20}}
-- 
-- snd test
-- =>
-- Game {_player1 = Player {_health = 9}, _player2 = Player {_health = 17}}
-- 

镜头允许书写

hit12 = player2.health -= 1

hit21 = player1.health -= 1

状态转换器(无论如何你都应该使用)允许将另一个 monad(如IO)混合到State,但基本上它都是纯的,并且像这样工作:

... 接受世界的状态并返回一个新世界,从而保持纯净。

正如引用的另一条评论。

于 2013-08-03T14:12:23.077 回答