программирование игр scala: улучшение положения объекта в функциональном стиле - PullRequest
13 голосов
/ 17 января 2011

Долгое время программист на Java медленно изучал scala (кстати, любил ее), и я думаю, что мой разум все еще сосредоточен на концепции функционального написания вещей.Сейчас я пытаюсь написать простой визуализатор для некоторых движущихся 2d текстур.Императивный подход достаточно прост, и я уверен, что большинство из вас узнает этот относительно вездесущий блок кода (материал был изменен для защиты невинных):у этого есть тонны изменчивого состояния, и его функции полны побочных эффектов.Я не могу позволить себе сойти с рук, лучше будет !

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

Ответы [ 5 ]

14 голосов
/ 17 января 2011

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

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

11 голосов
/ 17 января 2011

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

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

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

То есть вместо того, чтобы ваши классы обновлялись самостоятельно, пусть они возвращают новые копии самих себя.(Вы можете видеть, как это может быстро дорого обойтись. Для Тетриса нет ничего сложного. Для Crysis? Может быть, не так умно.) Кажется, что это просто отодвигает проблему на один уровень назад: теперь вам нужна переменная для 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's moved
  def moving(vel: VecXY, ms: Float) = {
   Iterator.iterate(this).(tt => new Transposed(tt.ori, position+ms*vel))
  }
}

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

2 голосов
/ 26 января 2011

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

По сути, они вводят "мир" (тип данных, который содержит все игровое состояние) и некоторые функции, такие как "галочка" (типа world -> world) и "onkeypress" (типа key * world - > Мир). Функция «рендеринга» затем берет мир и возвращает сцену, которая затем передается «реальному» рендереру.

1 голос
/ 18 января 2011

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

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

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
}

Одна приятная вещь о case-классах в Scala заключается в том, что они создают для вас метод copy () - вы просто передаете, какие параметры были изменены, а остальные сохраняют то же значение. Вы можете кодировать это вручную, если вы не используете классы case, но вам нужно помнить, чтобы обновлять метод copy всякий раз, когда вы меняете значения, присутствующие в классе.

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

0 голосов
/ 19 августа 2015

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...