通用组件

Crafty 有一些内置的组件,但创建自己的组件是一种很好的组织你游戏的方式。

在几个例子中,我们创建了彩色方块,并且我们重复了很多代码。例如,下面的代码创建两个正方形:

var sq1 = Crafty.e("2D, Canvas, Color")
    .attr({x:10, y:10, w:30, h:30})
    .color("red");
var sq1 = Crafty.e("2D, Canvas, Color")
    .attr({x:150, y:100, w:30, h:30})
    .color("green");

当然,你可以创建一个方法来方便地创建一个方块。但 Crafty 的方法是使用组件。定义一个组件使用 Crafty.c

Crafty.c("Square", {
    // This function will be called when the component is added to an entity
    // So it sets up the things that both our entities had in common
    init: function() {
        this.addComponent("2D, Canvas, Color");
        this.w = 30;
        this.h = 30;
    },

    // This function will be called when the component is removed from an entity
    // or right before entity is destroyed.
    // Useful for doing custom cleanup.
    remove: function() {
        // This function serves for logging.
        // Once your game is release ready you can disable logging
        // by setting Crafty.loggingEnabled to false
        Crafty.log('Square was removed!');
    },

    // Our two entities had different positions, 
    // so we define a method for setting the position
    place: function(x, y) {
        this.x = x;
        this.y = y;

        // There's no magic to method chaining.
        // To allow it, we have to return the entity!
        return this;
    }
})

Crafty.c 需要两个参数: 第一个参数是组件的名字, 第二个参数是一个对象,用来定义方法和属性。当一个组件添加到实体,它的所有属性和方法都会拷贝到这个实体上。 The init 方法是比较特殊的,它在组件添加到实体时被调用。你也可以定义一个 remove 函数,它会在实体移除组件或者实体销毁时被调用。

根据这些定义,我们来从新组织一下我们的代码:

var sq1 = Crafty.e("Square")
    .place(10, 10)
    .color("red");

var sq2 = Crafty.e("Square")
    .place(150, 100)
    .color("green");

我们已经抽取了这两个实体的共同点,并把它放在我们组件的 init 函数中。这是一种在实体间重用代码的常见方法。在某种程度上,它有点像"Square"继承了其他组件的方法;通过给一个实体添加"Square",我们自动地得到所有"Color"的方法。

记住,方法的链式调用(调用 e.place().color())只是因为我们明确地将 this 从我们的自定义方法返回。忘记这样做可能是错误的常见来源,所以如果您难以确定 “方法未定义” 的错误根源时,请记住这一点。

添加组件的简写方法

要快速声明在自定义组件初始化之前需要添加到实体中的其他组件的列表,可以使用 required 字段:

Crafty.c("Square", {
    // These components will be added to any entity with the "Square" component before it is initialized
    required: "2D, Canvas, Color"
});

绑定事件的简写方法

初始化组件时绑定事件,移除组件时解绑事件,这是很常见的。为了简化操作,你可以在组件对象中直接声明事件处理函数:

Crafty.c("Square", {
    required: "2D, Canvas, Color",

    // These handlers will be bound upon init, and unbound when the component is removed
    events: {
        // bind the given function to the blush event
        "Blush": function() {
            this.color("pink");
        },

        // Bind the named function to the "Blanch" event.
        "Blanch": "turnWhite"
    },

    turnWhite: function(){
        this.color("white");
    }
});

你可以使用一个函数对象,也可以使用组件具有的函数名称。(当你在超过一个的事件上下文中引用方法时后一种方式通常更有用。)

实现细节

有时候你可能需要知道组件是如何添加到实体。(如果组件之前已经添加到实体,再次添加则不产生任何作用)

  • 首先一个表示组件已添加的标识会被设置。
  • 然后组件的所有属性会被拷贝到实体,如果属性已经存在则覆盖。
  • 如果属性是对象或者数组,则只会拷贝引用。
  • 最后,组件的 init 方法会被调用。

共享对象陷阱

如上所述,对象和数组是通过引用复制的。如果你不小心,这会引起意想不到的行为:

Crafty.c("MyComponent", {
    sharedObject: {a:1, b:2}
});
var e1 = Crafty.e("MyComponent");
var e2 = Crafty.e("MyComponent");
e1.sharedObject.a = 5;
console.log(e2.sharedObject.a); // Logs 5!

如果你希望一个属性只属于一个对象,不希望在两个实体间共享,解决办法是在 init 方法中创建一个新对象:

Crafty.c("MyComponent", {
    init: function() {
        this.myObject = {a:1, b:2};
    }
});
var e1 = Crafty.e("MyComponent");
var e2 = Crafty.e("MyComponent");
e1.myObject.a = 5;
console.log(e2.myObject.a); // Logs the original value of 1