在使用纯函数式编程构建的游戏中,一个玩家怎么会对另一个玩家造成伤害(改变状态)呢?

时间:2013-08-03 10:43:17

标签: haskell functional-programming

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

如何用纯粹的功能性语言实现这一目标?

3 个答案:

答案 0 :(得分: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来解决这个问题。这听起来很可怕,但它正是我们上面所做的,只是更多的语法抽象。事实上,这也是IO对GHC的影响。我不知道你是否了解过Monads,但State monad通常是激励人心的例子。

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

答案 1 :(得分: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

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

答案 2 :(得分: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}}
-- 

Lenses允许写

hit12 = player2.health -= 1

hit21 = player1.health -= 1

State transformer(无论如何你应该使用)允许将另一个monad(如IO)与State混合,但基本上它都是纯粹的并且工作方式如下:

  

......接受世界的状态并返回一个新的世界   保持纯洁。

引用另一条评论。