ECSを使用する場合のユーザーフレンドリーなスクリプト?


8

私は現在、ゲーム開発に戻るために小さな趣味のプロジェクトを作成しており、ECS(エンティティコンポーネントシステム)を使用してエンティティを構造化することにしました。ECSのこの実装は、次のように構成されています。

  • エンティティ:私の場合int、コンポーネントのリストへのキーとして使用されるのは一意の識別子です。
  • コンポーネント:データのみを保持します。たとえば、Positionコンポーネントはxand y座標をMovement保持し、コンポーネントはspeedand direction変数を保持します。
  • システム:コンポーネントを処理します。たとえば、PositionおよびMovementコンポーネントを取得し、speedおよびdirectionを位置xy座標に追加します。

これは問題なく動作しますが、今度はゲームにスクリプト言語の形式でスクリプトを実装したいと思います。以前のプロジェクトでは、ゲームオブジェクトのOOP実装を使用しました。つまり、スクリプトは非常に単純明快でした。たとえば、単純なスクリプトは次のようになります。

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

ただし、ECSを使用する場合、エンティティ自体にはmoveToorのような関数はありませんgetInventory。代わりに、ECSスタイルで記述された上記のスクリプトは次のようになります。

 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

これは OOPバージョンに比べてはるかに冗長です。これはスクリプトがほとんどプログラマー以外(ゲームのプレーヤー)を対象としている場合には望ましくありません。

1つの解決策は、をカプセル化し、直接Entityなどの機能を提供しmoveTo、残りを内部で処理するラッパーオブジェクトのようなものにすることですが、このような解決策は、すべてのコンポーネントとすべての新しいコンポーネントが追加されると、新しい関数でラッパーオブジェクトを変更する必要があります。

以前にECSでスクリプトを実装したことのあるすべてのゲーム開発者に-どのようにそれをしましたか ここでの主な焦点は、エンドユーザーの使いやすさにあり、 "メンテナンス"コストは最小限に抑えられます(コンポーネントを追加するたびに変更する必要がないことが望ましいです)。


私はあなたの正確な実装について少し漠然としているので、コメントとしてこれを書きます(私はC ++を想定しています)。ここのどこかでテンプレートを利用できませんか?XコンポーネントをYコンポーネントに適用するには?その場合、コンポーネントは基本的な「適用」メソッドをオーバーライドし、それに適用できるコンポーネントのタイプに特化する必要があると想像します。SFINAEに依存することで、意図したとおりに機能することが保証されます。またはSystem、コンポーネントをデータ構造のままにできるように、クラスに特化することもできます。
NeomerArcana

moveToとして、MovementSystemなどのユースケースで、メソッドを基盤となるシステムの一部として公開しないでください。これにより、作成したスクリプトで使用できるだけでなく、必要な場所でC ++コードの一部としても使用できます。つまり、新しいシステムが追加されたときに新しいメソッドを公開する必要がありますが、いずれにしても、これらのシステムが導入するまったく新しい動作として期待されています。
Naros

私はそれを行う機会がありませんでしたが、より一般的に実行される操作にのみ「ショートカット」を追加することは可能ですか?
ヴァイランクール

回答:


1

スクリプトコンポーネントを使用してすべてのエンティティを操作するシステムScriptExecutionSystemを作成できます。スクリプトシステムに公開するのに役立つエンティティのすべてのコンポーネントを取得し、それらをスクリプト関数に渡します。

別のアプローチは、ユーザーにECSを採用させ、スクリプト言語を使用して独自のコンポーネントを定義し、独自のシステムを実装できるようにすることです。


0

ECSには長所と短所があります。ユーザーフレンドリーなスクリプトは、その長所の1つではありません。

ECSが解決する問題は、パフォーマンスを維持しながら、ゲーム内に多数の同様のものを同時に持つ能力です。しかし、このソリューションにはコストがかかります。使いやすいアーキテクチャのコストです。すべてのゲームに最適なアーキテクチャとは限りません。

たとえば、ECSはSpace Invadersにとっては良い選択でしたが、PacManにとってはそれほどではありませんでした。

したがって、求めていた答えは正確ではありませんが、ECSがあなたの仕事に適したツールではない可能性があります。

ラッパーを追加する場合は、オーバーヘッドのコストに注意してください。ラッパーでECSのパフォーマンスブーストを削除してしまうと、両方の世界で最悪の事態になります。


しかし、あなたの質問に直接答えるために-「以前にECSでスクリプトを実装したことがあるすべてのゲーム開発者に-どのようにそれをしましたか?」

ラッパーなしで、あなたがやっていることとほぼ同じです。エンティティには識別子のみがあります。コンポーネントにはデータしかありません。システムにはロジックしかありません。必要なコンポーネントを持つエンティティを受け入れるシステムが実行されます。システム、エンティティ、コンポーネントを自由に追加します。

私はかつて、黒板と呼ばれる4番目の側面を持つフレームワークを使用しました。これは基本的に、システムが相互に通信する方法でした。解決した以上の問題が発生しました。


関連: すべてのプロジェクトにエンティティコンポーネントシステムを実装する必要がありますか?


0

ECSを使用すると、単一の責任に分解できるため、移動するエンティティにはMoveComponentとMoveSpeedComponentという2つのデータコンポーネントが必要になります。

using System;
using Unity.Entities;

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

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

変換でこれらのコンポーネントをエンティティに追加します

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

変換とデータをシステムに移動できるようになったので、読みやすくするために入力システムを削除しましたが、入力システムについてさらに詳しく知りたい場合は、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;
        }
    }
}

上記のクラスはUnity.Mathmaticsを使用していることに注意してください。これは、通常のシステムでの作業に慣れているさまざまな数学関数を使用できるようにするのに最適です。これで、すべてのエンティティの動作を操作できるようになりました。ここでも入力を削除しましたが、これについては、記事で詳しく説明しています。

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

今、あなたはスピードで前進するエンティティを紹介することができます。

ただし、これにより、この動作ですべてのエンティティが移動するため、たとえばPlayerTagを追加した場合にタグを導入できます。次に、例のようにプレーヤーを移動したいだけの場合、playerTag IComponentDataを持つエンティティのみがMoveForwardを実行できます。未満。

記事でも詳しく説明しますが、典型的なComponentSystemでは次のようになります。

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

これの多くは、マイクガイグとのAngry Dotsプレゼンテーションでかなりよく説明されています。まだ見ていない場合は、チェックすることをお勧めします。それが終わった後、私は私の記事も指します。ECSでどのように動作するかを操作するのに慣れているものをいくつか取得するのに本当に役立つはずです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.