Script amigável ao usar um ECS?


8

No momento, estou criando um pequeno projeto de hobby para voltar ao desenvolvimento de jogos e decidi estruturar minhas entidades usando um ECS (Entity Component System). Esta implementação de um ECS está estruturada da seguinte forma:

  • Entidade : no meu caso, é um intidentificador exclusivo usado como chave para uma lista de componentes.
  • Componente : Armazena apenas dados, por exemplo, o Positioncomponente possui um xe ycoordenar, e o Movementcomponente contém um speede directionvariável.
  • Sistema : componentes canetas, por exemplo, que leva os Positione Movementcomponentes e adiciona o speede directionpara a posição de xe ycoordenadas.

Isso funciona bem, mas agora desejo implementar scripts nos meus jogos, na forma de uma linguagem de script. Em projetos anteriores, usei uma implementação OOP de objetos de jogo, o que significava que o script era bastante direto. Por exemplo, um script simples pode ser algo como isto:

function start()
    local future = entity:moveTo(pos1)
    wait(future)

    local response = entity:showDialog(dialog1)
    if wait(response) == 1 then
        local itemStack = entity:getInventory():removeItemByName("apple", 1)
        world:getPlayer():getInventory():addItemStack(itemStack)
    else
        entity:setBehavior(world:getPlayer(), BEHAVIOR_HOSTILE)
    end
end

No entanto, ao usar um ECS, a própria entidade não possui funções como , moveToou seja getInventory, o script acima, escrito no estilo ECS, seria algo como isto:

 function start()
    local movement = world:getComponent(MOVEMENT, entity)
    movement:moveTo(pos1)

    local position = world:getComponent(POSITION, entity)
    local future = Future:untilEquals(position.pos, pos1)
    wait(future)

    local dialogComp = world:getComponent(DIALOG, entity)
    local response = dialogComp:showDialog(dialog1)

    if wait(response) == 1 then
        local entityInventory = world:getComponent(INVENTORY, entity)
        local playerInventory = world:getComponent(INVENTORY, world:getPlayer())
        local itemStack = entityInventory:removeItemByName("apple", 1)
        playerInventory:addItemStack(itemStack)
    else
        local entityBehavior = world:getComponent(BEHAVIOR, entity)
        local playerBehavior = world:getComponent(BEHAVIOR, world:getPlayer())
        entityBehavior:set(playerBehavior, BEHAVIOR_HOSTILE)
    end
end

Isso é muito mais detalhado se comparado à versão OOP, o que não é desejável quando o script é voltado principalmente para não programadores (jogadores do jogo).

Uma solução seria ter algum tipo de objeto wrapper que encapsule Entitye forneça funções como moveTodiretamente e lide com o resto internamente, embora essa solução pareça subótima, pois é preciso muito trabalho para cobrir todos os componentes e todos os Quando um novo componente é adicionado, você precisa alterar o objeto wrapper com novas funções.

Para todos os desenvolvedores de jogos que já implementaram scripts em um ECS antes - como você fez isso? O foco principal aqui é a usabilidade do usuário final, com o menor custo possível de "manutenção" (de preferência, você não precisa alterá-lo toda vez que adicionar um componente).


Vou escrever isso como um comentário, porque sou um pouco vago sobre sua implementação exata (presumo C ++). Você não pode utilizar modelos em algum lugar aqui? Para aplicar o componente X ao componente Y? Imagino que os componentes precisariam substituir o método base "apply" e especializá-lo para os tipos de componentes que podem ser aplicados a ele. Confiar na SFINAE garantiria que funcionasse quando deveria. Ou pode ser especializado na System(s) classe (s) para permitir que os componentes permaneçam estruturas de dados.
NeomerArcana

Por que não expor o moveTométodo como parte do sistema subjacente no seu caso de uso, por exemplo, MovementSystem? Dessa forma, não somente então você poderá usá-lo nos scripts que escrever, mas também poderá usá-lo como parte do código C ++, sempre que precisar. Portanto, sim, você terá que expor novos métodos à medida que novos sistemas são adicionados, mas isso é de se esperar, pois seu comportamento totalmente novo é introduzido de qualquer maneira.
Naros 23/07/19

Não tive a chance de fazer isso, mas seria possível adicionar "atalhos" apenas para as operações mais executadas?
Vaillancourt

Respostas:


1

Você pode criar um sistema ScriptExecutionSystem que opera em todas as entidades com um componente Script. Ele obtém todos os componentes da entidade que podem ser úteis para expor ao sistema de script e os passa para a função com script.

Outra abordagem seria fazer com que seus usuários também adotassem o ECS e permitissem que eles definissem seus próprios componentes e implementassem seus próprios sistemas usando a linguagem de script.


0

A ECS tem seus prós e contras. O script fácil de usar não é um dos seus profissionais.

O problema que o ECS resolve é a capacidade de ter um grande número de coisas semelhantes em seu jogo ao mesmo tempo, mantendo o desempenho. Mas esta solução tem um custo - o custo de uma arquitetura fácil de usar. Não é a melhor arquitetura para todos os jogos.

Por exemplo, o ECS teria sido uma ótima opção para os Space Invaders , mas não tanto para o PacMan .

Portanto, não é exatamente a resposta que você estava procurando, mas é possível que o ECS não seja a ferramenta certa para o seu trabalho.

Se você adicionar um invólucro, observe o custo adicional. Se você acabar removendo o aumento de desempenho do ECS em seu invólucro, terá o pior dos dois mundos.


Mas, para responder diretamente à sua pergunta - "Para todos os desenvolvedores de jogos que já implementaram scripts em um ECS antes - como você fez isso?"

Praticamente exatamente como você está fazendo, sem um invólucro. As entidades têm apenas um identificador. Os componentes têm apenas dados. Os sistemas não têm nada além de lógica. Sistemas que aceitam entidades com os componentes necessários são executados. Adicione sistemas, entidades e componentes livremente.

Uma vez eu usei uma estrutura com um quarto aspecto, chamado quadro negro. Era basicamente uma maneira de os sistemas se comunicarem. Criou mais problemas do que resolveu.


Relacionado: Devo implementar o Entity Component System em todos os meus projetos?


0

Com o ECS, você pode dividir em uma única responsabilidade; portanto, qualquer entidade que se mova desejará dois componentes de dados: um MoveComponent e um MoveSpeedComponent.

using System;
using Unity.Entities;

[Serializable]
public struct MoveForward : IComponentData{}
////////////////////////////////////////////
using System;
using Unity.Entities;

[Serializable]
public struct MoveSpeed : IComponentData
{
public float Value;
}
///////////////////////////////////////////

agora, na sua conversão, você adiciona esses componentes às suas entidades

public class MoveForwardConversion : MonoBehaviour, IConvertGameObjectToEntity
{
public float speed = 50f;

public void Convert(Entity entity, EntityManager manager,       GameObjectConversionSystem conversionSystem)
{
    manager.AddComponent(entity, typeof(MoveForward));

    MoveSpeed moveSpeed = new MoveSpeed { Value = speed };
    manager.AddComponentData(entity, moveSpeed);     
}

Agora que temos a conversão e os dados que podemos mudar para o sistema, removi o sistema de entrada para facilitar a leitura, mas se você quiser saber mais sobre o sistema de entrada, terei tudo isso listado no meu artigo na próxima semana sobre o Unity Connect.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;

namespace Unity.Transforms
{
public class MoveForwardSystem : JobComponentSystem
{
    [BurstCompile]
    [RequireComponentTag(typeof(MoveForward))]
    struct MoveForwardRotation : IJobForEach<Translation, MoveSpeed>
    {
        public float dt;

        public void Execute(ref Translation pos, [ReadOnly] ref MoveSpeed speed)
        {
            pos.Value = pos.Value + (dt * speed.Value);            
           // pos.Value.z += playerInput.Horizontal;
        }
    }
}

observe que a classe acima está usando o Unity.Mathmatics. Isso é ótimo para poder usar diferentes funções matemáticas com as quais você está acostumado a trabalhar nos sistemas normais. Com tudo isso alinhado, agora você pode trabalhar no comportamento das entidades - novamente removi a entrada aqui, mas tudo isso é muito melhor explicado no artigo.

using Unity.Entities;
using UnityEngine;

public class EntityBehaviour : MonoBehaviour, IConvertGameObjectToEntity
{
public float speed = 22f;

void Update()
{
    Vector3 movement = transform.forward * speed * Time.deltaTime;
}
public void Convert(Entity entity, EntityManager manager, GameObjectConversionSystem conversionSystem)
{   
    //set speed of the entity
    MoveSpeed moveSpeed = new MoveSpeed { Value = speed };
    //take horizontal inputs for entites
    //PlayerInput horizontalinput = new PlayerInput { Horizontal = Input.GetAxis("Horizontal") };

    //add move component to entity
    manager.AddComponent(entity, typeof(MoveForward));
    //add component data  
    manager.AddComponentData(entity, moveSpeed);
   // manager.AddComponentData(entity, horizontalinput);
}
}

Agora você pode introduzir entidades que avançarão a uma velocidade.

Mas também isso moverá todas as entidades com esse comportamento para que você possa introduzir tags, por exemplo, se você adicionou uma PlayerTag, somente a entidade com playerTag IComponentData poderá executar o MoveForward se eu quiser mover o player apenas como o exemplo abaixo.

Vou me aprofundar nisso também no artigo, mas parece com isso em um ComponentSystem típico

    Entities.WithAll<PlayerTag>().ForEach((ref Translation pos) =>
    {
        pos = new Translation { Value =  /*PlayerPosition*/ };
    });

Muito disso é explicado muito bem na apresentação do Angry Dots com Mike Geig, se você ainda não viu, recomendo dar uma olhada. Apontarei para o meu artigo também depois que ele terminar. Deve ser realmente útil obter várias daquelas coisas com as quais você está acostumado a trabalhar, como gostaria no ECS.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.