Padrão para executar ações do jogo

11

Existe um padrão geralmente aceito para executar várias ações em um jogo? Uma maneira de um jogador executar ações e também que uma IA possa executar ações, como movimento, ataque, autodestruição etc.

Atualmente, tenho um BaseAction abstrato que usa genéricos do .NET para especificar os diferentes objetos retornados pelas várias ações. Tudo isso é implementado em um padrão semelhante ao Comando, onde cada ação é responsável por si mesma e faz tudo o que precisa.

Meu raciocínio para ser abstrato é para que eu possa ter um único ActionHandler, e a IA pode simplesmente enfileirar ações diferentes implementando a baseAction. E a razão pela qual é genérico é que as diferentes ações podem retornar informações de resultados relevantes para a ação (como ações diferentes podem ter resultados totalmente diferentes no jogo), juntamente com algumas implementações comuns antes da Ação e pós-Ação.

Então ... existe uma maneira mais aceita de fazer isso ou soa bem?

Arkiliknam
fonte
Parece bom, a pergunta é o que você quer dizer com fila? A maioria dos jogos tem uma resposta muito rápida? "A IA pode enfileirar ações diferentes"
AturSams 25/09/12
Bom ponto. Não há fila. Ele só precisa saber se está ocupado e, se não, executar a ação.
Arkiliknam

Respostas:

18

Eu não acho que há uma maneira aceitável de implementar este conceito, mas eu realmente gostaria de compartilhar como eu geralmente lidar com isso em meus jogos. É um pouco de uma combinação do padrão de design Command e do padrão Composite .

Eu tenho uma classe base abstrata para ações que nada mais é do que um invólucro em torno de um Updatemétodo chamado cada quadro e um Finishedsinalizador para indicar quando a ação terminou a execução.

abstract class Action
{
    abstract void Update(float elapsed);
    bool Finished;
}

Também uso o padrão de design composto para criar um tipo de ação capaz de hospedar e executar outras ações. Esta também é uma classe abstrata. Tudo se resume a:

abstract class CompositeAction : Action
{
    void Add(Action action) { Actions.Add(action); }
    List<Action> Actions;
}

Então, eu tenho duas implementações de ações compostas, uma para execução paralela e outra para execução seqüencial . Mas o mais interessante é que, como paralelo e sequência são ações, elas podem ser combinadas para criar fluxos de execução mais complexos.

class Parallel : CompositeAction
{
    override void Update(float elapsed) 
    {
        Actions.ForEach(a=> a.Update(elapsed));
        Actions.RemoveAll(a => a.Finished);
        Finished = Actions.Count == 0;
    }
}

E aquele que governa ações seqüenciais.

class Sequence : CompositeAction
{
    override void Update(float elapsed) 
    {
        if (Actions.Count > 0) 
        {
            Actions[0].Update(elapsed);
            if (Actions[0].Finished)
                Actions.RemoveAt(0);
        }
        Finished = Actions.Count == 0;
    }
 }

Com isso, é simplesmente uma questão de criar implementações de ações concretas e usar as ações Parallele Sequencepara controlar o fluxo de execução. Vou terminar com um exemplo:

// Create a parallel action to work as an action manager
Parallel actionManager = new Parallel();

// Send character1 to destination
Sequence actionGroup1 = new Sequence();
actionGroup1.Add(new MoveAction(character1, destination));
actionGroup1.Add(new TalkAction(character1, "Arrived at destination!"));
actionManager.Add(actionGroup1);

// Make character2 use a potion on himself
Sequence actionGroup2 = new Sequence();
actionGroup2.Add(new RemoveItemAction(character2, ItemType.Potion));
actionGroup2.Add(new SetHealthAction(character2, character2.MaxHealth));
actionGroup2.Add(new TalkAction(character2, "I feel better now!"));
actionManager.Add(actionGroup2);

// Every frame update the action manager
actionManager.Update(elapsed);

Eu usei com sucesso esse sistema para conduzir toda a jogabilidade em uma aventura gráfica antes, mas provavelmente deve funcionar para praticamente qualquer coisa. Também foi simples o suficiente para adicionar outros tipos de ações compostas, que foram usadas para criar loops e condicionais de execução.

David Gouveia
fonte
Parece uma solução muito boa. Por curiosidade, como você deixa a interface do usuário saber o que desenhar? Seus objetos de jogo (como personagens) contêm um estado que é usado para identificar o que aconteceu para fins de renderização ou é a própria ação que faz isso?
Arkiliknam
1
Geralmente, minhas ações alteram apenas o estado das entidades e quaisquer alterações na saída renderizada ocorrem como consequência dessa alteração de estado, não por meio das próprias ações. Por exemplo, com um renderizador de modo imediato, não há etapa extra necessária, pois o Drawmétodo já foi criado sobre o estado da entidade e as alterações são automáticas. Em um renderizador de modo retido como o Flash, você pode usar o padrão observável para fazer com que as alterações nas suas entidades se propaguem aos objetos de exibição ou faça a conexão manualmente dentro da própria entidade.
David Gouveia
1
Na primeira situação, digamos que sua Characterclasse tenha uma Positionpropriedade e um Drawmétodo que leia qual é o valor atual Positione desenhe a imagem correta. Nessa situação, você só precisa atualizar o valor de Positionque o resultado será visto automaticamente na tela.
David Gouveia
1
A segunda situação é quando você Charactertem uma Positionpropriedade, mas delega a renderização para algum tipo de Spriteobjeto que está sendo renderizado automaticamente por um gráfico de cena ou algo assim. Nesta situação, você deve garantir que tanto a posição do personagem quanto a do sprite estejam sempre sincronizadas, o que envolve um pouco mais de trabalho. Ainda assim, em ambos os casos, não vejo por que o gerente de ação deveria ter algo a ver com isso. :)
David Gouveia
1
Ambos os métodos têm vantagens e desvantagens. Eu segui o segundo método para o meu jogo 2D e às vezes me arrependo, porque é significativamente mais complicado manter tudo sincronizado. Mas também há vantagens, por exemplo, ao tentar detectar em qual entidade foi clicada ou o que deve ou não ser desenhado, porque tudo o que será renderizado está contido na mesma estrutura de dados, em vez de espalhado entre N tipos de entidade.
David Gouveia