Script e cinemática sem encadeamento

12

Eu tenho lutado com como implementar scripts no meu mecanismo de jogo. Eu só tenho alguns requisitos: deve ser intuitivo, não quero escrever uma linguagem personalizada, analisador e intérprete e não quero usar o encadeamento. (Tenho certeza de que há uma solução mais simples; não preciso do incômodo de vários segmentos da lógica do jogo.) Aqui está um exemplo de script, em Python (também conhecido como pseudocódigo):

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?")

Essa peça épica de contar histórias apresenta problemas de implementação. Não posso apenas avaliar todo o método de uma só vez, porque walk_toleva tempo de jogo. Se ele retornar imediatamente, Alice começará a caminhar até Bob e (no mesmo quadro) dirá olá (ou seja cumprimentada). Mas sewalk_to é uma chamada de bloqueio que retorna quando ela alcança Bob, meu jogo fica paralisado, porque está bloqueando o mesmo segmento de execução que faria Alice andar.

Eu considerei tornar cada função enfileirar uma ação - alice.walk_to(bob)empurraria um objeto para uma fila, que seria disparada depois que Alice alcançasse Bob, onde quer que ele estivesse. Isso é mais sutilmente quebrado: o iframo é avaliado imediatamente, para que Bob possa cumprimentar Alice, mesmo que esteja de costas para ela.

Como outros mecanismos / pessoas lidam com scripts sem criar threads? Estou começando a procurar em áreas que não sejam desenvolvedores de jogos, como cadeias de animação jQuery, para obter idéias. Parece que deve haver alguns bons padrões para esse tipo de problema.

ojrac
fonte
1
+1 para a pergunta, mas especialmente para "Python (aka pseudocódigo)" :) :)
Ricket
Python é como ter uma superpotência.
Ojrac

Respostas:

3

A maneira como algo como o Panda faz isso é com retornos de chamada. Em vez de bloquear, seria algo como

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)

Ter um retorno de chamada completo permite que você encadeie esses tipos de eventos com a profundidade que desejar.

EDIT: Exemplo de JavaScript, pois possui melhor sintaxe para este estilo:

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
fonte
Funciona o suficiente para +1, só acho errado projetar conteúdo. Estou disposto a fazer um trabalho extra do meu lado, para que os scripts pareçam simples e claros.
ojrac
1
@ojrac: Na verdade, desta maneira é melhor, porque aqui no mesmo script você pode dizer a vários atores para começar a andar ao mesmo tempo.
Bart van Heukelom
Ugh, bom argumento.
Ojrac
Para melhorar a legibilidade, no entanto, a definição de retorno de chamada pode ser aninhada dentro da chamada walk_to () ou colocada depois (para as duas situações: se o idioma suportar), para que o código que é chamado mais tarde seja visto mais tarde na fonte.
Bart van Heukelom
Sim, infelizmente, o Python não é ótimo para esse tipo de sintaxe. Parece muito melhor em JavaScript (veja acima, não é possível usar a formatação de código aqui).
coderanger
13

O termo que você deseja pesquisar aqui é " corotinas " (e geralmente a palavra-chave do idioma ou o nome da função é yield).

As corotinas são componentes do programa que generalizam sub-rotinas para permitir vários pontos de entrada para suspender e retomar a execução em determinados locais.

A implementação dependerá primeiro do seu idioma. Para um jogo, você deseja que a implementação seja o mais leve possível (mais leve que os fios ou até as fibras). A página da Wikipedia (vinculada) possui alguns links para várias implementações específicas de idiomas.

Ouvi dizer que Lua tem suporte interno para corotinas. O mesmo acontece com o GameMonkey.

O UnrealScript implementa isso com o que chama de "estados" e "funções latentes".

Se você usa C #, pode ver esta postagem do blog de Nick Gravelyn.

Além disso, a idéia de "cadeias de animação", embora não seja a mesma coisa, é uma solução viável para o mesmo problema. Nick Gravelyn também tem uma implementação em C # disso .

Andrew Russell
fonte
Boa captura, Tetrad;)
Andrew Russell
Isso é muito bom, mas não tenho certeza de que isso me leva a 100% do caminho. Parece que as corotinas permitem que você ceda ao método de chamada, mas eu quero uma maneira de renderizar a partir de um script Lua, até a pilha até o código C # sem escrever enquanto (walk_to ()! = Done) {yield}.
ojrac
@ojrac: Eu não sei sobre Lua, mas se você estiver usando o método C # de Nick Gravelyn, poderá retornar um delegado (ou um objeto que contenha um) que contenha a condição condicional de seu gerente de script verificar (o código de Nick retorna apenas uma vez que é implicitamente uma condicional). Você pode até fazer com que as próprias funções latentes retornem o delegado, para que você possa escrever: yield return walk_to();em seu script.
Andrew Russell
O rendimento em C # é incrível, mas estou otimizando minha solução para scripts simples e difíceis de bagunçar. É mais fácil explicar retornos de chamada do que rendimento, então aceitarei a outra resposta. Eu +2 se eu pudesse.
ojrac 5/09/10
1
Normalmente, você não precisa explicar a chamada de rendimento - pode envolvê-la na função "walk_to", por exemplo.
Kylotan
3

não enroscar é inteligente.

A maioria dos mecanismos de jogos funciona como uma série de estágios modulares, com coisas na memória que conduzem cada estágio. Para o seu "passeio ao exemplo", você geralmente tem um estágio de IA em que seus personagens caminhados estão em um estado em que não deveriam procurar inimigos para matar, um estágio de animação em que deveriam executar a animação X, um estágio de física (ou estágio de simulação), onde sua posição atual é atualizada etc.

no seu exemplo acima, 'alice' é um ator composto por peças que vivem em muitos desses estágios; portanto, um ator bloqueador. para tomar muitas decisões.

Em vez disso, uma função 'start_walk_to' provavelmente faria algo como:

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

Em seguida, o loop principal executa seu tick ai, seu tick físico, seu tick de animação e tick de cena, e o cutscene atualiza o estado de cada um dos subsistemas do seu mecanismo. O sistema de cena deve definitivamente rastrear o que cada uma de suas cenas está fazendo, e um sistema acionado por corotina para algo linear e determinístico como uma cena de corte pode fazer sentido.

O motivo dessa modularidade é que ela mantém as coisas simples e agradáveis ​​e, para alguns sistemas (como física e IA), você precisa conhecer o estado de tudo ao mesmo tempo para resolver as coisas corretamente e manter o jogo em um estado consistente .

espero que isto ajude!

Aaron Brady
fonte
Gosto do que você procura, mas realmente sinto muito pelo valor de actor.walk_to ([outro ator, uma posição fixa ou até uma função que retorna uma posição]). Meu objetivo é fornecer ferramentas simples e compreensíveis e lidar com toda a complexidade da parte de criação de conteúdo do jogo. Você também me ajudou a perceber que tudo o que realmente quero é uma maneira de tratar cada script como uma máquina de estados finitos.
Ojrac
feliz por poder ajudar! Eu senti que minha resposta foi um pouco fora de tópico :) definitivamente concordo com o valor de uma função actor.walk_to para atingir seus objetivos, estou ansioso para ouvir sobre sua implementação.
Aaron Brady
Parece que eu irei com uma mistura no estilo jQuery de retornos de chamada e funções encadeadas. Veja resposta aceita;)
ojrac