2011-01-17 25 views
13

programador de Java de mucho tiempo lentamente aprendiendo scala (amando por cierto), y creo que mi mente aún se está envolviendo en el concepto de escribir cosas funcionalmente. En este momento estoy intentando escribir un visualizador simple para algunas texturas en 2D en movimiento. El enfoque imperativo es bastante simple, y estoy seguro que la mayoría de ustedes reconocerán este bloque relativamente ubicua de código (cosas fue cambiado para proteger a los inocentes):programación de juego scala: posición de avance del objeto en un estilo funcional

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

Este código funcionará bien, sin embargo, tiene toneladas de estado mutable y sus funciones están repletas de efectos secundarios. No puedo permitirme salir con esto, ¡debe ser una mejor manera!

¿Alguien tiene una solución increíble, elegante y funcional para este simple problema? ¿Alguien sabe de una fuente donde podría aprender más sobre cómo resolver este tipo de problemas?

Respuesta

11

Los juegos a menudo son asuntos de alto rendimiento, en cuyo caso es posible que ese estado mutable sea justo lo que necesita.

Sin embargo, en este caso, hay un par de soluciones simples:

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

Es decir, en lugar de tener actualizar sus clases mismas, que vuelvan nuevas copias de sí mismos. (Se puede ver cómo esto puede ser costoso rápidamente. Para Tetris, no es gran cosa. Para Crysis? Tal vez no tan inteligente.) Esto parece que simplemente empuja el problema a un nivel: ahora necesita una var para el MovingTexture, ¿verdad? Nada de nada:

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

Esto producirá un flujo interminable de actualizaciones de posición en la misma dirección. Puede hacer cosas más complicadas para mezclar en la entrada del usuario o lo que sea.

Como alternativa, puede

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

Es decir, tenemos cosas de peso pesado no se actualizará y tienen puntos de vista más ligeros de los que los hacen parecer como si hubieran cambiado en la forma que desea que cambian .

+1

El consenso general (entre todas las respuestas) parece ser simplemente crear copias de mis objetos que representan el estado actual. Esto no es exactamente lo que estaba buscando, pero esta respuesta presenta algunos elementos de pensamiento realmente interesantes, como la definición de iteradores que expresan cómo cambian los valores (en lugar de expresar el estado de los valores) ... Esperaba una respuesta que encuentre alguna forma de expresar la posición como algún tipo de función (o iteración) que exprese cómo cambia la variable (como una integral en el cálculo). ¡Gracias por haberme introducido algunas cosas interesantes! – jtb

14

Esta respuesta tiene mucho más de lo que cabe en el espacio de una respuesta de stackoverflow, pero la mejor y más completa respuesta a preguntas como esta es usar algo llamado Functional Reactive Programming. La idea básica es representar cada cantidad variable en el tiempo o interactiva, no como una variable mutable, sino como una corriente inmutable de valores, uno por cada cantidad de tiempo. El truco consiste entonces en que, si bien cada valor está representado por una corriente de valores potencialmente infinita, las transmisiones se calculan de forma diferida (para que la memoria no se retome hasta que sea necesario) y los valores de las corrientes no se miden por cuantos de tiempo en el pasado (para que los cálculos anteriores puedan ser basura recolectada). El cálculo es muy funcional e inmutable, pero la parte del cálculo que está "mirando" cambia con el tiempo.

Esto es bastante complicado, y la combinación de flujos como este es complicado, especialmente si desea evitar fugas de memoria y hacer que todo funcione de manera segura y eficiente. Hay algunas bibliotecas de Scala que implementan la Programación Reactiva Funcional, pero la madurez aún no es muy alta. Lo más interesante es probablemente scala.react, descrito here.

+0

Creo que has dado en el clavo en cuanto a cómo pensar en este problema. Suenas como si supieras tus cosas, ojalá hubieras proporcionado algunos ejemplos de código simple ... pero tal vez tengas razón, este problema es demasiado amplio para expresar una solución concisa. – jtb

+0

Bueno, en realidad no trabajo mucho en el lado interactivo, y nunca he usado FRP con enojo. Realicé las lecturas, conocí la teoría, pero nunca corté el código. –

+0

Genial, bueno, nunca había oído hablar de FRP antes de que lo mencionaras, así que por lo menos ahora estoy más informado y tengo un trabajo interesante para leer. ¡Gracias! – jtb

1

Aquí hay una muestra de algún código en el que he estado trabajando que utiliza el enfoque de devolver una copia en lugar de mutar el estado directamente.Lo bueno de este tipo de enfoque, por lo menos en el lado del servidor, es que me permite implementar fácilmente la semántica del tipo de transacción. Si algo sale mal al hacer una actualización, es trivial para mí tener todo lo que se actualizó en un estado consistente.

El siguiente código es de un servidor de juegos en el que estoy trabajando, que hace algo similar a lo que estás haciendo, es para rastrear objetos que se mueven en segmentos de tiempo. Este enfoque no es tan espectacular como sugiere Dave Griffith, pero puede ser útil para la contemplación.

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 
} 

Una cosa buena acerca de las clases de casos en Scala es que crean el método de copia() para usted-- que acaba de pasar en la que los parámetros han cambiado, y los otros conservan el mismo valor. Puede codificar esto a mano si no está utilizando clases de casos, pero debe recordar actualizar el método de copia siempre que cambie los valores que están presentes en la clase.

En cuanto a los recursos, lo que realmente hizo una diferencia para mí fue pasar algo de tiempo haciendo cosas en Erlang, donde básicamente no hay más remedio que usar el estado inmutable. Tengo dos libros de Erlang que trabajé y estudié cuidadosamente cada ejemplo. Eso, además de forzarme a hacer algunas cosas en Erlang me hizo sentir mucho más cómodo trabajando con datos inmutables.

+0

Curiosamente, Erlang es el siguiente lenguaje que iba a intentar recoger (o tal vez Haskell), aunque mi débil cerebro todavía tiene mucho trabajo que hacer con Scala primero. Me gustan tus ejemplos! Sigo pensando que debería haber una manera más general de representar cómo cambian las variables en lugar de simplemente hacer copias de los objetos en cada iteración. ¿Alguna vez ha experimentado con la posición de representación como una función de la posición en el tiempo en lugar de como una var? – jtb

+0

No tengo. Empecé por mantener todo el estado para un actor dentro de una sola var, compuesto de múltiples val. Por ejemplo, el controlador de posición es un val y el estado del cerebro es otro val. Todos estos se envuelven en una sola variable de estado.Si tengo un método que cambia uno, cambia otro y posiblemente arroja una excepción en el medio, tengo la garantía de que las 3 operaciones funcionan o las 3 fallan y el estado se conserva. La razón por la que no lo he expresado como una función del tiempo es que las posiciones de los objetivos son controladas a veces por una clase cerebral, y algunas veces por un jugador. – Unoti

+0

Mencionaste que hay mucho que envuelve tu cerebro en Scala primero. Para mí, encontré a Scala mucho más difícil que Erlang ... al principio. Otras personas sienten de manera diferente, pero escribí pensamientos sobre Erlang vs. Scala durante el proceso de aprendizaje aquí http://tango11.com/news/scala-complexity-vs-erlang-complexity/ que bien podría interesarte. – Unoti

2

Hay un folleto llamado "Cómo diseñar mundos" (por los autores de "Cómo diseñar programas") que entra en una cierta extensión sobre un enfoque puramente funcional para la programación de aplicaciones interactivas.

Básicamente, introducen un "mundo" (un tipo de datos que contiene todo el estado del juego) y algunas funciones, como "tick" (de tipo mundo -> mundo) y "onkeypress" (de tipo clave * mundo -> mundo). Una función "render" toma un mundo y devuelve una escena, que luego pasa al renderizador "real".

0

Esta serie de artículos breves me ayudó como principiante, a pensar Funcionalmente en la resolución de problemas de programación. El juego es Retro (Pac Man), pero el programador no. http://prog21.dadgum.com/23.html

Cuestiones relacionadas