Сценарии и кинематика без потоков

12

Я боролся с тем, как реализовать скрипты в моем игровом движке. У меня есть только несколько требований: это должно быть интуитивно понятно, я не хочу писать собственный язык, парсер и интерпретатор, и я не хочу использовать многопоточность. (Я уверен, что есть более простое решение; мне не нужны хлопоты из нескольких игровых логических потоков.) Вот пример сценария на Python (он же псевдокод):

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    alice.walk_to(bob)
    if bob.can_see(alice):
        bob.say("Hello again!")
    else:
        alice.say("Excuse me, Bob?")

Этот эпический кусок повествования создает проблемы с реализацией. Я не могу просто оценить весь метод сразу, потому что walk_toзанимает время игры. Если он сразу же вернется, Алиса начнет подходить к Бобу и (в том же кадре) поздороваться (или будет встречена). Но если walk_toэто блокирующий вызов, который возвращается, когда она достигает Боба, то моя игра застревает, потому что она блокирует тот же поток выполнения, который заставил бы Алису ходить.

Я подумал о том, чтобы заставить каждую функцию ставить в очередь действие - помещать alice.walk_to(bob)объект в очередь, которая будет отбрасываться после того, как Алиса дойдет до Боба, где бы он ни находился. Это более тонко нарушено: ifветвь оценивается немедленно, поэтому Боб может приветствовать Алису, даже если его спина повернута к ней.

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

ojrac
источник
1
+1 за вопрос, но особенно за "Python (он же псевдокод)" :)
Ricket
Питон похож на сверхдержаву.
ojrac

Ответы:

3

То, как что-то вроде Panda делает это с помощью обратных вызовов. Вместо блокировки это было бы что-то вроде

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    def cb():
        if bob.can_see(alice):
            bob.say("Hello again!")
        else:
            alice.say("Excuse me, Bob?")
    alice.walk_to(bob, cb)

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

РЕДАКТИРОВАТЬ: пример JavaScript, так как это имеет лучший синтаксис для этого стиля:

function dramatic_scene(actors) {
    var alice = actors.alice;
    var bob = actors.bob;
    alice.walk_to(bob, function() {
        if(bob.can_see(alice)) {
            bob.say('Hello again!');
        } else {
            alice.say('Excuse me, Bob?');
        }
     });
}
coderanger
источник
Это работает достаточно для +1, я просто думаю, что это неправильно для разработки контента. Я готов проделать дополнительную работу, чтобы сценарии выглядели простыми и понятными.
ojrac
1
@ojrac: На самом деле этот путь лучше, потому что здесь в одном и том же сценарии вы можете сказать нескольким актерам начинать гулять одновременно.
Барт ван Хейкелом
Тьфу, хорошая мысль.
ojrac
Однако для улучшения читабельности определение обратного вызова может быть вложено в вызов walk_to () или помещено после него (для обоих вариантов: если язык поддерживает), чтобы код, вызываемый позже, был виден позже в источнике.
Барт ван Хейкелом
Да, к сожалению, Python не очень хорош для такого синтаксиса. В JavaScript это выглядит намного лучше (см. Выше, здесь нельзя использовать форматирование кода).
Coderanger
13

Здесь вы хотите найти термин « сопрограммы » (и обычно это ключевое слово языка или имя функции yield).

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

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

Я слышал, что Lua имеет встроенную поддержку сопрограмм. Как и GameMonkey.

UnrealScript реализует это с помощью так называемых «состояний» и «скрытых функций».

Если вы используете C #, вы можете посмотреть это сообщение в блоге Ника Грэйвлина.

Кроме того, идея «цепочек анимации», хотя и не одно и то же, является реальным решением той же проблемы. Ник Грэйвелин также имеет реализацию C # этого .

Эндрю Рассел
источник
Хороший улов, Тетрад;)
Эндрю Рассел
Это действительно хорошо, но я не уверен, что мне это удастся на 100%. Похоже, что сопрограммы позволяют вам уступать вызывающему методу, но я хочу получить способ от скрипта Lua, вплоть до стека до кода C # без записи while (walk_to ()! = Done) {yield}.
ojrac
@ojrac: я не знаю о Lua, но если вы используете метод Ника Грэвелина C #, вы можете вернуть делегат (или объект, содержащий его), который содержит условие для проверки вашим диспетчером скриптов (код Ника просто возвращает время что неявно является условным). Вы могли бы даже заставить скрытые функции сами возвращать делегат, чтобы вы могли написать: yield return walk_to();в своем скрипте.
Эндрю Рассел
Урожай в C # - это круто, но я оптимизирую свое решение для простых, трудоемких сценариев. Мне будет легче объяснять обратные вызовы, чем yield, поэтому я приму другой ответ. Я бы +2, если бы мог.
ojrac
1
Обычно вам не нужно объяснять вызов yield - вы можете обернуть это, например, в функцию "walk_to".
Kylotan
3

не собираться с резьбой - это умно.

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

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

Вместо этого функция 'start_walk_to', вероятно, будет делать что-то вроде:

def start_cutscene_walk_to(actor,target):
    actor.ai.setbrain(cutscene_brain)
    actor.physics.nocoll = 1
    actor.anims.force_anim('walk')
    # etc.

Затем ваш основной цикл запускает свой тик ai, тик физики, тик анимации и тик ролика, и ролик обновляет состояние каждой подсистемы в вашем движке. Система катсцены должна определенно отслеживать то, что делает каждый из ее катсцены, и система, управляемая сопрограммой, для чего-то линейного и детерминированного, такого как кат-сцена, может иметь смысл.

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

надеюсь это поможет!

Аарон Брейди
источник
Мне нравится то, что вы собираетесь, но на самом деле я сильно отношусь к значению actor.walk_to ([другой актер, фиксированная позиция или даже функция, которая возвращает позицию]). Моя цель - предоставить простые, понятные инструменты и справиться со всеми сложностями, не связанными с созданием контента в игре. Вы также помогли мне понять, что все, что я действительно хочу, - это способ рассматривать каждый сценарий как конечный автомат.
ojrac
Рад, что смог помочь! Я чувствовал, что мой ответ был немного не по теме :) определенно согласен со значением функции actor.walk_to для достижения ваших целей, я с нетерпением жду возможности услышать о вашей реализации.
Аарон Брэди,
Похоже, я пойду с набором обратных вызовов и связанных функций в стиле jQuery. Смотрите принятый ответ;)
ojrac