1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
10 /** yuu-ce - entity/component system for the Yuu engine
12 Game logic in Yuu is implemented via entities and components.
13 Entities (yuu.E) represent "things" in the world, and
14 components (yuu.C) individual properties or abilities of those
15 things. By attaching and detaching components, entities gain
16 and lose those abilities.
18 This system prioritizes for convenience and simplicity over
19 performance. Many common optimizations in E/C systems, like
20 component pooling and array-of-structures, are not
21 implemented. (And are of questionable value in a language like
25 var yT
= this.yT
|| require("./yT");
26 var yf
= this.yf
|| require("./yf");
29 constructor: function () {
30 /** Entity, a shell to customize with components
32 Entities exist as an aggregate of components (yuu.C).
33 They are used for components to talk to each other, or
34 other systems to handle systems in aggregate without
35 caring about the underlying details.
37 Entities expose components in two ways.
39 First, components may have one or more slots; when a
40 component is attached to an entity it slots itself
41 into those properties on that entity. For example, if
42 you attach a Transform component to an entity, you can
43 retrieve it via e.transform.
45 Second, components may have one or more message
46 taps. This allows them to listen for, and respond to,
47 messages sent to the entity. Unlike slots many
48 attached components may have the same tap.
53 this.attach
.apply(this, arguments
);
56 addChild: function (child
) { this.addChildren(child
); },
57 removeChild: function (child
) { this.removeChildren(child
); },
59 addChildren: function () {
60 yf
.stash("parent", this, arguments
);
61 this.children
= this.children
.concat(yf
.slice(arguments
));
64 removeChildren: function () {
65 this.children
= yf
.filter(
66 yf
.lacks
.bind(null, arguments
), this.children
);
67 yf
.stash("parent", null, arguments
);
71 /** Attach a component to this entity.
73 If the entity already has a component in the same slots,
74 an error will be thrown.
76 for (var j
= 0; j
< arguments
.length
; ++j
) {
79 for (i
= 0; i
< c
.SLOTS
.length
; ++i
)
80 if (this[c
.SLOTS
[i
].slot
])
81 throw new Error("Entity already has a " + c
.SLOTS
[i
]);
82 for (i
= 0; i
< c
.SLOTS
.length
; ++i
)
84 for (i
= 0; i
< c
.TAPS
.length
; ++i
)
85 this.taps
[c
.TAPS
[i
]] = (this.taps
[c
.TAPS
[i
]] || [])
93 /** Detach a component from this entity */
94 for (var j
= 0; j
< arguments
.length
; ++j
) {
97 for (i
= 0; i
< c
.SLOTS
.length
; ++i
)
98 if (this[c
.SLOTS
[i
].slot
] !== c
)
99 throw new Error("Entity has a wrong " + c
.SLOTS
[i
]);
100 for (i
= 0; i
< c
.SLOTS
.length
; ++i
)
101 delete this[c
.SLOTS
[i
]];
102 for (i
= 0; i
< c
.TAPS
.length
; ++i
)
103 this.taps
[c
.TAPS
[i
]] = yf
.without(this.taps
[c
.TAPS
[i
]], c
);
109 _message: function (name
, params
) {
110 var taps
= this.taps
[name
];
111 var children
= this.children
;
114 for (i
= 0; i
< taps
.length
; ++i
)
115 taps
[i
][name
].apply(taps
[i
], params
);
116 for (i
= 0; i
< children
.length
; ++i
)
117 children
[i
]._message(name
, params
);
119 message: function (name
) {
120 /** Message components listening on the named tap */
121 this._message(name
, yf
.tail(arguments
));
126 entity
: { value
: null, writable
: true },
127 SLOTS
: { value
: [], configurable
: true },
128 TAPS
: { value
: [], configurable
: true },
130 attached: function (entity
) { },
131 detached: function (entity
) { },
134 yuu
.DataC
= yT(yuu
.C
, {
135 /** A component for random scratch data
137 Storing this in a separate component rather than on the
138 entity directly reduces the chance of naming conflicts and
139 also the number of hidden classes.
142 constructor: function (data
) {
143 Object
.assign(this, data
|| {});
150 yuu
.Animation
= yT(yuu
.C
, {
151 constructor: function (timeline
, params
, completionHandler
, delay
) {
152 this.timeline
= yf
.mapValues(yf
.arrayify
, timeline
);
153 this.params
= params
;
154 this.completionHandler
= completionHandler
;
155 this.keys
= Object
.keys(timeline
)
156 .sort(function (a
, b
) {
157 return +this._lookup(a
) - +this._lookup(b
);
159 this._t1
= +this._lookup(yf
.last(this.keys
)) + 1;
160 this._t
= -(delay
|| 0);
165 attached: function () {
169 _lookup: function (k
) {
170 return (k
in this.params
) ? this.params
[k
] : k
;
173 set1: function (setter
) {
174 var $ = this.params
.$;
175 yf
.ipairs
.call(this, function (k
, v
) {
176 $[k
] = this._lookup(v
);
180 set: function (setters
) {
181 yf
.ipairs
.call(this, function (name
, setter
) {
182 var $ = this._lookup(name
);
183 yf
.ipairs
.call(this, function (k
, v
) {
184 $[k
] = this._lookup(v
);
189 _addTween: function (tweens
, instr
) {
190 var repeat
= instr
.repeat
|| 0;
191 var cycles
= Math
.abs(repeat
) + 1;
192 var easing
= yf
.isFunction(instr
.easing
)
194 : yuu
.Tween
[(instr
.easing
|| "ease").toUpperCase()];
195 var duration
, complete
;
197 if ("complete" in instr
) {
198 complete
= this._lookup(instr
.complete
);
199 duration
= (complete
- this._t
) / cycles
;
200 } else if ("duration" in instr
) {
201 duration
= this._lookup(instr
.duration
);
202 complete
= this._t
+ duration
* cycles
;
205 if (isFinite(cycles
)) {
207 new yuu
.Tween(tweens
, duration
, repeat
, easing
));
208 this._t1
= Math
.max(complete
+ 1, this._t1
);
210 this.entity
.attach(new yuu
.TweenC(
211 tweens
, duration
, repeat
, easing
));
215 tween1: function (tween
, instr
) {
216 var nt
= { $: this._lookup(instr
.$) || this.params
.$ };
217 yf
.ipairs
.call(this, function (k
, v
) {
218 nt
[k
] = [nt
.$[k
], this._lookup(v
)];
220 this._addTween([nt
], instr
);
223 tween: function (targets
, instr
) {
225 yf
.ipairs
.call(this, function (name
, tween
) {
226 var nt
= { $: this._lookup(name
) || this.params
.$ };
227 yf
.ipairs
.call(this, function (k
, v
) {
228 nt
[k
] = [nt
.$[k
], this._lookup(v
)];
232 this._addTween(tweens
, instr
);
235 tweenAll: function (tween
, instr
) {
237 var $s
= this._lookup(instr
.$s
) || this.params
.$s
;
238 yf
.irange
.call(this, function (i
) {
239 var nt
= { $: $s
[i
] };
240 yf
.ipairs
.call(this, function (k
, v
) {
241 nt
[k
] = [nt
.$[k
], this._lookup(v
)[i
]];
245 this._addTween(tweens
, instr
);
248 event: function (name
) {
249 this.params
[name
](this, this.params
);
252 _dispatch: function (instr
) {
254 this.set1(instr
.set1
);
258 this.tween1(instr
.tween1
, instr
);
260 this.tween(instr
.tween
, instr
);
262 this.tweenAll(instr
.tweenAll
, instr
);
264 this.event(instr
.event
);
270 for (var key
= this.keys
[this._pc
];
271 this._lookup(key
) <= t
;
272 key
= this.keys
[++this._pc
]) {
273 yf
.each
.call(this, this._dispatch
, this.timeline
[key
]);
276 for (i
= this._tweens
.length
- 1; i
>= 0; --i
) {
277 if (this._tweens
[i
].tick())
278 this._tweens
.splice(i
, 1);
281 if (++this._t
> this._t1
) {
282 if (this.completionHandler
)
283 this.completionHandler(this);
284 this.entity
.detach(this);
289 for (var i
= this._tweens
.length
- 1; i
>= 0; --i
)
290 this._tweens
[i
].tock(p
);
293 TAPS
: ["tick", "tock"]
297 /** Tween object properties over time
299 This component changes properties over time, and can
300 handle synchronizing multiple objects and multiple
303 The `property` is either a single object with the special
304 `$` property set to the object to tween and every other
305 property set to the properties to tween with values [min,
306 max], or a list of such objects. For example, to tween a.x
307 from 0 to 1, a.y from 2 to 3, and b.z from 1 to 2, you
310 [{ $: a, x: [0, 1], y: [2, 3] },
313 The `duration` is specified in ticks (e.g. calls to
316 `repeat` may be a positive number to repeat the tween that
317 many times, or a negative number to cycle back to the
318 minimum (and then back to the maximum, etc.) that many
319 times. `Infinity` will repeat the tween forever and
320 `-Infinity` will cycle the tween back and forth forever.
322 A custom easing equation may be provided. This is a
323 function which takes a p = [0, 1] and returns the eased p.
326 constructor: function (props
, duration
, repeat
, easing
) {
332 this.duration
= duration
|| 60;
333 this.repeat
= repeat
|| 0;
334 this.easing
= easing
|| yuu
.Tween
.LINEAR
;
335 yf
.each
.call(this, function (oab
) {
336 yf
.ipairs
.call(this, function (name
, ab
) {
338 this._object
.push(oab
.$);
339 this._property
.push(name
);
344 }, yf
.arrayify(props
));
349 var t
= this._count
/ this.duration
;
351 if (t
> 1 && !this.repeat
)
353 else if (t
>= 1 && this.repeat
) {
354 if (this.repeat
< 0) {
362 if (this.repeat
< 0) {
364 } else if (this.repeat
> 0) {
372 var t
= (this._count
+ p
- 1) / this.duration
;
377 _updateAt: function (t
) {
378 var p
= this.easing
? this.easing(t
) : t
;
379 for (var i
= 0; i
< this._object
.length
; ++i
) {
380 // a was the existing property, b was the one provided
381 // by the user. By lerping from b to a, the user can
382 // control the lerp type in some awkward cases -
383 // e.g. CSS DOM values are all exposed as strings so a
384 // will be a string/String, but if b is provided as a
385 // number/Number, this will lerp numerically.
387 // FIXME: This still doesn't work right if the lerp is
388 // later reversed due to negative repeats.
389 var object
= this._object
[i
];
390 var property
= this._property
[i
];
393 object
[property
] = yuu
.lerp(b
, a
, 1 - p
);
398 get: function () { return this._count
; },
399 set: function (v
) { this._count
= Math
.round(v
); },
402 duration
: { value
: 60, chainable
: true },
403 repeat
: { value
: 0, chainable
: true },
404 easing
: { value
: null, chainable
: true },
407 yuu
.TweenC
= yT(yuu
.C
, {
408 constructor: function () {
409 this._tween
= yf
.construct(yuu
.Tween
, arguments
);
413 if (this._tween
.tick())
414 this.entity
.detach();
417 tock
: { proxy
: "_tween.tock" },
418 count
: { alias
: "_tween.count" },
419 duration
: { alias
: "_tween.duration", chainable
: true },
420 repeat
: { alias
: "_tween.repeat", chainable
: true },
421 easing
: { alias
: "_tween.easing", chainable
: true },
423 TAPS
: ["tick", "tock"]
426 yuu
.Tween
.LINEAR
= null;
429 yuu
.Tween
.EASE = function (p
) {
432 This equation is from _Improving Noise_ (Perlin, 2002). It
433 is symmetrical around p=0.5 and has zero first and second
434 derivatives at p=0 and p=1.
436 return p
* p
* p
* (p
* (p
* 6.0 - 15.0) + 10.0);
439 yuu
.Tween
.EASE_IN = function (p
) {
443 yuu
.Tween
.METASPRING = function (amplitude
, pulsation
) {
444 /** A generator for springy tweens
446 The amplitude controls how far from the final position the
447 spring will bounce, as a multiple of the distance between the
448 start and end. A "normal" amplitude is around 0.5 to 1.5.
450 The pulsation constant controls the rigidity of the spring;
451 higher pulsation results in a spring that bounces more quickly
452 and more often during a fixed interval. A "normal" pulsation
453 constant is around 15 to 30.
455 return function (p
) {
456 return 1 + Math
.cos(pulsation
* p
+ Math
.PI
) * (1 - p
) * amplitude
;
460 yuu
.Tween
.STEPPED = function (segments
, alpha
) {
461 return function (p
) {
463 var lower
= Math
.floor(p
);
464 var upper
= Math
.floor((p
+ alpha
));
466 var p1
= 1 - (upper
- p
) / alpha
;
467 return (lower
+ p1
) / segments
;
469 return lower
/ segments
;
474 yuu
.Transform
= yT(yuu
.C
, {
475 /** A 3D position, rotation (as quaternion), and scale
477 This also serves as an object lesson for a simple slotted
480 constructor: function (position
, rotation
, scale
) {
481 this._position
= vec3
.clone(position
|| [0, 0, 0]);
482 this._rotation
= quat
.clone(rotation
|| [0, 0, 0, 1]);
483 this._scale
= vec3
.clone(scale
|| [1, 1, 1]);
484 this._matrix
= mat4
.create();
487 this._parentVersion
= null;
490 SLOTS
: ["transform"],
494 get: function () { return this._position
.slice(); },
495 set: function (v
) { this._dirty
= true;
496 vec3
.copy(this._position
, v
); }
500 get: function () { return this._rotation
.slice(); },
501 set: function (v
) { this._dirty
= true;
502 quat
.normalize(this._rotation
, v
); }
506 get: function () { return this._scale
.slice(); },
507 set: function (v
) { this._dirty
= true;
508 vec3
.copy(this._scale
, v
); }
512 get: function () { return this._position
[0]; },
513 set: function (x
) { this._dirty
= true; this._position
[0] = x
; }
517 get: function () { return this._position
[1]; },
518 set: function (x
) { this._dirty
= true; this._position
[1] = x
; }
522 get: function () { return this._position
[2]; },
523 set: function (x
) { this._dirty
= true; this._position
[2] = x
; }
525 xy
: { swizzle
: "xy", chainable
: true },
529 get: function () { return this._scale
[0]; },
530 set: function (x
) { this._dirty
= true; this._scale
[0] = x
; }
534 get: function () { return this._scale
[1]; },
535 set: function (x
) { this._dirty
= true; this._scale
[1] = x
; }
539 get: function () { return this._scale
[2]; },
540 set: function (x
) { this._dirty
= true; this._scale
[2] = x
; }
543 worldToLocal: function (p
) {
544 var x
= (p
.x
|| p
[0] || 0);
545 var y
= (p
.y
|| p
[1] || 0);
546 var z
= (p
.z
|| p
[2] || 0);
547 var local
= [x
, y
, z
];
548 var matrix
= mat4
.clone(this.matrix
);
549 return vec3
.transformMat4(local
, local
, mat4
.invert(matrix
, matrix
));
552 contains: function (p
) {
553 p
= this.worldToLocal(p
);
554 return p
[0] >= -0.5 && p
[0] < 0.5
555 && p
[1] >= -0.5 && p
[1] < 0.5
556 && p
[2] >= -0.5 && p
[2] < 0.5;
562 var q
= this._rotation
;
563 var x
= q
[0]; var sqx
= x
* x
;
564 var y
= q
[1]; var sqy
= y
* y
;
565 var z
= q
[2]; var sqz
= z
* z
;
567 var abcd
= w
* x
+ y
* z
;
569 return [2 * Math
.atan2(x
, w
), Math
.PI
/ 2, 0];
570 else if (abcd
< -0.499)
571 return [-2 * Math
.atan2(x
, w
), -Math
.PI
/ 2, 0];
573 var adbc
= w
* z
- x
* y
;
574 var acbd
= w
* y
- x
* z
;
575 return [Math
.atan2(2 * adbc
, 1 - 2 * (sqz
+ sqx
)),
577 Math
.atan2(2 * acbd
, 1 - 2 * (sqy
+ sqx
))];
581 set: function (ypr
) {
582 var q
= this._rotation
;
584 quat
.rotateZ(q
, q
, ypr
[0]);
585 quat
.rotateY(q
, q
, ypr
[2]);
586 quat
.rotateX(q
, q
, ypr
[1]);
591 yaw
: { aliasSynthetic
: "ypr[0]", chainable
: true },
592 pitch
: { aliasSynthetic
: "ypr[1]", chainable
: true },
593 roll
: { aliasSynthetic
: "ypr[2]", chainable
: true },
597 var pt
= this.entity
.parent
&& this.entity
.parent
.transform
;
598 var pm
= pt
&& pt
.matrix
;
599 var ptVersion
= pt
&& pt
._version
;
600 if (this._dirty
|| (ptVersion
!== this._parentVersion
)) {
601 var m
= this._matrix
;
603 mat4
.fromRotationTranslation(
604 m
, this._rotation
, this._position
);
605 mat4
.scale(m
, m
, this._scale
);
607 mat4
.multiply(m
, pm
, m
);
610 this._parentVersion
= ptVersion
;
611 this._version
= (this._version
+ 1) | 0;
618 yuu
.Ticker
= yT(yuu
.C
, {
619 /** Set a callback to run every n ticks
621 If the callback returns true, it is rescheduled for
622 execution (like setInterval). If it returns false, this
623 component is removed from the entity.
625 constructor: function (callback
, interval
, delay
) {
626 this.callback
= callback
;
627 this.interval
= interval
;
629 this._count
= -(delay
|| 0);
634 if (this._accum
=== this.interval
) {
636 if (!this.callback(this._count
++))
637 this.entity
.detach(this);
644 }).call(typeof exports
=== "undefined" ? this : exports
,
645 typeof exports
=== "undefined"
646 ? this.yuu
: (module
.exports
= require('./core')));