UNPKG

21.4 kBJavaScriptView Raw
1import { Constants } from "@croquet/croquet";
2import { m4_identity } from "..";
3// import { Constants } from "@croquet/worldcore-kernel";
4import { PM_Dynamic, GetPawn } from "./Pawn";
5import { v3_zero, q_identity, v3_unit, m4_scaleRotationTranslation, m4_translation, m4_rotationX, m4_multiply, v3_lerp, v3_equals,
6 q_slerp, q_equals, v3_isZero, q_isZero, q_normalize, q_multiply, v3_add, v3_scale, m4_rotationQ, v3_transform, q_euler, TAU, clampRad, q_axisAngle } from "./Vector";
7
8// Mixin
9//
10// This contains support for mixins that can be added to Views and Models. You need to
11// define View and Model mixins slightly differently, but they otherwise use the same
12// syntax.
13//
14// This approach is based on:
15//
16// https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
17// https://github.com/justinfagnani/mixwith.js
18
19
20// -- View Mixins --
21//
22// Mixins are defined as functions that transform a class into an extended version
23// of itself. The "mix" and "with" operators are semantic suger to make the construction
24// of the composite class look nice.
25//
26// Since you don't know what class a mixin will be added to, you should generally set them
27// up so they don't require arguments to their constructors and merely pass any parameter
28// they receive straight through.
29
30
31// -- Example --
32//
33// class Alpha {
34// constructor() {
35// }
36// }
37//
38// const Beta = superclass => class extends superclass {
39// constructor(...args) {
40// super(...args);
41// }
42// };
43//
44// const Gamma = superclass => class extends superclass {
45// constructor(...args) {
46// super(...args);
47// }
48// };
49//
50// class Delta extends mix(Alpha).with(Beta, Gamma) {
51// constructor() {
52// super();
53// }
54// }
55
56
57// -- Model Mixins --
58//
59// Model mixins work just like View Mixins, but you need to define an init function instead
60// of a constructor. Also you need to call RegisterMixin after you define them so they get
61// added to the hash of the model code.
62
63
64// -- Example --
65//
66// const ModelBeta = superclass => class extends superclass {
67// init(...args) {
68// super.init(...args);
69// }
70// };
71// RegisterMixin(ModelBeta);
72
73
74//-- Inheritance --
75//
76// Mixins can "inherit" from other mixins by including the parent function in the child's extension
77// definition:
78//
79// const ChildMixin = superclass => class extends ParentMixin(superclass) {};
80
81//------------------------------------------------------------------------------------------
82//-- Mixin ---------------------------------------------------------------------------------
83//------------------------------------------------------------------------------------------
84
85Constants.WC_MIXIN_REGISTRY = [];
86Constants.WC_MIXIN_USAGE = [];
87
88export const mix = superclass => new MixinFactory(superclass);
89export const RegisterMixin = mixin => Constants.WC_MIXIN_REGISTRY.push(mixin);
90
91class MixinFactory {
92 constructor(superclass) {
93 this.superclass = superclass;
94 }
95
96 with(...mixins) {
97 Constants.WC_MIXIN_USAGE.push(mixins);
98 return mixins.reduce((c, mixin) => mixin(c), this.superclass);
99 }
100}
101
102//------------------------------------------------------------------------------------------
103//-- Tree ----------------------------------------------------------------------------------
104//------------------------------------------------------------------------------------------
105
106// Tree actors can be put in a hierarchy with parents and children. The tree pawns maintain
107// their own hierarchy that mirrors the actor hierarchy.
108
109//-- Actor ---------------------------------------------------------------------------------
110
111export const AM_Tree = superclass => class extends superclass {
112
113 init(...args) {
114 this.listen("_parent", this.onChangeParent);
115 super.init(...args);
116 }
117
118 destroy() {
119 new Set(this.children).forEach(child => child.destroy());
120 this.set({parent: null});
121 super.destroy();
122 }
123
124 onChangeParent(d) {
125 if (d.o) d.o.removeChild(this);
126 if (d.v) d.v.addChild(this);
127 }
128
129 get parent() { return this._parent; }
130
131 addChild(c) { // This should never be called directly, use set instead
132 if (!this.children) this.children = new Set();
133 this.children.add(c);
134 }
135
136 removeChild(c) { // This should never be called directly, use set instead
137 if (this.children) this.children.delete(c);
138 }
139
140};
141RegisterMixin(AM_Tree);
142
143//-- Pawn ----------------------------------------------------------------------------------
144
145export const PM_Tree = superclass => class extends superclass {
146
147 constructor(...args) {
148 super(...args);
149 if (this.actor.parent) {
150 const parent = GetPawn(this.actor.parent.id);
151 parent.addChild(this.actor.id);
152 }
153 this.listen("_parent", this.onChangeParent);
154 }
155
156 get parent() {
157 if (this.actor.parent && !this._parent) this._parent = GetPawn(this.actor.parent.id);
158 return this._parent;
159 }
160
161 get children() {
162 if (this.actor.children && !this._children) this.actor.children.forEach(child => { this.addChild(child.id); })
163 return this._children;
164 }
165
166 onChangeParent(d) {
167 if (d.o) {
168 GetPawn(d.o.id).removeChild(this.actor.id);
169 }
170 if (d.v) {
171 GetPawn(d.v.id).addChild(this.actor.id);
172 }
173 }
174
175 addChild(id) {
176 const child = GetPawn(id);
177 if (!child) return;
178 if (!this._children) this._children = new Set();
179 this._children.add(child);
180 child._parent = this;
181 }
182
183 removeChild(id) {
184 const child = GetPawn(id);
185 if (!child) return;
186 if (this._children) this._children.delete(child);
187 child._parent = null;
188 }
189};
190
191//------------------------------------------------------------------------------------------
192//-- Spatial -------------------------------------------------------------------------------
193//------------------------------------------------------------------------------------------
194
195// Spatial actors have a translation, rotation and scale in 3D space. They inherit from Tree
196// so they also maintain a hierarchy of transforms.
197//
198// They don't have any view-side smoothing, so the pawn will change its transform to exactly
199// match the transform of the actor.
200
201//-- Actor ---------------------------------------------------------------------------------
202
203export const AM_Spatial = superclass => class extends AM_Tree(superclass) {
204
205 init(options) {
206 super.init(options);
207 this.listen("_scale", this.localChanged);
208 this.listen("_rotation", this.localChanged);
209 this.listen("_translation", this.localChanged);
210 }
211
212 localChanged() {
213 this.$local = null;
214 this.say("localChanged");
215 this.globalChanged();
216 }
217
218 globalChanged() {
219 this.$global = null;
220 this.say("globalChanged");
221 if (this.children) this.children.forEach(child => child.globalChanged());
222 }
223
224 get local() {
225 if (!this.$local) this.$local = m4_scaleRotationTranslation(this.scale, this.rotation, this.translation);
226 return [...this.$local];
227 }
228
229 get global() {
230 if (this.$global) return [...this.$global];
231 if (this.parent) {
232 this.$global = m4_multiply(this.local, this.parent.global);
233 } else {
234 this.$global = this.local;
235 }
236 return [...this.$global];
237 }
238
239 get translation() { return this._translation?[...this._translation] : v3_zero() };
240 set translation(v) { this.set({translation: v}) };
241
242 get rotation() { return this._rotation?[...this._rotation] : q_identity() };
243 set rotation(q) { this.set({rotation: q}) };
244
245 get scale() { return this._scale?[...this._scale] : [1,1,1] };
246 set scale(v) { this.set({scale: v}) };
247}
248RegisterMixin(AM_Spatial);
249
250
251//-- Pawn ----------------------------------------------------------------------------------
252
253export const PM_Spatial = superclass => class extends PM_Tree(superclass) {
254
255constructor(...args) {
256 super(...args);
257 this.listenOnce("globalChanged", this.onGlobalChanged);
258}
259
260onGlobalChanged() { this.say("viewGlobalChanged"); }
261
262get scale() { return this.actor.scale; }
263get translation() { return this.actor.translation; }
264get rotation() { return this.actor.rotation; }
265get local() { return this.actor.local; }
266get global() { return this.actor.global; }
267get lookGlobal() { return this.global; } // Allows objects to have an offset camera position
268
269};
270
271//------------------------------------------------------------------------------------------
272//-- Smoothed ------------------------------------------------------------------------------
273//------------------------------------------------------------------------------------------
274
275// Smoothed actors generate interpolation information when they get movement commands. Their
276// pawns use this to reposition themselves on every frame update.
277//
278// Setting translation/rotation/scale will pop the pawn to the new value. If you want the transition
279// to be interpolated, use moveTo, rotateTo, or scaleTo instead.
280
281//-- Actor ---------------------------------------------------------------------------------
282
283export const AM_Smoothed = superclass => class extends AM_Spatial(superclass) {
284
285 scaleTo(v) {
286 this._scale = v;
287 this.$local = null;
288 this.$global = null;
289 this.say("scaling");
290 }
291
292 rotateTo(q) {
293 this._rotation = q;
294 this.$local = null;
295 this.$global = null;
296 this.say("rotating");
297 }
298
299 translateTo(v) {
300 this._translation = v;
301 this.$local = null;
302 this.$global = null;
303 this.say("translating");
304 }
305
306 moveTo(v) { this.translateTo(v)}
307
308};
309RegisterMixin(AM_Smoothed);
310
311//-- Pawn ----------------------------------------------------------------------------------
312
313// Tug is a value between 0 and 1 that controls the weighting between the two
314// transforms. The closer tug is to 1, the more closely the pawn will track the actor,
315// but the more vulnerable the pawn is to latency/stutters in the simulation.
316
317// When the difference between actor and pawn scale/rotation/translation drops below an epsilon,
318// interpolation is paused
319
320const DynamicSpatial = superclass => PM_Dynamic(PM_Spatial(superclass)); // Merge dynamic and spatial base mixins
321
322export const PM_Smoothed = superclass => class extends DynamicSpatial(superclass) {
323
324 constructor(...args) {
325 super(...args);
326 this.tug = 0.2;
327
328 this._scale = this.actor.scale;
329 this._rotation = this.actor.rotation;
330 this._translation = this.actor.translation;
331 this._global = this.actor.global;
332
333 this.listenOnce("_scale", this.onScaleSet);
334 this.listenOnce("scaling", () => this.scaling = true)
335
336 this.listenOnce("_rotation", this.onRotationSet);
337 this.listenOnce("rotating", () => this.rotating = true)
338
339 this.listenOnce("_translation", this.onTranslationSet);
340 this.listenOnce("translating", () => this.translating = true)
341 }
342
343 set tug(t) {this._tug = t}
344 get tug() { return this._tug; }
345
346 set localOffset(m4) {
347 this._localOffset = m4;
348 this.onLocalChanged();
349 }
350 get localOffset() { return this._localOffset; }
351
352 get scale() { return this._scale; }
353 get rotation() { return this._rotation; }
354 get translation() { return this._translation; }
355
356 onScaleSet() {
357 this._scale = this.actor.scale;
358 this.onLocalChanged();
359 }
360
361 onRotationSet() {
362 this._rotation = this.actor.rotation;
363 this.onLocalChanged();
364 }
365
366 onTranslationSet() {
367 this._translation = this.actor.translation;
368 this.onLocalChanged();
369 }
370
371 onLocalChanged() {
372 this._local = null;
373 this._global = null;
374 }
375
376 onGlobalChanged() {
377 this._global = null;
378 }
379
380 get local() {
381 if (this._local) return this. _local;
382 if (this._localOffset) {
383 this._local = m4_multiply(this._localOffset, m4_scaleRotationTranslation(this._scale, this._rotation, this._translation));
384 return this._local;
385 }
386 this._local = m4_scaleRotationTranslation(this._scale, this._rotation, this._translation);
387 return this._local;
388 }
389
390 get global() {
391 if (this._global) return this._global;
392 if (this.parent && this.parent.global) {
393 this._global = m4_multiply(this.local, this.parent.global);
394 } else {
395 this._global = this.local;
396 }
397 this.say("viewGlobalChanged");
398 return this._global;
399 }
400
401 update(time, delta) {
402 super.update(time, delta);
403
404 let tug = this.tug;
405 if (delta) tug = Math.min(1, tug * delta / 15);
406
407 if (!this.scaling || v3_equals(this._scale, this.actor.scale, .0001)) {
408 this.scaling = false;
409 } else {
410 this._scale = v3_lerp(this._scale, this.actor.scale, tug);
411 this.onLocalChanged();
412 }
413
414 if (!this.rotating || q_equals(this._rotation, this.actor.rotation, 0.000001)) {
415 this.rotating = false;
416 } else {
417 this._rotation = q_slerp(this._rotation, this.actor.rotation, tug);
418 this.onLocalChanged();
419 }
420
421 if (!this.translating || v3_equals(this._translation, this.actor.translation, .0001)) {
422 this.translating = false;
423 } else {
424 this._translation = v3_lerp(this._translation, this.actor.translation, tug);
425 this.onLocalChanged();
426 }
427
428 if (!this._global) {
429 this.global;
430 if (this.children) this.children.forEach(child => child._global = null); // If our global changes, so do the globals of our children
431 }
432
433 }
434
435 postUpdate(time, delta) {
436 super.postUpdate(time, delta);
437 if (this.children) this.children.forEach(child => child.fullUpdate(time, delta));
438 }
439
440}
441
442//------------------------------------------------------------------------------------------
443//-- Predictive ----------------------------------------------------------------------------
444//------------------------------------------------------------------------------------------
445
446// Predictive actors maintain a primary view-side scale/rotation/translation that you can drive directly
447// from player inputs so they responds quickly to player input. On every frame this
448// transform is averaged with the official model-side values.
449//
450// If you're using them, you'll probably want to set:
451// * Session tps to 60 with no cheat beats
452// * AM_Predictive tick frequency to <16
453//
454// This will create the smoothest/fastest response.
455
456//-- Actor ---------------------------------------------------------------------------------
457
458export const AM_Predictive = superclass => class extends AM_Smoothed(superclass) {
459
460 get spin() { return this._spin?[...this._spin] : q_identity() }
461 get velocity() { return this._velocity?[...this._velocity] : v3_zero() }
462 get tickStep() {return this._tickStep || 15}
463
464 init(...args) {
465 super.init(...args);
466 this.listen("scaleTo", this.scaleTo);
467 this.listen("rotateTo", this.rotateTo);
468 this.listen("translateTo", this.translateTo);
469 this.listen("setVelocity", this.setVelocity);
470 this.listen("setSpin", this.setSpin);
471 this.listen("setVelocitySpin", this.setVelocitySpin);
472
473 this.future(0).tick(0);
474 }
475
476 tick(delta) {
477 if (!q_isZero(this.spin)) {
478 this.rotateTo(q_normalize(q_slerp(this.rotation, q_multiply(this.rotation, this.spin), delta)));
479 };
480 if (!v3_isZero(this.velocity)) {
481 const relative = v3_scale(this.velocity, delta);
482 const move = v3_transform(relative, m4_rotationQ(this.rotation));
483 this.moveTo(v3_add(this.translation, move));
484 }
485 if (!this.doomed) this.future(this.tickStep).tick(this.tickStep);
486 }
487
488 setVelocity(v) { this._velocity = v; }
489
490 setSpin(q) { this._spin = q; }
491
492 setVelocitySpin(vq){
493 this._velocity = vq[0];
494 this._spin = vq[1];
495 }
496
497};
498RegisterMixin(AM_Predictive);
499
500//-- Pawn ----------------------------------------------------------------------------------
501
502export const PM_Predictive = superclass => class extends PM_Smoothed(superclass) {
503
504 constructor(...args) {
505 super(...args);
506 this.spin = this.actor.spin;
507 this.velocity = this.actor.velocity;
508 }
509
510
511 scaleTo(v, throttle) {
512 this.say("scaleTo", v, throttle)
513 this.onLocalChanged();
514 }
515
516 rotateTo(q, throttle) {
517 this.say("rotateTo", q, throttle)
518 this.onLocalChanged();
519 }
520
521 translateTo(v, throttle) {
522 this.say("translateTo", v, throttle)
523 this.onLocalChanged();
524 }
525
526 moveTo(v, throttle) {this.translateTo(v,throttle); }
527
528 setVelocity(v, throttle) {
529 this.say("setVelocity", v, throttle)
530 this.onLocalChanged();
531 }
532
533 setSpin(q, throttle) {
534 this.say("setSpin", q, throttle)
535 this.onLocalChanged();
536 }
537
538 setVelocitySpin(vq, throttle){
539 this.say("setVelocitySpin", vq, throttle);
540 this.onLocalChanged();
541 }
542
543 update(time, delta) {
544
545 if (!q_isZero(this.spin)) {
546 this._rotation = q_normalize(q_slerp(this._rotation, q_multiply(this._rotation, this.spin), delta));
547 this._local = null;
548 this._global = null;
549 }
550
551 if (!v3_isZero(this.velocity)) {
552 const relative = v3_scale(this.velocity, delta);
553 const move = v3_transform(relative, m4_rotationQ(this.rotation));
554 this._translation = v3_add(this._translation, move);
555 this._local = null;
556 this._global = null;
557 }
558 super.update(time, delta);
559
560 }
561
562};
563
564// Old name for Predictive objects
565
566export const AM_Avatar = AM_Predictive;
567export const PM_Avatar = PM_Predictive;
568
569
570//------------------------------------------------------------------------------------------
571//-- MouselookAvatar -----------------------------------------------------------------------
572//------------------------------------------------------------------------------------------
573
574// MouselookAvatar is an extension of the normal avatar with a look direction that can be driven
575// by mouse or other continous xy inputs. The avatar internally stores pitch and yaw information
576// that can be used for animation if necessary. Both pitch and yaw are smoothed in the pawn.
577
578//-- Actor ---------------------------------------------------------------------------------
579
580export const AM_MouselookAvatar = superclass => class extends AM_Avatar(superclass) {
581
582 init(...args) {
583 this.listen("avatarLookTo", this.onLookTo);
584 super.init(...args);
585 this.set({rotation: q_euler(0, this.lookYaw, 0)});
586 }
587
588 get lookPitch() { return this._lookPitch || 0 };
589 get lookYaw() { return this._lookYaw || 0 };
590
591 onLookTo(e) {
592 this.set({lookPitch: e[0], lookYaw: e[1]});
593 this.rotateTo(q_euler(0, this.lookYaw, 0));
594 }
595
596}
597RegisterMixin(AM_MouselookAvatar);
598
599//-- Pawn ---------------------------------------------------------------------------------
600
601export const PM_MouselookAvatar = superclass => class extends PM_Avatar(superclass) {
602
603 constructor(...args) {
604 super(...args);
605
606 this._lookPitch = this.actor.lookPitch;
607 this._lookYaw = this.actor.lookYaw;
608
609 this.lookThrottle = 50; // MS between throttled lookTo events
610 this.lastlookTime = this.time;
611
612 this.lookOffset = [0,0,0]; // Vector displacing the camera from the avatar origin.
613 }
614
615 get lookPitch() { return this._lookPitch}
616 get lookYaw() { return this._lookYaw}
617
618 lookTo(pitch, yaw) {
619 this.setLookAngles(pitch, yaw);
620 this.lastLookTime = this.time;
621 this.lastLookCache = null;
622 this.say("avatarLookTo", [pitch, yaw]);
623 this.say("lookGlobalChanged");
624 }
625
626 throttledLookTo(pitch, yaw) {
627 pitch = Math.min(Math.PI/2, Math.max(-Math.PI/2, pitch));
628 yaw = clampRad(yaw);
629 if (this.time < this.lastLookTime + this.lookThrottle) {
630 this.setLookAngles(pitch, yaw);
631 this.lastLookCache = {pitch, yaw};
632 } else {
633 this.lookTo(pitch,yaw);
634 }
635 }
636
637 setLookAngles(pitch, yaw) {
638 this._lookPitch = pitch;
639 this._lookYaw = yaw;
640 this._rotation = q_euler(0, yaw, 0);
641 }
642
643 get lookGlobal() {
644 const pitchRotation = q_axisAngle([1,0,0], this.lookPitch);
645 const yawRotation = q_axisAngle([0,1,0], this.lookYaw);
646
647 const modelLocal = m4_scaleRotationTranslation(this.scale, yawRotation, this.translation)
648 let modelGlobal = modelLocal;
649 if (this.parent) modelGlobal = m4_multiply(modelLocal, this.parent.global);
650
651
652 const m0 = m4_translation(this.lookOffset);
653 const m1 = m4_rotationQ(pitchRotation);
654 const m2 = m4_multiply(m1, m0);
655 return m4_multiply(m2, modelGlobal);
656 }
657
658 update(time, delta) {
659 super.update(time, delta);
660
661 if (this.lastLookCache && this.time > this.lastLookTime + this.lookThrottle) {
662 this.lookTo(this.lastLookCache.pitch, this.lastLookCache.yaw);
663 }
664
665 }
666
667}
668
669