Neste post, falarei sobre como você pode implementar seqüências de ação e cenas em videogames. Este artigo é uma tradução deste artigo e, sobre o mesmo tópico, fiz uma apresentação na Lua em Moscou, portanto, se você preferir assistir ao vídeo, assista aqui . 
O código do artigo é escrito em Lua, mas pode ser facilmente escrito em outros idiomas (com exceção do método que usa corotinas, porque eles não estão em todos os idiomas). 
O artigo mostra como criar um mecanismo para gravar cenas do seguinte formato:
local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end 
Entrada
Sequências de ação são frequentemente encontradas em videogames. Por exemplo, em cenas: o personagem encontra o inimigo, diz alguma coisa para ele, o inimigo responde e assim por diante. A sequência de ações pode ser encontrada na jogabilidade. Dê uma olhada neste gif:

1. A porta se abre
2. O personagem entra na casa
3. A porta se fecha
4. A tela escurece gradualmente
5. O nível muda
6. A tela desaparece intensamente
7. O personagem entra no café
Sequências de ações também podem ser usadas para criar um script do comportamento dos NPCs ou implementar batalhas contra chefes nas quais o chefe executa algumas ações uma após a outra.
O problema
A estrutura de um loop de jogo padrão dificulta a implementação de seqüências de ação. Digamos que temos o seguinte loop de jogo:

 while game:isRunning() do processInput() dt = clock.delta() update(dt) render() end 
Queremos implementar a seguinte cena: o jogador se aproxima do NPC, o NPC diz: “Você conseguiu!”, E depois de uma breve pausa diz: “Obrigado!”. Em um mundo ideal, escreveríamos assim:
 player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you") 
E aqui estamos diante de um problema. Leva algum tempo para concluir as etapas. Algumas ações podem até aguardar a entrada do player (por exemplo, para fechar a caixa de diálogo). Em vez da função de 
delay , você não pode chamar o mesmo 
sleep - parece que o jogo está congelado.
Vamos dar uma olhada em algumas abordagens para resolver o problema.
bool, enum, máquinas de estado
A maneira mais óbvia de implementar uma sequência de ações é armazenar informações sobre o estado atual em bools, linhas ou enumerações. O código será algo como isto:
 function update(dt) if cutsceneState == 'playerGoingToNpc' then player:continueGoingTo(npc) if player:closeTo(npc) then cutsceneState = 'npcSayingYouDidIt' dialogueWindow:show("You did it!") end elseif cutsceneState == 'npcSayingYouDidIt' then if dialogueWindow:wasClosed() then cutsceneState = 'delay' end elseif ... ...  
Essa abordagem leva facilmente ao código de espaguete e a longas cadeias de expressões if-else, portanto, recomendo evitar esse método de resolver o problema.
Lista de ações
As listas de ações são muito semelhantes às máquinas de estado. Uma lista de ações é uma lista de ações que são executadas uma após a outra. No loop do jogo, a função de 
update é chamada para a ação atual, o que nos permite processar a entrada e renderizar o jogo, mesmo que a ação demore muito. Após a conclusão da ação, prosseguimos para a próxima.
Na cena que queremos implementar, precisamos implementar as seguintes ações: GoToAction, DialogueAction e DelayAction.
Para mais exemplos, usarei a biblioteca 
middleclass para OOP em Lua.
Veja como um 
DelayAction implementado:
 
A função 
ActionList:update fica assim:
 function ActionList:update(dt) if not self.isFinished then self.currentAction:update(dt) if self.currentAction.isFinished then self:goToNextAction() if not self.currentAction then self.isFinished = true end end end end 
E, finalmente, a implementação da própria cena:
 function makeCutsceneActionList(player, npc) return ActionList:new { GoToAction:new { entity = player, target = npc }, SayAction:new { entity = npc, text = "You did it!" }, DelayAction:new { delay = 0.5 }, SayAction:new { entity = npc, text = "Thank you" } } end  
Nota : em Lua, uma chamada para 
someFunction({ ... }) pode ser feita assim: 
someFunction{...} . Isso permite que você escreva 
DelayAction:new{ delay = 0.5 } vez de 
DelayAction:new({delay = 0.5}) .
Parece muito melhor. O código mostra claramente a sequência de ações. Se queremos adicionar uma nova ação, podemos fazê-lo facilmente. É muito simples criar classes como 
DelayAction para tornar as cenas de gravação mais convenientes.
Aconselho que você veja a apresentação de Sean Middleditch sobre listas de ações, que fornece exemplos mais complexos.
As listas de ações geralmente são muito úteis. Eu os usei nos meus jogos por um bom tempo e no geral fiquei feliz. Mas essa abordagem também tem desvantagens. Digamos que queremos implementar uma cena um pouco mais complexa:
 local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end 
Para fazer uma simulação if / else, você precisa implementar listas não lineares. Isso pode ser feito usando tags. Algumas ações podem ser marcadas e, em seguida, por alguma condição, em vez de passar para a próxima ação, você pode ir para uma ação com a tag desejada. Funciona, no entanto, não é tão fácil de ler e escrever como a função acima.
As corotinas de Lua tornam esse código uma realidade.
Coroutines
Noções básicas de Corua em Lua
Corutin é uma função que pode ser pausada e depois retomada. As corotinas são executadas no mesmo encadeamento que o programa principal. Nenhum novo encadeamento é criado para a rotina.
Para pausar a 
coroutine.yield , é necessário chamar 
coroutine.yield , para retomar - 
coroutine.resume . Um exemplo simples:
 local function f() print("hello") coroutine.yield() print("world!") end local c = coroutine.create(f) coroutine.resume(c) print("uhh...") coroutine.resume(c) 
Saída do Programa:
 ola
 uhh ...
 mundo
Aqui está como isso funciona. Primeiro, criamos 
coroutine.create usando 
coroutine.create . Após esta chamada, o corutin não inicia. Para que isso aconteça, precisamos executá-lo usando 
coroutine.resume . Em seguida, a função 
f é chamada, que escreve “olá” e faz uma pausa com 
coroutine.yield . É semelhante ao 
return , mas podemos retomar 
f com 
coroutine.resume .
Se você passar argumentos ao chamar 
coroutine.yield , eles se tornarão os valores de retorno da chamada correspondente a 
coroutine.resume no "fluxo principal".
Por exemplo:
 local function f() ... coroutine.yield(42, "some text") ... end ok, num, text = coroutine.resume(c) print(num, text)  
ok é uma variável que nos permite conhecer o status de uma corotina. Se 
ok for 
true , com a rotina está tudo bem, não ocorreram erros no interior. Os valores de retorno a seguir ( 
num , 
text ) são os mesmos argumentos que passamos para 
yield .
Se 
ok for 
false , ocorreu um erro na corotina, por exemplo, a função de 
error foi chamada dentro dela. Nesse caso, o segundo valor de retorno será uma mensagem de erro. Um exemplo de uma rotina na qual ocorre um erro:
 local function f() print(1 + notDefined) end c = coroutine.create(f) ok, msg = coroutine.resume(c) if not ok then print("Coroutine failed!", msg) end 
Conclusão:
 Coroutine falhou!  input: 4: tentativa de executar aritmética em um valor nulo (global 'notDefined')
O status da 
coroutine.status pode ser obtido chamando 
coroutine.status . Corutin pode estar nas seguintes condições:
- "Em execução" - a Coroutine está em execução no momento. coroutine.statusfoi chamado da própria corutin
- "Suspenso" - Corutin foi pausado ou nunca foi iniciado
- "Normal" - corutin está ativo, mas não é executado. Ou seja, corutin lançou outro corutin dentro de si
- “Inoperante” - execução concluída da corotina (ou seja, a função dentro da corotina concluída)
Agora, com a ajuda desse conhecimento, podemos implementar um sistema de sequências de ações e cenas baseadas em corotinas.
Criando cenas usando corutin
A seguir, como será a classe 
Action básica no novo sistema:
 function Action:launch() self:init() while not self.finished do local dt = coroutine.yield() self:update(dt) end self:exit() end 
A abordagem é semelhante às listas de ações: a função de 
update da ação é chamada até que a ação seja concluída. Mas aqui usamos corotinas e produzimos em cada iteração do loop do jogo ( 
Action:launch é chamado de alguma corotina). Em algum momento da 
update loop 
update jogo, retomamos a execução da cena atual assim:
 coroutine.resume(c, dt) 
E, finalmente, criando uma cena:
 function cutscene(player, npc) player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you") end  
Veja como a função de 
delay é implementada:
 function delay(time) action = DelayAction:new { delay = time } action:launch() end 
Criar esses invólucros melhora muito a legibilidade do código da cena. 
DelayAction implementado assim:
 
Essa implementação é idêntica à que usamos nas listas de ações! Vamos dar uma olhada na função 
Action:launch novamente:
 function Action:launch() self:init() while not self.finished do local dt = coroutine.yield()  
O principal aqui é o 
while , que é executado até a ação ser concluída. Parece algo como isto:

Vamos agora olhar para a função 
goTo :
 function Entity:goTo(target) local action = GoToAction:new { entity = self, target = target } action:launch() end function GoToAction:initialize(params) ... end function GoToAction:update(dt) if not self.entity:closeTo(self.target) then ...  
Coroutines vão bem com os eventos. Implemente a classe 
WaitForEventAction :
 function WaitForEventAction:initialize(params) self.finished = false eventManager:subscribe { listener = self, eventType = params.eventType, callback = WaitForEventAction.onEvent } end function WaitForEventAction:onEvent(event) self.finished = true end 
Esta função não precisa do método de 
update . Ele será executado (embora não faça nada ...) até receber um evento com o tipo necessário. Aqui está a aplicação prática desta classe - a implementação da função 
say :
 function Entity:say(text) DialogueWindow:show(text) local action = WaitForEventAction:new { eventType = 'DialogueWindowClosed' } action:launch() end 
Simples e legível. Quando a caixa de diálogo é fechada, ela despacha um evento do tipo 'DialogueWindowClosed`. A ação say termina e a próxima começa a executar.
Usando a corotina, você pode criar facilmente cenas não lineares e árvores de diálogo:
 local answer = girl:say('do_you_love_lua', { 'YES', 'NO' }) if answer == 'YES' then girl:setMood('happy') girl:say('happy_response') else girl:setMood('angry') girl:say('angry_response') end 

Neste exemplo, a função 
say é um pouco mais complexa do que a que mostrei anteriormente. Ele retorna a escolha do jogador no diálogo, no entanto, não é difícil de implementar. Por exemplo, 
WaitForEventAction pode ser usado 
WaitForEventAction , o que capturará o evento PlayerChoiceEvent e retornará a escolha do jogador cujas informações estarão contidas no objeto de evento.
Exemplos ligeiramente mais complexos
Com a corotina, você pode criar facilmente tutoriais e pequenas missões. Por exemplo:
 girl:say("Kill that monster!") waitForEvent('EnemyKilled') girl:setMood('happy') girl:say("You did it! Thank you!") 

Corotinas também podem ser usadas para IA. Por exemplo, você pode criar uma função com a qual o monstro se moverá ao longo de alguma trajetória:
 function followPath(monster, path) local numberOfPoints = path:getNumberOfPoints() local i = 0  

Quando o monstro vê o jogador, podemos simplesmente parar de cumprir a corotina e removê-la. Portanto, um loop infinito ( 
while true ) dentro do 
followPath não é realmente infinito.
Com corutin, você pode executar ações "paralelas". A cena não prosseguirá para a próxima ação até que as duas ações sejam concluídas. Por exemplo, faremos uma cena em que uma menina e um gato vão ao ponto de um amigo em velocidades diferentes. Depois que eles a procuram, o gato diz "miau".
 function cutscene(cat, girl, meetingPoint) local c1 = coroutine.create( function() cat:goTo(meetingPoint) end) local c2 = coroutine.create( function() girl:goTo(meetingPoint) end) c1.resume() c2.resume()  
A parte mais importante aqui é a função 
waitForFinish , que é um invólucro em torno da classe 
WaitForFinishAction , que pode ser implementada da seguinte maneira:
 function WaitForFinishAction:update(dt) if coroutine.status(self.c1) == 'dead' and coroutine.status(self.c2) == 'dead' then self.finished = true else if coroutine.status(self.c1) ~= 'dead' then coroutine.resume(self.c1, dt) end if coroutine.status(self.c2) ~= 'dead' then coroutine.resume(self.c2, dt) end end 
Você pode tornar essa classe mais poderosa se permitir a sincronização do enésimo número de ações.
Você também pode criar uma classe que esperará até que 
uma das corotinas seja concluída, em vez de esperar que 
todas as corotinas concluam a execução. Por exemplo, ele pode ser usado em minijogos de corrida. Dentro da corotina, haverá uma espera para que um dos pilotos chegue à linha de chegada e, em seguida, realize alguma sequência de ações.
Vantagens e desvantagens da corutina
As corotinas são um mecanismo muito útil. Usando-os, você pode escrever cenas e código de jogo fáceis de ler e modificar. Cortes desse tipo podem ser facilmente escritos por modders ou pessoas que não são programadores (por exemplo, designers de jogos ou níveis).
E tudo isso é realizado em um encadeamento, portanto, não há problema com a sincronização ou 
condição de corrida .
A abordagem tem desvantagens. Por exemplo, pode haver problemas com o salvamento. Digamos que seu jogo tenha um longo tutorial implementado com corotina. Durante este tutorial, o jogador não poderá salvar porque Para fazer isso, você precisará salvar o estado atual da corotina (que inclui toda a pilha e os valores das variáveis), para que, após um carregamento adicional do salvamento, você possa continuar o tutorial.
( 
Nota : usando a biblioteca 
PlutoLibrary, as corotinas podem ser serializadas, mas a biblioteca funciona apenas com Lua 5.1)
Esse problema não ocorre com as cenas, pois geralmente em jogos no meio da cena não é permitido.
O problema de um longo tutorial pode ser resolvido se você o dividir em pequenos pedaços. Suponha que um jogador passe pela primeira parte de um tutorial e precise ir para outra sala para continuar o tutorial. Nesse ponto, você pode fazer um ponto de verificação ou dar ao jogador a oportunidade de salvar. No save, escreveremos algo como "o jogador concluiu a parte 1 do tutorial". Em seguida, o jogador passará pela segunda parte do tutorial, para a qual já usaremos outra corotina. E assim por diante ... Ao carregar, apenas começamos a executar a corotina correspondente à parte pela qual o jogador deve passar.
Conclusão
Como você pode ver, existem várias abordagens diferentes para implementar uma sequência de ações e cenas. Parece-me que a abordagem da corotina é muito poderosa e estou feliz em compartilhá-la com os desenvolvedores. Espero que essa solução para o problema facilite sua vida e permita que você faça cenas épicas em seus jogos.