如何通过实体组件系统在Javascript中构建游戏

How to Build an Entity Component System Game in Javascript

Creating and manipulating abstractions is the essence of programming. There is no “correct” abstraction to solve a problem, but some abstractions are better suited for certain problems than others. Class-based, Object Oriented Programming (OOP), is the most widely used paradigm for organizing programs. There are others. Prototypical based languages, like Javascript, provide a different way of thinking around how to solve problems. Functional programming provides yet another completely different way to think about and solve problems. Programming languages are just one area where a different mindset can help solve problems better.

在程序设计中,创造并合理利用”抽象”思想是非常重要的。并不存在绝对”正确”地使用”抽象”的思想来解决问题的方法,但是一些特定的”抽象”思想会更适用于去解决特定的问题。基于类的面向对象的编程,广泛地使用这种思想去构建程序。当然也有其他的方式。基于原型的语言,例如javascript,提供了一种独特方式去思考如何解决这样的问题。函数式编程提供另一种完全不同的方式去考虑并且来解问题。不同的程序设计语言可以通过利用自身独特的特性去更好地解决问题。

Even within a class based or prototype based language, many methods exist for structuring code. One approach I’ve grown to love is a more data driven approach to code. One such technique is Entity-Component-System (ECS). While it is a general architecture pattern that could be applied to many domains, the predominant uses of it are in game development. In this post, I’ll cover the basic topics of ECS and we’ll build a basic HTML5 game about eating rectangles - oh-so creatively called “Rectangle Eater”.

不论是基于类还是基于原型语言,许多的方法都在使用结构化的方式构建代码。我变得更喜欢通过更多数据驱动的方法来构建代码。其中一个技术就是实体-组件系统(ECS),尽管它是一个很常见的设计模式被应用于许多领域,但它主要的用途还是应用在游戏开发中。在这篇文章中,我将通过构建基础的ECS系统并构建一个基于HTML5的游戏关于”吃方块”,用一个更具有创造性的名称来命名——”Rectangle Eater”。

Entity-Component-System

Discovering Entity Component System (ECS) was an “ah-hah” moment for me. With ECS, entities are just collections of components; just a collection of data.

研究实体组件系统对我来说是一个非常美妙的时刻。利用这种思想,实体仅仅是一连串组件的集合,也仅仅是一连串数据的集合。

?Entity: An entity is just an ID

?Component: Components are just data.

?System: Logic that runs on every entity that has a component of the system. For example, a “Renderer” system would draw all entities that have a “appearance” component.

?实体:一个实体就是一个ID

?组件:组件就是数据

?系统:逻辑上可以描述为控制每一个拥有组件的实体正常运转的系统。例如,一个渲染系统将会绘制所有拥有外观组件的实体。

With this approach, you avoid having to create gnarly inheritance chains. With this approach, for example, a half-orc isn’t some amalgamation of a human class and an orc class (which might inherit from a Monster class). With this approach, a half-orc is just a grouping of data.

通过这个方法,你可以避免去创建糟糕的继承链。例如,一个半兽人并不是通过一个人类的类和一个兽人的类(有可能继承自怪兽类)合成的,它仅仅一组数据而已。

An entity is just a like a record in a database. The components are the actual data. Here’s a high level example of what the data might look like for entities, shown by ID and components. The beauty of this system is that you can dynamically build entities - an entity can have whatever components (data) you want.

一个实体可以想象成在一个数据库中的一条记录。组件就是这条记录中的实际数据。下面是一个关于什么样的数据有可能看起来像实体,通过ID和组件进行展现的例子。对于这个系统最棒的是你可以动态地构建实体-一个实体可以包含任意你希望使用的组件(数据)。

component-health component-position component-appearance
entity1 100 {x: 0, y: 0}
entity2 {x: 0, y: 0}
entity3

Dynamic Data Driven Design

Everything is tagged as an entity. A bullet, for instance, might just have a “physics” and “appearance” component. Entity Component System is data driven. This approach allows greater flexibility and more expression. One benefit is the ability to dynamically add and remove components, even at run time. You could dynamically remove the appearance component to make invisible bullets, or add a “playerControllable” component to allow the bullet to be controlled by the player. No new classes required.

所有物体被标记作为一个实体。例如,一个子弹实例化的时候,有可能会含有一个”物理”属性的组件和一个”外观”组件。实体组件系统是通过数据进行驱动的。这种方法可以更灵活并且有更多表现方式。一个优势在于可以动态地添加并且移除组件,甚至可以在动态运行的时候。你可以动态移除外观组件使得子弹不可见,或者添加一个用户可控制的组件允许子弹被玩家控制。不需要新添加一个类。

This can potentially be a problem as systems have to iterate through all entities. Of course, it’s not terribly difficult to optimize and structure code so not all entities are hit each iteration if you have too many, but it’s helpful to keep this constraint in mind, especially for browser based games.

当然这也可能潜在地成为系统中的一个问题,因为会通过迭代器遍历所有实体。当然,也不是很困难地去优化并通过结构化方式进行代码构建,如果你有很多个实体的话,并不是所有的实体需要通过迭代器进行遍历。如果你认为这些优化很有帮助的话,尤其是针对基于浏览器游戏的优化。

Assemblages

One benefit of a Class based approach is the ability to easily create multiple objects of the same type. If I want one hundred orcs, I can just create a hundred orc objects and know what properties they’ll all have. This can be accomplished with ECS through an abstraction called an assemblage, which is just a way to easily create entities that have some grouping of components. For instance, a Human assemblage might contain “position”, “name”, “health”, and “appearance” components. A Sword assemblage might just have “appearance” and “name”.

基于类方法的另一个优势是快速简单地创建同类型的多个物体。假设我们需要100个半兽人,我们仅仅需要创建100个半兽人的物体并且知道他们所具有的属性。这可以利用ECS通过装配的方式来实现,装配是一种可以使用几组组件便捷地创建实体的方法。例如,一个人类的装配可能需要包含位置信息,名称,健康和外观组件。一柄剑的装配可能只需要外观和名称的组件。

One benefit this provides over normal Class inheritance is the ability to easily add on (or remove) components from assemblages. Since it’s data driven, you can manipulate and change them programmatically based on whatever parameters you desire. Maybe you want to create a ton of humans but have some of them be invisible - no need for a new class, just remove the “appearance” component from that entity.

另一个优势在于提供继承于一些基础的类有能力快速地进行添加或者移除组件的功能,主要通过装配方式来实现。由于是通过数据进行驱动,你可以操作并且改变他们编码的方式基于任何你希望使用的参数。也许你想创建大量的人类但是让部分人不可见-不需要创建一个新的类,只需要移除他们实体上的外观组件就可以了。

Code

This is not an attempt to build out a robust ECS library. This is designed to be an overview of Entity Component System implemented in Javascript. It’s not the best or most optimized way to do it; but it can provide a foundation for a concrete understanding of how everything fits together. All code can be found on github.

本文所尝试构建的ECS系统并不是尝试去创建一个非常完善的ECS类库。而是通过javascript实现并设计一个具备大体框架的实体组件系统。并没有通过最好或者最优化的方式去实现这个系统。但是它可以提供一个基础用来更好地理解整个系统如何相互运转和工作。所有的代码都可以在Github上找到。

Entity

The abstraction is that an entity is just an ID; a container of components. Let’s start by creating a function which we can create entities from. Each entity will have just an idand components property. (Note: the follow expects a global ECS object to exist, which looks like var ECS = {};)

这个抽象的概念可以理解为一个实体就是一个ID。一个组件的容器。

/* =========================================================================
 *
 * Entity.js
 *  Definition of our "Entity". Abstractly, an entity is basically an ID.
 *  Here we implement an entity as a container of data (container of components)
 *
 * ========================================================================= */
ECS.Entity = function Entity(){
    // 伪随机ID
    this.id = (+new Date()).toString(16) +
        (Math.random() * 100000000 | 0).toString(16) +
        ECS.Entity.prototype._count;

    // 计数器递增,实体数量增加
    ECS.Entity.prototype._count++;

//组件的数据
    this.components = {};

    return this;
};
// 实体数量初始化
ECS.Entity.prototype._count = 0;

ECS.Entity.prototype.addComponent = function addComponent ( component ){
    // 添加一个组件到实体
    this.components[component.name] = component;
    return this;
};
ECS.Entity.prototype.removeComponent = function removeComponent ( componentName ){
    // 通过移除引用来移除实体上的组件
    // 允许使用任意组件的方法或者名称

    var name = componentName; 

    if(typeof componentName === ‘function‘){
        // 获取组件方法的名称
        name = componentName.prototype.name;
    }

    delete this.components[name];
    return this;
};

ECS.Entity.prototype.print = function print () {
    // 打印实体信息
    console.log(JSON.stringify(this, null, 4));
    return this;
};

To create a new entity, we’d simply call it like: var entity = new ECS.Entity();.

There’s not a lot going on in the code here. First, in the function itself, we generate an ID based on the current time, a call to Math.random(), and a counter based on the total number of created entities. This ensures we get a unique ID for each entity. We increment the counter (prototype properties are shared across all object instances; sort of similar to a class variable). Then, we create an empty object to stick components (the data) in.

We expose an addComponent and removeComponent function on the prototype (again, single functions in memory shared across all object instances). Each take in a component object and add or remove the passed in component from the Entity. Lastly, the print method simply JSON-ifies the entity, providing all the data. We could use this to dump out and reload data later (e.g., saving).

So, at the core, an Entity is little more than an object with some data properties. We’ll cover how to create multiple Entities soon, and where assemblages fit in.

创建一个新的实体,我们可以简单地利用如下的形式进行调用:var entity=new ECS.Entity();

这里并不需要大量的代码。首先,在这个方法本身,我们生成一个ID基于当前的时间,并通过随机函数和一个计数器来创造实体。这样可以保证更我们获取一个独一无二的ID对每个实体。(译者注:GUID方式应该会更方便)我们累加计数器之后,我们创建一个空的物体作为组件并初始化。

我们暴露addComponent和removeComponent的方法。每个实体都可以添加或者移除在实体上的组件。最后,将整个实体的信息以JSON的格式进行打印,提供所有的数据。我们就可以将这些数据导出并且未来重新获取数据。

因此对于实体来说,一个实体更近似于一个对象附带一些数据属性。接下来我们将了解如何创建多个实体对象,并且让他们可以通过装配的方式来使用。

Component

Here’s where the data part of data driven programming kicks in. I’ve structured components similarly to entities; for example:

下面是可以使数据驱动生效的程序代码。我通过结构化构建的组件与实体相似。

ECS.Components.Health = function ComponentHealth ( value ){

value = value || 20;

this.value = value;

return this;

};

ECS.Components.Health.prototype.name = ‘health’;

To get a Health component, you’d simply create it with new ECS.Components.Health( VALUE )where VALUE is an optional starting value (20 if nothing is passed in). Importantly, there is a name property on the prototype which tells the Entity what to call the component. For example, to create an entity then give it a health component:

为了获取生命的组件,你可以简单通过new ECS.Components.Health(VALUE)来创建 VALUE是一个可选的初始化数值。更为重要的是,这里有一个名称的属性可以告诉实体如何调用组件。下面我们来创建一个实体并给他赋予一个生命组件。

var entity = new ECS.Entity();

entity.addComponent( new ECS.Components.Health() );

That’s all that is required to add a component to an entity. If we printed the entity out now (entity.print();), we’d see something like:

如果我们现在打印出实体的信息,就可以看到。

{ “id”: “1479f3d15bd4bf98f938300430178”,

“components”: {

“health”: {

“value”: 20

}

}

}

That’s it - it’s just data! We could change the entity’s health by modifying it directly, e.g., entity.components.health.value = 40; We can have any kind of data nesting we want; for example, if we created a position component with x and y data values, we’d get as output:

这仅仅是数据!我们可以改变实体的生命通过直接修改,例如: entity.components.health.value=40;我们可以将任何我们希望的数据嵌入进去。例如,我们可以对坐标组件嵌入x和y数据,我们获得下面的数据

{

“id”: “1479f3d15bd4bf98f938300430178”,

“components”: {

“health”: {

“value”: 20

},

“position”: {

“x”: 426,

“y”: 98

}

}

}

Since components are just data, that don’t have any logic. (Depending on what works for you, you could add some prototype functions to components that would aide in data calculations, but it’s helpful to view components just as data). So we have a bunch of data now, but to do anything interesting we need to run operations on it. That’s where Systemscome in.

因为组件仅仅是数据,不需要任何的逻辑。(当然这是由你要做什么样的工作所决定的,你可以添加一些方法到组件上可以进行数据的计算,但是最好的是将组件仅仅视为数据)。因此我们需要将数据进行打包,但是要做任何有趣的事情的话,我们就需要在系统中进行运行和操作。

/* =========================================================================
 *
 * Components.js
 *  This contains all components for the tutorial (ideally, components would
 *  each live in their own module)
 *
 *  Components are just data.
 *
 * ========================================================================= */

// 外观
// --------------------------------------
ECS.Components.Appearance = function ComponentAppearance ( params ){
    // 外观具体定义的数据类似于颜色和大小
    params = params || {};

    this.colors = params.colors;
    if(!this.colors){
        // 随机颜色和大小
        this.colors = {
            r: 0,
            g: 100,
            b: 150
        };
    }

    this.size = params.size || (1 + (Math.random() * 30 | 0));

    return this;
};
ECS.Components.Appearance.prototype.name = ‘appearance‘;

// 生命
// --------------------------------------
ECS.Components.Health = function ComponentHealth ( value ){
    value = value || 20;
    this.value = value;

    return this;
};
ECS.Components.Health.prototype.name = ‘health‘;

// 坐标
// --------------------------------------
ECS.Components.Position = function ComponentPosition ( params ){
    params = params || {};

    // Generate random values if not passed in
    // NOTE: For the tutorial we‘re coupling the random values to the canvas‘
    // width / height, but ideally this would be decoupled (the component should
    // not need to know the canvas‘s dimensions)
    this.x = params.x || 20 + (Math.random() * (ECS.$canvas.width - 20) | 0);
    this.y = params.y || 20 + (Math.random() * (ECS.$canvas.height - 20) | 0);

    return this;
};
ECS.Components.Position.prototype.name = ‘position‘;

// 用户控制
// --------------------------------------
ECS.Components.PlayerControlled = function ComponentPlayerControlled ( params ){
    this.pc = true;
    return this;
};
ECS.Components.PlayerControlled.prototype.name = ‘playerControlled‘;

// 碰撞器
// --------------------------------------
ECS.Components.Collision = function ComponentCollision ( params ){
    this.collides = true;
    return this;
};
ECS.Components.Collision.prototype.name = ‘collision‘;

System

Systems run your game’s logic. They take in entities and run operations on entities that have specific components the system requires. This way of thinking is a bit inverted from typical Class based programming.

系统是用来运行整个游戏的逻辑。它可以调用实体并且操作运行系统所需要的实体中的具体组件。这种思维方式比较类似于基于类编程中的典型-依赖倒置原则。

In Class based programming, to model a cat, a Cat Class would exist. You’d create cat objects and to get the cat to meow, you’d call the speak() method. The functionality lives inside of the object. The object is not just data, but also functionality.

在基于类的编程中,建立一只猫的模型,需要一个猫类型的类。你可以创建一个猫的对象并且可以让猫去发出叫声,你需要调用speak()的方法。这个函数存在于对象之中。这个对象不仅是数据,还是方法。

With ECS, to model a Cat you’d first create an entity. Then, you’d add some components that cats have (size, name, etc.). If you wanted the entity to be able to meow, maybe you’d give it a speak component with a value of “meow”. The distinction here though is that this is just data - maybe it looks like:

而对于ECS模式来说,对猫进行建模,你可以先创建一个实体。然后你将一些组件加到猫这个实体身上。如果你希望实体可以拥有叫的方法,你可以给他一个叫的组件。这里看起来还仅仅只是数据。

{

“id”: “f279f3d85bd4bf98f938300430178”,

“components”: {

“speak”: {

“sound”: “meeeooowww”

}

}

}

The entity can do nothing by itself. So, to get a “cat” entity to speak, you’d use a speakSystem. (Note: the component name and system name do not have to be 1:1, this is just an example. Most systems use multiple different components). The system would look for all entities that have a speak component, then run some logic - plugging in the entity’s data.

实体自己本身是不可以做任何操作的。因此为了可以让猫的实体说话,你可以使用对话系统。(注:组件名称和系统名称不需要一一对应,这仅仅只是一个例子。大部分的系统使用多种不同的组件)。系统可以获取到所有的实体并有一个说话的组件,然后可以运行一些逻辑-封在实体中的数据。

The functionality happens in Systems, not on the objects themselves. You’ll have many different systems that are tailored for your game. Systems are where your main game logic lives. For our rectangle eating game, we only need a few systems: collision, decay, render, and userInput.

出现在系统中的方法,不是定义在对象自身。你可以有许多不同的系统来定制你自己的游戏。系统存在于你主要的游戏逻辑之中。对于我们的吃方块游戏来说。我们只需要几个系统:碰撞系统,衰减系统,渲染系统和用户输入系统。

The way I’ve sturctured systems is to take in all entities (here, the entities are an object of key:value pairs of entityId: entityObject). Let’s take a look at a snippet of the rendersystem. Note, the systems are just functions that take in entities.

我们构建的结构化系统用来包含所有的实体(这里,实体是对象的key)。下面我们来看一下渲染系统的片段。

ECS.systems.render = function systemRender ( entities ) {
 // Here, we‘ve implemented systems as functions which take in an array of
 // entities. An optimization would be to have some layer which only
 // feeds in relevant entities to the system, but for demo purposes we‘ll
 // assume all entities are passed in and iterate over them.

 // This happens each tick, so we need to clear out the previous rendered
 // state
 clearCanvas();

 var curEntity, fillStyle; 

 // iterate over all entities
 for( var entityId in entities ){
 curEntity = entities[entityId];

 // Only run logic if entity has relevant components
 //
 // For rendering, we need appearance and position. Your own render
 // system would use whatever other components specific for your game
 if( curEntity.components.appearance && curEntity.components.position ){

 // Build up the fill style based on the entity‘s color data
 fillStyle = ‘rgba(‘ + [
 curEntity.components.appearance.colors.r,
 curEntity.components.appearance.colors.g,
 curEntity.components.appearance.colors.b
 ];

 if(!curEntity.components.collision){
 // If the entity does not have a collision component, give it
 // some transparency
 fillStyle += ‘,0.1)‘;
 } else {
 // Has a collision component
 fillStyle += ‘,1)‘;
 }

 ECS.context.fillStyle = fillStyle;

 // Color big squares differently
 if(!curEntity.components.playerControlled &&
 curEntity.components.appearance.size > 12){
 ECS.context.fillStyle = ‘rgba(0,0,0,0.8)‘;
 }

 // draw a little black line around every rect
 ECS.context.strokeStyle = ‘rgba(0,0,0,1)‘;

 // draw the rect
 ECS.context.fillRect(
 curEntity.components.position.x - curEntity.components.appearance.size,
 curEntity.components.position.y - curEntity.components.appearance.size,
 curEntity.components.appearance.size * 2,
 curEntity.components.appearance.size * 2
);
 // stroke it
 ECS.context.strokeRect(
 curEntity.components.position.x - curEntity.components.appearance.size,
 curEntity.components.position.y - curEntity.components.appearance.size,
 curEntity.components.appearance.size * 2,
 curEntity.components.appearance.size * 2
);
 }
 }
 };

The logic here is simple. First, we clear the canvas before doing anything. Then, we iterate over all entities. This system renders entities, but we only care about entities that have an appearance and position. In our game, all entities have these components - but if we wanted to create invisible rectangles that the player could interact with, all we’d have to do is remove the appearance component. So, after we’ve found the entities which contain the relevant data for the system, we can do operations on them.

这里的逻辑非常简单。首先,我们在做所有操作之前需要清空画布canvas.然后,我们需要遍历每一个实体。这个系统用来渲染实体,但是我们仅仅关心实体可以有一个外观以及一个坐标。在这个游戏之中,所有的实体都需要有这些组件,但是我们希望创建一个不可见的方块可以使得用户进行交互,我们仅仅需要的是移除这个实体的外观组件。因此,在我们找到在系统中包涵相关数据的实体,我们可以对它们进行操作。

In this system, we just render the entity based on the colors properties in the appearancecomponent. One benefit too is that we don’t have to set all the appearance properties here - we might set some in the collision system, or in the health system, or in the decay system; we have complete flexibility over what roles we want to assign to each system. Because the systems are driven by data, we don’t have to limit our thinking to just “methods on classes and objects.” We can have as many systems as want, as complex or simple as we want that target whatever kinds of entities we want.

在这个系统之中,我们仅仅渲染实体基于颜色属性在外观组件之中。一个优势是在这里我们不需要设置所有的外观属性-我们也可以设置一些属性在碰撞系统,或者在生命系统,亦或是衰减系统。这样在给每个系统分配角色时更具灵活性。因为系统是依靠数据来驱动的,我们不会将想法限制于类和对象的方法。我们可以创造尽可能多的系统,不论是复杂的还是简单的亦或是各种各样的实体。

Overview of Rectangle Eater’s systems:

构建吃方块系统的概览

?collision: Handles collision, updating the data for health on collision, and removing / adding new entities (rectangles). Bulk of the game’s logic.

?decay: Handles rectangles getting small and losing health. Any entity with a health component (e.g., most rectangles and the player controlled rectangle) will be affected. This is where a lot of the “fun” configuration happens. If the rectangle decays and goes away too fast then the game is too hard - if it’s too slow, it’s not fun.

?render: Handles rendering entities based on their appearance component.

?userInput: Allows the player to move around entities that have a PlayerControlledcomponent.

?碰撞器:处理碰撞,更新生命值的数据通过碰撞,并且可以移除或是添加新的实体(方块)。主要游戏逻辑。

?衰减:处理方块最小以及会丢失的生命值。任何实体都有一个生命值的组件都将会受到影响。(例如,一般方块和玩家控制方块)这里可以配置很多有趣的属性。如果方块衰减以及消逝的速度太快会使得游戏太难-如果太慢,又会很无聊。

?渲染:处理渲染实体基于他们外观组件。

?用户输入:允许玩家移动实体并拥有一个玩家控制器组件。

In the collision system of our game, we check for collisions between the user controlled entity (specified by a PlayerControlled component and handled via the userInput system) and all other entities with a collision component. If a collision occurs, we update the entity’s health (via the health component), remove the collided entity immediately (the systems are data driven - there’s no problem dynamically adding or removing entities on a per system level), then finally randomly add some new rectangles (most of these will decay over time, and when they get smaller they give you more health when you collide with them - something else we check for in this collision system).

在我们游戏的碰撞系统中,我们检测碰撞器在用户控制实体和其他所有带有碰撞组件的实体之间是否存在碰撞关系。如果一个碰撞出现,我们更新实体的生命值,并且立刻移除被碰撞到的实体,最后随机添加多个新的方块(方块衰减时间以及他们获取最小的生命值你可以自己设置以及碰撞到他们时的参数-其它的一些参数我们需要在这个碰撞系统中进行检测)。

Like in a normal game loop, the order which the systems gets called is also important. If your render before the decay system is called, for example, you’ll always be a tick behind.

一般游戏循环中,系统调用的顺序也很重要。如果你在衰减系统调用之前进行渲染,你将总会看到那一帧之前的情形。

Gluing it all together

The final step involves connecting all the pieces together. For our game, we just need to do a few things:

1.Setup the initial entities

2.Setup the order of the systems which we want to use

3.Setup a game loop, which calls each system and passed in all the entities

4.Setup a lose condition

1 Let’s take a look at the code for setting up the initial entities and the player entity:

最后一步包涵将所有部分进行有机的结合在一起。对于这个游戏来说,我们仅仅需要做以下几步:

1.设置初始化实体

2.设置希望我们调用的系统顺序

3.启动游戏循环主循环可以调用每个系统以及所有实体

4.设置一个失败状态

下面让我们一起通过代码来设置初始化的实体和玩家实体

var self = this;
 var entities = {}; // object containing { id: entity  }
 var entity;

 // Create a bunch of random entities
 for(var i=0; i < 20; i++){
 entity = new ECS.Entity();
 entity.addComponent( new ECS.Components.Appearance());
 entity.addComponent( new ECS.Components.Position());

 // % chance for decaying rects
 if(Math.random() < 0.8){
 entity.addComponent( new ECS.Components.Health() );
 }

 // NOTE: If we wanted some rects to not have collision, we could set it
 // here. Could provide other gameplay mechanics perhaps?
 entity.addComponent( new ECS.Components.Collision());

 entities[entity.id] = entity;
 }

 // PLAYER entity
 // ----------------------------------
 // Make the last entity the "PC" entity - it must be player controlled,
 // have health and collision components
 entity = new ECS.Entity();
 entity.addComponent( new ECS.Components.Appearance());
 entity.addComponent( new ECS.Components.Position());
 entity.addComponent( new ECS.Components.Collision() );
 entity.addComponent( new ECS.Components.PlayerControlled() );
 entity.addComponent( new ECS.Components.Health() );

 // we can also edit any component, as it‘s just data
 entity.components.appearance.colors.g = 255;
 entities[entity.id] = entity;

 // store reference to entities
 ECS.entities = entities;

Note how we can modify any of the component data directly. It’s all data that can be manipulated however and whenever you want! The player entity step could be even further simplified by using assemblages, which are basically entity templates. For instance (using our assemblages):

我们可以直接修改任何组件的数值。这里所有的数据任何时间你都可以进行操作!玩家实体设置通过使用装配可以更为简化,基于最基本的实体模板。

entity = new ECS.Assemblages.CollisionRect();

entity.addComponent( new ECS.Components.Health());

entity.addComponent( new ECS.Components.PlayerControlled() );

2 Next, we setup the order of the systems:

// Setup systems

// ———————————-

// Setup the array of systems. The order of the systems is likely critical,

// so ensure the systems are iterated in the right order

var systems = [

ECS.systems.userInput,

ECS.systems.collision,

ECS.systems.decay,

ECS.systems.render

];

3 Then, a simple game loop

// Game loop

// ———————————-

function gameLoop (){

// Simple game loop

for(var i=0,len=systems.length; i < len; i++){

// Call the system and pass in entities

// NOTE: One optimal solution would be to only pass in entities

// that have the relevant components for the system, instead of

// forcing the system to iterate over all entities

systemsi;

}

// Run through the systems.

// continue the loop

if(self._running !== false){

requestAnimationFrame(gameLoop);

}

}

// Kick off the game loop

requestAnimationFrame(gameLoop);

4 Finally, a lose condition

// Lose condition

// ———————————-

this._running = true; // is the game going?

this.endGame = function endGame(){

self._running = false;

document.getElementById(‘final-score’).innerHTML = +(ECS.$score.innerHTML);

document.getElementById(‘game-over’).className = ”;

// set a small timeout to make sure we set the background

setTimeout(function(){

document.getElementById(‘game-canvas’).className = ‘game-over’;

}, 100);

};

Conclusion

Programming is a complex endeavor by nature. We program in abstractions, and different frameworks for thinking can make certain problems easier to solve. Data driven programming is one framework for thinking of how to write programs. It’s not the best fit for all problems, but can make some problems much easier to solve and understand. Thinking of code as data is a powerful concept. Entity Component System is a pattern that fits game development well. Try taking the passenger seat. Try letting data drive your code.

编程的本质非常复杂且需要长时间的努力。我们借用抽象的思想以及不同的框架来思考如何使得问题变得更容易去解决。以数据为驱动的编程是一个思考如何编程的框架。他并不适用于所有问题,但是可以使得一些问题变得更简单去解决并理解。将代码作为数据来思考是一个非常不错的想法。实体组件系统是一个模式可以很好地适用于游戏开发。尝试用数据来驱动代码吧!

那么因为翻译的比较快,所以有些地方难免有些问题,如果发现希望大家多多指正。下一篇我将会分析一下这个游戏源码以及ECS模式的框架图。

时间: 2024-10-05 20:22:18

如何通过实体组件系统在Javascript中构建游戏的相关文章

如何在cocos2d-x中使用ECS(实体-组件-系统)架构方法开发一个游戏?

引言 在我的博客中,我曾经翻译了几篇关于ECS的文章.这些文章都是来自于Game Development网站.如果你对这个架构方式还不是很了解的话,欢迎阅读理解 组件-实体-系统和实现 组件-实体-系统. 我发现这个架构方式,是在浏览GameDev上的文章的时候了解到的.很久以前,就知道了有这么个架构方法,只是一直没有机会自己实践下.这一次,我就抽空,根据网上对ECS系统的讨论,采用了一种实现方法,来实现一个. 我很喜欢做游戏,所以同样的,还是用游戏实例来实践这个架构方法.我将会采用cocos2

《Entity Framework 6 Recipes》中文翻译系列 (20) -----第四章 ASP.NET MVC中使用实体框架之在MVC中构建一个CRUD示例

翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第四章  ASP.NET MVC中使用实体框架 ASP.NET是一个免费的Web框架,它支持3种不同的技术来创建websites(网站)和Web应用:他们分别是,Web Pages,Web Forms,和MVC.虽然MVC是一种非常流行的,有完整的用于软件开发模式理论的技术,但它在ASP.NET中却是一种新的技术. 目前最新的版本是2012年发布的ASP.NET MVC4.自从2008年发布

JavaScript中的原型和继承

请在此暂时忘记之前学到的面向对象的一切知识.这里只需要考虑赛车的情况.是的,就是赛车. 最近我正在观看 24 Hours of Le Mans ,这是法国流行的一项赛事.最快的车被称为 Le Mans 原型车.这些车虽然是由"奥迪"或"标致"这些厂商制造的,可它们并不是你在街上或速公路上所见到的那类汽车.它们是专为参加高速耐力赛事而制造出来的. 厂家投入巨额资金,用于研发.设计.制造这些原型车,而工程师们总是努力尝试将这项工程做到极致.他们在合金.生物燃料.制动技术

【转】JavaScript中的原型和继承

请在此暂时忘记之前学到的面向对象的一切知识.这里只需要考虑赛车的情况.是的,就是赛车. 最近我正在观看 24 Hours of Le Mans ,这是法国流行的一项赛事.最快的车被称为 Le Mans 原型车.这些车虽然是由“奥迪”或“标致”这些厂商制造的,可它们并不是你在街上或速公路上所见到的那类汽车.它们是专为参加高速耐力赛事而制造出来的. 厂家投入巨额资金,用于研发.设计.制造这些原型车,而工程师们总是努力尝试将这项工程做到极致.他们在合金.生物燃料.制动技术.轮胎的化合物成分和安全特性上

warning MSB3162: 所选的“Microsoft Report Viewer 2012 Runtime”项需要“Microsoft.SqlServer.SQLSysClrTypes.11.0”。在“系统必备”对话框中选择缺少的系统必备组件,或者为缺少的系统必备组件创建引导程序包。

warning MSB3162: 所选的"Microsoft Report Viewer 2012 Runtime"项需要"Microsoft.SqlServer.SQLSysClrTypes.11.0".在"系统必备"对话框中选择缺少的系统必备组件,或者为缺少的系统必备组件创建引导程序包. 发布ReportViewer程序遇到这个问题. 到处查了下,发现解决方法是这样的. 1)打开这个路径下的xml文件: C:\Program Files (x

【转】十个JavaScript中易犯的小错误,你中了几枪?

在今天,JavaScript已经成为了网页编辑的核心.尤其是过去的几年,互联网见证了在SPA开发.图形处理.交互等方面大量JS库的出现. 如果初次打交道,很多人会觉得js很简单.确实,对于很多有经验的工程师,或者甚至是初学者而言,实现基本的js功能几乎毫无障碍.但是JS的真实功能却比很多人想象的要更加多样.复杂.JavaScript的许多细节规定会让你的网页出现很多意想不到的bug,搞懂这些bug,对于成为一位有经验的JS开发者很重要. 常见错误一:对于this关键词的不正确引用 我曾经听一位喜

C++、Java、JavaScript中的日志(log)

编程思想之日志记录 什么是log? 相信你一定用日记写过点滴心事,或是用空间.微信.微博刷着动态,记录你每天的喜怒哀乐!在程序中也有一种类似的东西,记录着他主人(应用程序)每天的行踪,他叫日志(log).日记--是人类生活的记事本,日志(log)--是程序运行状况的记事本. 顾名思义,日志(log,后面均以log称之)就是用来记录程序每天的运行状况的,比如程序出现异常的情况,或是某个关键点,功某个重要的数据或交易等.这里的每天不是说每天一记,可以是伴随着程序运行的始终,只要程序在运行着就一直在记

详解JavaScript中的Url编码/解码,表单提交中网址编码

本文主要针对URI编解码的相关问题做了介绍,对Url编码中哪些字符需要编码.为什么需要编码做了详细的说明,并对比分析了Javascript 中和 编解码相关的几对函数escape / unescape,encodeURI / decodeURI和 encodeURIComponent / decodeURIComponent. 预备知识 foo://example.com:8042/over/there?name=ferret#nose \_/ \______________/ \_______

十个JavaScript中易犯的小错误,你中了几枪?

序言 在今天,JavaScript已经成为了网页编辑的核心.尤其是过去的几年,互联网见证了在SPA开发.图形处理.交互等方面大量JS库的出现. 如果初次打交道,很多人会觉得js很简单.确实,对于很多有经验的工程师,或者甚至是初学者而言,实现基本的js功能几乎毫无障碍.但是JS的真实功能却比很多人想象的要更加多样.复杂.JavaScript的许多细节规定会让你的网页出现很多意想不到的bug,搞懂这些bug,对于成为一位有经验的JS开发者很重要. 常见错误一:对于this关键词的不正确引用 我曾经听