Scala игровое программирование: продвижение позиции объекта в функциональном стиле

Долгое время 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
    }
}

Этот код будет работать очень хорошо, однако он имеет тонны изменчивого состояния, а его функции изобилуют побочными эффектами. Я не могу позволить себе уйти от этого, должен быть лучший способ!

У кого-нибудь есть удивительное, элегантное, функциональное решение этой простой проблемы? Кто-нибудь знает об источнике, где я мог бы больше узнать о решении таких проблем?

Ответы

Ответ 1

Игры часто представляют собой высокопроизводительные дела, и в этом случае вы можете обнаружить, что изменяемое состояние - это именно то, что вам нужно.

Однако в этом случае существует несколько простых решений:

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

То есть вместо того, чтобы ваши классы обновлялись, попросите их вернуть новые копии. (Вы можете видеть, как это может стать дорогостоящим. Для Tetris нет ничего сложного. Для Crysis? Может быть, это не так умно.) Кажется, что это просто подталкивает проблему к одному уровню: теперь вам нужен var для MovingTexture, правильно? Совсем нет:

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

Это приведет к бесконечному потоку обновлений положения в том же направлении. Вы можете делать более сложные вещи для смешивания в пользовательском вводе или еще что-то.

Кроме того, вы можете

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 moved
  def moving(vel: VecXY, ms: Float) = {
   Iterator.iterate(this).(tt => new Transposed(tt.ori, position+ms*vel))
  }
}

То есть, тяжелые вещи никогда не обновляются и имеют более легкие взгляды на них, которые заставляют их выглядеть так, как будто они изменились так, как вы их изменили.

Ответ 2

В этом случае больше подходит этот ответ, чем может быть помещен в пространство одного ответа stackoverflow, но лучший и наиболее полный ответ на такие вопросы - использовать что-то, называемое функционально-реактивным программированием. Основная идея состоит в том, чтобы представлять каждое изменяющееся во времени или интерактивное количество не как изменяемую переменную, а как неизменный поток значений, по одному для каждого кванта времени. Тогда трюк заключается в том, что, хотя каждое значение представляет собой потенциально бесконечный поток значений, потоки лениво вычисляются (чтобы память не занималась до тех пор, пока это не понадобится), а значения потоков не учитываются для квантов времени в (так что предыдущие вычисления могут быть собраны в мусор). Расчет хорошо функциональный и неизменный, но часть вычисления, которое вы "смотрели на", меняется со временем.

Все это довольно сложно, и объединение потоков, подобных этому, является сложным, особенно если вы хотите избежать утечек памяти и все работает в потокобезопасном и эффективном режиме. Существует несколько библиотек Scala, которые реализуют функциональное реактивное программирование, но зрелость еще не очень высока. Самое интересное, вероятно, scala.react, описано здесь.

Ответ 3

Там есть брошюра под названием "Как создавать миры" (авторы "Как разрабатывать программы" ), которая идет в некоторой степени о чисто функциональном подходе к программированию интерактивных приложений.

В принципе, они вводят "мир" (тип данных, который содержит все состояние игры), и некоторые функции, такие как "тик" (типа world → world) и "onkeypress" (типа key * world → мир). Затем функция "рендеринга" берет мир и возвращает сцену, которая затем передается в "реальный" рендерер.

Ответ 4

Здесь пример кода, над которым я работал, использует подход к возврату копии, а не к мутирующему состоянию напрямую. Приятная вещь в этом виде подхода, по крайней мере на стороне сервера, заключается в том, что он позволяет мне легко реализовать семантику типа транзакции. Если что-то пойдет не так во время обновления, для меня тривиально, чтобы все было обновлено в согласованном состоянии.

Ниже приведен код с игрового сервера, над которым я работаю, что делает что-то похожее на то, что вы делаете, для отслеживания объектов, которые перемещаются во временных срезах. Этот подход не столь впечатляющий, как предлагает Дейв Гриффит, но он может быть полезен вам для созерцания.

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() - вы просто передаете параметры, которые изменили, а остальные сохраняют одно и то же значение. Вы можете запрограммировать это вручную, если вы не используете классы case, но вам нужно помнить о том, чтобы обновлять метод копирования всякий раз, когда вы изменяете, какие значения присутствуют в классе.

Что касается ресурсов, то для меня действительно было важно потратить некоторое время на то, чтобы делать что-то в Эрланге, где нет принципиального выбора, кроме как использовать неизменяемое состояние. У меня есть две книги Эрланг, над которыми я работал, и внимательно изучил каждый пример. Это, а также заставляя меня кое-что сделать в Erlang, сделало меня намного более удобным с работой с неизменяемыми данными.

Ответ 5

Эта серия коротких статей помогла мне в качестве новичка, в мышлении Функционально в решении задач программирования. В игре есть Ретро (Pac Man), но программист - нет. http://prog21.dadgum.com/23.html