Manipulando componentes com script e "nativos" em um sistema de entidade baseado em componente

11

Atualmente, estou tentando implementar um sistema de entidades baseado em componentes, em que uma entidade é basicamente apenas um ID e alguns métodos auxiliares que unem vários componentes para formar um objeto de jogo. Alguns objetivos incluem:

  1. Os componentes contêm apenas estado (por exemplo, posição, saúde, quantidade de munição) => a lógica entra em "sistemas", que processam esses componentes e seu estado (por exemplo, PhysicsSystem, RenderSystem, etc.)
  2. Quero implementar componentes e sistemas em C # puro e por meio de scripts (Lua). Basicamente, quero poder definir componentes e sistemas completamente novos diretamente em Lua sem precisar recompilar minha fonte de C #.

Agora, estou procurando idéias sobre como lidar com isso de maneira eficiente e consistente, portanto, não preciso usar sintaxe diferente para acessar componentes C # ou componentes Lua, por exemplo.

Minha abordagem atual seria implementar componentes C # usando propriedades públicas regulares, provavelmente decoradas com alguns atributos que informam o editor sobre valores e coisas padrão. Então eu teria uma classe C # "ScriptComponent", que apenas agrupa uma tabela Lua internamente, com essa tabela sendo criada por um script e mantendo todo o estado desse tipo de componente específico. Eu realmente não quero acessar muito esse estado do lado do C #, pois não saberia em tempo de compilação quais ScriptComponents com quais propriedades estariam disponíveis para mim. Ainda assim, o editor precisará acessar isso, mas uma interface simples como a seguinte deve ser suficiente:

public ScriptComponent : IComponent
{
    public T GetProperty<T>(string propertyName) {..}
    public void SetProperty<T>(string propertyName, T value) {..}
}

Isso simplesmente acessaria a tabela Lua e definiria e recuperaria essas propriedades de Lua, além de poder ser facilmente incluído nos componentes C # puros (mas use as propriedades C # regulares, por meio de reflexão ou algo assim). Isso seria usado apenas no editor, não pelo código normal do jogo; portanto, o desempenho não é tão importante aqui. Seria necessário gerar ou escrever à mão algumas descrições de componentes, documentando quais propriedades um determinado tipo de componente realmente oferece, mas isso não seria um problema enorme e poderia ser suficientemente automatizado.

No entanto, como acessar componentes do lado da Lua? Se eu telefonar para algo como getComponentsFromEntity(ENTITY_ID)provavelmente vou obter vários componentes nativos do C #, incluindo "ScriptComponent", como dados do usuário. Acessar os valores da tabela embrulhada Lua resultaria na chamada do GetProperty<T>(..)método, em vez de acessar as propriedades diretamente, como nos outros componentes do C #.

Talvez escreva um getComponentsFromEntity()método especial apenas para ser chamado de Lua, que retorna todos os componentes C # nativos como dados do usuário, exceto "ScriptComponent", onde retornará a tabela agrupada. Mas haverá outros métodos relacionados a componentes e eu realmente não quero duplicar todos esses métodos para serem chamados do código C # ou do script Lua.

O objetivo final seria lidar com todos os tipos de componentes da mesma forma, sem nenhuma sintaxe especial de caso diferenciando componentes nativos e componentes Lua - especialmente do lado Lua. Por exemplo, eu gostaria de poder escrever um script Lua assim:

entity = getEntity(1);
nativeComponent = getComponent(entity, "SomeNativeComponent")
scriptComponent = getComponent(entity, "SomeScriptComponent")

nativeComponent.NativeProperty = 5
scriptComponent.ScriptedProperty = 3

O script não deve se importar com o tipo de componente que ele realmente possui e eu gostaria de usar os mesmos métodos que usaria no lado do C # para recuperar, adicionar ou remover componentes.

Talvez haja algumas implementações de exemplo de integração de scripts com sistemas de entidades como esse?

Mario
fonte
1
Você está preso em usar Lua? Fiz coisas semelhantes ao script de um aplicativo C # com outras linguagens CLR sendo analisadas em tempo de execução (Boo, no meu caso). Como você já está executando o código de alto nível em um ambiente gerenciado e é tudo IL, é fácil subclassificar as classes existentes do lado "script" e passar instâncias dessas classes de volta ao aplicativo host. No meu caso, eu até armazenava em cache um conjunto de scripts compilados para aumentar a velocidade de carregamento e o reconstruí quando os scripts mudavam.
justinian
Não, eu não estou preso usando Lua. Pessoalmente, eu gostaria muito de usá-lo por causa de sua simplicidade, tamanho pequeno, velocidade e popularidade (portanto, as chances de as pessoas já estarem familiarizadas com isso parecem maiores), mas estou aberto a alternativas.
1213 Mario Mario
Posso perguntar por que você deseja ter recursos de definição igual entre C # e seu ambiente de script? O design normal é a definição de componente em C # e, em seguida, 'assembly de objeto' na linguagem de script. Eu estou querendo saber que problema surgiu que esta é a solução para?
James
1
O objetivo é tornar o sistema de componentes extensível de fora do código-fonte C #. Não apenas definindo valores de propriedades em arquivos XML ou de script, mas definindo componentes totalmente novos com novas propriedades e novos sistemas que os processam. Isso é amplamente inspirado nas descrições de Scott Bilas do sistema de objetos no Dungeon Siege, onde eles tinham componentes C ++ "nativos" e também um "GoSkritComponent", que é apenas um invólucro para um script: scottbilas.com/games/dungeon -siege
Mario
Cuidado para não cair na armadilha de criar uma interface de script ou plug-in tão complexa que se aproxime de uma reescrita da ferramenta original (neste caso, C # ou Visual Studio).
3Dave

Respostas:

6

Um corolário da resposta do FxIII é que você deve projetar tudo (que seria modificável) na linguagem de script primeiro (ou pelo menos uma parte bastante decente) para garantir que sua lógica de integração realmente preveja a modificação. Quando você tiver certeza de que sua integração de script é versátil o suficiente, reescreva os bits necessários em C #.

Eu pessoalmente odeio o dynamicrecurso no C # 4.0 (eu sou um viciado em linguagem estática) - mas este é sem dúvida um cenário em que deve ser usado e é onde realmente brilha. Seus componentes C # seriam simplesmente objetos fortes / expando comportamentais .

public class Villian : ExpandoComponent
{
    public void Sound() { Console.WriteLine("Cackle!"); }
}

//...

dynamic villian = new Villian();
villian.Position = new Vector2(10, 10); // Add a property.
villian.Sound(); // Use a method that was defined in a strong context.

Seus componentes Lua seriam completamente dinâmicos (o mesmo artigo acima deve fornecer uma idéia de como implementar isso). Por exemplo:

// LuaSharp is my weapon of choice.
public class LuaObject : DynamicObject
{
    private LuaTable _table;

    public LuaObject(LuaTable table)
    {
        _table = table;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        var methodName = binder.Name;
        var function = (LuaFunction)_table[methodName];
        if (function == null)
        {
            return base.TryInvokeMember(binder, args, out result);
        }
        else
        {
            var resultArray = function.Call(args);
            if (resultArray == null)
                result = null;
            else if (resultArray.Length == 1)
                result = resultArray[0];
            else
                result = resultArray;
            return true;
        }
    }

    // Other methods for properties etc.
}

Agora, como você está usando, dynamicvocê pode usar objetos Lua como se fossem classes reais em C #.

dynamic cSharpVillian = new Villian();
dynamic luaVillian = new LuaObject(_script["teh", "villian"]);
cSharpVillian.Sound();
luaVillian.Sound(); // This will call into the Lua function.
Jonathan Dickinson
fonte
Eu concordo totalmente com o primeiro parágrafo, muitas vezes faço o meu trabalho dessa maneira. Escrever código que você sabe que pode ter que reimplementá-lo em linguagem de nível inferior, é como tomar um empréstimo SABENDO que você terá que pagar por juros. Os designers de TI geralmente tomam empréstimos: isso é bom quando sabem que o fizeram e mantêm suas dívidas sob controle.
FxIII 30/03/12
2

Prefiro fazer o oposto: escreva tudo usando uma linguagem dinâmica e, em seguida, localize as garrafas (se houver). Depois de encontrar um, você pode reimplementar seletivamente as funcionalidades envolvidas usando um C # ou (melhor) C / C ++.

Digo isso principalmente porque o que você está tentando fazer é confinar as flexibilidades do seu sistema na parte C #; À medida que o sistema se torna complexo, seu "mecanismo" de C # começará a parecer um intérprete dinâmico, provavelmente não o melhor. A questão é: você está disposto a investir a maior parte do seu tempo na criação de um mecanismo C # genérico ou a ampliar seu jogo?

Se o seu alvo for o segundo, como eu acredito, você provavelmente usará seu tempo melhor se concentrando na sua mecânica de jogo, permitindo que um intérprete experiente execute o código dinâmico.

FxIII
fonte
Sim, ainda há esse vago medo de desempenho ruim para alguns dos principais sistemas, se eu fizer tudo através de scripts. Nesse caso, eu teria que mover essa funcionalidade de volta para C # (ou qualquer outra coisa) ... então, de qualquer maneira, eu precisaria de uma boa maneira de interagir com o sistema de componentes do lado do C #. Esse é o principal problema aqui: criar um sistema de componentes que eu possa usar igualmente bem em C # e script.
1313 Mario Mario
Eu pensei que o C # é usado como algum tipo de ambiente "acelerado de script" para coisas como C ++ ou C? De qualquer forma, se você não tiver certeza de qual idioma você terminará, comece a tentar escrever alguma lógica em Lua e C #. Aposto que no final você acabará sendo mais rápido em C # na maioria das vezes, devido aos recursos avançados de ferramentas e depuração de editor.
007 Imi
4
+1 Eu concordo com isso por outro motivo - se você deseja que seu jogo seja extensível, você deve comer sua própria comida de cachorro: você pode integrar um mecanismo de script apenas para descobrir que sua comunidade em potencial de modificação não pode fazer nada com isto.
Jonathan Dickinson