斯卡拉游戏编程:以功能风格推进对象位置

时间:2011-01-17 01:58:51

标签: scala functional-programming

很久以来java程序员慢慢学习scala(顺便爱上它),我认为我的思想仍然围绕着功能性写东西的概念。现在我正在尝试为一些移动的2D纹理编写一个简单的可视化器。命令式方法很简单,我相信大多数人都会认识到这个相对无处不在的代码块(改变了东西以保护无辜者):

class MovingTexture(var position, var velocity) extends Renders with Advances {
    def render : Unit = {...}
    def advance(milliseconds : Float) : Unit = {
        position = position + velocity * milliseconds
    }
}

这段代码可以正常工作,但它有大量的可变状态,其功能充满了副作用。我不能让自己侥幸逃脱,必须更好的方式!

有没有人对这个简单的问题有一个惊人的,优雅的,功能性的解决方案?有没有人知道我可以学到更多关于解决这些问题的来源?

5 个答案:

答案 0 :(得分:14)

这个答案的方式多于一个stackoverflow响应的空间,但对这类问题的最佳和最完整的答案是使用一种称为功能反应式编程的东西。基本思想是将每个时变或交互量表示为不是可变变量,而是表示不可变的值流,每个时间量一个。然后诀窍是,虽然每个值都是由潜在的无限值流表示,但是流被懒惰地计算(以便在需要之前不占用内存),并且不会在时间量子中查看流值。过去(以便之前的计算可能是垃圾收集)。计算功能很好且不可变,但您“看”的计算部分会随着时间而变化。

这一切都非常复杂,像这样组合流是很棘手的,特别是如果你想避免内存泄漏并让一切都以线程安全和有效的方式工作。有一些Scala库实现了功能反应式编程,但成熟度还不是很高。最有趣的可能是scala.react,描述为here

答案 1 :(得分:11)

游戏通常是高性能事务,在这种情况下,您可能会发现可变状态就是您需要的事物。

但是,在这种情况下,有几个简单的解决方案:

case class MovingTexture(position: VecXY, velocity: VecXY) extends Renders with Advances {
  def advance(ms: Float) = copy(position = position + ms*velocity
  def accelerate(acc: Float, ms: Float) = copy(velocity = velocity + ms*acc)
  ...
}

也就是说,让他们自己更新新的副本,而不是让你的类自我更新。 (你可以看到这可能会很快变得昂贵。对于俄罗斯方块,没什么大不了的。对于孤岛危机?也许不那么聪明。)这似乎只是将问题推回到一个层面:现在你需要{{1}的变量}, 对?完全没有:

MovingTexture

这将在同一方向产生无穷无尽的位置更新。你可以做更复杂的事情来混合用户输入或诸如此类的东西。

或者,你可以

Iterator.iterate(MovingTexture(home, defaultSpeed))(_.advance(defaultStep))

也就是说,重量级的东西永远不会被更新,并且它们的重量较轻,这使得它们看起来好像已经改变了你希望它们改变的方式。

答案 2 :(得分:2)

有一本名为“如何设计世界”的小册子(由“如何设计程序”的作者),这篇小册子对于编写交互式应用程序的纯功能方法有一定的了解。

基本上,他们引入了一个“世界”(包含所有游戏状态的数据类型),以及一些函数,例如“tick”(类型为world - > world)和“onkeypress”(类型为key * world) - >世界)。然后“渲染”函数接受一个世界并返回一个场景,然后将其传递给“真实”渲染器。

答案 3 :(得分:1)

以下是我一直在研究的一些代码示例,它使用的方法是返回副本而不是直接改变状态。至少在服务器端,这种方法的好处在于它使我能够轻松实现事务类型语义。如果在进行更新时出现问题,我仍然可以在一致的状态下更新所有内容。

下面的代码来自我正在处理的游戏服务器,它与您正在做的事情类似,它用于跟踪在时间片中移动的对象。这种方法并不像戴夫格里菲斯所暗示的那样引人注目,但它对你的沉思可能有一些用处。

case class PosController(
    pos: Vector3 = Vector3.zero,
    maxSpeed: Int = 90,
    velocity: Vector3 = Vector3.zero,
    target: Vector3 = Vector3.zero
) {
    def moving = !velocity.isZero

    def update(elapsed: Double) = {
        if (!moving)
            this
        else {
            val proposedMove = velocity * elapsed
            // If we're about to overshoot, then stop at the exact position.
            if (proposedMove.mag2 > pos.dist2(target))
                copy(velocity = Vector3.zero, pos = target)
            else
                copy(pos = pos + proposedMove)
        }
    }

    def setTarget(p: Vector3) = {
        if (p == pos)
            this
        else {
            // For now, go immediately to max velocity in the correct direction.
            val direction = (p - pos).norm
            val newVel = direction * maxSpeed
            copy(velocity = direction * maxSpeed, target = p)
        }
    }

    def setTargetRange(p: Vector3, range: Double) = {
        val delta = p - pos
        // Already in range?
        if (delta.mag2 < range * range)
            this
        else {
            // We're not in range. Select a spot on a line between them and us, at max range.
            val d = delta.norm * range
            setTarget(p - d)
        }
    }

    def eta = if (!moving) 0.0 else pos.dist(target) / maxSpeed
}

Scala中的案例类的一个好处是它们为您创建了copy()方法 - 您只需传入哪些参数已更改,其他参数保留相同的值。如果您不使用案例类,则可以手动编写代码,但每当更改类中存在的值时,您都需要记住更新复制方法。

关于资源,对我来说真正有用的是花一些时间在Erlang做事,在那里基本上别无选择,只能使用不可变状态。我有两本Erlang书籍,我仔细研究过每一个例子。这一点,加上迫使我自己在Erlang中完成一些事情,这让我对使用不可变数据更加舒服。

答案 4 :(得分:0)

这一系列短篇文章帮助我作为初学者,在功能上解决编程问题。这场比赛是复古(吃豆人),但程序员不是。 http://prog21.dadgum.com/23.html