Benutzerfreundliches Scripting bei Verwendung eines ECS?

8

Ich erstelle gerade ein kleines Hobbyprojekt, um wieder in die Spieleentwicklung einzusteigen, und ich habe beschlossen, meine Entitäten mithilfe eines ECS (Entity Component System) zu strukturieren. Diese Implementierung eines ECS ist wie folgt strukturiert:

  • Entität : In meinem Fall handelt es sich um eine eindeutige intKennung, die als Schlüssel für eine Liste von Komponenten verwendet wird.
  • Komponente : Enthält nur Daten, z. B. enthält die PositionKomponente eine xund eine yKoordinate und die MovementKomponente eine speedund eine directionVariable.
  • System : Behandelt Komponenten, z. B. nimmt es die Positionund MovementKomponenten und fügt das speedund directionzu den Positionen xund yKoordinaten hinzu.

Das funktioniert gut, aber jetzt möchte ich Skripte in Form einer Skriptsprache in meine Spiele implementieren. In früheren Projekten habe ich eine OOP-Implementierung von Spielobjekten verwendet, was bedeutete, dass die Skripterstellung ziemlich einfach war. Ein einfaches Skript könnte beispielsweise so aussehen:

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

Wenn Sie jedoch ein ECS verwenden, hat die Entität selbst keine Funktionen wie moveTooder getInventory, stattdessen würde das obige Skript im ECS-Stil ungefähr so ​​aussehen:

 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

Dies ist im Vergleich zur OOP-Version viel ausführlicher, was nicht wünschenswert ist, wenn sich das Scripting hauptsächlich an Nicht-Programmierer (Spieler des Spiels) richtet.

Eine Lösung wäre, eine Art Wrapper-Objekt zu haben, das ein kapselt Entityund Funktionen wie moveTodirekt bereitstellt und den Rest intern erledigt, obwohl eine solche Lösung nicht optimal erscheint, da es viel Arbeit erfordert, alle und alle Komponenten abzudecken Wenn eine neue Komponente hinzugefügt wird, müssen Sie das Wrapper-Objekt mit neuen Funktionen ändern.

An alle Spieleentwickler, die zuvor Skripte in einem ECS implementiert haben - wie haben Sie das gemacht? Das Hauptaugenmerk liegt hier auf der Benutzerfreundlichkeit für den Endbenutzer bei möglichst geringen "Wartungskosten" (vorzugsweise müssen Sie diese nicht jedes Mal ändern, wenn Sie eine Komponente hinzufügen).

Charanor
quelle
Ich werde dies als Kommentar schreiben, da ich etwas vage über Ihre genaue Implementierung bin (ich gehe von C ++ aus). Könnten Sie hier nicht irgendwo Vorlagen verwenden? So wenden Sie die X-Komponente gegen die Y-Komponente an? Ich stelle mir dann vor, dass Komponenten die Basismethode "Anwenden" überschreiben und sie auf die Arten von Komponenten spezialisieren müssten, die darauf angewendet werden können. Wenn Sie sich auf SFINAE verlassen, wird sichergestellt, dass es funktioniert, wenn es beabsichtigt ist. Oder es könnte auf die SystemKlasse (n) spezialisiert sein, damit die Komponenten Datenstrukturen bleiben können.
NeomerArcana
Warum machen Sie die moveToMethode in Ihrem Anwendungsfall nicht als Teil des zugrunde liegenden Systems verfügbar, z. B. MovementSystem? Auf diese Weise können Sie es nicht nur in den von Ihnen geschriebenen Skripten verwenden, sondern auch dort, wo Sie es benötigen, als Teil des C ++ - Codes verwenden. Ja, Sie müssen neue Methoden verfügbar machen, wenn neue Systeme hinzugefügt werden, aber das ist zu erwarten, da diese Systeme ohnehin ein völlig neues Verhalten einführen.
Naros
Ich hatte noch keine Gelegenheit dazu, aber wäre es möglich, "Verknüpfungen" nur zu den häufiger ausgeführten Operationen hinzuzufügen?
Vaillancourt

Antworten:

1

Sie können ein System ScriptExecutionSystem erstellen, das auf allen Entitäten mit einer Skriptkomponente ausgeführt wird. Es erhält alle Komponenten der Entität, die nützlich sein könnten, um sie dem Skriptsystem zugänglich zu machen, und übergibt diese an die Skriptfunktion.

Ein anderer Ansatz wäre, Ihre Benutzer dazu zu bringen, auch ECS zu nutzen und ihnen zu ermöglichen, ihre eigenen Komponenten zu definieren und ihre eigenen Systeme mithilfe der Skriptsprache zu implementieren.

Philipp
quelle
0

ECS hat seine Vor- und Nachteile. Benutzerfreundliches Scripting gehört nicht zu seinen Profis.

Das Problem, das ECS löst, ist die Fähigkeit, eine große Anzahl ähnlicher Dinge gleichzeitig in Ihrem Spiel zu haben und gleichzeitig die Leistung beizubehalten. Diese Lösung hat jedoch ihre Kosten - die Kosten einer benutzerfreundlichen Architektur. Es ist nicht die beste Architektur für jedes Spiel.

Zum Beispiel wäre ECS eine gute Wahl für Space Invaders gewesen , aber nicht so sehr für PacMan .

Also nicht genau die Antwort, nach der Sie gesucht haben, aber es ist möglich, dass ECS einfach nicht das richtige Werkzeug für Ihren Job ist.

Wenn Sie einen Wrapper hinzufügen, beachten Sie die Gemeinkosten. Wenn Sie am Ende die Leistungssteigerung von ECS in Ihrem Wrapper entfernen, haben Sie das Schlimmste aus beiden Welten.


Aber um Ihre Frage direkt zu beantworten: "An alle Spieleentwickler, die zuvor Skripte in einem ECS implementiert haben - wie haben Sie das gemacht?"

Ziemlich genau so, wie Sie es tun, ohne Wrapper. Entitäten haben nichts als eine Kennung. Komponenten haben nichts als Daten. Systeme haben nichts als Logik. Systeme, die Entitäten mit den erforderlichen Komponenten akzeptieren, werden durchlaufen. Fügen Sie Systeme, Entitäten und Komponenten frei hinzu.

Ich habe einmal ein Framework mit einem vierten Aspekt verwendet, eine Tafel. Es war im Grunde eine Möglichkeit für Systeme, miteinander zu kommunizieren. Es hat mehr Probleme geschaffen als gelöst.


Verwandte: Sollte ich Entity Component System in allen meinen Projekten implementieren?

Evorlor
quelle
0

Mit ECS können Sie auf eine einzelne Verantwortung aufteilen, sodass jede sich bewegende Entität zwei Datenkomponenten benötigt: eine MoveComponent und eine MoveSpeedComponent.

using System;
using Unity.Entities;

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

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

Jetzt fügen Sie bei Ihrer Konvertierung diese Komponenten Ihren Entitäten hinzu

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

Nachdem wir nun Konvertierung und Daten haben, die wir in das System verschieben können, habe ich das Eingabesystem aus Gründen der Lesbarkeit entfernt. Wenn Sie jedoch mehr über das Eingabesystem erfahren möchten, werden alle in meinem Artikel nächste Woche über Unity Connect aufgelistet.

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

Beachten Sie, dass die obige Klasse Unity.Mathmatics verwendet. Dies ist ideal, um verschiedene mathematische Funktionen verwenden zu können, mit denen Sie in normalen Systemen gewohnt sind. Damit können Sie jetzt am Verhalten der Entitäten arbeiten. Ich habe die Eingabe hier wieder entfernt, aber das alles wird im Artikel viel besser erklärt.

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

Jetzt können Sie Entitäten einführen, die sich mit einer Geschwindigkeit vorwärts bewegen.

Dadurch wird jedoch auch jede Entität mit diesem Verhalten verschoben, sodass Sie Tags einführen können. Wenn Sie beispielsweise ein PlayerTag hinzugefügt haben, kann nur die Entität mit dem PlayerTag IComponentData den MoveForward ausführen, wenn ich den Player nur wie im Beispiel verschieben möchte unten.

Darauf werde ich auch im Artikel näher eingehen, aber in einem typischen ComponentSystem sieht es so aus

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

Vieles davon wird in der Angry Dots-Präsentation mit Mike Geig ziemlich gut erklärt. Wenn Sie es noch nicht gesehen haben, empfehle ich, es sich anzusehen. Ich werde auch auf meinen Artikel verweisen, wenn er fertig ist. Sollte wirklich hilfreich sein, um einige der Dinge zu bekommen, mit denen Sie es gewohnt sind, so zu arbeiten, wie Sie es in ECS möchten.

Justin Markwell
quelle