Des scripts conviviaux lors de l'utilisation d'un ECS?

8

Je suis en train de créer un petit projet de loisir pour me remettre au développement de jeux, et j'ai décidé de structurer mes entités à l'aide d'un ECS (Entity Component System). Cette implémentation d'un ECS est structurée comme suit:

  • Entité : dans mon cas, il s'agit d'un intidentifiant unique utilisé comme clé d'une liste de composants.
  • Composant : nePosition contient que des données, par exemple, le composant contient une xet une ycoordonnée, et le Movementcomposant contient une variable speedet direction.
  • Système : composants Poignées, par exemple , il prend les Positionet Movementcomposants et ajoute le speedet directionà la position de xet ycoordonnées.

Cela fonctionne bien, mais maintenant je souhaite implémenter des scripts dans mes jeux, sous la forme d'un langage de script. Dans les projets précédents, j'ai utilisé une implémentation OOP d'objets de jeu, ce qui signifiait que les scripts étaient assez simples. Par exemple, un script simple pourrait ressembler à ceci:

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

Cependant, lors de l'utilisation d'un ECS, l'entité elle-même n'a aucune fonction comme moveToou getInventory, à la place, le script ci-dessus écrit en style ECS ressemblerait à ceci:

 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

C'est beaucoup plus verbeux par rapport à la version OOP, ce qui n'est pas souhaitable lorsque les scripts sont destinés principalement à des non-programmeurs (joueurs du jeu).

Une solution serait d'avoir une sorte d'objet wrapper qui encapsule une Entityet fournit des fonctions telles que moveTodirectement, et gère le reste en interne, bien qu'une telle solution semble sous-optimale car il faut beaucoup de travail pour couvrir tous les composants, et chaque chaque fois qu'un nouveau composant est ajouté, vous devrez modifier l'objet wrapper avec de nouvelles fonctions.

À tous les développeurs de jeux qui ont déjà implémenté des scripts dans un ECS - comment avez-vous procédé? L'objectif principal ici est la convivialité pour l'utilisateur final, avec le moins de frais de "maintenance" possible (de préférence, vous n'avez pas besoin de le changer chaque fois que vous ajoutez un composant).

Charanor
la source
Je vais écrire ceci comme un commentaire car je suis un peu vague sur votre implémentation exacte (je suppose C ++). Ne pourriez-vous pas utiliser des modèles quelque part ici? Pour appliquer le composant X contre le composant Y? J'imagine alors que les composants devraient remplacer la méthode "apply" de base et la spécialiser pour les types de composants qui peuvent lui être appliqués. S'appuyer sur SFINAE garantirait que cela fonctionne quand il est censé le faire. Ou il pourrait être spécialisé dans la System/ les classe (s) pour permettre aux composants de rester des structures de données.
NeomerArcana
Pourquoi ne pas exposer la moveTométhode en tant que partie du système sous-jacent dans votre cas d'utilisation, par exemple MovementSystem? De cette façon, vous pouvez non seulement l'utiliser dans les scripts que vous écrivez, mais vous pouvez également l'utiliser dans le code C ++ là où vous en avez besoin. Alors oui, vous devrez exposer de nouvelles méthodes au fur et à mesure que de nouveaux systèmes seront ajoutés, mais c'est normal car son tout nouveau comportement introduit de toute façon ces systèmes.
Naros
Je n'ai pas eu l'occasion de le faire, mais serait-il possible d'ajouter des "raccourcis" uniquement aux opérations les plus courantes?
Vaillancourt

Réponses:

1

Vous pouvez créer un système ScriptExecutionSystem qui fonctionne sur toutes les entités avec un composant Script. Il obtient tous les composants de l'entité qui pourraient être utiles pour être exposés au système de script et les transmet à la fonction scriptée.

Une autre approche consisterait à faire en sorte que vos utilisateurs adoptent également ECS et leur permettent de définir leurs propres composants et d'implémenter leurs propres systèmes à l'aide du langage de script.

Philipp
la source
0

ECS a ses avantages et ses inconvénients. Les scripts conviviaux ne sont pas l'un de ses avantages.

Le problème résolu par ECS est la possibilité d'avoir un grand nombre de choses similaires dans votre jeu en même temps tout en conservant les performances. Mais cette solution a un coût - le coût d'une architecture facile à utiliser. Ce n'est pas la meilleure architecture pour chaque jeu.

Par exemple, ECS aurait été un bon choix pour Space Invaders , mais pas tant pour PacMan .

Ce n'est donc pas exactement la réponse que vous cherchiez, mais il est possible qu'ECS ne soit tout simplement pas le bon outil pour votre travail.

Si vous ajoutez un wrapper, surveillez les frais généraux. Si vous finissez par supprimer l'amélioration des performances d'ECS dans votre wrapper, alors vous avez le pire des deux mondes.


Mais pour répondre directement à votre question - "À tous les développeurs de jeux qui ont déjà implémenté des scripts dans un ECS - comment avez-vous procédé?"

À peu près exactement comme vous le faites, sans emballage. Les entités n'ont qu'un identifiant. Les composants n'ont que des données. Les systèmes n'ont que de la logique. Les systèmes qui acceptent des entités avec les composants requis sont exécutés. Ajoutez librement des systèmes, des entités et des composants.

J'ai utilisé une fois un cadre avec un quatrième aspect, appelé un tableau noir. C'était essentiellement un moyen pour les systèmes de communiquer entre eux. Il a créé plus de problèmes qu'il n'en a résolu.


Connexes: Dois-je implémenter Entity Component System dans tous mes projets?

Evorlor
la source
0

Avec ECS, vous pouvez diviser en une seule responsabilité, donc toute entité qui se déplace voudrait deux composants de données: un MoveComponent et un MoveSpeedComponent.

using System;
using Unity.Entities;

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

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

maintenant dans votre conversion, vous ajoutez ces composants à vos entités

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);     
}

Maintenant que nous avons la conversion et les données que nous pouvons déplacer vers le système, j'ai supprimé le système d'entrée pour plus de lisibilité, mais si vous souhaitez en savoir plus sur le système d'entrée, je les ai toutes répertoriées dans mon article de la semaine prochaine sur 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;
        }
    }
}

notez que la classe ci-dessus utilise les Unity.Mathmatics. C'est idéal pour pouvoir utiliser différentes fonctions mathématiques avec lesquelles vous avez l'habitude de travailler dans les systèmes normaux. Avec tout cela en ligne, vous pouvez maintenant travailler sur le comportement des entités - encore une fois, j'ai supprimé l'entrée ici, mais cela est beaucoup mieux expliqué dans l'article.

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);
}
}

Vous pouvez maintenant introduire des entités qui avanceront à grande vitesse.

Mais cela déplacera également chaque entité avec ce comportement afin que vous puissiez introduire des balises, par exemple si vous avez ajouté un PlayerTag, alors seule l'entité avec le playerTag IComponentData pourra effectuer le MoveForward si je veux seulement déplacer le joueur comme l'exemple au dessous de.

Je vais approfondir cela également dans l'article, mais cela ressemble à ceci dans un ComponentSystem typique

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

Une grande partie de cela est assez bien expliquée dans la présentation d'Angry Dots avec Mike Geig, si vous ne l'avez pas encore vu, je vous recommande de le vérifier. Je signalerai également mon article une fois qu'il sera terminé. Cela devrait vraiment être utile pour obtenir plusieurs de ces choses avec lesquelles vous êtes habitué à travailler comme vous le souhaitez dans ECS.

Justin Markwell
la source