基于实体组件系统的引擎


9

注意:我正在用Javascript编程,但是在大多数情况下,它应该与语言无关。

我正在考虑将引擎转换为基于ECS的引擎。

我有基本的想法(注意:这是错误的,请参见我的回答):

实体是游戏对象。
组件是可以“粘合”到实体的功能(reactToInput())或状态(position)的一部分。
系统具有它们管理和更新的实体的列表。

但是,我不确定我是否可以获得实现和一些细节...

问题:系统可以在不同种类的实体上运行吗?我通常以Scene在引擎中称为类的示例为例,它现在也将用于此目的。场景是所有可以渲染,更新,影响渲染(灯光)的对象的容器,甚至将来甚至可能是2DSoundEmitter对象。它具有一个高级界面,因此用户无需担心他正在播放的对象的类型scene.add()以及所有类似的东西。

我意识到这Scene可能是一个系统。它接受实体,进行存储,然后可以调用其更新方法,甚至可以进行一些状态更改。但是,有一个问题:如上所述,Scene可以为不同类型的对象喂食!例如,在场景中既包含可渲染对象(“可绘制对象”)又包含灯光的情况下,我该怎么办?互动之前,我应该让它进行类型检查吗?或者,我应该在更低的层次上解决它:制作一个LightSource可以添加到任何对象的组件,并且灯光将仅仅是具有LightSourcePosition组件的实体。可以接受吗?

另外,仍然使用常规继承和传统类是否是一个好习惯?例如,我只是不知道我Renderer会是什么!它不是一个系统,因为它的唯一功能是获取摄像机和场景,渲染所有内容并应用效果(例如阴影)。它还管理游戏的上下文,宽度和高度,进行翻译……但是它仍然不是系统!

编辑:您是否可以链接在ECS上找到的任何资源?我很难找到好的。


2
我不会在页面上重新发布答案,而只是给出以下链接:gamedev.stackexchange.com/questions/23533/… 实体不应该源自实体,实体之间的任何差异都应该通过组件来实现。通常,您将需要每个主要系统的界面(渲染,物理,网络,输入,音频等)。我设置渲染器的方式是查询场景中可渲染的实体,然后场景管理器向其上具有渲染组件的每个实体询问渲染信息。
Nic Foster

1
T = Machine博客上的组件设计(因为您要求的很好)
John McDonald

实体框架的代码和讨论:gamadu.com/artemis
Patrick Hughes

@JohnMcDonald,我正在对该文章发表评论,尽管它正在等待审核。您可以在这里看到它:t-machine.org/index.php/2007/12/22/…。我是“ Yanbane”。
jcora

此外,@ NicFoster(约翰在T = Machine上链接到的文章)描述的内容与您的回答有点不同...对于Dave,实体没有组件列表,它们只是名称。就像“ flsjn304”一样-这是一个实体。它存储在“某处”。我必须重新阅读一遍,以了解他是否实际上将组件保留在系统中,这对我来说似乎奇怪!
jcora 2012年

Answers:


6

让我看看通过尝试理解为Web / UI JS开发人员是否对我有帮助。另外,在语言不可知论方面也不要太过分。许多其他语言建立的模式值得研究,但由于其灵活性而可以在JS中非常不同地应用,或者由于该语言的可塑性而实际上并不是必需的。如果您在编写代码时考虑到JS具有与更传统的面向OOP的语言相同的边界集,则可能会吹牛。

首先,在“不要使用OOP”因素上,请记住,与其他语言相比,JavaScript对象就像橡皮泥一样,由于JS不属于类,您实际上必须竭尽全力构建级联继承方案的噩梦基于和合成自然而然。如果您要在JS中实现一些愚蠢的类或原型递减系统,请考虑放弃它。在JS中,我们使用闭包,原型,然后像糖果一样传递函数。这令人作呕,肮脏和错误,但又功能强大,简洁,这就是我们喜欢的方式。

实际上,继承繁重的方法在设计模式中被明确说明是一种反模式,这是有充分理由的,因为凡是走过15个或15个以上级别的类或类结构的人,都可以尝试找出方法的无效版本在哪里。来自可以告诉你。

我不知道为什么有这么多的程序员喜欢这样做(尤其是出于某种原因而编写JavaScript的Java程序员),但是使用起来太糟糕了,难以理解并且完全无法维护。在这里和那里都可以继承,但是在JS中并不是必须的。在使用更诱人的快捷方式的语言中,它实际上应该保留给更多的抽象体系结构问题,而不是更多的字面化建模方案,例如通过包含BunnyRabbit的继承链来弗兰肯斯坦僵尸实现,因为它确实起作用了。这不是很好的代码重用。这是一场维护噩梦。

作为JS开发人员,基于实体/组件/系统的引擎将我作为系统/模式来解决设计问题,然后将对象进行组合以实现高度粒度的实现。换句话说,儿童游戏采用JavaScript之类的语言。但是让我看看我是否先正确地做过这个。

  • 实体-您正在设计的特定事物。我们正在更多地谈论专有名词的方向(当然,实际上不是)。不是“场景”,而是“ IntroAreaLevelOne”。IntroAreaLevelOne可能位于某种SceneEntity框内,但我们关注的是与其他相关事物不同的特定事物。在代码中,实体实际上只是一个名称(或ID),它与一堆需要实现或建立的东西(组件)相关,才能有用。

  • 组件-实体需要的事物类型。这些是通用名词。像WalkingAnimation。在WalkingAnimation中,我们可以获得更具体的信息,例如“ Shambling”(对于僵尸和植物怪物来说是不错的选择),或者“ ChickenWalker”(对于ed-209ish反向机器人版本非常有用)。注意:不确定如何与这样的3D模型渲染脱钩-也许是一个废话的例子,但我更是JS专业人士,而不是经验丰富的游戏开发人员。在JS中,我会将映射机制与组件放在同一盒子中。组件本身可能只是逻辑上的问题,而更多的路线图则告诉您的系统,甚至在需要系统时也要实施什么(在我尝试ECS时,某些组件只是属性集的集合)。建立组件后,

  • 系统-真正的程序性肉在这里。构建并链接了AI系统,实现了渲染,建立了动画序列,等等...我正在解决这些问题,大部分都留给了想象力,但在示例System.AI中,它吸收了一堆属性并吐出了一个功能用于将事件处理程序添加到最终在实现中使用的对象。System.AI的关键在于它涵盖了多种组件类型。您可以使用一个组件来整理所有AI东西,但这样做是误解了使事情变得细粒度的观点。

牢记目标:我们希望使非设计人员能够轻松地插入某种GUI界面,以通过最大化和匹配对他们有意义的范例中的组件来轻松地调整不同种类的东西,并且我们希望远离流行的任意代码方案,它们比修改或维护要容易得多。

所以在JS中,也许像这样。游戏开发人员请告诉我我是否犯错了:

//I'm going with simple objects of flags over arrays of component names
//easier to read and can provide an opt-out default
//Assume a genre-bending stealth assassin game

//new (function etc... is a lazy way to define a constructor and auto-instantiate
var npcEntities = new (function NpcEntities(){

    //note: {} in JS is an object literal, a simple obj namespace (a dictionary)
    //plain ol' internal var in JS is akin to a private member
    var default={ //most NPCs are humanoids and critters - why repeat things?
        speedAttributes:true,
        maneuverAttributes:true,
        combatAttributes:true,
        walkingAnimation:true,
        runningAnimation:true,
        combatAnimation:true,
        aiOblivious:true,
        aiAggro:true,
        aiWary:true, //"I heard something!"
        aiFearful:true
    };

    //this. exposes as public

    this.zombie={ //zombies are slow, but keep on coming so don't need these
        runningAnimation:false,
        aiFearful:false
    };

    this.laserTurret={ //most defaults are pointless so ignore 'em
        ignoreDefault:true,
        combatAttributes:true,
        maneuverAttrubtes:true, //turning speed only
    };
    //also this.nerd, this.lawyer and on and on...

    //loop runs on instantiation which we're forcing on the spot

    //note: it would be silly to repeat this loop in other entity collections
    //but I'm spelling it out to keep things straight-forward.
    //Probably a good example of a place where one-level inheritance from
    //a more general entity class might make sense with hurting the pattern.
    //In JS, of course, that would be completely unnecessary. I'd just build a
    //constructor factory with a looping function new objects could access via
    //closure.

    for(var x in npcEntities){

        var thisEntity = npcEntities[x];

        if(!thisEntity.ignoreDefaults){

            thisEntity = someObjectXCopyFunction(defaults,thisEntity);
            //copies entity properties over defaults

        }
        else {
            //remove nonComponent property since we loop again later
            delete thisEntity.ignoreDefaults;
        }
    }
})() //end of entity instantiation

var npcComponents = {
    //all components should have public entityMap properties

    //No systems in use here. Just bundles of related attributes
    speedAttributes: new (function SpeedAttributes(){
        var shamblingBiped = {
            walkingAcceleration:1,
            topWalking:3
        },
        averageMan = {
            walkingAcceleration:3,
            runningAcceleration:4,
            topWalking: 4,
            topRunning: 6
        },
        programmer = {
            walkingAcceleration:1,
            runningAcceleration:100,
            topWalking:2
            topRunning:2000
        }; //end local/private vars

        //left is entity names | right is the component subcategory
        this.entityMap={
            zombie:shamblingBiped,
            lawyer:averageMan,
            nerd:programmer,
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(), //end speedAttributes

    //Now an example of an AI component - maps to function used to set eventHandlers
    //functions which, because JS is awesome we can pass around like candy
    //I'll just use some imaginary systems on this one

    aiFearful: new (function AiFearful(){
        var averageMan = Systems.AI({ //builds and returns eventSetting function
            fearThreshold:70, //%hitpoints remaining
            fleeFrom:'lastAttacker',
            tactic:'avoidIntercept',
            hazardAwareness:'distracted'
        }),
        programmer = Systems.AI({
            fearThreshold:95,
            fleeFrom:'anythingMoving',
            tactic:'beeline',
            hazardAwareness:'pantsCrappingPanic'
        });//end local vars/private members


         this.entityMap={
            lawyer:averageMan,
            nerd:averageMan, //nerds can run like programmers but are less cowardly
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(),//and more components...

    //Systems.AI is general and would get called for all the AI components.
    //It basically spits out functions used to set events on NPC objects that
    //determine their behavior. You could do it all in one shot but
    //the idea is to keep it granular enough for designers to actually tweak stuff
    //easily without tugging on developer pantlegs constantly.
    //e.g. SuperZombies, zombies, but slightly tougher, faster, smarter
}//end npcComponents

function createNPCConstructor(npcType){

    var components = npcEntities[npcType],

    //objConstructor is returned but components is still accessible via closure.

    objConstructor = function(){
        for(var x in components){
            //object iteration <property> in <object>

            var thisComponent = components[x];

            if(typeof thisComponent === 'function'){
                thisComponent.apply(this);
                //fires function as if it were a property of instance
                //would allow the function to add additional properties and set
                //event handlers via the 'this' keyword
            }
            else {
                objConstructor.prototype[x] = thisComponent;
                //public property accessed via reference to constructor prototype
                //good for low memory footprint among other things
            }
        }
    }
    return objConstructor;
}

var npcBuilders= {}; //empty object literal
for (var x in npcEntities){
    npcConstructors[x] = createNPCConstructor(x);
}

现在,任何时候需要NPC,您都可以使用 npcBuilders.<npcName>();

GUI可以插入npcEntities和components对象,并允许设计人员通过简单地混合和匹配组件来调整旧实体或创建新实体(尽管其中没有用于非默认组件的机制,但可以在运行时动态添加特殊组件)代码,只要有一个定义的组件即可。


回顾这六年后,我不确定我是否理解自己的答案。这可以改善吗?
Erik Reppen '18

1

我已经在注释中提供的友善的文章中阅读了Entity Systems,但是我仍然有一些疑问,因此我提出了另一个问题

首先,我的定义是错误的。实体组件只是愚蠢的数据持有者,而系统提供了所有功能。

我学到了足够的知识,可以在这里解决我的大部分问题,所以我会回答。

Scene我正在谈论的课程不应该是系统。但是,它应该是一个中央管理器,可以容纳所有实体,提供消息,甚至可以管理系统。它也可以用作实体的分类工厂,我决定像这样使用它。它可以采用任何类型的实体,但是随后必须将该实体馈送到适当的系统(出于性能原因,除非按位检查,否则不应执行任何类型检查)。

Adam 建议,在实现ES时,我不应该使用任何OOP ,但我发现没有理由不为对象提供实体和组件的方法,而不仅仅是愚蠢的数据持有者。

Renderer可简单地实施为一个系统。它将维护一个可绘制对象的列表,并draw()每隔16ms 调用其渲染组件的方法一次。


1
“实体和组件只是愚蠢的数据持有者,而系统提供了所有功能”“调用它们的渲染组件的draw()方法”您仍然感到困惑,除了“ draw”方法完全击败了Rendering系统的目的。而且我也不明白为什么场景图不能成为Renderer的一部分,它只是一个方便的工具,您始终可以将“ drawable”组件实现为节点。让场景图负责的不仅仅是场景,这是不必要的,并且我敢肯定,调试起来会很麻烦。
德雷塔

@dreta,当前的渲染器(引擎的非ES实现)进行转换,更改相机,添加Alpha材质,并在将来绘制各种效果,GUI和阴影。将这些东西分组似乎很自然。场景不应该负责创建存储实体吗?还是应该用其他东西存储它们?创建部分可能只是将用户提供的组件聚合在一起的几行,它根本不是在“创建”任何东西,仅仅是实例化。
jcora 2012年

并不是每个对象都是可渲染的,不是每个对象都可以与声音碰撞或发出声音,而场景对象却要进行极端耦合,为什么呢?这只是编写和调试的痛苦。实体用于标识对象,组件保存数据以及系统对该数据进行操作。为什么要把所有这些融合在一起,而不是像RenderingSystem和SoundSystem这样的适当系统,而仅当实体具有所有必需组件时才打扰那些系统。
德雷塔

1
通常可以将阴影投射附加到光源上,尽管您可以只创建一个组件“ CastsShadow”并在渲染动态对象时寻找它。如果您正在执行2D,那么这只是订购的基本问题,简单的画家算法将为您解决此问题。TBH,您太早担心了。您会在需要的时候弄清楚这一点,而脑海中只有一个想法,现在您只是在困惑自己。您不能指望第一时间就把所有事情都做好,这只是不可能的事情。当您到达那座桥时,您将越过那座桥。
德雷塔

1
“实体和组件只是愚蠢的数据持有者,而系统提供了所有功能。” 不必要。它们是某些人的方法。但是没有其他人。看一下Unity引擎-所有行为都在组件中。
Kylotan

-2

依赖关系管理简介101。

本课程假定您具有依赖项注入和存储库设计的基本知识。

依赖注入只是对象彼此交谈(通过消息/信号/代理/任何对象)而无需直接耦合的一种奇特的方式。

它的用语是:“ new是胶水”。

我将在C#中对此进行演示。

public interface IEntity
{
    int[] Position { get; }
    int[] Size { get; }
    bool Update();
    void Render();
}

public interface IRenderSystem
{
    void Draw(IEntity entity);
}

public interface IMovementSystem
{
    bool CanMoveLeft();
    void MoveLeft();
    bool CanMoveRight();
    void MoveRight();
    bool CanMoveUp();
    void MoveUp();
    bool CanMoveDown();
    void MoveDown();
    bool Moved();
    int[] Position { get; set; }
}

public interface IInputSystem
{
    string Direction { get; set; }
}

public class Player : IEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public bool Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();

        return _movementSystem.Moved();
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

这是输入,移动和渲染的基本系统。该Player班是在这种情况下,实体和组件的接口。本Player类并不知道如何具体的内部IRenderSystemIMovementSystemIInputSystem工作。但是,这些接口提供了一种Player发送信号的方式(例如,在IRenderSystem上调用Draw),而无需取决于最终结果的实现方式。

例如,以我的IMovementSystem的实现为例:

public interface IGameMap
{
    string LeftOf(int[] currentPosition);
    string RightOf(int[] currentPosition);
    string UpOf(int[] currentPosition);
    string DownOf(int[] currentPosition);
}

public class MovementSystem : IMovementSystem
{
    private readonly IGameMap _gameMap;
    private int[] _previousPosition;
    private readonly int[] _currentPosition;
    public MovementSystem(IGameMap gameMap, int[] initialPosition)
    {
        _gameMap = gameMap;
        _currentPosition = initialPosition;
        _previousPosition = initialPosition;
    }

    public bool CanMoveLeft()
    {
        return _gameMap.LeftOf(_currentPosition) == "Unoccupied";
    }

    public void MoveLeft()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]--;
    }

    public bool CanMoveRight()
    {
        return _gameMap.RightOf(_currentPosition) == "Unoccupied";
    }

    public void MoveRight()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]++;
    }

    public bool CanMoveUp()
    {
        return _gameMap.UpOf(_currentPosition) == "Unoccupied";
    }

    public void MoveUp()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]--;
    }

    public bool CanMoveDown()
    {
        return _gameMap.DownOf(_currentPosition) == "Unoccupied";
    }

    public void MoveDown()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]++;
    }

    public bool Moved()
    {
        return _previousPosition == _currentPosition;
    }

    public int[] Position
    {
        get { return _currentPosition; }
    }
}

MovementSystem可以有自己的依赖关系,Player甚至都不在乎。通过使用接口,可以创建游戏状态机:

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IEntity>();
        foreach (var entity in _entities)
        {
            if(entity.Update())
                _renderQueue.Add(entity);
        }
        // Linq version for those interested
        //_renderQueue.AddRange(_entities.Where(e => e.Update()));
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

这是一个可爱的游戏(也可以进行单元测试)的开始。

并添加了一些内容和一些多态性:

public interface IEntity
{
}

public interface IRenderableEntity : IEntity
{
    void Render();        
}

public interface IUpdateableEntity : IEntity
{
    void Update();
    bool Updated { get; }
}

public interface IRenderSystem
{
    void Draw(IRenderableEntity entity);
}

// new player class
public class Player : IRenderableEntity, IUpdateableEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public void Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();
    }

    public bool Updated
    {
        get { return _movementSystem.Moved(); }
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IRenderableEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IRenderableEntity>();
        foreach (var entity in _entities)
        {
            if (entity is IUpdateableEntity)
            {
                var updateEntity = entity as IUpdateableEntity;
                updateEntity.Update();
            }

            if (entity is IRenderableEntity)
            {
                var renderEntity = entity as IRenderableEntity;
                _renderQueue.Add(renderEntity);
            }
        }
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

现在,我们有了基于聚合接口和松散继承的原始实体/组件系统。


1
这与组件设计相反:)如果您希望一个播放器发出声音而另一个不发出声音,您会怎么做?
Kikaimaru 2012年

@Kikaimaru在不播放声音的ISoundSystem中传递。即什么也不做。
达斯汀·金根

3
-1,不是因为它是不好的代码,而是因为它与基于组件的体系结构根本不相关-实际上,组件试图避免这种接口的泛滥。
Kylotan

@Kylotan我想我的理解一定是错误的。
达斯汀·金根
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.