Альтернативы сопрограммам - PullRequest
8 голосов
/ 16 марта 2011

Этот пример был использован в другом вопросе, чтобы проиллюстрировать, как сопрограммы могут использоваться для сценария катсцены в видеоигре:

bob.walkto(jane)
bob.lookat(jane)
bob.say("How are you?")
wait(2)
jane.say("Fine")
...

Каждая функция уступает основному движку, который выполняет анимацию, синхронизацию и т. Д.прежде чем возобновить сопрограмму.Возможной альтернативой сопрограммам будет очередь событий вместо кода, но тогда нужно реализовать логику управления и циклы как события.Есть ли другие альтернативы сопрограмм, которые могут быть использованы для реализации такого рода функциональности?Я видел обратные вызовы, упомянутые в некоторых статьях, но я не уверен, как будет выглядеть код.

Ответы [ 3 ]

3 голосов
/ 16 марта 2011

Вы не упомянули, какой язык используете, поэтому я напишу это на Lua с ориентацией объекта, предоставленной middleclass - https://github.com/kikito/middleclass (заявление об отказе: я - создатель среднего класса)

Еще один вариант - разделить ваши ролики на «списки действий». Это, вероятно, будет лучше сочетаться с вашим кодом, если у вас уже есть игровой цикл, который вызывает метод 'update' в списках объектов.

Как это:

helloJane = CutScene:new(
  WalkAction:new(bob, jane),
  LookAction:new(bob, jane),
  SayAction:new(bob, "How are you?"),
  WaitAction:new(2),
  SayAction:new(jane, "Fine")
)

Действия будут иметь атрибут status с тремя возможными значениями: 'new', 'running', 'finished'. Все «классы действий» будут подклассами Action, которые будут определять методы start и stop, а также инициализировать статус по умолчанию 'new'. Будет также метод по умолчанию update, который выдает ошибку

Action = class('Action')

function Action:initialize() self.status = 'new' end

function Action:stop() self.status = 'finished' end

function Action:start() self.status = 'running' end

function Action:update(dt)
  error('You must re-define update on the subclasses of Action')
end

Подклассы Действия могут улучшить эти методы и реализовать update. Например, вот WaitAction:

WaitAction = class('WaitAction', Action) -- subclass of Action

function WaitAction:start()
  Action.start(self) -- invoke the superclass implementation of start
  self.startTime = os.getTime() -- or whatever you use to get the time
end

function WaitAction:update(dt)
  if os.getTime() - self.startTime >= 2 then
    self:stop() -- use the superclass implementation of stop
  end
end

Единственная недостающая часть реализации - это CutScene. CutScene будет в основном иметь три вещи: * Список действий для выполнения * Ссылка на текущее действие или индекс этого действия в списке действий. * Метод обновления, подобный следующему:

function CutScene:update(dt)
  local currentAction = self:getCurrentAction()
  if currentAction then
    currentAction:update(dt)
    if currentAction.status == 'finished' then
      self:moveToNextAction()
      -- more refinements can be added here, for example detecting the end of actions
    end
  end
end

С этой структурой единственное, что вам нужно, это ваш игровой цикл, вызывающий helloJane:update(dt) на каждой итерации цикла. И вы избавляетесь от необходимости сопрограмм.

3 голосов
/ 16 марта 2011

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

Но я не вижу систему событий как альтернативу. Больше как дополнение, которое вы, скорее всего, захотите иметь в дополнение к системе сценариев на основе сопрограмм.

Пример (в несколько связном C ++):

Вы реализовали поведение, используя сопрограммы по следующим направлениям:

class EnterHouse : public NPCBehavior
{
    EnterHouse(House theHouse) { _theHouse = theHouse; }
    void Begin() { _theHouse.AddNPC(NPC()); }
    void Update()
    {
        NPC().WalkTo(_theHouse.GetDoor().Position());
        NPC().FaceObject(_theHouse.GetDoor());
        NPC().PlayAnimation(NPC().Animations().Get(eAnimKnockOnDoor));
        Sleep(1.0f);       
        NPC().OpenDoor(_theHouse.GetDoor());
    }
    void End() { /* Nothing */ }

    private House _theHouse;
}

Представьте себе, что методы в NPC сами будут создавать объекты NPCBehavior, помещать их в какой-либо стек поведения и возвращаться из вызова после завершения этого поведения.

Вызов Sleep(1.0f) приведет к планировщику сценариев и позволит запускать другие сценарии. WalkTo, FaceObject, PlayAnimation и OpenDoor также вызовут Sleep для получения дохода. Либо на основе известной продолжительности анимации, чтобы периодически просыпаться, чтобы увидеть, что поисковик и система передвижения сделаны пешком или что-то еще.

Что произойдет, если NPC столкнется с ситуацией, с которой ему придется столкнуться на пути к двери? Вам не нужно проверять все эти события везде в вашем коде на основе сопрограмм. Наличие системы событий, дополняющей сопрограммы, сделает это легко:

Мусорная корзина опрокидывается на : Мусорная корзина может транслировать событие всем ближайшим NPC. Объект NPC решает поместить новый объект поведения в свой стек, чтобы пойти и исправить его. Поведение WalkTo где-то уступает в вызове Sleep, но теперь поведение FixTrashcan выполняется из-за события. Когда FixTrashcan завершится, поведение WalkTo проснется от Sleep и никогда не узнает об инциденте с мусорной корзиной. Но он все еще будет на пути к двери, а под ним мы все еще бежим EnterHouse.

Произошел взрыв : Взрыв передает событие точно так же, как мусорная корзина, но на этот раз объект NPC решает сбросить его рабочие режимы и нажать FleeInPanic. Он не вернется к EnterHouse.

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

Если вы еще не видели этой статьи Томаса Тонга о том, как реализовать однопоточные сопрограммы в C / C ++, я очень рекомендую это.

Он использует только малейший бит встроенной сборки (одну инструкцию) для сохранения указателя стека, и код легко переносится на целую кучу платформ. Я работал на Wintel, Xbox 360, PS3 и Wii.

Еще одна приятная вещь в настройке планировщика / сценария состоит в том, что становится ненужным голодать за пределами экрана или удаленные AI-персонажи / объекты сценариев, если вам нужны ресурсы для чего-то еще. Просто соедините его с системой приоритетов в вашем планировщике, и все готово.

2 голосов
/ 16 марта 2011

Обратные вызовы (псевдокод в стиле C #):

bob.walkto(jane, () => {
    bob.lookat(jane), () => {
        bob.say.....
    })
})

Определенно не самый удобный способ.

Другой подход - Фьючерс (также известный как обещания):

futureChain = bob.walkto(jane)
  .whenDone(bob.lookAt(jane))
  .whenDone(...)
  .after(2 seconds, jane.Say("fine"));

futureChain.run();

Один интересный язык, на который стоит обратить внимание: E - он имеет встроенную поддержку фьючерсов с более приятным синтаксисом, чем выше.

...