1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license https://www.gnu.org/licenses/gpl-2.0.html
4 @source: https://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 var animationOps
= [];
152 var Animation
= yuu
.Animation
= yT(yuu
.C
, {
153 constructor: function (timeline
, params
, completionHandler
, delay
) {
154 this.timeline
= yf
.mapValues(yf
.arrayify
, timeline
);
155 this.params
= params
;
156 this.completionHandler
= completionHandler
;
157 this.keys
= Object
.keys(timeline
)
158 .sort(function (a
, b
) {
159 return +this._lookup(a
) - +this._lookup(b
);
161 this._t1
= +this._lookup(yf
.last(this.keys
)) + 1;
162 this._t
= -(delay
|| 0);
167 attached: function () {
171 _lookup: function (k
) {
172 return (k
in this.params
) ? this.params
[k
] : k
;
175 set1: function (setter
) {
176 var $ = this.params
.$;
177 yf
.ipairs
.call(this, function (k
, v
) {
178 $[k
] = this._lookup(v
);
182 set: function (setters
) {
183 yf
.ipairs
.call(this, function (name
, setter
) {
184 var $ = this._lookup(name
);
185 yf
.ipairs
.call(this, function (k
, v
) {
186 $[k
] = this._lookup(v
);
191 _addTween: function (tweens
, instr
) {
192 var repeat
= instr
.repeat
|| 0;
193 var cycles
= Math
.abs(repeat
) + 1;
194 var easing
= yf
.isFunction(instr
.easing
)
196 : yuu
.Tween
[(instr
.easing
|| "ease").toUpperCase()];
197 var duration
, complete
;
199 if ("complete" in instr
) {
200 complete
= this._lookup(instr
.complete
);
201 duration
= (complete
- this._t
) / cycles
;
202 } else if ("duration" in instr
) {
203 duration
= this._lookup(instr
.duration
);
204 complete
= this._t
+ duration
* cycles
;
207 if (isFinite(cycles
)) {
209 new yuu
.Tween(tweens
, duration
, repeat
, easing
));
210 this._t1
= Math
.max(complete
+ 1, this._t1
);
212 this.entity
.attach(new yuu
.TweenC(
213 tweens
, duration
, repeat
, easing
));
217 tween1: function (tween
, instr
) {
218 var nt
= { $: this._lookup(instr
.$) || this.params
.$ };
219 yf
.ipairs
.call(this, function (k
, v
) {
220 nt
[k
] = [nt
.$[k
], this._lookup(v
)];
222 this._addTween([nt
], instr
);
225 tween: function (targets
, instr
) {
227 yf
.ipairs
.call(this, function (name
, tween
) {
228 var nt
= { $: this._lookup(name
) || this.params
.$ };
229 yf
.ipairs
.call(this, function (k
, v
) {
230 nt
[k
] = [nt
.$[k
], this._lookup(v
)];
234 this._addTween(tweens
, instr
);
237 tweenAll: function (tween
, instr
) {
239 var $s
= this._lookup(instr
.$s
) || this.params
.$s
;
240 yf
.irange
.call(this, function (i
) {
241 var nt
= { $: $s
[i
] };
242 yf
.ipairs
.call(this, function (k
, v
) {
243 nt
[k
] = [nt
.$[k
], this._lookup(v
)[i
]];
247 this._addTween(tweens
, instr
);
250 event: function (name
) {
251 this.params
[name
](this, this.params
);
254 _dispatch: function (instr
) {
256 this.set1(instr
.set1
);
260 this.tween1(instr
.tween1
, instr
);
262 this.tween(instr
.tween
, instr
);
264 this.tweenAll(instr
.tweenAll
, instr
);
266 this.event(instr
.event
);
267 animationOps
.forEach(function (f
) {
269 f
.call(this, instr
[f
.name
], instr
);
276 for (var key
= this.keys
[this._pc
];
277 this._lookup(key
) <= t
;
278 key
= this.keys
[++this._pc
]) {
279 yf
.each
.call(this, this._dispatch
, this.timeline
[key
]);
282 for (i
= this._tweens
.length
- 1; i
>= 0; --i
) {
283 if (this._tweens
[i
].tick())
284 this._tweens
.splice(i
, 1);
287 if (++this._t
> this._t1
) {
288 if (this.completionHandler
)
289 this.completionHandler(this);
290 this.entity
.detach(this);
295 for (var i
= this._tweens
.length
- 1; i
>= 0; --i
)
296 this._tweens
[i
].tock(p
);
299 TAPS
: ["tick", "tock"]
302 Animation
.registerOperation = function (f
) {
303 animationOps
.push(f
);
307 /** Tween object properties over time
309 This component changes properties over time, and can
310 handle synchronizing multiple objects and multiple
313 The `property` is either a single object with the special
314 `$` property set to the object to tween and every other
315 property set to the properties to tween with values [min,
316 max], or a list of such objects. For example, to tween a.x
317 from 0 to 1, a.y from 2 to 3, and b.z from 1 to 2, you
320 [{ $: a, x: [0, 1], y: [2, 3] },
323 The `duration` is specified in ticks (e.g. calls to
326 `repeat` may be a positive number to repeat the tween that
327 many times, or a negative number to cycle back to the
328 minimum (and then back to the maximum, etc.) that many
329 times. `Infinity` will repeat the tween forever and
330 `-Infinity` will cycle the tween back and forth forever.
332 A custom easing equation may be provided. This is a
333 function which takes a p = [0, 1] and returns the eased p.
336 constructor: function (props
, duration
, repeat
, easing
) {
342 this.duration
= duration
|| 60;
343 this.repeat
= repeat
|| 0;
344 this.easing
= easing
|| yuu
.Tween
.LINEAR
;
345 yf
.each
.call(this, function (oab
) {
346 yf
.ipairs
.call(this, function (name
, ab
) {
348 this._object
.push(oab
.$);
349 this._property
.push(name
);
354 }, yf
.arrayify(props
));
359 var t
= this._count
/ this.duration
;
361 if (t
> 1 && !this.repeat
)
363 else if (t
>= 1 && this.repeat
) {
364 if (this.repeat
< 0) {
372 if (this.repeat
< 0) {
374 } else if (this.repeat
> 0) {
382 var t
= (this._count
+ p
- 1) / this.duration
;
387 _updateAt: function (t
) {
388 var p
= this.easing
? this.easing(t
) : t
;
389 for (var i
= 0; i
< this._object
.length
; ++i
) {
390 // a was the existing property, b was the one provided
391 // by the user. By lerping from b to a, the user can
392 // control the lerp type in some awkward cases -
393 // e.g. CSS DOM values are all exposed as strings so a
394 // will be a string/String, but if b is provided as a
395 // number/Number, this will lerp numerically.
397 // FIXME: This still doesn't work right if the lerp is
398 // later reversed due to negative repeats.
399 var object
= this._object
[i
];
400 var property
= this._property
[i
];
403 object
[property
] = yuu
.lerp(b
, a
, 1 - p
);
408 get: function () { return this._count
; },
409 set: function (v
) { this._count
= Math
.round(v
); },
412 duration
: { value
: 60, chainable
: true },
413 repeat
: { value
: 0, chainable
: true },
414 easing
: { value
: null, chainable
: true },
417 yuu
.TweenC
= yT(yuu
.C
, {
418 constructor: function () {
419 this._tween
= yf
.construct(yuu
.Tween
, arguments
);
423 if (this._tween
.tick())
424 this.entity
.detach();
427 tock
: { proxy
: "_tween.tock" },
428 count
: { alias
: "_tween.count" },
429 duration
: { alias
: "_tween.duration", chainable
: true },
430 repeat
: { alias
: "_tween.repeat", chainable
: true },
431 easing
: { alias
: "_tween.easing", chainable
: true },
433 TAPS
: ["tick", "tock"]
436 yuu
.Tween
.LINEAR
= null;
439 yuu
.Tween
.EASE = function (p
) {
442 This equation is from _Improving Noise_ (Perlin, 2002). It
443 is symmetrical around p=0.5 and has zero first and second
444 derivatives at p=0 and p=1.
446 return p
* p
* p
* (p
* (p
* 6.0 - 15.0) + 10.0);
449 yuu
.Tween
.EASE_IN = function (p
) {
453 yuu
.Tween
.METASPRING = function (amplitude
, pulsation
) {
454 /** A generator for springy tweens
456 The amplitude controls how far from the final position the
457 spring will bounce, as a multiple of the distance between the
458 start and end. A "normal" amplitude is around 0.5 to 1.5.
460 The pulsation constant controls the rigidity of the spring;
461 higher pulsation results in a spring that bounces more quickly
462 and more often during a fixed interval. A "normal" pulsation
463 constant is around 15 to 30.
465 return function (p
) {
466 return 1 + Math
.cos(pulsation
* p
+ Math
.PI
) * (1 - p
) * amplitude
;
470 yuu
.Tween
.STEPPED = function (segments
, alpha
) {
471 return function (p
) {
473 var lower
= Math
.floor(p
);
474 var upper
= Math
.floor((p
+ alpha
));
476 var p1
= 1 - (upper
- p
) / alpha
;
477 return (lower
+ p1
) / segments
;
479 return lower
/ segments
;
484 yuu
.Transform
= yT(yuu
.C
, {
485 /** A 3D position, rotation (as quaternion), and scale
487 This also serves as an object lesson for a simple slotted
490 constructor: function (position
, rotation
, scale
) {
491 this._position
= vec3
.clone(position
|| [0, 0, 0]);
492 this._rotation
= quat
.clone(rotation
|| [0, 0, 0, 1]);
493 this._scale
= vec3
.clone(scale
|| [1, 1, 1]);
494 this._matrix
= mat4
.create();
497 this._parentVersion
= null;
500 SLOTS
: ["transform"],
504 get: function () { return this._position
.slice(); },
505 set: function (v
) { this._dirty
= true;
506 vec3
.copy(this._position
, v
); }
510 get: function () { return this._rotation
.slice(); },
511 set: function (v
) { this._dirty
= true;
512 quat
.normalize(this._rotation
, v
); }
516 get: function () { return this._scale
.slice(); },
517 set: function (v
) { this._dirty
= true;
518 vec3
.copy(this._scale
, v
); }
522 get: function () { return this._position
[0]; },
523 set: function (x
) { this._dirty
= true; this._position
[0] = x
; }
527 get: function () { return this._position
[1]; },
528 set: function (x
) { this._dirty
= true; this._position
[1] = x
; }
532 get: function () { return this._position
[2]; },
533 set: function (x
) { this._dirty
= true; this._position
[2] = x
; }
535 xy
: { swizzle
: "xy", chainable
: true },
539 get: function () { return this._scale
[0]; },
540 set: function (x
) { this._dirty
= true; this._scale
[0] = x
; }
544 get: function () { return this._scale
[1]; },
545 set: function (x
) { this._dirty
= true; this._scale
[1] = x
; }
549 get: function () { return this._scale
[2]; },
550 set: function (x
) { this._dirty
= true; this._scale
[2] = x
; }
553 worldToLocal: function (p
) {
554 var x
= (p
.x
|| p
[0] || 0);
555 var y
= (p
.y
|| p
[1] || 0);
556 var z
= (p
.z
|| p
[2] || 0);
557 var local
= [x
, y
, z
];
558 var matrix
= mat4
.clone(this.matrix
);
559 return vec3
.transformMat4(local
, local
, mat4
.invert(matrix
, matrix
));
562 contains: function (p
) {
563 p
= this.worldToLocal(p
);
564 return p
[0] >= -0.5 && p
[0] < 0.5
565 && p
[1] >= -0.5 && p
[1] < 0.5
566 && p
[2] >= -0.5 && p
[2] < 0.5;
572 var q
= this._rotation
;
573 var x
= q
[0]; var sqx
= x
* x
;
574 var y
= q
[1]; var sqy
= y
* y
;
575 var z
= q
[2]; var sqz
= z
* z
;
577 var abcd
= w
* x
+ y
* z
;
579 return [2 * Math
.atan2(x
, w
), Math
.PI
/ 2, 0];
580 else if (abcd
< -0.499)
581 return [-2 * Math
.atan2(x
, w
), -Math
.PI
/ 2, 0];
583 var adbc
= w
* z
- x
* y
;
584 var acbd
= w
* y
- x
* z
;
585 return [Math
.atan2(2 * adbc
, 1 - 2 * (sqz
+ sqx
)),
587 Math
.atan2(2 * acbd
, 1 - 2 * (sqy
+ sqx
))];
591 set: function (ypr
) {
592 var q
= this._rotation
;
594 quat
.rotateZ(q
, q
, ypr
[0]);
595 quat
.rotateY(q
, q
, ypr
[2]);
596 quat
.rotateX(q
, q
, ypr
[1]);
601 yaw
: { aliasSynthetic
: "ypr[0]", chainable
: true },
602 pitch
: { aliasSynthetic
: "ypr[1]", chainable
: true },
603 roll
: { aliasSynthetic
: "ypr[2]", chainable
: true },
607 var pt
= this.entity
.parent
&& this.entity
.parent
.transform
;
608 var pm
= pt
&& pt
.matrix
;
609 var ptVersion
= pt
&& pt
._version
;
610 if (this._dirty
|| (ptVersion
!== this._parentVersion
)) {
611 var m
= this._matrix
;
613 mat4
.fromRotationTranslation(
614 m
, this._rotation
, this._position
);
615 mat4
.scale(m
, m
, this._scale
);
617 mat4
.multiply(m
, pm
, m
);
620 this._parentVersion
= ptVersion
;
621 this._version
= (this._version
+ 1) | 0;
628 yuu
.Ticker
= yT(yuu
.C
, {
629 /** Set a callback to run every n ticks
631 If the callback returns true, it is rescheduled for
632 execution (like setInterval). If it returns false, this
633 component is removed from the entity.
635 constructor: function (callback
, interval
, delay
) {
636 this.callback
= callback
;
637 this.interval
= interval
;
639 this._count
= -(delay
|| 0);
644 if (this._accum
=== this.interval
) {
646 if (!this.callback(this._count
++))
647 this.entity
.detach(this);
654 }).call(typeof exports
=== "undefined" ? this : exports
,
655 typeof exports
=== "undefined"
656 ? this.yuu
: (module
.exports
= require('./core')));