开发者

scala game programming: advancing object position in a functional style

Long time java pro开发者_运维知识库grammer slowly learning scala (loving it by the way), and I think my mind is still wrapping itself around the concept of writing things functionally. Right now I'm attempting to write a simple visualizer for some moving 2d textures. The imperative approach is simple enough, and I'm sure most of you will recognize this relatively ubiquitous block of code (stuff was changed to protect the innocent):

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

This code will work just fine, however it has tons of mutable state and its functions are replete with side effects. I can't let myself get away with this, there must be a better way!

Does anyone have an amazing, elegant, functional solution to this simple problem? Does anyone know of a source where I could learn more about solving these sorts of problems?


There's way more to this answer than can be fit in the space of one stackoverflow response, but the best and most complete answer to questions like this is to use something called Functional Reactive Programming. The basic idea is to represent each time-varying or interactive quantity not as a mutable variable, but rather as an immutable stream of values, one for each time quanta. The trick then is that while each value is a represented by a potentially infinite stream of values, the streams are lazily calculated (so that memory isn't taken up until needed), and stream values aren't looked at for time quanta in the past (so that the previous calculations may be garbage collected). The calculation is nicely functional and immutable, but the part of the calculation you are "looking at" changes with time.

This is all rather intricate, and combining streams like this is tricky, particularly if you wish to avoid memory leaks and have everything work in a thread-safe and efficient manner. There are a few Scala libraries that implement Functional Reactive Programming, but maturity isn't yet very high. The most interesting is probably scala.react, described here .


Games are often high-performance affairs, in which case you may find that mutable state is just the thing you need.

However, in this case, there are a couple of simple solutions:

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)
  ...
}

That is, instead of having your classes update themselves, have them return new copies of themselves. (You can see how this could get expensive quickly. For Tetris, no big deal. For Crysis? Maybe not so smart.) This seems like it just pushes the problem back one level: now you need a var for the MovingTexture, right? Not at all:

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

This will produce an endless stream of position updates in the same direction. You can do more complicated things to mix in user input or whatnot.

Alternatively, you can

class Origin extends Renders {
  // All sorts of expensive stuff goes here
}
class Transposed(val ori: Origin, val position: VecXY) extends Renders with Advances {
  // Wrap TextureAtOrigin with inexpensive methods to make it act like it's moved
  def moving(vel: VecXY, ms: Float) = {
   Iterator.iterate(this).(tt => new Transposed(tt.ori, position+ms*vel))
  }
}

That is, have heavyweight things never be updated and have lighter-weight views of them that make them look as though they've changed in the way that you want them changed.


There's a brochure called "How to design worlds" (by the authors of "How to design programs") which goes into some length about a purely functional approach to programming interactive applications.

Basically, they introduce a "world" (a datatype that contains all the game state), and some functions, such as "tick" (of type world -> world) and "onkeypress" (of type key * world -> world). A "render" function then takes a world and returns a scene, which is then passed to the "real" renderer.


Here's a sample of some code I've been working on that uses the approach of returning a copy rather than mutating state directly. The nice thing about this kind of approach, on the server side at least, is that it enables me to easily implement transaction-type semantics. If something goes wrong while doing an update, it's trivial for me to still have everything that was updated in a consistent state.

The code below is from a game server I'm working on, which does something similar to what you're doing, it's for tracking objects that are moving around in time slices. This approach isn't as spectacular as what Dave Griffith suggests, but it may be of some use to you for contemplation.

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
}

One nice thing about case classes in Scala is that they create the copy() method for you-- you just pass in which parameters have changed, and the others retain the same value. You can code this by hand if you're not using case classes, but you need to remember to update the copy method whenever you change what values are present in the class.

Regarding resources, what really made a difference for me was spending some time doing things in Erlang, where there is basically no choice but to use immutable state. I have two Erlang books I worked through and studied every example carefully. That, plus forcing myself to get some things done in Erlang made me a lot more comfortable with working with immutable data.


This series of short articles helped me as a beginner, in thinking Functionally in solving programming problems. The game is Retro (Pac Man), but the programmer is not. http://prog21.dadgum.com/23.html

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜