diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a69cfc1c6..eec8b6a30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes +- `ex.Physics.debug` properties for Debug drawing are now moved to `engine.debug.physics`, `engine.debug.collider`, and `engine.debug.body`. + - Old `debugDraw(ctx: CanvasRenderingContext2D)` methods are removed. - Collision `Pair`'s are now between Collider's and not bodies - `PerlinNoise` has been removed from the core repo will now be offered as a [plugin](https://github.com/excaliburjs/excalibur-perlin) - Legacy drawing implementations are moved behind `ex.LegacyDrawing` new Graphics implemenations of `Sprite`, `SpriteSheet`, `Animation` are now the default import. @@ -36,7 +38,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Removes `Entity.components` as a way to access, add, and remove components - Camera.z has been renamed to property `zoom` which is the zoom factor - Camera.zoom(...) has been renamed to function `zoomOverTime()` -- TileMap no longer needs registered SpriteSheets, `Sprite`'s can be added directly to `Cell`'s with `addSprite` +- TileMap no longer needs registered SpriteSheets, `Sprite`'s can be added directly to `Cell`'s with `addGraphic` - The confusing `TileSprite` type is removed (Related to TileMap plugin updates https://github.com/excaliburjs/excalibur-tiled/issues/4, https://github.com/excaliburjs/excalibur-tiled/issues/23, https://github.com/excaliburjs/excalibur-tiled/issues/108) - Directly changing debug drawing by `engine.isDebug = value` has been replaced by `engine.showDebug(value)` and `engine.toggleDebug()` ([#1655](https://github.com/excaliburjs/Excalibur/issues/1655)) - `UIActor` Class instances need to be replaced to `ScreenElement` (This Class it's marked as Obsolete) ([#1656](https://github.com/excaliburjs/Excalibur/issues/1656)) @@ -55,6 +57,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- The `ExcaliburGraphicsContext` now supports drawing debug text +- `Entity` may also now optionally have a `name`, this is useful for finding entities by name or when displaying in debug mode. +- New `DebugSystem` ECS system will show debug drawing output for things toggled on/off in the `engine.debug` section, this allows for a less cluttered debug experience. + - Each debug section now has a configurable color. - Turn on WebGL support with `ex.Flags.useWebGL()` - Added new helpers to `CollisionGroup` to define groups that collide with specified groups `CollisionGroup.collidesWith([groupA, groupB])` - Combine groups with `const groupAandB = CollisionGroup.combine([groupA, groupB])` diff --git a/sandbox/datgui/dat.gui.js b/sandbox/datgui/dat.gui.js new file mode 100644 index 0000000000..0725382b53 --- /dev/null +++ b/sandbox/datgui/dat.gui.js @@ -0,0 +1,13 @@ +/** + * dat-gui JavaScript Controller Library + * http://code.google.com/p/dat-gui + * + * Copyright 2011 Data Arts Team, Google Creative Lab + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.dat={})}(this,function(e){"use strict";function t(e,t){var n=e.__state.conversionName.toString(),o=Math.round(e.r),i=Math.round(e.g),r=Math.round(e.b),s=e.a,a=Math.round(e.h),l=e.s.toFixed(1),d=e.v.toFixed(1);if(t||"THREE_CHAR_HEX"===n||"SIX_CHAR_HEX"===n){for(var c=e.hex.toString(16);c.length<6;)c="0"+c;return"#"+c}return"CSS_RGB"===n?"rgb("+o+","+i+","+r+")":"CSS_RGBA"===n?"rgba("+o+","+i+","+r+","+s+")":"HEX"===n?"0x"+e.hex.toString(16):"RGB_ARRAY"===n?"["+o+","+i+","+r+"]":"RGBA_ARRAY"===n?"["+o+","+i+","+r+","+s+"]":"RGB_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+"}":"RGBA_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+",a:"+s+"}":"HSV_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+"}":"HSVA_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+",a:"+s+"}":"unknown format"}function n(e,t,n){Object.defineProperty(e,t,{get:function(){return"RGB"===this.__state.space?this.__state[t]:(I.recalculateRGB(this,t,n),this.__state[t])},set:function(e){"RGB"!==this.__state.space&&(I.recalculateRGB(this,t,n),this.__state.space="RGB"),this.__state[t]=e}})}function o(e,t){Object.defineProperty(e,t,{get:function(){return"HSV"===this.__state.space?this.__state[t]:(I.recalculateHSV(this),this.__state[t])},set:function(e){"HSV"!==this.__state.space&&(I.recalculateHSV(this),this.__state.space="HSV"),this.__state[t]=e}})}function i(e){if("0"===e||S.isUndefined(e))return 0;var t=e.match(U);return S.isNull(t)?0:parseFloat(t[1])}function r(e){var t=e.toString();return t.indexOf(".")>-1?t.length-t.indexOf(".")-1:0}function s(e,t){var n=Math.pow(10,t);return Math.round(e*n)/n}function a(e,t,n,o,i){return o+(e-t)/(n-t)*(i-o)}function l(e,t,n,o){e.style.background="",S.each(ee,function(i){e.style.cssText+="background: "+i+"linear-gradient("+t+", "+n+" 0%, "+o+" 100%); "})}function d(e){e.style.background="",e.style.cssText+="background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);",e.style.cssText+="background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"}function c(e,t,n){var o=document.createElement("li");return t&&o.appendChild(t),n?e.__ul.insertBefore(o,n):e.__ul.appendChild(o),e.onResize(),o}function u(e){X.unbind(window,"resize",e.__resizeHandler),e.saveToLocalStorageIfPossible&&X.unbind(window,"unload",e.saveToLocalStorageIfPossible)}function _(e,t){var n=e.__preset_select[e.__preset_select.selectedIndex];n.innerHTML=t?n.value+"*":n.value}function h(e,t,n){if(n.__li=t,n.__gui=e,S.extend(n,{options:function(t){if(arguments.length>1){var o=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:o,factoryArgs:[S.toArray(arguments)]})}if(S.isArray(t)||S.isObject(t)){var i=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:i,factoryArgs:[t]})}},name:function(e){return n.__li.firstElementChild.firstElementChild.innerHTML=e,n},listen:function(){return n.__gui.listen(n),n},remove:function(){return n.__gui.remove(n),n}}),n instanceof q){var o=new Q(n.object,n.property,{min:n.__min,max:n.__max,step:n.__step});S.each(["updateDisplay","onChange","onFinishChange","step","min","max"],function(e){var t=n[e],i=o[e];n[e]=o[e]=function(){var e=Array.prototype.slice.call(arguments);return i.apply(o,e),t.apply(n,e)}}),X.addClass(t,"has-slider"),n.domElement.insertBefore(o.domElement,n.domElement.firstElementChild)}else if(n instanceof Q){var i=function(t){if(S.isNumber(n.__min)&&S.isNumber(n.__max)){var o=n.__li.firstElementChild.firstElementChild.innerHTML,i=n.__gui.__listening.indexOf(n)>-1;n.remove();var r=f(e,n.object,n.property,{before:n.__li.nextElementSibling,factoryArgs:[n.__min,n.__max,n.__step]});return r.name(o),i&&r.listen(),r}return t};n.min=S.compose(i,n.min),n.max=S.compose(i,n.max)}else n instanceof K?(X.bind(t,"click",function(){X.fakeEvent(n.__checkbox,"click")}),X.bind(n.__checkbox,"click",function(e){e.stopPropagation()})):n instanceof Z?(X.bind(t,"click",function(){X.fakeEvent(n.__button,"click")}),X.bind(t,"mouseover",function(){X.addClass(n.__button,"hover")}),X.bind(t,"mouseout",function(){X.removeClass(n.__button,"hover")})):n instanceof $&&(X.addClass(t,"color"),n.updateDisplay=S.compose(function(e){return t.style.borderLeftColor=n.__color.toString(),e},n.updateDisplay),n.updateDisplay());n.setValue=S.compose(function(t){return e.getRoot().__preset_select&&n.isModified()&&_(e.getRoot(),!0),t},n.setValue)}function p(e,t){var n=e.getRoot(),o=n.__rememberedObjects.indexOf(t.object);if(-1!==o){var i=n.__rememberedObjectIndecesToControllers[o];if(void 0===i&&(i={},n.__rememberedObjectIndecesToControllers[o]=i),i[t.property]=t,n.load&&n.load.remembered){var r=n.load.remembered,s=void 0;if(r[e.preset])s=r[e.preset];else{if(!r[se])return;s=r[se]}if(s[o]&&void 0!==s[o][t.property]){var a=s[o][t.property];t.initialValue=a,t.setValue(a)}}}}function f(e,t,n,o){if(void 0===t[n])throw new Error('Object "'+t+'" has no property "'+n+'"');var i=void 0;if(o.color)i=new $(t,n);else{var r=[t,n].concat(o.factoryArgs);i=ne.apply(e,r)}o.before instanceof z&&(o.before=o.before.__li),p(e,i),X.addClass(i.domElement,"c");var s=document.createElement("span");X.addClass(s,"property-name"),s.innerHTML=i.property;var a=document.createElement("div");a.appendChild(s),a.appendChild(i.domElement);var l=c(e,a,o.before);return X.addClass(l,he.CLASS_CONTROLLER_ROW),i instanceof $?X.addClass(l,"color"):X.addClass(l,H(i.getValue())),h(e,l,i),e.__controllers.push(i),i}function m(e,t){return document.location.href+"."+t}function g(e,t,n){var o=document.createElement("option");o.innerHTML=t,o.value=t,e.__preset_select.appendChild(o),n&&(e.__preset_select.selectedIndex=e.__preset_select.length-1)}function b(e,t){t.style.display=e.useLocalStorage?"block":"none"}function v(e){var t=e.__save_row=document.createElement("li");X.addClass(e.domElement,"has-save"),e.__ul.insertBefore(t,e.__ul.firstChild),X.addClass(t,"save-row");var n=document.createElement("span");n.innerHTML=" ",X.addClass(n,"button gears");var o=document.createElement("span");o.innerHTML="Save",X.addClass(o,"button"),X.addClass(o,"save");var i=document.createElement("span");i.innerHTML="New",X.addClass(i,"button"),X.addClass(i,"save-as");var r=document.createElement("span");r.innerHTML="Revert",X.addClass(r,"button"),X.addClass(r,"revert");var s=e.__preset_select=document.createElement("select");if(e.load&&e.load.remembered?S.each(e.load.remembered,function(t,n){g(e,n,n===e.preset)}):g(e,se,!1),X.bind(s,"change",function(){for(var t=0;t=0;n--)t=[e[n].apply(this,t)];return t[0]}},each:function(e,t,n){if(e)if(A&&e.forEach&&e.forEach===A)e.forEach(t,n);else if(e.length===e.length+0){var o=void 0,i=void 0;for(o=0,i=e.length;o1?S.toArray(arguments):arguments[0];return S.each(O,function(t){if(t.litmus(e))return S.each(t.conversions,function(t,n){if(T=t.read(e),!1===L&&!1!==T)return L=T,T.conversionName=n,T.conversion=t,S.BREAK}),S.BREAK}),L},B=void 0,N={hsv_to_rgb:function(e,t,n){var o=Math.floor(e/60)%6,i=e/60-Math.floor(e/60),r=n*(1-t),s=n*(1-i*t),a=n*(1-(1-i)*t),l=[[n,a,r],[s,n,r],[r,n,a],[r,s,n],[a,r,n],[n,r,s]][o];return{r:255*l[0],g:255*l[1],b:255*l[2]}},rgb_to_hsv:function(e,t,n){var o=Math.min(e,t,n),i=Math.max(e,t,n),r=i-o,s=void 0,a=void 0;return 0===i?{h:NaN,s:0,v:0}:(a=r/i,s=e===i?(t-n)/r:t===i?2+(n-e)/r:4+(e-t)/r,(s/=6)<0&&(s+=1),{h:360*s,s:a,v:i/255})},rgb_to_hex:function(e,t,n){var o=this.hex_with_component(0,2,e);return o=this.hex_with_component(o,1,t),o=this.hex_with_component(o,0,n)},component_from_hex:function(e,t){return e>>8*t&255},hex_with_component:function(e,t,n){return n<<(B=8*t)|e&~(255<this.__max&&(n=this.__max),void 0!==this.__step&&n%this.__step!=0&&(n=Math.round(n/this.__step)*this.__step),D(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"setValue",this).call(this,n)}},{key:"min",value:function(e){return this.__min=e,this}},{key:"max",value:function(e){return this.__max=e,this}},{key:"step",value:function(e){return this.__step=e,this.__impliedStep=e,this.__precision=r(e),this}}]),t}(),Q=function(e){function t(e,n,o){function i(){l.__onFinishChange&&l.__onFinishChange.call(l,l.getValue())}function r(e){var t=d-e.clientY;l.setValue(l.getValue()+t*l.__impliedStep),d=e.clientY}function s(){X.unbind(window,"mousemove",r),X.unbind(window,"mouseup",s),i()}F(this,t);var a=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,o));a.__truncationSuspended=!1;var l=a,d=void 0;return a.__input=document.createElement("input"),a.__input.setAttribute("type","text"),X.bind(a.__input,"change",function(){var e=parseFloat(l.__input.value);S.isNaN(e)||l.setValue(e)}),X.bind(a.__input,"blur",function(){i()}),X.bind(a.__input,"mousedown",function(e){X.bind(window,"mousemove",r),X.bind(window,"mouseup",s),d=e.clientY}),X.bind(a.__input,"keydown",function(e){13===e.keyCode&&(l.__truncationSuspended=!0,this.blur(),l.__truncationSuspended=!1,i())}),a.updateDisplay(),a.domElement.appendChild(a.__input),a}return j(t,W),P(t,[{key:"updateDisplay",value:function(){return this.__input.value=this.__truncationSuspended?this.getValue():s(this.getValue(),this.__precision),D(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),q=function(e){function t(e,n,o,i,r){function s(e){e.preventDefault();var t=_.__background.getBoundingClientRect();return _.setValue(a(e.clientX,t.left,t.right,_.__min,_.__max)),!1}function l(){X.unbind(window,"mousemove",s),X.unbind(window,"mouseup",l),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}function d(e){var t=e.touches[0].clientX,n=_.__background.getBoundingClientRect();_.setValue(a(t,n.left,n.right,_.__min,_.__max))}function c(){X.unbind(window,"touchmove",d),X.unbind(window,"touchend",c),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}F(this,t);var u=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,{min:o,max:i,step:r})),_=u;return u.__background=document.createElement("div"),u.__foreground=document.createElement("div"),X.bind(u.__background,"mousedown",function(e){document.activeElement.blur(),X.bind(window,"mousemove",s),X.bind(window,"mouseup",l),s(e)}),X.bind(u.__background,"touchstart",function(e){1===e.touches.length&&(X.bind(window,"touchmove",d),X.bind(window,"touchend",c),d(e))}),X.addClass(u.__background,"slider"),X.addClass(u.__foreground,"slider-fg"),u.updateDisplay(),u.__background.appendChild(u.__foreground),u.domElement.appendChild(u.__background),u}return j(t,W),P(t,[{key:"updateDisplay",value:function(){var e=(this.getValue()-this.__min)/(this.__max-this.__min);return this.__foreground.style.width=100*e+"%",D(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),Z=function(e){function t(e,n,o){F(this,t);var i=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n)),r=i;return i.__button=document.createElement("div"),i.__button.innerHTML=void 0===o?"Fire":o,X.bind(i.__button,"click",function(e){return e.preventDefault(),r.fire(),!1}),X.addClass(i.__button,"button"),i.domElement.appendChild(i.__button),i}return j(t,z),P(t,[{key:"fire",value:function(){this.__onChange&&this.__onChange.call(this),this.getValue().call(this.object),this.__onFinishChange&&this.__onFinishChange.call(this,this.getValue())}}]),t}(),$=function(e){function t(e,n){function o(e){u(e),X.bind(window,"mousemove",u),X.bind(window,"touchmove",u),X.bind(window,"mouseup",r),X.bind(window,"touchend",r)}function i(e){_(e),X.bind(window,"mousemove",_),X.bind(window,"touchmove",_),X.bind(window,"mouseup",s),X.bind(window,"touchend",s)}function r(){X.unbind(window,"mousemove",u),X.unbind(window,"touchmove",u),X.unbind(window,"mouseup",r),X.unbind(window,"touchend",r),c()}function s(){X.unbind(window,"mousemove",_),X.unbind(window,"touchmove",_),X.unbind(window,"mouseup",s),X.unbind(window,"touchend",s),c()}function a(){var e=R(this.value);!1!==e?(p.__color.__state=e,p.setValue(p.__color.toOriginal())):this.value=p.__color.toString()}function c(){p.__onFinishChange&&p.__onFinishChange.call(p,p.__color.toOriginal())}function u(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__saturation_field.getBoundingClientRect(),n=e.touches&&e.touches[0]||e,o=n.clientX,i=n.clientY,r=(o-t.left)/(t.right-t.left),s=1-(i-t.top)/(t.bottom-t.top);return s>1?s=1:s<0&&(s=0),r>1?r=1:r<0&&(r=0),p.__color.v=s,p.__color.s=r,p.setValue(p.__color.toOriginal()),!1}function _(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__hue_field.getBoundingClientRect(),n=1-((e.touches&&e.touches[0]||e).clientY-t.top)/(t.bottom-t.top);return n>1?n=1:n<0&&(n=0),p.__color.h=360*n,p.setValue(p.__color.toOriginal()),!1}F(this,t);var h=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n));h.__color=new I(h.getValue()),h.__temp=new I(0);var p=h;h.domElement=document.createElement("div"),X.makeSelectable(h.domElement,!1),h.__selector=document.createElement("div"),h.__selector.className="selector",h.__saturation_field=document.createElement("div"),h.__saturation_field.className="saturation-field",h.__field_knob=document.createElement("div"),h.__field_knob.className="field-knob",h.__field_knob_border="2px solid ",h.__hue_knob=document.createElement("div"),h.__hue_knob.className="hue-knob",h.__hue_field=document.createElement("div"),h.__hue_field.className="hue-field",h.__input=document.createElement("input"),h.__input.type="text",h.__input_textShadow="0 1px 1px ",X.bind(h.__input,"keydown",function(e){13===e.keyCode&&a.call(this)}),X.bind(h.__input,"blur",a),X.bind(h.__selector,"mousedown",function(){X.addClass(this,"drag").bind(window,"mouseup",function(){X.removeClass(p.__selector,"drag")})}),X.bind(h.__selector,"touchstart",function(){X.addClass(this,"drag").bind(window,"touchend",function(){X.removeClass(p.__selector,"drag")})});var f=document.createElement("div");return S.extend(h.__selector.style,{width:"122px",height:"102px",padding:"3px",backgroundColor:"#222",boxShadow:"0px 1px 3px rgba(0,0,0,0.3)"}),S.extend(h.__field_knob.style,{position:"absolute",width:"12px",height:"12px",border:h.__field_knob_border+(h.__color.v<.5?"#fff":"#000"),boxShadow:"0px 1px 3px rgba(0,0,0,0.5)",borderRadius:"12px",zIndex:1}),S.extend(h.__hue_knob.style,{position:"absolute",width:"15px",height:"2px",borderRight:"4px solid #fff",zIndex:1}),S.extend(h.__saturation_field.style,{width:"100px",height:"100px",border:"1px solid #555",marginRight:"3px",display:"inline-block",cursor:"pointer"}),S.extend(f.style,{width:"100%",height:"100%",background:"none"}),l(f,"top","rgba(0,0,0,0)","#000"),S.extend(h.__hue_field.style,{width:"15px",height:"100px",border:"1px solid #555",cursor:"ns-resize",position:"absolute",top:"3px",right:"3px"}),d(h.__hue_field),S.extend(h.__input.style,{outline:"none",textAlign:"center",color:"#fff",border:0,fontWeight:"bold",textShadow:h.__input_textShadow+"rgba(0,0,0,0.7)"}),X.bind(h.__saturation_field,"mousedown",o),X.bind(h.__saturation_field,"touchstart",o),X.bind(h.__field_knob,"mousedown",o),X.bind(h.__field_knob,"touchstart",o),X.bind(h.__hue_field,"mousedown",i),X.bind(h.__hue_field,"touchstart",i),h.__saturation_field.appendChild(f),h.__selector.appendChild(h.__field_knob),h.__selector.appendChild(h.__saturation_field),h.__selector.appendChild(h.__hue_field),h.__hue_field.appendChild(h.__hue_knob),h.domElement.appendChild(h.__input),h.domElement.appendChild(h.__selector),h.updateDisplay(),h}return j(t,z),P(t,[{key:"updateDisplay",value:function(){var e=R(this.getValue());if(!1!==e){var t=!1;S.each(I.COMPONENTS,function(n){if(!S.isUndefined(e[n])&&!S.isUndefined(this.__color.__state[n])&&e[n]!==this.__color.__state[n])return t=!0,{}},this),t&&S.extend(this.__color.__state,e)}S.extend(this.__temp.__state,this.__color.__state),this.__temp.a=1;var n=this.__color.v<.5||this.__color.s>.5?255:0,o=255-n;S.extend(this.__field_knob.style,{marginLeft:100*this.__color.s-7+"px",marginTop:100*(1-this.__color.v)-7+"px",backgroundColor:this.__temp.toHexString(),border:this.__field_knob_border+"rgb("+n+","+n+","+n+")"}),this.__hue_knob.style.marginTop=100*(1-this.__color.h/360)+"px",this.__temp.s=1,this.__temp.v=1,l(this.__saturation_field,"left","#fff",this.__temp.toHexString()),this.__input.value=this.__color.toString(),S.extend(this.__input.style,{backgroundColor:this.__color.toHexString(),color:"rgb("+n+","+n+","+n+")",textShadow:this.__input_textShadow+"rgba("+o+","+o+","+o+",.7)"})}}]),t}(),ee=["-moz-","-o-","-webkit-","-ms-",""],te={load:function(e,t){var n=t||document,o=n.createElement("link");o.type="text/css",o.rel="stylesheet",o.href=e,n.getElementsByTagName("head")[0].appendChild(o)},inject:function(e,t){var n=t||document,o=document.createElement("style");o.type="text/css",o.innerHTML=e;var i=n.getElementsByTagName("head")[0];try{i.appendChild(o)}catch(e){}}},ne=function(e,t){var n=e[t];return S.isArray(arguments[2])||S.isObject(arguments[2])?new Y(e,t,arguments[2]):S.isNumber(n)?S.isNumber(arguments[2])&&S.isNumber(arguments[3])?S.isNumber(arguments[4])?new q(e,t,arguments[2],arguments[3],arguments[4]):new q(e,t,arguments[2],arguments[3]):S.isNumber(arguments[4])?new Q(e,t,{min:arguments[2],max:arguments[3],step:arguments[4]}):new Q(e,t,{min:arguments[2],max:arguments[3]}):S.isString(n)?new J(e,t):S.isFunction(n)?new Z(e,t,""):S.isBoolean(n)?new K(e,t):null},oe=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){setTimeout(e,1e3/60)},ie=function(){function e(){F(this,e),this.backgroundElement=document.createElement("div"),S.extend(this.backgroundElement.style,{backgroundColor:"rgba(0,0,0,0.8)",top:0,left:0,display:"none",zIndex:"1000",opacity:0,WebkitTransition:"opacity 0.2s linear",transition:"opacity 0.2s linear"}),X.makeFullscreen(this.backgroundElement),this.backgroundElement.style.position="fixed",this.domElement=document.createElement("div"),S.extend(this.domElement.style,{position:"fixed",display:"none",zIndex:"1001",opacity:0,WebkitTransition:"-webkit-transform 0.2s ease-out, opacity 0.2s linear",transition:"transform 0.2s ease-out, opacity 0.2s linear"}),document.body.appendChild(this.backgroundElement),document.body.appendChild(this.domElement);var t=this;X.bind(this.backgroundElement,"click",function(){t.hide()})}return P(e,[{key:"show",value:function(){var e=this;this.backgroundElement.style.display="block",this.domElement.style.display="block",this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)",this.layout(),S.defer(function(){e.backgroundElement.style.opacity=1,e.domElement.style.opacity=1,e.domElement.style.webkitTransform="scale(1)"})}},{key:"hide",value:function(){var e=this,t=function t(){e.domElement.style.display="none",e.backgroundElement.style.display="none",X.unbind(e.domElement,"webkitTransitionEnd",t),X.unbind(e.domElement,"transitionend",t),X.unbind(e.domElement,"oTransitionEnd",t)};X.bind(this.domElement,"webkitTransitionEnd",t),X.bind(this.domElement,"transitionend",t),X.bind(this.domElement,"oTransitionEnd",t),this.backgroundElement.style.opacity=0,this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)"}},{key:"layout",value:function(){this.domElement.style.left=window.innerWidth/2-X.getWidth(this.domElement)/2+"px",this.domElement.style.top=window.innerHeight/2-X.getHeight(this.domElement)/2+"px"}}]),e}(),re=function(e){if(e&&"undefined"!=typeof window){var t=document.createElement("style");return t.setAttribute("type","text/css"),t.innerHTML=e,document.head.appendChild(t),e}}(".dg ul{list-style:none;margin:0;padding:0;width:100%;clear:both}.dg.ac{position:fixed;top:0;left:0;right:0;height:0;z-index:0}.dg:not(.ac) .main{overflow:hidden}.dg.main{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear}.dg.main.taller-than-window{overflow-y:auto}.dg.main.taller-than-window .close-button{opacity:1;margin-top:-1px;border-top:1px solid #2c2c2c}.dg.main ul.closed .close-button{opacity:1 !important}.dg.main:hover .close-button,.dg.main .close-button.drag{opacity:1}.dg.main .close-button{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear;border:0;line-height:19px;height:20px;cursor:pointer;text-align:center;background-color:#000}.dg.main .close-button.close-top{position:relative}.dg.main .close-button.close-bottom{position:absolute}.dg.main .close-button:hover{background-color:#111}.dg.a{float:right;margin-right:15px;overflow-y:visible}.dg.a.has-save>ul.close-top{margin-top:0}.dg.a.has-save>ul.close-bottom{margin-top:27px}.dg.a.has-save>ul.closed{margin-top:0}.dg.a .save-row{top:0;z-index:1002}.dg.a .save-row.close-top{position:relative}.dg.a .save-row.close-bottom{position:fixed}.dg li{-webkit-transition:height .1s ease-out;-o-transition:height .1s ease-out;-moz-transition:height .1s ease-out;transition:height .1s ease-out;-webkit-transition:overflow .1s linear;-o-transition:overflow .1s linear;-moz-transition:overflow .1s linear;transition:overflow .1s linear}.dg li:not(.folder){cursor:auto;height:27px;line-height:27px;padding:0 4px 0 5px}.dg li.folder{padding:0;border-left:4px solid rgba(0,0,0,0)}.dg li.title{cursor:pointer;margin-left:-4px}.dg .closed li:not(.title),.dg .closed ul li,.dg .closed ul li>*{height:0;overflow:hidden;border:0}.dg .cr{clear:both;padding-left:3px;height:27px;overflow:hidden}.dg .property-name{cursor:default;float:left;clear:left;width:40%;overflow:hidden;text-overflow:ellipsis}.dg .c{float:left;width:60%;position:relative}.dg .c input[type=text]{border:0;margin-top:4px;padding:3px;width:100%;float:right}.dg .has-slider input[type=text]{width:30%;margin-left:0}.dg .slider{float:left;width:66%;margin-left:-5px;margin-right:0;height:19px;margin-top:4px}.dg .slider-fg{height:100%}.dg .c input[type=checkbox]{margin-top:7px}.dg .c select{margin-top:5px}.dg .cr.function,.dg .cr.function .property-name,.dg .cr.function *,.dg .cr.boolean,.dg .cr.boolean *{cursor:pointer}.dg .cr.color{overflow:visible}.dg .selector{display:none;position:absolute;margin-left:-9px;margin-top:23px;z-index:10}.dg .c:hover .selector,.dg .selector.drag{display:block}.dg li.save-row{padding:0}.dg li.save-row .button{display:inline-block;padding:0px 6px}.dg.dialogue{background-color:#222;width:460px;padding:15px;font-size:13px;line-height:15px}#dg-new-constructor{padding:10px;color:#222;font-family:Monaco, monospace;font-size:10px;border:0;resize:none;box-shadow:inset 1px 1px 1px #888;word-wrap:break-word;margin:12px 0;display:block;width:440px;overflow-y:scroll;height:100px;position:relative}#dg-local-explain{display:none;font-size:11px;line-height:17px;border-radius:3px;background-color:#333;padding:8px;margin-top:10px}#dg-local-explain code{font-size:10px}#dat-gui-save-locally{display:none}.dg{color:#eee;font:11px 'Lucida Grande', sans-serif;text-shadow:0 -1px 0 #111}.dg.main::-webkit-scrollbar{width:5px;background:#1a1a1a}.dg.main::-webkit-scrollbar-corner{height:0;display:none}.dg.main::-webkit-scrollbar-thumb{border-radius:5px;background:#676767}.dg li:not(.folder){background:#1a1a1a;border-bottom:1px solid #2c2c2c}.dg li.save-row{line-height:25px;background:#dad5cb;border:0}.dg li.save-row select{margin-left:5px;width:108px}.dg li.save-row .button{margin-left:5px;margin-top:1px;border-radius:2px;font-size:9px;line-height:7px;padding:4px 4px 5px 4px;background:#c5bdad;color:#fff;text-shadow:0 1px 0 #b0a58f;box-shadow:0 -1px 0 #b0a58f;cursor:pointer}.dg li.save-row .button.gears{background:#c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;height:7px;width:8px}.dg li.save-row .button:hover{background-color:#bab19e;box-shadow:0 -1px 0 #b0a58f}.dg li.folder{border-bottom:0}.dg li.title{padding-left:16px;background:#000 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.2)}.dg .closed li.title{background-image:url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==)}.dg .cr.boolean{border-left:3px solid #806787}.dg .cr.color{border-left:3px solid}.dg .cr.function{border-left:3px solid #e61d5f}.dg .cr.number{border-left:3px solid #2FA1D6}.dg .cr.number input[type=text]{color:#2FA1D6}.dg .cr.string{border-left:3px solid #1ed36f}.dg .cr.string input[type=text]{color:#1ed36f}.dg .cr.function:hover,.dg .cr.boolean:hover{background:#111}.dg .c input[type=text]{background:#303030;outline:none}.dg .c input[type=text]:hover{background:#3c3c3c}.dg .c input[type=text]:focus{background:#494949;color:#fff}.dg .c .slider{background:#303030;cursor:ew-resize}.dg .c .slider-fg{background:#2FA1D6;max-width:100%}.dg .c .slider:hover{background:#3c3c3c}.dg .c .slider:hover .slider-fg{background:#44abda}\n");te.inject(re);var se="Default",ae=function(){try{return!!window.localStorage}catch(e){return!1}}(),le=void 0,de=!0,ce=void 0,ue=!1,_e=[],he=function e(t){var n=this,o=t||{};this.domElement=document.createElement("div"),this.__ul=document.createElement("ul"),this.domElement.appendChild(this.__ul),X.addClass(this.domElement,"dg"),this.__folders={},this.__controllers=[],this.__rememberedObjects=[],this.__rememberedObjectIndecesToControllers=[],this.__listening=[],o=S.defaults(o,{closeOnTop:!1,autoPlace:!0,width:e.DEFAULT_WIDTH}),o=S.defaults(o,{resizable:o.autoPlace,hideable:o.autoPlace}),S.isUndefined(o.load)?o.load={preset:se}:o.preset&&(o.load.preset=o.preset),S.isUndefined(o.parent)&&o.hideable&&_e.push(this),o.resizable=S.isUndefined(o.parent)&&o.resizable,o.autoPlace&&S.isUndefined(o.scrollable)&&(o.scrollable=!0);var i=ae&&"true"===localStorage.getItem(m(this,"isLocal")),r=void 0,s=void 0;if(Object.defineProperties(this,{parent:{get:function(){return o.parent}},scrollable:{get:function(){return o.scrollable}},autoPlace:{get:function(){return o.autoPlace}},closeOnTop:{get:function(){return o.closeOnTop}},preset:{get:function(){return n.parent?n.getRoot().preset:o.load.preset},set:function(e){n.parent?n.getRoot().preset=e:o.load.preset=e,E(this),n.revert()}},width:{get:function(){return o.width},set:function(e){o.width=e,w(n,e)}},name:{get:function(){return o.name},set:function(e){o.name=e,s&&(s.innerHTML=o.name)}},closed:{get:function(){return o.closed},set:function(t){o.closed=t,o.closed?X.addClass(n.__ul,e.CLASS_CLOSED):X.removeClass(n.__ul,e.CLASS_CLOSED),this.onResize(),n.__closeButton&&(n.__closeButton.innerHTML=t?e.TEXT_OPEN:e.TEXT_CLOSED)}},load:{get:function(){return o.load}},useLocalStorage:{get:function(){return i},set:function(e){ae&&(i=e,e?X.bind(window,"unload",r):X.unbind(window,"unload",r),localStorage.setItem(m(n,"isLocal"),e))}}}),S.isUndefined(o.parent)){if(this.closed=o.closed||!1,X.addClass(this.domElement,e.CLASS_MAIN),X.makeSelectable(this.domElement,!1),ae&&i){n.useLocalStorage=!0;var a=localStorage.getItem(m(this,"gui"));a&&(o.load=JSON.parse(a))}this.__closeButton=document.createElement("div"),this.__closeButton.innerHTML=e.TEXT_CLOSED,X.addClass(this.__closeButton,e.CLASS_CLOSE_BUTTON),o.closeOnTop?(X.addClass(this.__closeButton,e.CLASS_CLOSE_TOP),this.domElement.insertBefore(this.__closeButton,this.domElement.childNodes[0])):(X.addClass(this.__closeButton,e.CLASS_CLOSE_BOTTOM),this.domElement.appendChild(this.__closeButton)),X.bind(this.__closeButton,"click",function(){n.closed=!n.closed})}else{void 0===o.closed&&(o.closed=!0);var l=document.createTextNode(o.name);X.addClass(l,"controller-name"),s=c(n,l);X.addClass(this.__ul,e.CLASS_CLOSED),X.addClass(s,"title"),X.bind(s,"click",function(e){return e.preventDefault(),n.closed=!n.closed,!1}),o.closed||(this.closed=!1)}o.autoPlace&&(S.isUndefined(o.parent)&&(de&&(ce=document.createElement("div"),X.addClass(ce,"dg"),X.addClass(ce,e.CLASS_AUTO_PLACE_CONTAINER),document.body.appendChild(ce),de=!1),ce.appendChild(this.domElement),X.addClass(this.domElement,e.CLASS_AUTO_PLACE)),this.parent||w(n,o.width)),this.__resizeHandler=function(){n.onResizeDebounced()},X.bind(window,"resize",this.__resizeHandler),X.bind(this.__ul,"webkitTransitionEnd",this.__resizeHandler),X.bind(this.__ul,"transitionend",this.__resizeHandler),X.bind(this.__ul,"oTransitionEnd",this.__resizeHandler),this.onResize(),o.resizable&&y(this),r=function(){ae&&"true"===localStorage.getItem(m(n,"isLocal"))&&localStorage.setItem(m(n,"gui"),JSON.stringify(n.getSaveObject()))},this.saveToLocalStorageIfPossible=r,o.parent||function(){var e=n.getRoot();e.width+=1,S.defer(function(){e.width-=1})}()};he.toggleHide=function(){ue=!ue,S.each(_e,function(e){e.domElement.style.display=ue?"none":""})},he.CLASS_AUTO_PLACE="a",he.CLASS_AUTO_PLACE_CONTAINER="ac",he.CLASS_MAIN="main",he.CLASS_CONTROLLER_ROW="cr",he.CLASS_TOO_TALL="taller-than-window",he.CLASS_CLOSED="closed",he.CLASS_CLOSE_BUTTON="close-button",he.CLASS_CLOSE_TOP="close-top",he.CLASS_CLOSE_BOTTOM="close-bottom",he.CLASS_DRAG="drag",he.DEFAULT_WIDTH=245,he.TEXT_CLOSED="Close Controls",he.TEXT_OPEN="Open Controls",he._keydownHandler=function(e){"text"===document.activeElement.type||72!==e.which&&72!==e.keyCode||he.toggleHide()},X.bind(window,"keydown",he._keydownHandler,!1),S.extend(he.prototype,{add:function(e,t){return f(this,e,t,{factoryArgs:Array.prototype.slice.call(arguments,2)})},addColor:function(e,t){return f(this,e,t,{color:!0})},remove:function(e){this.__ul.removeChild(e.__li),this.__controllers.splice(this.__controllers.indexOf(e),1);var t=this;S.defer(function(){t.onResize()})},destroy:function(){if(this.parent)throw new Error("Only the root GUI should be removed with .destroy(). For subfolders, use gui.removeFolder(folder) instead.");this.autoPlace&&ce.removeChild(this.domElement);var e=this;S.each(this.__folders,function(t){e.removeFolder(t)}),X.unbind(window,"keydown",he._keydownHandler,!1),u(this)},addFolder:function(e){if(void 0!==this.__folders[e])throw new Error('You already have a folder in this GUI by the name "'+e+'"');var t={name:e,parent:this};t.autoPlace=this.autoPlace,this.load&&this.load.folders&&this.load.folders[e]&&(t.closed=this.load.folders[e].closed,t.load=this.load.folders[e]);var n=new he(t);this.__folders[e]=n;var o=c(this,n.domElement);return X.addClass(o,"folder"),n},removeFolder:function(e){this.__ul.removeChild(e.domElement.parentElement),delete this.__folders[e.name],this.load&&this.load.folders&&this.load.folders[e.name]&&delete this.load.folders[e.name],u(e);var t=this;S.each(e.__folders,function(t){e.removeFolder(t)}),S.defer(function(){t.onResize()})},open:function(){this.closed=!1},close:function(){this.closed=!0},hide:function(){this.domElement.style.display="none"},show:function(){this.domElement.style.display=""},onResize:function(){var e=this.getRoot();if(e.scrollable){var t=X.getOffset(e.__ul).top,n=0;S.each(e.__ul.childNodes,function(t){e.autoPlace&&t===e.__save_row||(n+=X.getHeight(t))}),window.innerHeight-t-20GUI\'s constructor:\n\n \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI\'s constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n\n
\n\n
\n\n'),this.parent)throw new Error("You can only call remember on a top level GUI.");var e=this;S.each(Array.prototype.slice.call(arguments),function(t){0===e.__rememberedObjects.length&&v(e),-1===e.__rememberedObjects.indexOf(t)&&e.__rememberedObjects.push(t)}),this.autoPlace&&w(this,this.width)},getRoot:function(){for(var e=this;e.parent;)e=e.parent;return e},getSaveObject:function(){var e=this.load;return e.closed=this.closed,this.__rememberedObjects.length>0&&(e.preset=this.preset,e.remembered||(e.remembered={}),e.remembered[this.preset]=x(this)),e.folders={},S.each(this.__folders,function(t,n){e.folders[n]=t.getSaveObject()}),e},save:function(){this.load.remembered||(this.load.remembered={}),this.load.remembered[this.preset]=x(this),_(this,!1),this.saveToLocalStorageIfPossible()},saveAs:function(e){this.load.remembered||(this.load.remembered={},this.load.remembered[se]=x(this,!0)),this.load.remembered[e]=x(this),this.preset=e,g(this,e,!0),this.saveToLocalStorageIfPossible()},revert:function(e){S.each(this.__controllers,function(t){this.getRoot().load.remembered?p(e||this.getRoot(),t):t.setValue(t.initialValue),t.__onFinishChange&&t.__onFinishChange.call(t,t.getValue())},this),S.each(this.__folders,function(e){e.revert(e)}),e||_(this.getRoot(),!1)},listen:function(e){var t=0===this.__listening.length;this.__listening.push(e),t&&C(this.__listening)},updateDisplay:function(){S.each(this.__controllers,function(e){e.updateDisplay()}),S.each(this.__folders,function(e){e.updateDisplay()})}});var pe={Color:I,math:N,interpret:R},fe={Controller:z,BooleanController:K,OptionController:Y,StringController:J,NumberController:W,NumberControllerBox:Q,NumberControllerSlider:q,FunctionController:Z,ColorController:$},me={dom:X},ge={GUI:he},be=he,ve={color:pe,controllers:fe,dom:me,gui:ge,GUI:be};e.color=pe,e.controllers=fe,e.dom=me,e.gui=ge,e.GUI=be,e.default=ve,Object.defineProperty(e,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index bcd721a1bd..6470d93f8e 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -51,23 +51,7 @@ declare module dat { } var gui = new dat.GUI({name: 'Excalibur'}); -var actorfolder = gui.addFolder('Flags'); -actorfolder.add(ex.Debug, 'showDrawingCullBox'); -actorfolder.add(ex.Debug, 'showCameraFocus'); -actorfolder.add(ex.Debug, 'showCameraViewport'); -actorfolder.add(ex.Debug, 'showActorAnchor'); -actorfolder.add(ex.Debug, 'showActorId'); -actorfolder.add(ex.Debug, 'showActorUnitCircle'); -var folder = gui.addFolder('Physics Flags'); -folder.add(ex.Physics, 'enabled') -folder.add(ex.Physics.debug, 'showColliderBounds') -folder.add(ex.Physics.debug, 'showColliderGeometry') -folder.add(ex.Physics.debug, 'showColliderNormals') -folder.add(ex.Physics.debug, 'showContacts') -folder.add(ex.Physics.debug, 'showMotionVectors') -folder.add(ex.Physics.debug, 'broadphaseDebug') -folder.add(ex.Physics, "positionIterations") -folder.add(ex.Physics, "velocityIterations") + var stats = new Stats(); stats.showPanel(0); @@ -75,7 +59,30 @@ document.body.appendChild(stats.dom); var bootstrap = (game: ex.Engine) => { gui.add({toggleDebug: false}, 'toggleDebug').onChange(() => game.toggleDebug()); - var entities = gui.addFolder('Entities'); + var supportedKeys = ['filter', 'entity', 'transform', 'motion', 'body', 'collider', 'physics', 'graphics', 'camera']; + for (let key of supportedKeys) { + let folder = gui.addFolder(key); + if (game.debug[key]) { + for (let option in game.debug[key]) { + if (option) { + if (option.toLocaleLowerCase().includes('color')) { + folder.addColor(game.debug[key], option); + } else { + if (Array.isArray(game.debug[key][option])) { + continue; + } + folder.add(game.debug[key], option); + } + } + } + } + } + + var physics = gui.addFolder('Physics Flags'); + physics.add(ex.Physics, 'enabled') + physics.add(ex.Physics, "positionIterations", 1, 15, 1); + physics.add(ex.Physics, "velocityIterations", 1, 15, 1); + game.on("preframe", () => { stats.begin(); }); @@ -83,14 +90,14 @@ var bootstrap = (game: ex.Engine) => { stats.end(); }); - game.currentScene.on('entityadded', (e: any) => { - var entity: ex.Entity = e.target; - var obj = {id: entity.id, name: entity.constructor.name, types: entity.types}; + // game.currentScene.on('entityadded', (e: any) => { + // var entity: ex.Entity = e.target; + // var obj = {id: entity.id, name: entity.constructor.name, types: entity.types}; - var pos = entities.addFolder(`${obj.id}:${obj.name}`) - pos.add({pos: entity.get(ex.TransformComponent).pos.toString()}, 'pos'); - pos.add({types: obj.types.join(', ')}, 'types'); - }); + // var pos = entities.addFolder(`${obj.id}:${obj.name}`) + // pos.add({pos: entity.get(ex.TransformComponent).pos.toString()}, 'pos'); + // pos.add({types: obj.types.join(', ')}, 'types'); + // }); return { stats, gui } } @@ -496,6 +503,7 @@ game.add(follower); // player.enableCapturePointer = true; // player.collisionType = ex.CollisionType.Active; var player = new ex.Actor({ + name: 'player', pos: new ex.Vector(100, -200), width: 32, height: 96, @@ -515,7 +523,13 @@ follower.actions player.rotation = 0; // Health bar example -var healthbar = new ex.Actor({x: 0, y: -70, width: 140, height: 5, color: new ex.Color(0, 255, 0)}); +var healthbar = new ex.Actor({ + name: 'player healthbar', + x: 0, + y: -70, + width: 140, + height: 5, + color: new ex.Color(0, 255, 0)}); player.addChild(healthbar); // player.onPostDraw = (ctx: CanvasRenderingContext2D) => { // ctx.fillStyle = 'red'; diff --git a/sandbox/stats/stats.js b/sandbox/stats/stats.js new file mode 100644 index 0000000000..2f1a0e1361 --- /dev/null +++ b/sandbox/stats/stats.js @@ -0,0 +1,5 @@ +// stats.js - http://github.com/mrdoob/stats.js +(function(f,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"===typeof define&&define.amd?define(e):f.Stats=e()})(this,function(){var f=function(){function e(a){c.appendChild(a.dom);return a}function u(a){for(var d=0;d=g+1E3&&(r.update(1E3*a/(c-g),100),g=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/ + 1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){k=this.end()},domElement:c,setMode:u}};f.Panel=function(e,f,l){var c=Infinity,k=0,g=Math.round,a=g(window.devicePixelRatio||1),r=80*a,h=48*a,t=3*a,v=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=h;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,h);b.fillStyle=f;b.fillText(e,t,v); + b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(h,w){c=Math.min(c,h);k=Math.max(k,h);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=f;b.fillText(g(h)+" "+e+" ("+g(c)+"-"+g(k)+")",t,v);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,g((1-h/w)*p))}}};return f}); \ No newline at end of file diff --git a/sandbox/tests/collisionvelocity/vel.ts b/sandbox/tests/collisionvelocity/vel.ts index ba6b82b17a..4b3b20fdc8 100644 --- a/sandbox/tests/collisionvelocity/vel.ts +++ b/sandbox/tests/collisionvelocity/vel.ts @@ -7,11 +7,11 @@ var engine = new ex.Engine({ pointerScope: ex.Input.PointerScope.Canvas }); engine.showDebug(true); +engine.debug.body.showAll = true; +engine.debug.collider.showGeometry = true; +engine.debug.physics.showCollisionContacts = true; +engine.debug.physics.showCollisionNormals = true; ex.Physics.acc.setTo(0, 200); -ex.Physics.debug.showMotionVectors = true; -ex.Physics.debug.showColliderGeometry = true; -ex.Physics.debug.showContacts = true; -ex.Physics.debug.showCollisionNormals = true; var floor = new ex.Actor({ x: engine.halfDrawWidth, diff --git a/sandbox/tests/physics/fastphysics.ts b/sandbox/tests/physics/fastphysics.ts index 8134911143..0762ac8827 100644 --- a/sandbox/tests/physics/fastphysics.ts +++ b/sandbox/tests/physics/fastphysics.ts @@ -1,17 +1,16 @@ /// +ex.Physics.useRealisticPhysics() var game = new ex.Engine({ width: 600, height: 400 }); -ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.Realistic; - -ex.Physics.debug.broadphaseDebug = false; -ex.Physics.debug.showColliderGeometry = true; -ex.Physics.debug.showMotionVectors = true; -ex.Physics.debug.showColliderBounds = true; -ex.Physics.debug.showContacts = true; -ex.Physics.debug.showNormals = true; + +game.debug.physics.showBroadphaseSpacePartitionDebug = false; +game.debug.physics.showCollisionContacts = true; +game.debug.collider.showGeometry = true; +game.debug.motion.showAll = true; +game.debug.collider.showBounds = true; ex.Physics.acc.setTo(0, 300); //ex.Physics.dynamicTreeVelocityMultiplyer = 1; game.currentScene.camera.zoom = 0.5; diff --git a/sandbox/tests/physics/physics.ts b/sandbox/tests/physics/physics.ts index 0da7adee19..275026e1f2 100644 --- a/sandbox/tests/physics/physics.ts +++ b/sandbox/tests/physics/physics.ts @@ -7,16 +7,15 @@ var game = new ex.Engine({ game.backgroundColor = ex.Color.Black; game.showDebug(true); +game.debug.physics.showBroadphaseSpacePartitionDebug = false; +game.debug.physics.showCollisionContacts = true; +game.debug.collider.showGeometry = true; +game.debug.collider.showBounds = true; +game.debug.motion.showAll = true; +game.debug.body.showMotion = true; ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.Realistic; ex.Physics.bodiesCanSleepByDefault = true; -ex.Physics.debug.broadphaseDebug = false; -ex.Physics.debug.showColliderGeometry = true; -ex.Physics.debug.showMotionVectors = true; -ex.Physics.debug.showSleepMotion = true; -ex.Physics.debug.showColliderBounds = true; -ex.Physics.debug.showContacts = true; -ex.Physics.debug.showNormals = true; ex.Physics.gravity = ex.vec(0, 100); @@ -25,14 +24,6 @@ var folder = gui.addFolder('Physics Flags'); folder.add(ex.Physics, 'enabled') folder.add(ex.Physics, 'bodiesCanSleepByDefault') folder.add(ex.Physics, 'warmStart') -folder.add(ex.Physics.debug, 'showColliderBounds') -folder.add(ex.Physics.debug, 'showColliderGeometry') -folder.add(ex.Physics.debug, 'showColliderNormals') -folder.add(ex.Physics.debug, 'showContacts') -folder.add(ex.Physics.debug, 'showNormals') -folder.add(ex.Physics.debug, 'showSleepMotion') -folder.add(ex.Physics.debug, 'showMotionVectors') -folder.add(ex.Physics.debug, 'broadphaseDebug') folder.add(ex.Physics, 'sleepEpsilon', 0.01, 2, .05); folder.add(ex.Physics, 'wakeThreshold', 0.01, 2, .05); folder.add(ex.Physics, 'positionIterations', 0, 30, 1); diff --git a/sandbox/tests/physics/physics2.ts b/sandbox/tests/physics/physics2.ts index 20ce1cca3e..4da7b6bc81 100644 --- a/sandbox/tests/physics/physics2.ts +++ b/sandbox/tests/physics/physics2.ts @@ -1,5 +1,6 @@ /// +ex.Physics.useRealisticPhysics(); var game = new ex.Engine({ width: 600, height: 400 @@ -7,8 +8,7 @@ var game = new ex.Engine({ game.showDebug(true); -ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.Realistic; -ex.Physics.debug.broadphaseDebug = true; +game.debug.physics.showBroadphaseSpacePartitionDebug = true; game.currentScene.camera.x = 0; game.currentScene.camera.y = 0; diff --git a/sandbox/tests/trigger/trigger.ts b/sandbox/tests/trigger/trigger.ts index d0b11f025d..dba1048872 100644 --- a/sandbox/tests/trigger/trigger.ts +++ b/sandbox/tests/trigger/trigger.ts @@ -5,8 +5,8 @@ var game = new ex.Engine({ height: 400 }); -ex.Physics.debug.showColliderGeometry = true; -ex.Physics.debug.showColliderBounds = true; +engine.debug.collider.showGeometry = true; +engine.debug.collider.showBounds = true; game.showDebug(true); diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 597b872f92..203bf42180 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -44,7 +44,6 @@ import { Entity } from './EntityComponentSystem/Entity'; import { CanvasDrawComponent } from './Drawing/CanvasDrawComponent'; import { TransformComponent } from './EntityComponentSystem/Components/TransformComponent'; import { MotionComponent } from './EntityComponentSystem/Components/MotionComponent'; -import { Debug } from './Debug'; import { GraphicsComponent } from './Graphics/GraphicsComponent'; import { Rectangle } from './Graphics/Rectangle'; import { Flags, Legacy } from './Flags'; @@ -66,6 +65,10 @@ export function isActor(x: any): x is Actor { * Actor contructor options */ export interface ActorArgs { + /** + * Optionally set the name of the actor, default is 'anonymous' + */ + name?: string; /** * Optionally set the x position of the actor, default is 0 */ @@ -486,10 +489,29 @@ export class Actor extends Entity implements Actionable, Eventable, PointerEvent constructor(config?: ActorArgs) { super(); - const { x, y, pos, scale, width, height, collider, vel, acc, rotation, angularVelocity, z, color, visible, anchor, collisionType } = { + const { + name, + x, + y, + pos, + scale, + width, + height, + collider, + vel, + acc, + rotation, + angularVelocity, + z, + color, + visible, + anchor, + collisionType + } = { ...config }; + this._setName(name); this.anchor = anchor ?? Actor.defaults.anchor.clone(); this.addComponent(new TransformComponent()); @@ -1228,66 +1250,8 @@ export class Actor extends Entity implements Actionable, Eventable, PointerEvent * @param ctx The rendering context */ /* istanbul ignore next */ - public debugDraw(ctx: CanvasRenderingContext2D) { - this.emit('predebugdraw', new PreDebugDrawEvent(ctx, this)); - - // Draw actor Id - if (Debug.showActorId) { - ctx.fillText('id: ' + this.id, this.collider.bounds.left + 3, this.collider.bounds.top + 10); - } - - // Draw actor anchor Vector - if (Debug.showActorAnchor) { - ctx.fillStyle = Color.Yellow.toString(); - ctx.beginPath(); - ctx.arc(this.getGlobalPos().x, this.getGlobalPos().y, 3, 0, Math.PI * 2); - ctx.closePath(); - ctx.fill(); - } - - // Culling Box debug draw - for (let j = 0; j < this.traits.length; j++) { - if (this.traits[j] instanceof Traits.OffscreenCulling && Debug.showDrawingCullBox) { - (this.traits[j]).cullingBox.debugDraw(ctx); // eslint-disable-line - } - } - - // Unit Circle debug draw - if (Debug.showActorUnitCircle) { - ctx.strokeStyle = Color.Yellow.toString(); - ctx.beginPath(); - const radius = Math.min(this.width, this.height); - ctx.arc(this.getGlobalPos().x, this.getGlobalPos().y, radius, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - const ticks: { [key: string]: number } = { - '0 Pi': 0, - 'Pi/2': Math.PI / 2, - Pi: Math.PI, - '3/2 Pi': (3 * Math.PI) / 2 - }; - - const oldFont = ctx.font; - for (const tick in ticks) { - ctx.fillStyle = Color.Yellow.toString(); - ctx.font = '14px'; - ctx.textAlign = 'center'; - ctx.fillText( - tick, - this.getGlobalPos().x + Math.cos(ticks[tick]) * (radius + 10), - this.getGlobalPos().y + Math.sin(ticks[tick]) * (radius + 10) - ); - } - - ctx.font = oldFont; - } - - // Draw child actors - // for (let i = 0; i < this.children.length; i++) { - // this.children[i].debugDraw(ctx); - // } - - this.emit('postdebugdraw', new PostDebugDrawEvent(ctx, this)); + public debugDraw(_ctx: CanvasRenderingContext2D) { + // pass } // #endregion } diff --git a/src/engine/Camera.ts b/src/engine/Camera.ts index 809925728c..6f5a1211e3 100644 --- a/src/engine/Camera.ts +++ b/src/engine/Camera.ts @@ -8,7 +8,6 @@ import { PreUpdateEvent, PostUpdateEvent, GameEvent, InitializeEvent } from './E import { Class } from './Class'; import { BoundingBox } from './Collision/BoundingBox'; import { Logger } from './Util/Log'; -import { Debug } from './Debug'; import { ExcaliburGraphicsContext } from './Graphics/Context/ExcaliburGraphicsContext'; import { watchAny } from './Util/Watch'; @@ -739,31 +738,8 @@ export class Camera extends Class implements CanUpdate, CanInitialize { } /* istanbul ignore next */ - public debugDraw(ctx: CanvasRenderingContext2D) { - if (Debug.showCameraFocus) { - const focus = this.getFocus(); - ctx.fillStyle = 'red'; - ctx.strokeStyle = 'white'; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.arc(focus.x, focus.y, 15, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - - ctx.beginPath(); - ctx.arc(focus.x, focus.y, 5, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - } - - if (Debug.showCameraViewport) { - ctx.beginPath(); - ctx.setLineDash([5, 15]); - ctx.lineWidth = 5; - ctx.strokeStyle = 'white'; - ctx.strokeRect(this.viewport.left, this.viewport.top, this.viewport.width, this.viewport.height); - ctx.closePath(); - } + public debugDraw(_ctx: CanvasRenderingContext2D) { + // pass } private _isDoneShaking(): boolean { diff --git a/src/engine/Collision/BodyComponent.ts b/src/engine/Collision/BodyComponent.ts index cf500384bd..7131bc27ad 100644 --- a/src/engine/Collision/BodyComponent.ts +++ b/src/engine/Collision/BodyComponent.ts @@ -9,8 +9,6 @@ import { CollisionGroup } from './Group/CollisionGroup'; import { EventDispatcher } from '../EventDispatcher'; import { createId, Id } from '../Id'; import { clamp } from '../Util/Util'; -import { DrawUtil } from '../Util/Index'; -import { Color } from '../Color'; import { ColliderComponent } from './ColliderComponent'; export interface BodyComponentOptions { @@ -402,24 +400,7 @@ export class BodyComponent extends Component<'ex.body'> implements Clonable { this._collider.owner = this.owner; this.events.wire(collider.events); this.$colliderAdded.notifyAll(collider); + this.update(); } } @@ -60,7 +61,7 @@ export class ColliderComponent extends Component<'ex.collider'> { } public update() { - const tx = this.owner.get(TransformComponent); + const tx = this.owner?.get(TransformComponent); if (this.collider) { this.collider.owner = this.owner; if (tx) { diff --git a/src/engine/Collision/CollisionSystem.ts b/src/engine/Collision/CollisionSystem.ts index bf623f56d2..c91bf1c814 100644 --- a/src/engine/Collision/CollisionSystem.ts +++ b/src/engine/Collision/CollisionSystem.ts @@ -1,14 +1,9 @@ -import { Camera } from '../Camera'; -import { Color } from '../Color'; import { Entity } from '../EntityComponentSystem'; import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { AddedEntity, isAddedSystemEntity, RemovedEntity, System, SystemType } from '../EntityComponentSystem/System'; import { CollisionEndEvent, CollisionStartEvent, ContactEndEvent, ContactStartEvent } from '../Events'; import { CollisionResolutionStrategy, Physics } from './Physics'; -import { Scene } from '../Scene'; -import { DrawUtil } from '../Util/Index'; -// import { BodyComponent } from './BodyComponent'; import { ArcadeSolver } from './Solver/ArcadeSolver'; import { Collider } from './Shapes/Collider'; import { CollisionContact } from './Detection/CollisionContact'; @@ -17,12 +12,14 @@ import { RealisticSolver } from './Solver/RealisticSolver'; import { CollisionSolver } from './Solver/Solver'; import { ColliderComponent } from './ColliderComponent'; import { CompositeCollider } from './Shapes/CompositeCollider'; +import { Engine, ExcaliburGraphicsContext, Scene } from '..'; export class CollisionSystem extends System { public readonly types = ['ex.transform', 'ex.motion', 'ex.collider'] as const; public systemType = SystemType.Update; public priority = -1; + private _engine: Engine; private _realisticSolver = new RealisticSolver(); private _arcadeSolver = new ArcadeSolver(); private _processor = new DynamicTreeCollisionProcessor(); @@ -32,9 +29,6 @@ export class CollisionSystem extends System this._processor.track(c); private _untrackCollider = (c: Collider) => this._processor.untrack(c); - // Ctx and camera are used for the debug draw - private _camera: Camera; - notify(message: AddedEntity | RemovedEntity) { if (isAddedSystemEntity(message)) { const colliderComponent = message.data.get(ColliderComponent); @@ -52,7 +46,7 @@ export class CollisionSystem extends System { - DrawUtil.point(ctx, Color.Red, p); - }); - } - if (Physics.debug.showCollisionNormals) { - contact.points.forEach((p) => { - DrawUtil.vector(ctx, Color.Cyan, p, contact.normal, 30); - }); - } - } - } - ctx.restore(); + debug(ex: ExcaliburGraphicsContext) { + this._processor.debug(ex); } public runContactStartEnd() { diff --git a/src/engine/Collision/Detection/CollisionProcessor.ts b/src/engine/Collision/Detection/CollisionProcessor.ts index 867465ef26..1186b9711f 100644 --- a/src/engine/Collision/Detection/CollisionProcessor.ts +++ b/src/engine/Collision/Detection/CollisionProcessor.ts @@ -2,6 +2,7 @@ import { Pair } from './Pair'; import { Collider } from '../Shapes/Collider'; import { CollisionContact } from './CollisionContact'; +import { ExcaliburGraphicsContext } from '../..'; /** * Definition for collision processor @@ -27,5 +28,5 @@ export interface CollisionProcessor { /** * Draw any debug information */ - debugDraw(ctx: CanvasRenderingContext2D, delta: number): void; + debug(ex: ExcaliburGraphicsContext, delta: number): void; } diff --git a/src/engine/Collision/Detection/DynamicTree.ts b/src/engine/Collision/Detection/DynamicTree.ts index bf5a9ff08d..660e6abca7 100644 --- a/src/engine/Collision/Detection/DynamicTree.ts +++ b/src/engine/Collision/Detection/DynamicTree.ts @@ -6,6 +6,7 @@ import { Logger } from '../../Util/Log'; import { Id } from '../../Id'; import { Entity } from '../../EntityComponentSystem/Entity'; import { BodyComponent } from '../BodyComponent'; +import { Color, ExcaliburGraphicsContext } from '../..'; /** * Dynamic Tree Node used for tracking bounds within the tree @@ -474,18 +475,15 @@ export class DynamicTree> { return helper(this.root); } - public debugDraw(ctx: CanvasRenderingContext2D) { + public debug(ex: ExcaliburGraphicsContext) { // draw all the nodes in the Dynamic Tree const helper = (currentNode: TreeNode) => { if (currentNode) { if (currentNode.isLeaf()) { - ctx.lineWidth = 1; - ctx.strokeStyle = 'green'; + currentNode.bounds.draw(ex, Color.Green); } else { - ctx.lineWidth = 1; - ctx.strokeStyle = 'white'; + currentNode.bounds.draw(ex, Color.White); } - currentNode.bounds.debugDraw(ctx); if (currentNode.left) { helper(currentNode.left); diff --git a/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts b/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts index 084eede1b7..d0e6c46f94 100644 --- a/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts +++ b/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts @@ -10,11 +10,9 @@ import { Logger } from '../../Util/Log'; import { CollisionType } from '../CollisionType'; import { Collider } from '../Shapes/Collider'; import { CollisionContact } from '../Detection/CollisionContact'; -import { Color } from '../../Color'; -import { ConvexPolygon } from '../Shapes/ConvexPolygon'; -import { DrawUtil } from '../../Util/Index'; import { BodyComponent } from '../BodyComponent'; import { CompositeCollider } from '../Shapes/CompositeCollider'; +import { ExcaliburGraphicsContext } from '../..'; /** * Responsible for performing the collision broadphase (locating potential colllisions) and @@ -216,9 +214,12 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor { public narrowphase(pairs: Pair[], stats?: FrameStats): CollisionContact[] { let contacts: CollisionContact[] = []; for (let i = 0; i < pairs.length; i++) { - contacts = contacts.concat(pairs[i].collide()); - if (stats && contacts.length > 0) { - stats.physics.collidersHash[pairs[i].id] = pairs[i]; + const newContacts = pairs[i].collide(); + contacts = contacts.concat(newContacts); + if (stats && newContacts.length > 0) { + for (const c of newContacts) { + stats.physics.contacts.set(c.id, c); + } } } if (stats) { @@ -242,34 +243,7 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor { return updated; } - /* istanbul ignore next */ - public debugDraw(ctx: CanvasRenderingContext2D) { - if (Physics.debug.broadphaseDebug) { - this._dynamicCollisionTree.debugDraw(ctx); - } - - if (Physics.debug.showColliderGeometry) { - for (const collider of this._colliders) { - const body = collider.owner.get(BodyComponent); - if (Physics.debug.showColliderBounds) { - collider.bounds.debugDraw(ctx, Color.Yellow); - } - - if (Physics.debug.showColliderGeometry) { - let color = Color.Green; - if (body.sleeping || body.collisionType === CollisionType.Fixed) { - color = Color.Gray; - } - collider.debugDraw(ctx, color); - } - - if (Physics.debug.showColliderNormals && collider instanceof ConvexPolygon) { - for (const side of collider.getSides()) { - DrawUtil.point(ctx, Color.Blue, side.midpoint); - DrawUtil.vector(ctx, Color.Yellow, side.midpoint, side.normal(), 30); - } - } - } - } + public debug(ex: ExcaliburGraphicsContext) { + this._dynamicCollisionTree.debug(ex); } } diff --git a/src/engine/Collision/MotionSystem.ts b/src/engine/Collision/MotionSystem.ts index ca4114fa1b..f1ce23d707 100644 --- a/src/engine/Collision/MotionSystem.ts +++ b/src/engine/Collision/MotionSystem.ts @@ -1,12 +1,8 @@ -import { Camera } from '../Camera'; -import { Color } from '../Color'; import { Entity } from '../EntityComponentSystem'; import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { System, SystemType } from '../EntityComponentSystem/System'; import { Physics } from './Physics'; -import { Scene } from '../Scene'; -import { DrawUtil } from '../Util/Index'; import { BodyComponent } from './BodyComponent'; import { CollisionType } from './CollisionType'; import { EulerIntegrator } from './Integrator'; @@ -16,16 +12,9 @@ export class MotionSystem extends System { public systemType = SystemType.Update; public priority = -1; - private _entities: Entity[] = []; - private _camera: Camera; - initialize(scene: Scene) { - this._camera = scene.camera; - } - update(_entities: Entity[], elapsedMs: number): void { let transform: TransformComponent; let motion: MotionComponent; - this._entities = _entities; for (const entity of _entities) { transform = entity.get(TransformComponent); motion = entity.get(MotionComponent); @@ -46,27 +35,7 @@ export class MotionSystem extends System { } } - debugDraw(ctx: CanvasRenderingContext2D) { - ctx.save(); - this._camera.draw(ctx); - for (const entity of this._entities) { - const transform = entity.get(TransformComponent); - const motion = entity.get(MotionComponent); - if (Physics.debug.showMotionVectors) { - DrawUtil.vector(ctx, Color.Yellow, transform.pos, motion.acc.add(Physics.acc)); - DrawUtil.vector(ctx, Color.Blue, transform.pos, motion.vel); - DrawUtil.point(ctx, Color.Red, transform.pos); - } - if (Physics.debug.showSleepMotion) { - const pos = transform.pos; - const body = entity.get(BodyComponent); - if (body) { - ctx.fillStyle = 'yellow'; - ctx.font = '18px'; - ctx.fillText(body.sleepMotion.toString(), pos.x, pos.y); - } - } - } - ctx.restore(); + debugDraw(_ctx: CanvasRenderingContext2D) { + // pass } } diff --git a/src/engine/Collision/Physics.ts b/src/engine/Collision/Physics.ts index 2f9321493e..fdf0270831 100644 --- a/src/engine/Collision/Physics.ts +++ b/src/engine/Collision/Physics.ts @@ -1,43 +1,6 @@ import { Vector } from '../Math/vector'; import { obsolete } from '../Util/Decorators'; -export class PhysicsDebug { - /** - * Globally switches the debug information for the broadphase strategy - */ - public static broadphaseDebug: boolean = false; - /** - * Show the normals as a result of collision on the screen. - */ - public static showCollisionNormals: boolean = false; - /** - * Show the position, velocity, and acceleration as graphical vectors. - */ - public static showMotionVectors: boolean = false; - - /** - * Show the amount of motion a body has accumulated - */ - public static showSleepMotion: boolean = false; - /** - * Show the axis-aligned bounding boxes of the collision bodies on the screen. - */ - public static showColliderBounds: boolean = false; - /** - * Show the bounding collision area shapes - */ - public static showColliderGeometry: boolean = false; - - public static showColliderNormals: boolean = false; - /** - * Show points of collision interpreted by excalibur as a result of collision. - */ - public static showContacts: boolean = false; - /** - * Show the surface normals of the collision areas. - */ - public static showNormals: boolean = false; -} /** * Possible collision resolution strategies @@ -95,11 +58,6 @@ export class Physics { */ public static enabled = true; - /** - * Physics debug toggles for debug mode - */ - public static debug = PhysicsDebug; - /** * Gets or sets the broadphase pair identification strategy. * diff --git a/src/engine/Collision/Shapes/CircleCollider.ts b/src/engine/Collision/Shapes/CircleCollider.ts index aa812cf32f..982a148810 100644 --- a/src/engine/Collision/Shapes/CircleCollider.ts +++ b/src/engine/Collision/Shapes/CircleCollider.ts @@ -12,7 +12,8 @@ import { Color } from '../../Color'; import { Collider } from './Collider'; import { ClosestLineJumpTable } from './ClosestLineJumpTable'; -import { Transform } from '../../EntityComponentSystem'; +import { Transform, TransformComponent } from '../../EntityComponentSystem'; +import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext'; export interface CircleColliderOptions { /** @@ -172,7 +173,8 @@ export class CircleCollider extends Collider { * Get the axis aligned bounding box for the circle collider in world coordinates */ public get bounds(): BoundingBox { - const bodyPos = this._transform?.pos ?? Vector.Zero; + const tx = this._transform as TransformComponent; + const bodyPos = tx?.globalPos ?? Vector.Zero; return new BoundingBox( this.offset.x + bodyPos.x - this.radius, this.offset.y + bodyPos.y - this.radius, @@ -235,6 +237,12 @@ export class CircleCollider extends Collider { ctx.fill(); } + public debug(ex: ExcaliburGraphicsContext, color: Color) { + const tx = this._transform as TransformComponent; + const pos = tx?.globalPos ? tx?.globalPos.add(this.offset) : this.offset; + ex.drawCircle(pos, this.radius, color); + } + /* istanbul ignore next */ public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Green) { const transform = this._transform; diff --git a/src/engine/Collision/Shapes/Collider.ts b/src/engine/Collision/Shapes/Collider.ts index 90549ddd94..4ce714d90f 100644 --- a/src/engine/Collision/Shapes/Collider.ts +++ b/src/engine/Collision/Shapes/Collider.ts @@ -9,6 +9,7 @@ import { Clonable } from '../../Interfaces/Clonable'; import { Entity, Transform } from '../../EntityComponentSystem'; import { createId, Id } from '../../Id'; import { EventDispatcher } from '../../EventDispatcher'; +import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext'; /** * A collision collider specifies the geometry that can detect when other collision colliders intersect @@ -111,6 +112,7 @@ export abstract class Collider implements Clonable { */ abstract draw(ctx: CanvasRenderingContext2D, color?: Color, pos?: Vector): void; + abstract debug(ex: ExcaliburGraphicsContext, color: Color): void; /** * Draw any debug information */ diff --git a/src/engine/Collision/Shapes/CompositeCollider.ts b/src/engine/Collision/Shapes/CompositeCollider.ts index 15bbf1249d..6aff4ad93f 100644 --- a/src/engine/Collision/Shapes/CompositeCollider.ts +++ b/src/engine/Collision/Shapes/CompositeCollider.ts @@ -2,6 +2,7 @@ import { Util } from '../..'; import { Pair } from '../Detection/Pair'; import { Color } from '../../Color'; import { Transform } from '../../EntityComponentSystem'; +import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext'; import { Line } from '../../Math/line'; import { Projection } from '../../Math/projection'; import { Ray } from '../../Math/ray'; @@ -237,6 +238,14 @@ export class CompositeCollider extends Collider { collider.draw(ctx, color, pos); } } + + public debug(ex: ExcaliburGraphicsContext, color: Color) { + const colliders = this.getColliders(); + for (const collider of colliders) { + collider.debug(ex, color); + } + } + debugDraw(ctx: CanvasRenderingContext2D, color: Color): void { const colliders = this.getColliders(); for (const collider of colliders) { diff --git a/src/engine/Collision/Shapes/ConvexPolygon.ts b/src/engine/Collision/Shapes/ConvexPolygon.ts index e460d76513..3709db9252 100644 --- a/src/engine/Collision/Shapes/ConvexPolygon.ts +++ b/src/engine/Collision/Shapes/ConvexPolygon.ts @@ -11,6 +11,7 @@ import { Ray } from '../../Math/ray'; import { ClosestLineJumpTable } from './ClosestLineJumpTable'; import { Transform, TransformComponent } from '../../EntityComponentSystem'; import { Collider } from './Collider'; +import { ExcaliburGraphicsContext } from '../..'; export interface ConvexPolygonOptions { /** @@ -325,10 +326,10 @@ export class ConvexPolygon extends Collider { * Get the axis aligned bounding box for the polygon collider in world coordinates */ public get bounds(): BoundingBox { - // const points = this.getTransformedPoints(); - const scale = this._transform?.scale ?? Vector.One; - const rotation = this._transform?.rotation ?? 0; - const pos = (this._transform?.pos ?? Vector.Zero).add(this.offset); + const tx = this._transform as TransformComponent; + const scale = tx?.globalScale ?? Vector.One; + const rotation = tx?.globalRotation ?? 0; + const pos = (tx?.globalPos ?? Vector.Zero).add(this.offset); return this.localBounds.scale(scale).rotate(rotation).translate(pos); } @@ -419,6 +420,15 @@ export class ConvexPolygon extends Collider { ctx.closePath(); ctx.fill(); } + public debug(ex: ExcaliburGraphicsContext, color: Color) { + const firstPoint = this.getTransformedPoints()[0]; + const points = [firstPoint, ...this.getTransformedPoints(), firstPoint]; + for (let i = 0; i < points.length - 1; i++) { + ex.drawLine(points[i], points[i + 1], color, 2); + ex.drawCircle(points[i], 2, color); + ex.drawCircle(points[i + 1], 2, color); + } + } /* istanbul ignore next */ public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Red) { diff --git a/src/engine/Collision/Shapes/Edge.ts b/src/engine/Collision/Shapes/Edge.ts index 9de9a565f0..f26dfe6408 100644 --- a/src/engine/Collision/Shapes/Edge.ts +++ b/src/engine/Collision/Shapes/Edge.ts @@ -11,7 +11,8 @@ import { Ray } from '../../Math/ray'; import { Color } from '../../Color'; import { Collider } from './Collider'; import { ClosestLineJumpTable } from '../Shapes/ClosestLineJumpTable'; -import { Transform } from '../../EntityComponentSystem/Components/TransformComponent'; +import { Transform, TransformComponent } from '../../EntityComponentSystem/Components/TransformComponent'; +import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext'; export interface EdgeOptions { /** @@ -64,19 +65,20 @@ export class Edge extends Collider { } private _getBodyPos(): Vector { - const bodyPos = this._transform?.pos ?? Vector.Zero; + const tx = this._transform as TransformComponent; + const bodyPos = tx?.globalPos ?? Vector.Zero; return bodyPos; } private _getTransformedBegin(): Vector { - const transform = this._transform; - const angle = transform ? transform.rotation : 0; + const tx = this._transform as TransformComponent; + const angle = tx ? tx.globalRotation : 0; return this.begin.rotate(angle).add(this._getBodyPos()); } private _getTransformedEnd(): Vector { - const transform = this._transform; - const angle = transform ? transform.rotation : 0; + const tx = this._transform as TransformComponent; + const angle = tx ? tx.globalRotation : 0; return this.end.rotate(angle).add(this._getBodyPos()); } @@ -271,6 +273,14 @@ export class Edge extends Collider { ctx.stroke(); } + public debug(ex: ExcaliburGraphicsContext, color: Color) { + const begin = this._getTransformedBegin(); + const end = this._getTransformedEnd(); + ex.drawLine(begin, end, color, 2); + ex.drawCircle(begin, 2, color); + ex.drawCircle(end, 2, color); + } + /* istanbul ignore next */ public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Red) { const begin = this._getTransformedBegin(); diff --git a/src/engine/Debug.ts b/src/engine/Debug/Debug.ts similarity index 76% rename from src/engine/Debug.ts rename to src/engine/Debug/Debug.ts index c8d038b9c7..a3d287c06e 100644 --- a/src/engine/Debug.ts +++ b/src/engine/Debug/Debug.ts @@ -1,6 +1,7 @@ import { DebugFlags, ColorBlindFlags } from './DebugFlags'; -import { Pair } from './Collision/Detection/Pair'; -import { Engine } from './Engine'; +import { Engine } from '../Engine'; +import { Color } from '../Color'; +import { CollisionContact } from '../Collision/Detection/CollisionContact'; /** * Debug stats containing current and previous frame statistics @@ -10,13 +11,6 @@ export interface DebugStats { prevFrame: FrameStats; } -/** - * Hash containing the [[Pair.id]]s of pairs that collided in a frame - */ -export interface CollidersHash { - [pairId: string]: Pair; -} - /** * Represents a frame's individual statistics */ @@ -122,9 +116,9 @@ export interface PhysicsStatistics { collisions: number; /** - * A Hash storing the [[Pair.id]]s of [[Pair]]s that collided in the frame + * Copy of the current frame contacts (only updated if debug is toggled on) */ - collidersHash: CollidersHash; + contacts: Map; /** * Gets the number of fast moving bodies using raycast continuous collisions in the scene @@ -166,13 +160,6 @@ export class Debug implements DebugFlags { this.colorBlindMode = new ColorBlindFlags(this._engine); } - public static showDrawingCullBox = false; - public static showCameraFocus = false; - public static showCameraViewport = false; - public static showActorAnchor = false; - public static showActorId = false; - public static showActorUnitCircle = false; - /** * Performance statistics */ @@ -195,6 +182,127 @@ export class Debug implements DebugFlags { * @warning Will reduce FPS. */ public colorBlindMode: ColorBlindFlags; + + /** + * Filter debug context to named entities or entity ids + */ + public filter: { useFilter: boolean; nameQuery: string; ids: number[] } = { + /** + * Toggle filter on or off (default off) must be on for DebugDraw to use filters + */ + useFilter: false, + /** + * Query for entities by name, if the entity name contains `nameQuery` it will be included + */ + nameQuery: '', + /** + * Query for Entity ids, if the id matches it will be included + */ + ids: [] + }; + + /** + * Entity debug settings + */ + public entity = { + showAll: false, + showId: true, + showName: false + }; + + /** + * Transform component debug settings + */ + public transform = { + showAll: false, + + showPosition: false, + positionColor: Color.Yellow, + + showScale: false, + scaleColor: Color.Green, + + showRotation: false, + rotationColor: Color.Blue + }; + + /** + * Graphics component debug settings + */ + public graphics = { + showAll: false, + + showBounds: true, + boundsColor: Color.Yellow + }; + + /** + * Collider component debug settings + */ + public collider = { + showAll: false, + + showBounds: true, + boundsColor: Color.Blue, + + showOwner: false, + + showGeometry: true, + geometryColor: Color.Green + }; + + /** + * Physics simulation debug settings + */ + public physics = { + showAll: false, + + showBroadphaseSpacePartitionDebug: false, + + showCollisionNormals: false, + collisionNormalColor: Color.Cyan, + + showCollisionContacts: true, + collisionContactColor: Color.Red + }; + + /** + * Motion component debug settings + */ + public motion = { + showAll: false, + + showVelocity: false, + velocityColor: Color.Yellow, + + showAcceleration: false, + accelerationColor: Color.Red + }; + + /** + * Body component debug settings + */ + public body = { + showAll: false, + + showCollisionGroup: false, + showCollisionType: false, + showSleeping: false, + showMotion: false, + showMass: false + }; + + /** + * Camera debug settings + */ + public camera = { + showAll: false, + + showFocus: false, + focusColor: Color.Red, + + showZoom: false + }; } /** @@ -229,7 +337,7 @@ export class FrameStats implements FrameStatistics { private _graphicsStats: GraphicsStatistics = { drawCalls: 0, drawnImages: 0 - } + }; /** * Zero out values or clone other IFrameStat stats. Allows instance reuse. @@ -345,7 +453,7 @@ export class FrameStats implements FrameStatistics { export class PhysicsStats implements PhysicsStatistics { private _pairs: number = 0; private _collisions: number = 0; - private _collidersHash: CollidersHash = {}; + private _contacts: Map = new Map(); private _fastBodies: number = 0; private _fastBodyCollisions: number = 0; private _broadphase: number = 0; @@ -360,7 +468,7 @@ export class PhysicsStats implements PhysicsStatistics { if (otherStats) { this.pairs = otherStats.pairs; this.collisions = otherStats.collisions; - this.collidersHash = otherStats.collidersHash; + this.contacts = otherStats.contacts; this.fastBodies = otherStats.fastBodies; this.fastBodyCollisions = otherStats.fastBodyCollisions; this.broadphase = otherStats.broadphase; @@ -368,7 +476,7 @@ export class PhysicsStats implements PhysicsStatistics { } else { this.pairs = this.collisions = this.fastBodies = 0; this.fastBodyCollisions = this.broadphase = this.narrowphase = 0; - this.collidersHash = {}; + this.contacts.clear(); } } @@ -399,12 +507,12 @@ export class PhysicsStats implements PhysicsStatistics { this._collisions = value; } - public get collidersHash(): CollidersHash { - return this._collidersHash; + public get contacts(): Map { + return this._contacts; } - public set collidersHash(colliders: CollidersHash) { - this._collidersHash = colliders; + public set contacts(contacts: Map) { + this._contacts = contacts; } public get fastBodies(): number { diff --git a/src/engine/DebugFlags.ts b/src/engine/Debug/DebugFlags.ts similarity index 81% rename from src/engine/DebugFlags.ts rename to src/engine/Debug/DebugFlags.ts index 3904803a6e..ad62833222 100644 --- a/src/engine/DebugFlags.ts +++ b/src/engine/Debug/DebugFlags.ts @@ -1,5 +1,5 @@ -import { Engine } from './Engine'; -import { ColorBlindCorrector, ColorBlindness } from './PostProcessing/Index'; +import { Engine } from '../Engine'; +import { ColorBlindCorrector, ColorBlindness } from '../PostProcessing/Index'; export interface DebugFlags { colorBlindMode: ColorBlindFlags; diff --git a/src/engine/Debug/DebugSystem.ts b/src/engine/Debug/DebugSystem.ts new file mode 100644 index 0000000000..c418febb1d --- /dev/null +++ b/src/engine/Debug/DebugSystem.ts @@ -0,0 +1,286 @@ +import { Engine } from '../Engine'; +import { Scene } from '../Scene'; +import { Camera } from '../Camera'; +import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; +import { ColliderComponent } from '../Collision/ColliderComponent'; +import { CoordPlane, Entity, TransformComponent } from '../EntityComponentSystem'; +import { System, SystemType } from '../EntityComponentSystem/System'; +import { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext'; +import { vec, Vector } from '../Math/vector'; +import { BodyComponent, CollisionSystem, CompositeCollider, GraphicsComponent, Particle, Util } from '..'; + +export class DebugSystem extends System { + public readonly types = ['ex.transform'] as const; + public readonly systemType = SystemType.Draw; + public priority = 999; // lowest priority + private _graphicsContext: ExcaliburGraphicsContext; + private _collisionSystem: CollisionSystem; + private _camera: Camera; + private _engine: Engine; + + public initialize(scene: Scene): void { + this._graphicsContext = scene.engine.graphicsContext; + this._camera = scene.camera; + this._engine = scene.engine; + this._collisionSystem = scene.world.systemManager.get(CollisionSystem); + } + + update(entities: Entity[], _delta: number): void { + if (!this._engine.isDebug) { + return; + } + + const filterSettings = this._engine.debug.filter; + + let id: number; + let name: string; + const entitySettings = this._engine.debug.entity; + + let tx: TransformComponent; + const txSettings = this._engine.debug.transform; + + let motion: MotionComponent; + const motionSettings = this._engine.debug.motion; + + let collider: ColliderComponent; + const colliderSettings = this._engine.debug.collider; + + const physicsSettings = this._engine.debug.physics; + + let graphics: GraphicsComponent; + const graphicsSettings = this._engine.debug.graphics; + + let body: BodyComponent; + const bodySettings = this._engine.debug.body; + + const cameraSettings = this._engine.debug.camera; + for (const entity of entities) { + if (entity.hasTag('offscreen')) { + // skip offscreen entities + continue; + } + if (entity instanceof Particle) { + // Particles crush the renderer :( + continue; + } + if (filterSettings.useFilter) { + const allIds = filterSettings.ids.length === 0; + const idMatch = allIds || filterSettings.ids.includes(entity.id); + if (!idMatch) { + continue; + } + const allNames = filterSettings.nameQuery === ''; + const nameMatch = allNames || entity.name.includes(filterSettings.nameQuery); + if (!nameMatch) { + continue; + } + } + + let cursor = Vector.Zero; + const lineHeight = vec(0, 16); + id = entity.id; + name = entity.name; + tx = entity.get(TransformComponent); + + // This optionally sets our camera based on the entity coord plan (world vs. screen) + this._pushCameraTransform(tx); + + this._graphicsContext.save(); + + this._applyTransform(entity); + if (tx) { + if (txSettings.showAll || txSettings.showPosition) { + this._graphicsContext.debug.drawPoint(Vector.Zero, { size: 2, color: txSettings.positionColor }); + this._graphicsContext.debug.drawText(`pos${tx.pos.toString(2)}`, cursor); + cursor = cursor.add(lineHeight); + } + + if (entitySettings.showAll || entitySettings.showId) { + this._graphicsContext.debug.drawText(`id(${id}) ${tx.parent ? 'child of id(' + tx.parent?.owner?.id + ')' : ''}`, cursor); + cursor = cursor.add(lineHeight); + } + + if (entitySettings.showAll || entitySettings.showName) { + this._graphicsContext.debug.drawText(`name(${name})`, cursor); + cursor = cursor.add(lineHeight); + } + + if (txSettings.showAll || txSettings.showRotation) { + this._graphicsContext.drawLine( + Vector.Zero, + Vector.fromAngle(tx.rotation).scale(50).add(Vector.Zero), + txSettings.rotationColor, + 2 + ); + this._graphicsContext.debug.drawText(`rot deg(${Util.toDegrees(tx.rotation).toFixed(2)})`, cursor); + cursor = cursor.add(lineHeight); + } + + if (txSettings.showAll || txSettings.showScale) { + this._graphicsContext.drawLine(Vector.Zero, tx.scale.add(Vector.Zero), txSettings.scaleColor, 2); + } + } + + motion = entity.get(MotionComponent); + if (motion) { + if (motionSettings.showAll || motionSettings.showVelocity) { + this._graphicsContext.debug.drawText(`vel${motion.vel.toString(2)}`, cursor); + this._graphicsContext.drawLine(Vector.Zero, motion.vel, motionSettings.velocityColor, 2); + cursor = cursor.add(lineHeight); + } + + if (motionSettings.showAll || motionSettings.showAcceleration) { + this._graphicsContext.drawLine(Vector.Zero, motion.acc, motionSettings.accelerationColor, 2); + } + } + + graphics = entity.get(GraphicsComponent); + if (graphics) { + if (graphicsSettings.showAll || graphicsSettings.showBounds) { + const bounds = graphics.localBounds; + bounds.draw(this._graphicsContext, graphicsSettings.boundsColor); + } + } + + body = entity.get(BodyComponent); + if (body) { + if (bodySettings.showAll || bodySettings.showCollisionGroup) { + this._graphicsContext.debug.drawText(`collision group(${body.group.name})`, cursor); + cursor = cursor.add(lineHeight); + } + + if (bodySettings.showAll || bodySettings.showCollisionType) { + this._graphicsContext.debug.drawText(`collision type(${body.collisionType})`, cursor); + cursor = cursor.add(lineHeight); + } + + if (bodySettings.showAll || bodySettings.showMass) { + this._graphicsContext.debug.drawText(`mass(${body.mass})`, cursor); + cursor = cursor.add(lineHeight); + } + + if (bodySettings.showAll || bodySettings.showMotion) { + this._graphicsContext.debug.drawText(`motion(${body.sleepMotion})`, cursor); + cursor = cursor.add(lineHeight); + } + + if (bodySettings.showAll || bodySettings.showSleeping) { + this._graphicsContext.debug.drawText(`sleeping(${body.canSleep ? 'cant sleep' : body.sleeping})`, cursor); + cursor = cursor.add(lineHeight); + } + } + + this._graphicsContext.restore(); + + // Colliders live in world space already so after the restore() + collider = entity.get(ColliderComponent); + if (collider) { + if (colliderSettings.showAll || colliderSettings.showGeometry) { + collider.collider.debug(this._graphicsContext, colliderSettings.geometryColor); + } + if (colliderSettings.showAll || colliderSettings.showBounds) { + if (collider.collider instanceof CompositeCollider) { + const colliders = collider.collider.getColliders(); + for (const collider of colliders) { + const bounds = collider.bounds; + const pos = vec(bounds.left, bounds.top); + this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, { color: colliderSettings.boundsColor }); + if (colliderSettings.showAll || colliderSettings.showOwner) { + this._graphicsContext.debug.drawText(`owner id(${collider.owner.id})`, pos); + } + } + collider.bounds.draw(this._graphicsContext, colliderSettings.boundsColor); + } else { + const bounds = collider.bounds; + const pos = vec(bounds.left, bounds.top); + this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, { color: colliderSettings.boundsColor }); + if (colliderSettings.showAll || colliderSettings.showOwner) { + this._graphicsContext.debug.drawText(`owner id(${collider.owner.id})`, pos); + } + } + } + } + + this._popCameraTransform(tx); + } + + this._graphicsContext.save(); + this._camera.draw(this._graphicsContext); + if (physicsSettings.showAll || physicsSettings.showBroadphaseSpacePartitionDebug) { + this._collisionSystem.debug(this._graphicsContext); + } + if (physicsSettings.showAll || physicsSettings.showCollisionContacts || physicsSettings.showCollisionNormals) { + for (const [_, contact] of this._engine.debug.stats.currFrame.physics.contacts) { + if (physicsSettings.showAll || physicsSettings.showCollisionContacts) { + for (const point of contact.points) { + this._graphicsContext.debug.drawPoint(point, { size: 5, color: physicsSettings.collisionContactColor }); + } + } + + if (physicsSettings.showAll || physicsSettings.showCollisionNormals) { + for (const point of contact.points) { + this._graphicsContext.debug.drawLine(point, contact.normal.scale(30).add(point), { + color: physicsSettings.collisionNormalColor + }); + } + } + } + } + this._graphicsContext.restore(); + + if (cameraSettings) { + this._graphicsContext.save(); + this._camera.draw(this._graphicsContext); + if (cameraSettings.showAll || cameraSettings.showFocus) { + this._graphicsContext.drawCircle(this._camera.pos, 4, cameraSettings.focusColor); + } + if (cameraSettings.showAll || cameraSettings.showZoom) { + this._graphicsContext.debug.drawText(`zoom(${this._camera.zoom})`, this._camera.pos); + } + this._graphicsContext.restore(); + } + + this._graphicsContext.flush(); + } + + /** + * This applies the current entity transform to the graphics context + * @param entity + */ + private _applyTransform(entity: Entity): void { + const ancestors = entity.getAncestors(); + for (const ancestor of ancestors) { + const transform = ancestor?.get(TransformComponent); + if (transform) { + this._graphicsContext.translate(transform.pos.x, transform.pos.y); + this._graphicsContext.scale(transform.scale.x, transform.scale.y); + this._graphicsContext.rotate(transform.rotation); + } + } + } + + /** + * Applies the current camera transform if in world coordinates + * @param transform + */ + private _pushCameraTransform(transform: TransformComponent) { + // Establish camera offset per entity + if (transform.coordPlane === CoordPlane.World) { + this._graphicsContext.save(); + if (this._camera) { + this._camera.draw(this._graphicsContext); + } + } + } + + /** + * Resets the current camera transform if in world coordinates + * @param transform + */ + private _popCameraTransform(transform: TransformComponent) { + if (transform.coordPlane === CoordPlane.World) { + // Apply camera world offset + this._graphicsContext.restore(); + } + } +} diff --git a/src/engine/Debug/index.ts b/src/engine/Debug/index.ts new file mode 100644 index 0000000000..51da2d076e --- /dev/null +++ b/src/engine/Debug/index.ts @@ -0,0 +1,3 @@ +export * from './Debug'; +export * from './DebugFlags'; +export * from './DebugSystem'; diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index 3348de3ff9..2eb82dba8d 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -34,7 +34,7 @@ import { Color } from './Color'; import { Scene } from './Scene'; import { Entity } from './EntityComponentSystem/Entity'; import { PostProcessor } from './PostProcessing/PostProcessor'; -import { Debug, DebugStats } from './Debug'; +import { Debug, DebugStats } from './Debug/Debug'; import { Class } from './Class'; import * as Input from './Input/Index'; import * as Events from './Events'; diff --git a/src/engine/EntityComponentSystem/Components/TransformComponent.ts b/src/engine/EntityComponentSystem/Components/TransformComponent.ts index f3f971ef7e..bac94bc86f 100644 --- a/src/engine/EntityComponentSystem/Components/TransformComponent.ts +++ b/src/engine/EntityComponentSystem/Components/TransformComponent.ts @@ -106,6 +106,16 @@ export class TransformComponent extends Component<'ex.transform'> implements Tra } } + public getGlobalTransform(): Transform { + return { + pos: this.globalPos, + scale: this.globalScale, + rotation: this.globalRotation, + z: this.z, + coordPlane: this.coordPlane + }; + } + public get parent(): TransformComponent | null { return this?.owner?.parent?.get(TransformComponent); } diff --git a/src/engine/EntityComponentSystem/Entity.ts b/src/engine/EntityComponentSystem/Entity.ts index a5162be0a7..a251b78367 100644 --- a/src/engine/EntityComponentSystem/Entity.ts +++ b/src/engine/EntityComponentSystem/Entity.ts @@ -60,8 +60,9 @@ export function isRemovedComponent(x: Message): x is RemovedCom export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUpdate { private static _ID = 0; - constructor(components?: Component[]) { + constructor(components?: Component[], name?: string) { super(); + this._setName(name); if (components) { for (const component of components) { this.addComponent(component); @@ -74,6 +75,16 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp */ public id: number = Entity._ID++; + private _name: string = 'anonymous'; + protected _setName(name: string) { + if (name) { + this._name = name; + } + } + public get name(): string { + return this._name; + } + public get events(): EventDispatcher { return this.eventDispatcher; } @@ -273,7 +284,6 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp return result; } - /** * Creates a deep copy of the entity and a copy of all its components */ diff --git a/src/engine/EntityComponentSystem/EntityManager.ts b/src/engine/EntityComponentSystem/EntityManager.ts index 5b86f2ba5d..b6b6373847 100644 --- a/src/engine/EntityComponentSystem/EntityManager.ts +++ b/src/engine/EntityComponentSystem/EntityManager.ts @@ -126,6 +126,10 @@ export class EntityManager implements Observer e.name === name); + } + public clear(): void { for (const entity of this.entities) { this.removeEntity(entity); diff --git a/src/engine/EntityComponentSystem/System.ts b/src/engine/EntityComponentSystem/System.ts index 90820902c2..3fe69e3787 100644 --- a/src/engine/EntityComponentSystem/System.ts +++ b/src/engine/EntityComponentSystem/System.ts @@ -86,11 +86,6 @@ implements Observer { */ postupdate?(engine: ContextType, elapsedMs: number): void; - /** - * Optionally run a debug draw step to visualize the internals of the system - */ - debugDraw?(ctx: CanvasRenderingContext2D, elapsedMs: number): void; - /** * Systems observe when entities match their types or no longer match their types, override * @param _entityAddedOrRemoved diff --git a/src/engine/EntityComponentSystem/SystemManager.ts b/src/engine/EntityComponentSystem/SystemManager.ts index 3a71f28106..27bf993bc3 100644 --- a/src/engine/EntityComponentSystem/SystemManager.ts +++ b/src/engine/EntityComponentSystem/SystemManager.ts @@ -2,6 +2,10 @@ import { System, SystemType } from './System'; import { Scene, Util } from '..'; import { World } from './World'; +export interface SystemCtor { + new (): T; +} + /** * The SystemManager is responsible for keeping track of all systems in a scene. * Systems are scene specific @@ -15,6 +19,15 @@ export class SystemManager { public initialized = false; constructor(private _world: World) {} + /** + * Get a system registered in the manager by type + * @param systemType + * @returns + */ + public get(systemType: SystemCtor): T | null { + return this.systems.find((s) => s instanceof systemType) as unknown as T; + } + /** * Adds a system to the manager, it will now be updated every frame * @param system diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts index e09e2cb40d..83f73358be 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts @@ -50,6 +50,13 @@ export interface DebugDraw { * @param pointOptions */ drawPoint(point: Vector, pointOptions?: PointGraphicsOptions): void; + + /** + * Draw debug text + * @param text + * @param pos + */ + drawText(text: string, pos: Vector): void; } export interface ExcaliburGraphicsContext { @@ -117,6 +124,32 @@ export interface ExcaliburGraphicsContext { dheight?: number ): void; + /** + * Draw a solid line to the Excalibur Graphics context + * @param start + * @param end + * @param color + * @param thickness + */ + drawLine(start: Vector, end: Vector, color: Color, thickness: number): void; + + /** + * Draw a solid rectangle to the Excalibur Graphics context + * @param pos + * @param width + * @param height + * @param color + */ + drawRectangle(pos: Vector, width: number, height: number, color: Color): void; + + /** + * Draw a solid circle to the Excalibur Graphics context + * @param pos + * @param radius + * @param color + */ + drawCircle(pos: Vector, radius: number, color: Color): void; + /** * Save the current state of the canvas to the stack (transforms and opacity) */ diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts index 1bded1c305..b272322a89 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts @@ -10,8 +10,10 @@ import { Vector } from '../../Math/vector'; import { Color } from '../../Color'; import { StateStack } from './state-stack'; import { GraphicsDiagnostics } from '../GraphicsDiagnostics'; +import { DebugText } from './debug-text'; class ExcaliburGraphicsContext2DCanvasDebug implements DebugDraw { + private _debugText = new DebugText(); constructor(private _ex: ExcaliburGraphicsContext2DCanvas) {} /** * Draw a debug rectangle to the context @@ -59,6 +61,10 @@ class ExcaliburGraphicsContext2DCanvasDebug implements DebugDraw { this._ex.__ctx.closePath(); this._ex.__ctx.restore(); } + + drawText(text: string, pos: Vector) { + this._debugText.write(this._ex, text, pos); + } } export class ExcaliburGraphicsContext2DCanvas implements ExcaliburGraphicsContext { @@ -169,6 +175,40 @@ export class ExcaliburGraphicsContext2DCanvas implements ExcaliburGraphicsContex GraphicsDiagnostics.DrawnImagesCount = 1; } + public drawLine(start: Vector, end: Vector, color: Color, thickness = 1) { + this.__ctx.save(); + this.__ctx.beginPath(); + this.__ctx.strokeStyle = color.toString(); + this.__ctx.moveTo(this.snapToPixel ? ~~start.x : start.x, this.snapToPixel ? ~~start.y : start.y); + this.__ctx.lineTo(this.snapToPixel ? ~~end.x : end.x, this.snapToPixel ? ~~end.y : end.y); + this.__ctx.lineWidth = thickness; + this.__ctx.stroke(); + this.__ctx.closePath(); + this.__ctx.restore(); + } + + public drawRectangle(pos: Vector, width: number, height: number, color: Color) { + this.__ctx.save(); + this.__ctx.fillStyle = color.toString(); + this.__ctx.fillRect( + this.snapToPixel ? ~~pos.x : pos.x, + this.snapToPixel ? ~~pos.y : pos.y, + this.snapToPixel ? ~~width : width, + this.snapToPixel ? ~~height : height + ); + this.__ctx.restore(); + } + + public drawCircle(pos: Vector, radius: number, color: Color) { + this.__ctx.save(); + this.__ctx.beginPath(); + this.__ctx.fillStyle = color.toString(); + this.__ctx.arc(this.snapToPixel ? ~~pos.x : pos.x, this.snapToPixel ? ~~pos.y : pos.y, radius, 0, Math.PI * 2); + this.__ctx.fill(); + this.__ctx.closePath(); + this.__ctx.restore(); + } + debug = new ExcaliburGraphicsContext2DCanvasDebug(this); /** diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index 55c0dd9bfe..f0a48ac3a4 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -19,8 +19,10 @@ import { ImageRenderer } from './image-renderer'; import { PointRenderer } from './point-renderer'; import { Canvas } from '../Canvas'; import { GraphicsDiagnostics } from '../GraphicsDiagnostics'; +import { DebugText } from './debug-text'; class ExcaliburGraphicsContextWebGLDebug implements DebugDraw { + private _debugText = new DebugText(); constructor(private _webglCtx: ExcaliburGraphicsContextWebGL) {} /** @@ -55,6 +57,10 @@ class ExcaliburGraphicsContextWebGLDebug implements DebugDraw { drawPoint(point: Vector, pointOptions: PointGraphicsOptions = { color: Color.Black, size: 5 }): void { this._webglCtx.__pointRenderer.addPoint(point, pointOptions.color, pointOptions.size); } + + drawText(text: string, pos: Vector) { + this._debugText.write(this._webglCtx, text, pos); + } } export interface WebGLGraphicsContextInfo { @@ -225,6 +231,18 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this.__imageRenderer.addImage(image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight); } + public drawLine(start: Vector, end: Vector, color: Color, thickness = 1) { + this.__imageRenderer.addLine(color, start, end, thickness); + } + + public drawRectangle(pos: Vector, width: number, height: number, color: Color) { + this.__imageRenderer.addRectangle(color, pos, width, height); + } + + public drawCircle(pos: Vector, radius: number, color: Color) { + this.__imageRenderer.addCircle(pos, radius, color); + } + debug = new ExcaliburGraphicsContextWebGLDebug(this); public save(): void { diff --git a/src/engine/Graphics/Context/debug-font.png b/src/engine/Graphics/Context/debug-font.png new file mode 100644 index 0000000000..c4873941ac Binary files /dev/null and b/src/engine/Graphics/Context/debug-font.png differ diff --git a/src/engine/Graphics/Context/debug-text.ts b/src/engine/Graphics/Context/debug-text.ts new file mode 100644 index 0000000000..1ed05279d9 --- /dev/null +++ b/src/engine/Graphics/Context/debug-text.ts @@ -0,0 +1,53 @@ +import { ExcaliburGraphicsContext, ImageSource, SpriteFont, SpriteSheet } from '..'; +import { Vector } from '../..'; +import debugFont from './debug-font.png'; + +/** + * Internal debugtext helper + */ +export class DebugText { + constructor() { + this.load(); + } + + /** + * base64 font + */ + public readonly fontSheet = debugFont; + public size: number = 16; + private _imageSource: ImageSource; + private _spriteSheet: SpriteSheet; + private _spriteFont: SpriteFont; + public load() { + this._imageSource = new ImageSource(this.fontSheet); + return this._imageSource.load().then(() => { + this._spriteSheet = SpriteSheet.fromGrid({ + image: this._imageSource, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + this._spriteFont = new SpriteFont({ + alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ,!\'&."?-()+ ', + caseInsensitive: true, + spriteSheet: this._spriteSheet, + spacing: -6 + }); + }); + } + + /** + * Writes debug text using the built in sprint font + * @param ctx + * @param text + * @param pos + */ + public write(ctx: ExcaliburGraphicsContext, text: string, pos: Vector) { + if (this._imageSource.isLoaded()) { + this._spriteFont.render(ctx, text, pos.x, pos.y); + } + } +} diff --git a/src/engine/Graphics/Context/draw-image-command.ts b/src/engine/Graphics/Context/draw-image-command.ts index cf6842b2b6..a8c09a0b83 100644 --- a/src/engine/Graphics/Context/draw-image-command.ts +++ b/src/engine/Graphics/Context/draw-image-command.ts @@ -3,12 +3,22 @@ import { BoundingBox } from '../../Collision/Index'; import { Color } from '../../Color'; import { Pool, Poolable } from '../../Util/Pool'; import { HTMLImageSource } from './ExcaliburGraphicsContext'; +import { vec, Vector } from '../../Math/vector'; + +export enum DrawCommandType { + Image = 'image', + Line = 'line', + Rectangle = 'rectangle', + Circle = 'circle' +} export class DrawImageCommand implements Poolable { _pool: Pool = undefined; public snapToPixel: boolean = true; public image: HTMLImageSource; + public color: Color; + public type = DrawCommandType.Image; public opacity: number = 1; public width: number = 0; public height: number = 0; @@ -61,6 +71,7 @@ export class DrawImageCommand implements Poolable { dwidth?: number, dheight?: number ) { + this.type = DrawCommandType.Image; this.image = image; this.width = image?.width || swidth || 0; this.height = image?.height || sheight || 0; @@ -90,6 +101,88 @@ export class DrawImageCommand implements Poolable { return this; } + public initRect(color: Color, start: Vector, width: number, height: number) { + this.type = DrawCommandType.Rectangle; + this.color = color; + this.width = width; + this.height = height; + let index = 0; + this._geom[index++] = [start.x, start.y]; + this._geom[index++] = [start.x, start.y + this.height]; + this._geom[index++] = [start.x + this.width, start.y]; + this._geom[index++] = [start.x + this.width, start.y]; + this._geom[index++] = [start.x, start.y + this.height]; + this._geom[index++] = [start.x + this.width, start.y + this.height]; + + if (this.snapToPixel) { + for (const point of this._geom) { + point[0] = ~~point[0]; + point[1] = ~~point[1]; + } + } + return this; + } + + public initLine(color: Color, start: Vector, end: Vector, thickness: number) { + this.type = DrawCommandType.Line; + this.color = color; + const dir = end.sub(start).normalize(); + const normal = dir.perpendicular(); + const halfThick = thickness / 2; + const startTop = normal.scale(halfThick).add(start); + const startBottom = normal.scale(-halfThick).add(start); + const endTop = normal.scale(halfThick).add(end); + const endBottom = normal.scale(-halfThick).add(end); + + /** + * +---------------------^----------------------+ + * | | (normal) | + * (startx, starty)------------------>(endx, endy) + * | | + * + -------------------------------------------+ + */ + + let index = 0; + this._geom[index++] = [startTop.x, startTop.y]; + this._geom[index++] = [endTop.x, endTop.y]; + this._geom[index++] = [startBottom.x, startBottom.y]; + this._geom[index++] = [startBottom.x, startBottom.y]; + this._geom[index++] = [endTop.x, endTop.y]; + this._geom[index++] = [endBottom.x, endBottom.y]; + + if (this.snapToPixel) { + for (const point of this._geom) { + point[0] = ~~point[0]; + point[1] = ~~point[1]; + } + } + return this; + } + + public initCircle(pos: Vector, radius: number, color: Color) { + this.type = DrawCommandType.Circle; + this.color = color; + const topLeft = pos.add(vec(-radius, -radius)); + const topRight = pos.add(vec(radius, -radius)); + const bottomRight = pos.add(vec(radius, radius)); + const bottomLeft = pos.add(vec(-radius, radius)); + let index = 0; + this._geom[index++] = [topLeft.x, topLeft.y]; + this._geom[index++] = [topRight.x, topRight.y]; + this._geom[index++] = [bottomLeft.x, bottomLeft.y]; + this._geom[index++] = [bottomLeft.x, bottomLeft.y]; + this._geom[index++] = [topRight.x, topRight.y]; + this._geom[index++] = [bottomRight.x, bottomRight.y]; + + if (this.snapToPixel) { + for (const point of this._geom) { + point[0] = ~~point[0]; + point[1] = ~~point[1]; + } + } + return this; + } + public dispose() { this.image = null; this.width = 0; diff --git a/src/engine/Graphics/Context/image-renderer.ts b/src/engine/Graphics/Context/image-renderer.ts index da8323bf73..a748d70f44 100644 --- a/src/engine/Graphics/Context/image-renderer.ts +++ b/src/engine/Graphics/Context/image-renderer.ts @@ -2,13 +2,15 @@ import { Shader } from './shader'; import imageVertexSource from './shaders/image-vertex.glsl'; import imageFragmentSource from './shaders/image-fragment.glsl'; import { BatchCommand } from './batch'; -import { DrawImageCommand } from './draw-image-command'; +import { DrawCommandType, DrawImageCommand } from './draw-image-command'; import { Graphic } from '../Graphic'; import { ensurePowerOfTwo } from './webgl-util'; import { BatchRenderer } from './renderer'; import { WebGLGraphicsContextInfo } from './ExcaliburGraphicsContextWebGL'; import { TextureLoader } from './texture-loader'; import { HTMLImageSource } from './ExcaliburGraphicsContext'; +import { Color } from '../../Color'; +import { Vector } from '../..'; export class BatchImage extends BatchCommand { public textures: WebGLTexture[] = []; @@ -63,9 +65,11 @@ export class BatchImage extends BatchCommand { } add(command: DrawImageCommand) { - const texture = TextureLoader.load(command.image); - if (this.textures.indexOf(texture) === -1) { - this.textures.push(texture); + if (command.type === DrawCommandType.Image) { + const texture = TextureLoader.load(command.image); + if (this.textures.indexOf(texture) === -1) { + this.textures.push(texture); + } } this.commands.push(command); @@ -120,6 +124,7 @@ export class ImageRenderer extends BatchRenderer { shader.addAttribute('a_texcoord', 2, gl.FLOAT); shader.addAttribute('a_textureIndex', 1, gl.FLOAT); shader.addAttribute('a_opacity', 1, gl.FLOAT); + shader.addAttribute('a_color', 4, gl.FLOAT); shader.addUniformMatrix('u_matrix', this._contextInfo.matrix.data); // Initialize texture slots to [0, 1, 2, 3, 4, .... maxGPUTextures] shader.addUniformIntegerArray( @@ -133,14 +138,36 @@ export class ImageRenderer extends BatchRenderer { let newSource = source.replace('%%count%%', maxTextures.toString()); let texturePickerBuilder = ''; for (let i = 0; i < maxTextures; i++) { - texturePickerBuilder += ` } else if (v_textureIndex <= ${i}.5) {\n - gl_FragColor = texture2D(u_textures[${i}], v_texcoord);\n - gl_FragColor.w = gl_FragColor.w * v_opacity;\n`; + if (i === 0) { + texturePickerBuilder += `if (v_textureIndex <= ${i}.5) {\n`; + } else { + texturePickerBuilder += ` else if (v_textureIndex <= ${i}.5) {\n`; + } + texturePickerBuilder += ` color = texture2D(u_textures[${i}], v_texcoord);\n`; + texturePickerBuilder += ` }\n`; } newSource = newSource.replace('%%texture_picker%%', texturePickerBuilder); return newSource; } + public addCircle(pos: Vector, radius: number, color: Color) { + const command = this.commands.get().initCircle(pos, radius, color); + command.applyTransform(this._contextInfo.transform.current, this._contextInfo.state.current.opacity); + this.addCommand(command); + } + + public addRectangle(color: Color, pos: Vector, width: number, height: number) { + const command = this.commands.get().initRect(color, pos, width, height); + command.applyTransform(this._contextInfo.transform.current, this._contextInfo.state.current.opacity); + this.addCommand(command); + } + + public addLine(color: Color, start: Vector, end: Vector, thickness = 1) { + const command = this.commands.get().initLine(color, start, end, thickness); + command.applyTransform(this._contextInfo.transform.current, this._contextInfo.state.current.opacity); + this.addCommand(command); + } + public addImage( graphic: HTMLImageSource, sx: number, @@ -170,26 +197,41 @@ export class ImageRenderer extends BatchRenderer { let sy: number = 0; let sw: number = 0; let sh: number = 0; - let potWidth: number = 0; - let potHeight: number = 0; + let potWidth: number = 1; + let potHeight: number = 1; let textureId = 0; + let commandColor = Color.Transparent; for (const command of batch.commands) { sx = command.view[0]; sy = command.view[1]; sw = command.view[2]; sh = command.view[3]; - potWidth = ensurePowerOfTwo(command.image.width || command.width); - potHeight = ensurePowerOfTwo(command.image.height || command.height); + potWidth = ensurePowerOfTwo(command.image?.width || command.width); + potHeight = ensurePowerOfTwo(command.image?.height || command.height); textureId = batch.getBatchTextureId(command); + if (command.type === DrawCommandType.Line || command.type === DrawCommandType.Rectangle) { + textureId = -1; // sentinel for no image rect + commandColor = command.color; + } + if (command.type === DrawCommandType.Circle) { + textureId = -2; // sentinel for circle + commandColor = command.color; + } // potential optimization when divding by 2 (bitshift) // Modifying the images to poweroftwo images warp the UV coordinates - const uvx0 = sx / potWidth; - const uvy0 = sy / potHeight; - const uvx1 = (sx + sw) / potWidth; - const uvy1 = (sy + sh) / potHeight; + let uvx0 = sx / potWidth; + let uvy0 = sy / potHeight; + let uvx1 = (sx + sw) / potWidth; + let uvy1 = (sy + sh) / potHeight; + if (textureId === -2) { + uvx0 = 0; + uvy0 = 0; + uvx1 = 1; + uvy1 = 1; + } // Quad update // (0, 0, z) z-index doesn't work in batch rendering between batches @@ -204,6 +246,11 @@ export class ImageRenderer extends BatchRenderer { vertexBuffer[vertIndex++] = textureId; // opacity vertexBuffer[vertIndex++] = command.opacity; + // color + vertexBuffer[vertIndex++] = commandColor.r / 255; + vertexBuffer[vertIndex++] = commandColor.g / 255; + vertexBuffer[vertIndex++] = commandColor.b / 255; + vertexBuffer[vertIndex++] = commandColor.a; // (0, 1) vertexBuffer[vertIndex++] = command.geometry[1][0]; // x + 0 * width; @@ -217,6 +264,11 @@ export class ImageRenderer extends BatchRenderer { vertexBuffer[vertIndex++] = textureId; // opacity vertexBuffer[vertIndex++] = command.opacity; + // color + vertexBuffer[vertIndex++] = commandColor.r / 255; + vertexBuffer[vertIndex++] = commandColor.g / 255; + vertexBuffer[vertIndex++] = commandColor.b / 255; + vertexBuffer[vertIndex++] = commandColor.a; // (1, 0) vertexBuffer[vertIndex++] = command.geometry[2][0]; // x + 1 * width; @@ -230,6 +282,11 @@ export class ImageRenderer extends BatchRenderer { vertexBuffer[vertIndex++] = textureId; // opacity vertexBuffer[vertIndex++] = command.opacity; + // color + vertexBuffer[vertIndex++] = commandColor.r / 255; + vertexBuffer[vertIndex++] = commandColor.g / 255; + vertexBuffer[vertIndex++] = commandColor.b / 255; + vertexBuffer[vertIndex++] = commandColor.a; // (1, 0) vertexBuffer[vertIndex++] = command.geometry[3][0]; // x + 1 * width; @@ -243,6 +300,11 @@ export class ImageRenderer extends BatchRenderer { vertexBuffer[vertIndex++] = textureId; // opacity vertexBuffer[vertIndex++] = command.opacity; + // color + vertexBuffer[vertIndex++] = commandColor.r / 255; + vertexBuffer[vertIndex++] = commandColor.g / 255; + vertexBuffer[vertIndex++] = commandColor.b / 255; + vertexBuffer[vertIndex++] = commandColor.a; // (0, 1) vertexBuffer[vertIndex++] = command.geometry[4][0]; // x + 0 * width; @@ -256,6 +318,11 @@ export class ImageRenderer extends BatchRenderer { vertexBuffer[vertIndex++] = textureId; // opacity vertexBuffer[vertIndex++] = command.opacity; + // color + vertexBuffer[vertIndex++] = commandColor.r / 255; + vertexBuffer[vertIndex++] = commandColor.g / 255; + vertexBuffer[vertIndex++] = commandColor.b / 255; + vertexBuffer[vertIndex++] = commandColor.a; // (1, 1) vertexBuffer[vertIndex++] = command.geometry[5][0]; // x + 1 * width; @@ -269,6 +336,11 @@ export class ImageRenderer extends BatchRenderer { vertexBuffer[vertIndex++] = textureId; // opacity vertexBuffer[vertIndex++] = command.opacity; + // color + vertexBuffer[vertIndex++] = commandColor.r / 255; + vertexBuffer[vertIndex++] = commandColor.g / 255; + vertexBuffer[vertIndex++] = commandColor.b / 255; + vertexBuffer[vertIndex++] = commandColor.a; } return vertIndex / this.vertexSize; diff --git a/src/engine/Graphics/Context/line-renderer.ts b/src/engine/Graphics/Context/line-renderer.ts index 9ceae14d6e..3935199647 100644 --- a/src/engine/Graphics/Context/line-renderer.ts +++ b/src/engine/Graphics/Context/line-renderer.ts @@ -43,7 +43,9 @@ export class LineRenderer extends BatchRenderer { const cmd = this.commands.get(); cmd.start = this._contextInfo.transform.current.multv(start); cmd.end = this._contextInfo.transform.current.multv(end); - cmd.color = color; + cmd.color.r = color.r; + cmd.color.g = color.g; + cmd.color.b = color.b; cmd.color.a = cmd.color.a * this._contextInfo.state.current.opacity; this.addCommand(cmd); } diff --git a/src/engine/Graphics/Context/shaders/image-fragment.glsl b/src/engine/Graphics/Context/shaders/image-fragment.glsl index ed54dad19c..e54f68dd47 100644 --- a/src/engine/Graphics/Context/shaders/image-fragment.glsl +++ b/src/engine/Graphics/Context/shaders/image-fragment.glsl @@ -1,3 +1,6 @@ +#ifdef GL_OES_standard_derivatives +#extension GL_OES_standard_derivatives : enable +#endif precision mediump float; // UV coord @@ -6,25 +9,40 @@ varying vec2 v_texcoord; // Texture index varying lowp float v_textureIndex; +// Color coord to blend with image +varying lowp vec4 v_color; + // Opacity varying float v_opacity; uniform sampler2D u_textures[%%count%%]; +float circle(in vec2 st, in float radius) { + vec2 dist = st - vec2(0.5); + float r = dot(dist, dist) * 4.0; + float delta = fwidth(r); + return 1.0 - smoothstep(radius - delta, radius + delta, r); +} + void main() { + float r = 0.0, delta = 0.0, alpha = 1.0; // In order to support the most efficient sprite batching, we have multiple // textures loaded into the gpu (usually 8) this picker logic skips over textures // that do not apply to a particular sprite. - // Error color - vec4 color = vec4(1.0, 0.0, 0.0, 1.0); - - // Always at least 1 texture at 0 - if (v_textureIndex <= .5) { - gl_FragColor = texture2D(u_textures[0], v_texcoord); - gl_FragColor.w = gl_FragColor.w * v_opacity; - %%texture_picker%% + vec4 color; + // -1 If there is no texture to sample we are drawing a solid geometry (rectangles) + if (v_textureIndex == -1.0) { + color = v_color; + color.w = color.w * v_opacity; + // -2 If there is no texture we are drawing a circle + } else if (v_textureIndex == -2.0) { + color = v_color; + color.a = color.a * circle(v_texcoord, .95); } else { - gl_FragColor = color; + // GLSL is templated out to pick the right texture and set the vec4 color + %%texture_picker%% } + color.w = color.w * v_opacity; + gl_FragColor = color; } \ No newline at end of file diff --git a/src/engine/Graphics/Context/shaders/image-vertex.glsl b/src/engine/Graphics/Context/shaders/image-vertex.glsl index c34b062d70..989643036f 100644 --- a/src/engine/Graphics/Context/shaders/image-vertex.glsl +++ b/src/engine/Graphics/Context/shaders/image-vertex.glsl @@ -4,6 +4,9 @@ attribute vec4 a_position; attribute float a_opacity; varying float v_opacity; +attribute vec4 a_color; +varying vec4 v_color; + // UV coordinate attribute vec2 a_texcoord; varying vec2 v_texcoord; @@ -25,4 +28,6 @@ void main() { v_texcoord = a_texcoord; // Pass through the texture number to the fragment shader v_textureIndex = a_textureIndex; + // Pass through the color to the fragment shader + v_color = a_color; } \ No newline at end of file diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts index 1ca3773122..80c97667f8 100644 --- a/src/engine/Graphics/GraphicsSystem.ts +++ b/src/engine/Graphics/GraphicsSystem.ts @@ -1,9 +1,7 @@ -import { isActor } from '../Actor'; import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext'; import { Scene } from '../Scene'; import { GraphicsComponent } from './GraphicsComponent'; -import { Vector, vec } from '../Math/vector'; -import { Color } from '../Color'; +import { vec } from '../Math/vector'; import { CoordPlane, TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { Entity } from '../EntityComponentSystem/Entity'; import { Camera } from '../Camera'; @@ -11,6 +9,7 @@ import { System, SystemType, TagComponent } from '../EntityComponentSystem'; import { Engine } from '../Engine'; import { GraphicsDiagnostics } from './GraphicsDiagnostics'; import { EnterViewPortEvent, ExitViewPortEvent } from '../Events'; +import { GraphicsGroup } from '.'; export class GraphicsSystem extends System { public readonly types = ['ex.transform', 'ex.graphics'] as const; @@ -73,8 +72,6 @@ export class GraphicsSystem extends System { this._y = y; } - private _x = 0; + protected _x = 0; /** * Get the x component of the vector */ @@ -114,7 +114,7 @@ export class Vector implements Clonable { this._x = val; } - private _y = 0; + protected _y = 0; /** * Get the y component of the vector */ @@ -354,7 +354,10 @@ export class Vector implements Clonable { /** * Returns a string representation of the vector. */ - public toString(): string { + public toString(fixed?: number): string { + if (fixed) { + return `(${this.x.toFixed(fixed)}, ${this.y.toFixed(fixed)})`; + } return `(${this.x}, ${this.y})`; } } diff --git a/src/engine/Resources/Gif.ts b/src/engine/Resources/Gif.ts index baeed260e2..c2cdbdc313 100644 --- a/src/engine/Resources/Gif.ts +++ b/src/engine/Resources/Gif.ts @@ -357,7 +357,7 @@ export class ParseGif { const parseAppExt = (block: any) => { const parseNetscapeExt = (block: any) => { this.checkBytes.push(this._st.readByte()); // Always 3 - block.unknown = this._st.readByte(); // ??? Always 1? What is this? + block.unknown = this._st.readByte(); // Q: Always 1? What is this? block.iterations = this._st.readUnsigned(); block.terminator = this._st.readByte(); if (this._handler.app && this._handler.app.NETSCAPE && this._handler.app.NETSCAPE(block)) { diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts index 3e683c7b74..a139a66ae3 100644 --- a/src/engine/Scene.ts +++ b/src/engine/Scene.ts @@ -32,6 +32,7 @@ import { Entity } from './EntityComponentSystem/Entity'; import { GraphicsSystem } from './Graphics/GraphicsSystem'; import { CanvasDrawingSystem } from './Drawing/CanvasDrawingSystem'; import { Flags, Legacy } from './Flags'; +import { DebugSystem } from './Debug/DebugSystem'; /** * [[Actor|Actors]] are composed together into groupings called Scenes in * Excalibur. The metaphor models the same idea behind real world @@ -114,6 +115,7 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact } else { this.world.add(new GraphicsSystem()); } + this.world.add(new DebugSystem()); } public on(eventName: Events.initialize, handler: (event: InitializeEvent) => void): void; @@ -375,16 +377,12 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact /** * Draws all the actors' debug information in the Scene. Called by the [[Engine]]. * @param ctx The current rendering context + * @deprecated */ /* istanbul ignore next */ public debugDraw(ctx: CanvasRenderingContext2D) { this.emit('predebugdraw', new PreDebugDrawEvent(ctx, this)); - // this._collisionProcessor.debugDraw(ctx, 20); - for (const system of this.world.systemManager.systems) { - if (system.debugDraw) { - system.debugDraw(ctx, 1); - } - } + // pass this.emit('postdebugdraw', new PostDebugDrawEvent(ctx, this)); } diff --git a/src/engine/Util/Pool.ts b/src/engine/Util/Pool.ts index ba0e794225..8b44bc0ad9 100644 --- a/src/engine/Util/Pool.ts +++ b/src/engine/Util/Pool.ts @@ -1,3 +1,5 @@ +import { Logger } from '..'; + export interface Poolable { /** * Any type that is a member of a an object pool will have a reference to teh pool @@ -11,6 +13,7 @@ export class Pool { public totalAllocations = 0; public index = 0; public objects: Type[] = []; + private _logger = Logger.getInstance(); constructor( public builder: (...args: any[]) => Type, @@ -49,7 +52,9 @@ export class Pool { get(...args: any[]): Type { if (this.index === this.maxObjects) { // TODO implement hard or soft cap - throw new Error('Max pooled objects reached, possible memory leak?'); + this._logger.warn('Max pooled objects reached, possible memory leak? Doubling'); + this.maxObjects = this.maxObjects * 2; + // throw new Error('Max pooled objects reached, possible memory leak?'); } if (this.objects[this.index]) { diff --git a/src/engine/index.ts b/src/engine/index.ts index 8e1905b51a..287d6427ac 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -17,7 +17,7 @@ export * from './Math/Index'; export * from './Camera'; export * from './Class'; export * from './Configurable'; -export * from './Debug'; +export * from './Debug/index'; export * from './EventDispatcher'; export * from './Events/MediaEvents'; export * from './Events'; diff --git a/src/spec/ActorSpec.ts b/src/spec/ActorSpec.ts index d995af8c5e..be078ca105 100644 --- a/src/spec/ActorSpec.ts +++ b/src/spec/ActorSpec.ts @@ -38,6 +38,7 @@ describe('A game actor', () => { spyOn(actor, 'debugDraw'); engine.start(); + collisionSystem.initialize(scene); ex.Physics.useArcadePhysics(); ex.Physics.acc.setTo(0, 0); diff --git a/src/spec/AlgebraSpec.ts b/src/spec/AlgebraSpec.ts index 8795e4f64c..69621949bd 100644 --- a/src/spec/AlgebraSpec.ts +++ b/src/spec/AlgebraSpec.ts @@ -244,6 +244,12 @@ describe('Vectors', () => { expect(c.x).not.toBe(v.x); expect(c.y).not.toBe(v.y); }); + + it('can be printed toString(fixed)', () => { + const v = new ex.Vector(1.2345, 2.345); + + expect(v.toString(2)).toBe('(1.23, 2.35)'); + }); }); describe('Rays', () => { diff --git a/src/spec/CollisionShapeSpec.ts b/src/spec/CollisionShapeSpec.ts index 0fdf273e59..f0c34ae8f6 100644 --- a/src/spec/CollisionShapeSpec.ts +++ b/src/spec/CollisionShapeSpec.ts @@ -1,10 +1,11 @@ import * as ex from '@excalibur'; -import { ExcaliburMatchers, ensureImagesLoaded } from 'excalibur-jasmine'; +import { ExcaliburMatchers, ensureImagesLoaded, ExcaliburAsyncMatchers } from 'excalibur-jasmine'; import { TestUtils } from './util/TestUtils'; describe('Collision Shape', () => { beforeAll(() => { jasmine.addMatchers(ExcaliburMatchers); + jasmine.addAsyncMatchers(ExcaliburAsyncMatchers); }); describe('a Circle', () => { @@ -291,20 +292,36 @@ describe('Collision Shape', () => { }); }); - it('can be drawn with actor', (done) => { + it('can be debug drawn', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + const circle = new ex.CircleCollider({ + offset: new ex.Vector(50, 50), + radius: 30 + }); + + ctx.clear(); + + circle.debug(ctx, ex.Color.Red); + + ctx.flush(); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/CollisionShapeSpec/circle-debug.png'); + }); + + it('can be drawn with actor', async () => { const circleActor = new ex.Actor({ pos: new ex.Vector(100, 100), color: ex.Color.Blue }); circleActor.collider.useCircleCollider(100); - scene.add(circleActor); scene.draw(engine.ctx, 100); - ensureImagesLoaded(engine.canvas, 'src/spec/images/CollisionShapeSpec/circle.png').then(([canvas, image]) => { - expect(canvas).toEqualImage(image); - done(); - }); + await expectAsync(engine.canvas).toEqualImage('src/spec/images/CollisionShapeSpec/circle.png'); }); it('can calculate the distance to another circle', () => { diff --git a/src/spec/CompositeColliderSpec.ts b/src/spec/CompositeColliderSpec.ts index b60aa112a3..13756ea60b 100644 --- a/src/spec/CompositeColliderSpec.ts +++ b/src/spec/CompositeColliderSpec.ts @@ -146,7 +146,7 @@ describe('A CompositeCollider', () => { const rayRight = new Ray(vec(-200, 0), Vector.Right); - const leftBox = compCollider.rayCast(rayRight); //? + const leftBox = compCollider.rayCast(rayRight); expect(leftBox).toEqual(vec(-100, 0)); const rayDown = new Ray(vec(0, -200), Vector.Down); @@ -185,6 +185,26 @@ describe('A CompositeCollider', () => { expect(compCollider.contains(vec(-101, 0))).toBe(false); }); + it('can be debug drawn', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 300; + canvasElement.height = 300; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + const compCollider = new ex.CompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(200, 10, Vector.Half)]); + const tx = new TransformComponent(); + tx.pos = ex.vec(150, 150); + compCollider.update(tx); + + ctx.clear(); + + compCollider.debug(ctx, ex.Color.Red); + + ctx.flush(); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/CompositeColliderSpec/composite.png'); + }); + it('is separated into a series of colliders in the dynamic tree', () => { const compCollider = new ex.CompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(200, 10, Vector.Half)]); diff --git a/src/spec/DebugSystemSpec.ts b/src/spec/DebugSystemSpec.ts new file mode 100644 index 0000000000..924426db3c --- /dev/null +++ b/src/spec/DebugSystemSpec.ts @@ -0,0 +1,144 @@ +import * as ex from '@excalibur'; +import { ExcaliburAsyncMatchers } from 'excalibur-jasmine'; +import { TestUtils } from './util/TestUtils'; + +describe('DebugSystem', () => { + beforeAll(() => { + jasmine.addAsyncMatchers(ExcaliburAsyncMatchers); + }); + let engine: ex.Engine; + + beforeEach(() => { + engine = TestUtils.engine(); + engine.toggleDebug(); + engine.currentScene.world.clearSystems(); + engine.currentScene._initialize(engine); + }); + + afterEach(() => { + engine.stop(); + engine = null; + }); + + it('exists', () => { + expect(ex.DebugSystem).toBeDefined(); + }); + + it('can show transform and entity info', async () => { + const debugSystem = new ex.DebugSystem(); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: 0, y: 0, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + engine.debug.transform.showAll = true; + engine.debug.entity.showAll = true; + debugSystem.update([actor], 100); + + engine.graphicsContext.flush(); + + await expectAsync(engine.canvas).toEqualImage('src/spec/images/DebugSystemSpec/transform.png'); + }); + + it('can show motion info', async () => { + const debugSystem = new ex.DebugSystem(); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: 0, y: 0, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + actor.vel = ex.vec(100, 0); + actor.acc = ex.vec(100, -100); + engine.debug.motion.showAll = true; + debugSystem.update([actor], 100); + + engine.graphicsContext.flush(); + + await expectAsync(engine.canvas).toEqualImage('src/spec/images/DebugSystemSpec/motion.png'); + }); + + it('can show body info', async () => { + const debugSystem = new ex.DebugSystem(); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: -100, y: 0, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + actor.vel = ex.vec(100, 0); + actor.acc = ex.vec(100, -100); + engine.debug.body.showAll = true; + debugSystem.update([actor], 100); + + engine.graphicsContext.flush(); + + await expectAsync(engine.canvas).toEqualImage('src/spec/images/DebugSystemSpec/body.png'); + }); + + it('can show collider info', async () => { + const debugSystem = new ex.DebugSystem(); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: -100, y: 0, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + engine.debug.collider.showAll = true; + debugSystem.update([actor], 100); + + engine.graphicsContext.flush(); + + await expectAsync(engine.canvas).toEqualImage('src/spec/images/DebugSystemSpec/collider.png'); + }); + + it('can show composite collider info', async () => { + const debugSystem = new ex.DebugSystem(); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: -100, y: 0, width: 50, height: 50, color: ex.Color.Yellow }); + actor.collider.useCompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(150, 20), ex.Shape.Box(10, 150)]); + actor.id = 0; + engine.debug.collider.showAll = true; + debugSystem.update([actor], 100); + + engine.graphicsContext.flush(); + + await expectAsync(engine.canvas).toEqualImage('src/spec/images/DebugSystemSpec/composite-collider.png'); + }); + + it('can show graphics info', async () => { + const debugSystem = new ex.DebugSystem(); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: -100, y: 0, width: 50, height: 50 }); + actor.graphics.use(new ex.Rectangle({ width: 200, height: 100, color: ex.Color.Red })); + actor.id = 0; + engine.debug.collider.showBounds = false; + engine.debug.collider.showGeometry = false; + engine.debug.collider.showOwner = false; + engine.debug.graphics.showAll = true; + debugSystem.update([actor], 100); + + engine.graphicsContext.flush(); + + await expectAsync(engine.canvas).toEqualImage('src/spec/images/DebugSystemSpec/graphics.png'); + }); +}); diff --git a/src/spec/DebugTextSpec.ts b/src/spec/DebugTextSpec.ts new file mode 100644 index 0000000000..22c8a7cd7e --- /dev/null +++ b/src/spec/DebugTextSpec.ts @@ -0,0 +1,62 @@ +import * as ex from '@excalibur'; +import { ExcaliburAsyncMatchers } from 'excalibur-jasmine'; + +/** + * + */ +function flushWebGLCanvasTo2D(source: HTMLCanvasElement): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = source.width; + canvas.height = source.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(source, 0, 0); + return canvas; +} + +describe('DebugText', () => { + beforeAll(() => { + jasmine.addAsyncMatchers(ExcaliburAsyncMatchers); + }); + + it('exists', () => { + expect(ex.DebugText); + }); + + it('can write text (2DCanvas)', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + const debugText = new ex.DebugText(); + + await debugText.load(); + + ctx.clear(); + + debugText.write(ctx, 'some text', ex.vec(0, 50)); + + ctx.flush(); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/DebugTextSpec/draw-canvas2d.png'); + }); + + it('can write text (WebGL)', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement }); + + const debugText = new ex.DebugText(); + + await debugText.load(); + + ctx.clear(); + + debugText.write(ctx, 'some text', ex.vec(0, 50)); + + ctx.flush(); + + await expectAsync(flushWebGLCanvasTo2D(canvasElement)).toEqualImage('src/spec/images/DebugTextSpec/draw-webgl.png'); + }); +}); diff --git a/src/spec/EntityManagerSpec.ts b/src/spec/EntityManagerSpec.ts index 983a748783..b39c168dde 100644 --- a/src/spec/EntityManagerSpec.ts +++ b/src/spec/EntityManagerSpec.ts @@ -72,4 +72,27 @@ describe('An EntityManager', () => { entity.removeComponent(componentA); entity.processComponentRemoval(); }); + + it('can find entities by name', () => { + const entityManager = new ex.EntityManager(new ex.World(null)); + const entity = new ex.Entity([], 'some-e'); + const entity2 = new ex.Entity(); + entityManager.addEntity(entity); + entityManager.addEntity(entity2); + expect(entityManager.getByName('some-e')).toEqual([entity]); + expect(entityManager.getByName('anonymous')).toEqual([entity2]); + }); + + it('can clear entities', () => { + const entityManager = new ex.EntityManager(new ex.World(null)); + const entity = new ex.Entity([], 'some-e'); + const entity2 = new ex.Entity(); + entityManager.addEntity(entity); + entityManager.addEntity(entity2); + + expect(entityManager.entities.length).toBe(2); + entityManager.clear(); + entityManager.processEntityRemovals(); + expect(entityManager.entities.length).toBe(0); + }); }); diff --git a/src/spec/EntitySpec.ts b/src/spec/EntitySpec.ts index fe97ae6966..e6fcf8c85e 100644 --- a/src/spec/EntitySpec.ts +++ b/src/spec/EntitySpec.ts @@ -29,6 +29,16 @@ describe('An entity', () => { expect(entity2.id).not.toBe(entity3.id); }); + it('can have a name', () => { + const e = new ex.Entity([], 'my-name'); + expect(e.name).toBe('my-name'); + }); + + it('has a default name', () => { + const e = new ex.Entity(); + expect(e.name).toBe('anonymous'); + }); + it('can be killed', () => { const entity = new ex.Entity(); expect(entity.isKilled()).toBe(false); diff --git a/src/spec/ExcaliburGraphicsContextSpec.ts b/src/spec/ExcaliburGraphicsContextSpec.ts index 585a34f6c5..a8e3d7d9c9 100644 --- a/src/spec/ExcaliburGraphicsContextSpec.ts +++ b/src/spec/ExcaliburGraphicsContextSpec.ts @@ -132,6 +132,57 @@ describe('The ExcaliburGraphicsContext', () => { await expectAsync(canvasElement).toEqualImage('src/spec/images/ExcaliburGraphicsContextSpec/2d-transform.png'); }); + it('can draw rectangle', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); + sut.flush(); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png'); + }); + + it('can draw circle', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + sut.flush(); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/ExcaliburGraphicsContextSpec/2d-circle.png'); + }); + + it('can draw a line', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Blue, 5); + sut.flush(); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/ExcaliburGraphicsContextSpec/2d-line.png'); + }); + it('can snap drawings to pixel', async () => { const canvasElement = document.createElement('canvas'); canvasElement.width = 100; @@ -235,6 +286,11 @@ describe('The ExcaliburGraphicsContext', () => { sut.clear(); sut.drawImage(rect._bitmap, 20, 20); + // sut.opacity = .5; + // sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Green); + // sut.drawLine(ex.vec(10, 10), ex.vec(90, 90), ex.Color.Red, 5); + // sut.drawLine(ex.vec(90, 10), ex.vec(10, 90), ex.Color.Red, 5); + // sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); sut.flush(); await expectAsync(flushWebGLCanvasTo2D(canvasElement)).toEqualImage( @@ -304,6 +360,59 @@ describe('The ExcaliburGraphicsContext', () => { await expectAsync(flushWebGLCanvasTo2D(canvasElement)).toEqualImage('src/spec/images/ExcaliburGraphicsContextSpec/webgl-rect.png'); }); + it('can draw rectangle', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); + sut.flush(); + + await expectAsync(flushWebGLCanvasTo2D(canvasElement)).toEqualImage( + 'src/spec/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png' + ); + }); + + it('can draw circle', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + sut.flush(); + + await expectAsync(flushWebGLCanvasTo2D(canvasElement)).toEqualImage('src/spec/images/ExcaliburGraphicsContextSpec/webgl-circle.png'); + }); + + it('can draw a line', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Blue, 5); + sut.flush(); + + await expectAsync(flushWebGLCanvasTo2D(canvasElement)).toEqualImage('src/spec/images/ExcaliburGraphicsContextSpec/webgl-line.png'); + }); + it('can transform the context', async () => { const canvasElement = document.createElement('canvas'); canvasElement.width = 100; diff --git a/src/spec/PoolSpec.ts b/src/spec/PoolSpec.ts index 9b7daa4ec6..bbc22a26d7 100644 --- a/src/spec/PoolSpec.ts +++ b/src/spec/PoolSpec.ts @@ -10,25 +10,35 @@ class MockPoolable implements ex.Poolable { } describe('An object Pool', () => { - it('should exist', () => { expect(ex.Pool).toBeDefined(); }); it('can be constructed', () => { - const pool = new ex.Pool(() => new MockPoolable(), (m) => m.dispose(), 10); + const pool = new ex.Pool( + () => new MockPoolable(), + (m) => m.dispose(), + 10 + ); expect(pool).toBeDefined(); }); it('can get instances up to the maximum', () => { - const pool = new ex.Pool(() => new MockPoolable(), (m) => m.dispose(), 10); + const pool = new ex.Pool( + () => new MockPoolable(), + (m) => m.dispose(), + 10 + ); + const logger = ex.Logger.getInstance(); + spyOn(logger, 'warn'); for (let i = 0; i < 10; i++) { const instance = pool.get(); } + expect(pool.totalAllocations).toBe(10); - expect(() => { - pool.get(); - }).toThrow(); + expect(logger.warn).not.toHaveBeenCalled(); + pool.get(); + expect(logger.warn).toHaveBeenCalledOnceWith('Max pooled objects reached, possible memory leak? Doubling'); }); it('can get instances and return them to get recycled', () => { @@ -52,9 +62,13 @@ describe('An object Pool', () => { }); it('can borrow a single instance temporarily', () => { - const pool = new ex.Pool(() => new MockPoolable(), (m) => m.dispose(), 10); + const pool = new ex.Pool( + () => new MockPoolable(), + (m) => m.dispose(), + 10 + ); pool.get(); - pool.borrow(instance => { + pool.borrow((instance) => { expect(instance).toBeDefined(); expect(pool.index).toBe(2); }); @@ -62,9 +76,13 @@ describe('An object Pool', () => { }); it('can be used in a using which are then reclaimed', () => { - const pool = new ex.Pool(() => new MockPoolable(), (m) => m.dispose(), 10); + const pool = new ex.Pool( + () => new MockPoolable(), + (m) => m.dispose(), + 10 + ); for (let i = 0; i < 10; i++) { - pool.using(pool => { + pool.using((pool) => { for (let i = 0; i < 10; i++) { pool.get(); } @@ -75,8 +93,12 @@ describe('An object Pool', () => { }); it('can have instances unhooked from the pool', () => { - const pool = new ex.Pool(() => new MockPoolable(), (m) => m.dispose(), 10); - const is = pool.using(pool => { + const pool = new ex.Pool( + () => new MockPoolable(), + (m) => m.dispose(), + 10 + ); + const is = pool.using((pool) => { const instances = []; for (let i = 0; i < 10; i++) { const i = pool.get(); @@ -90,4 +112,4 @@ describe('An object Pool', () => { expect(is[i]._pool).toBe(undefined); } }); -}); \ No newline at end of file +}); diff --git a/src/spec/TransformComponentSpec.ts b/src/spec/TransformComponentSpec.ts index 600a480fd2..7c9e155523 100644 --- a/src/spec/TransformComponentSpec.ts +++ b/src/spec/TransformComponentSpec.ts @@ -1,7 +1,6 @@ import * as ex from '@excalibur'; import { ExcaliburMatchers } from 'excalibur-jasmine'; - describe('A TransformComponent', () => { beforeAll(() => { jasmine.addMatchers(ExcaliburMatchers); @@ -100,13 +99,13 @@ describe('A TransformComponent', () => { // Changing a child global scale affects childs local and not parent scale childTx.globalScale = ex.vec(1, 1); expect(parentTx.scale).toBeVector(ex.vec(2, 3)); - expect(childTx.scale).toBeVector(ex.vec(1/2, 1/3)); + expect(childTx.scale).toBeVector(ex.vec(1 / 2, 1 / 3)); // can change scale by prop childTx.globalScale.x = 3; childTx.globalScale.y = 4; expect(parentTx.scale).toBeVector(ex.vec(2, 3)); - expect(childTx.scale).toBeVector(ex.vec(3/2, 4/3)); + expect(childTx.scale).toBeVector(ex.vec(3 / 2, 4 / 3)); }); it('can have parent/child relations with rotation', () => { @@ -128,4 +127,22 @@ describe('A TransformComponent', () => { expect(childTx.rotation).toBeCloseTo(Math.PI); // Math.PI + Math.PI = 2PI = 0 global }); -}); \ No newline at end of file + it('can retrieve the global transform', () => { + const parent = new ex.Entity([new ex.TransformComponent()]); + const child = new ex.Entity([new ex.TransformComponent()]); + parent.addChild(child); + + const parentTx = parent.get(ex.TransformComponent); + const childTx = child.get(ex.TransformComponent); + + // Changing a parent position influences the child global position + parentTx.pos = ex.vec(100, 200); + parentTx.rotation = Math.PI; + parentTx.scale = ex.vec(2, 3); + + expect(childTx.pos).toBeVector(ex.vec(0, 0)); + expect(childTx.getGlobalTransform().pos).toBeVector(ex.vec(100, 200)); + expect(childTx.getGlobalTransform().rotation).toBe(Math.PI); + expect(childTx.getGlobalTransform().scale).toBeVector(ex.vec(2, 3)); + }); +}); diff --git a/src/spec/images/CollisionShapeSpec/circle-debug.png b/src/spec/images/CollisionShapeSpec/circle-debug.png new file mode 100644 index 0000000000..6fa83235a8 Binary files /dev/null and b/src/spec/images/CollisionShapeSpec/circle-debug.png differ diff --git a/src/spec/images/CollisionShapeSpec/circle.png b/src/spec/images/CollisionShapeSpec/circle.png index 2e3f689cdd..547217cf7c 100644 Binary files a/src/spec/images/CollisionShapeSpec/circle.png and b/src/spec/images/CollisionShapeSpec/circle.png differ diff --git a/src/spec/images/CompositeColliderSpec/composite.png b/src/spec/images/CompositeColliderSpec/composite.png new file mode 100644 index 0000000000..8a4ddced86 Binary files /dev/null and b/src/spec/images/CompositeColliderSpec/composite.png differ diff --git a/src/spec/images/DebugSystemSpec/body.png b/src/spec/images/DebugSystemSpec/body.png new file mode 100644 index 0000000000..f03d617887 Binary files /dev/null and b/src/spec/images/DebugSystemSpec/body.png differ diff --git a/src/spec/images/DebugSystemSpec/collider.png b/src/spec/images/DebugSystemSpec/collider.png new file mode 100644 index 0000000000..4849bc24e4 Binary files /dev/null and b/src/spec/images/DebugSystemSpec/collider.png differ diff --git a/src/spec/images/DebugSystemSpec/composite-collider.png b/src/spec/images/DebugSystemSpec/composite-collider.png new file mode 100644 index 0000000000..9a883485b7 Binary files /dev/null and b/src/spec/images/DebugSystemSpec/composite-collider.png differ diff --git a/src/spec/images/DebugSystemSpec/graphics.png b/src/spec/images/DebugSystemSpec/graphics.png new file mode 100644 index 0000000000..b609744995 Binary files /dev/null and b/src/spec/images/DebugSystemSpec/graphics.png differ diff --git a/src/spec/images/DebugSystemSpec/motion.png b/src/spec/images/DebugSystemSpec/motion.png new file mode 100644 index 0000000000..af4ab07d75 Binary files /dev/null and b/src/spec/images/DebugSystemSpec/motion.png differ diff --git a/src/spec/images/DebugSystemSpec/transform.png b/src/spec/images/DebugSystemSpec/transform.png new file mode 100644 index 0000000000..e3f44aa906 Binary files /dev/null and b/src/spec/images/DebugSystemSpec/transform.png differ diff --git a/src/spec/images/DebugTextSpec/draw-canvas2d.png b/src/spec/images/DebugTextSpec/draw-canvas2d.png new file mode 100644 index 0000000000..f90355aad7 Binary files /dev/null and b/src/spec/images/DebugTextSpec/draw-canvas2d.png differ diff --git a/src/spec/images/DebugTextSpec/draw-webgl.png b/src/spec/images/DebugTextSpec/draw-webgl.png new file mode 100644 index 0000000000..f90355aad7 Binary files /dev/null and b/src/spec/images/DebugTextSpec/draw-webgl.png differ diff --git a/src/spec/images/DebugTextSpec/draw.png b/src/spec/images/DebugTextSpec/draw.png new file mode 100644 index 0000000000..f90355aad7 Binary files /dev/null and b/src/spec/images/DebugTextSpec/draw.png differ diff --git a/src/spec/images/ExcaliburGraphicsContextSpec/2d-circle.png b/src/spec/images/ExcaliburGraphicsContextSpec/2d-circle.png new file mode 100644 index 0000000000..e81d32d644 Binary files /dev/null and b/src/spec/images/ExcaliburGraphicsContextSpec/2d-circle.png differ diff --git a/src/spec/images/ExcaliburGraphicsContextSpec/2d-line.png b/src/spec/images/ExcaliburGraphicsContextSpec/2d-line.png new file mode 100644 index 0000000000..f46fb2cce0 Binary files /dev/null and b/src/spec/images/ExcaliburGraphicsContextSpec/2d-line.png differ diff --git a/src/spec/images/ExcaliburGraphicsContextSpec/webgl-circle.png b/src/spec/images/ExcaliburGraphicsContextSpec/webgl-circle.png new file mode 100644 index 0000000000..a787c5ef81 Binary files /dev/null and b/src/spec/images/ExcaliburGraphicsContextSpec/webgl-circle.png differ diff --git a/src/spec/images/ExcaliburGraphicsContextSpec/webgl-line.png b/src/spec/images/ExcaliburGraphicsContextSpec/webgl-line.png new file mode 100644 index 0000000000..28ea41c835 Binary files /dev/null and b/src/spec/images/ExcaliburGraphicsContextSpec/webgl-line.png differ diff --git a/src/spec/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png b/src/spec/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png new file mode 100644 index 0000000000..c4f1f053da Binary files /dev/null and b/src/spec/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png differ diff --git a/wallaby.js b/wallaby.js index 412239265b..83eb04046d 100644 --- a/wallaby.js +++ b/wallaby.js @@ -5,6 +5,7 @@ module.exports = function (wallaby) { return { files: [ { pattern: 'src/engine/**/*.ts', load: false }, + { pattern: 'src/engine/**/*.glsl', load: false }, { pattern: 'src/spec/images/**/*.mp3' }, { pattern: 'src/spec/images/**/*.png' }, { pattern: 'src/spec/images/**/*.gif' },