game.html 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Making a Game</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – Making a Game">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="/manual/resources/lesson.css">
  12. <link rel="stylesheet" href="/manual/resources/lang.css">
  13. </head>
  14. <body>
  15. <div class="container">
  16. <div class="lesson-title">
  17. <h1>Making a Game</h1>
  18. </div>
  19. <div class="lesson">
  20. <div class="lesson-main">
  21. <p>Many people want to write games using three.js. This article
  22. will hopefully give you some ideas on how to start.</p>
  23. <p>At least at the time I'm writing this article it's probably going to be the
  24. longest article on this site. It's possible the code here is massively over
  25. engineered but as I wrote each new feature I'd run into a problem that needed a
  26. solution I'm used to from other games I've written. In other words each new
  27. solution seemed important so I'll try to show why. Of course the smaller your
  28. game the less you might need some of the solutions shown here but this is a
  29. pretty small game and yet with the complexities of 3D characters many things
  30. take more organization than they might with 2D characters.</p>
  31. <p>As an example if you're making PacMan in 2D, when PacMan turns a corner
  32. that happens instantly at 90 degrees. There is no in-between step. But
  33. in a 3D game often we need the character to rotate over several frames.
  34. That simple change can add a bunch of complexity and require different
  35. solutions.</p>
  36. <p>The majority of the code here will not really be three.js and
  37. that's important to note, <strong>three.js is not a game engine</strong>.
  38. Three.js is a 3D library. It provides a <a href="scenegraph.html">scene graph</a>
  39. and features for displaying 3D objects added to that scene graph
  40. but it does not provide all the other things needed to make a game.
  41. No collisions, no physics, no input systems, no path finding, etc, etc...
  42. So, we'll have to provide those things ourselves.</p>
  43. <p>I ended up writing quite a bit of code to make this simple <em>unfinished</em>
  44. game like thing and again, it's certainly possible I over engineered and there
  45. are simpler solutions but I feel like I actually didn't write
  46. enough code and hopefully I can explain what I think is missing.</p>
  47. <p>Many of the ideas here are heavily influenced by <a href="https://unity.com">Unity</a>.
  48. If you're not familiar with Unity that probably does not matter.
  49. I only bring it up as 10s of 1000s of games have shipped using
  50. these ideas.</p>
  51. <p>Let's start with the three.js parts. We need to load models for our game.</p>
  52. <p>At <a href="https://opengameart.org">opengameart.org</a> I found this <a href="https://opengameart.org/content/lowpoly-animated-knight">animated knight
  53. model</a> by <a href="https://opengameart.org/users/quaternius">quaternius</a></p>
  54. <div class="threejs_center"><img src="../resources/images/knight.jpg" style="width: 375px;"></div>
  55. <p><a href="https://opengameart.org/users/quaternius">quaternius</a> also made <a href="https://opengameart.org/content/lowpoly-animated-farm-animal-pack">these animated animals</a>.</p>
  56. <div class="threejs_center"><img src="../resources/images/animals.jpg" style="width: 606px;"></div>
  57. <p>These seem like good models to start with so the first thing we need to
  58. do is load them.</p>
  59. <p>We covered <a href="load-gltf.html">loading glTF files before</a>.
  60. The difference this time is we need to load multiple models and
  61. we can't start the game until all the models are loaded.</p>
  62. <p>Fortunately three.js provides the <a href="/docs/#api/en/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a> just for this purpose.
  63. We create a <a href="/docs/#api/en/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a> and pass it to the other loaders. The
  64. <a href="/docs/#api/en/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a> provides both <a href="/docs/#api/en/loaders/managers/LoadingManager#onProgress"><code class="notranslate" translate="no">onProgress</code></a> and
  65. <a href="/docs/#api/en/loaders/managers/LoadingManager#onLoad"><code class="notranslate" translate="no">onLoad</code></a> properties we can attach callbacks to.
  66. The <a href="/docs/#api/en/loaders/managers/LoadingManager#onLoad"><code class="notranslate" translate="no">onLoad</code></a> callback will be called when
  67. all files have been loaded. The <a href="/docs/#api/en/loaders/managers/LoadingManager#onProgress"><code class="notranslate" translate="no">onProgress</code></a> callback
  68. as called after each individual file arrives to give as a chance to show
  69. loading progress.</p>
  70. <p>Starting with the code from <a href="load-gltf.html">loading a glTF file</a> I removed all
  71. the code related to framing the scene and added this code to load all models.</p>
  72. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const manager = new THREE.LoadingManager();
  73. manager.onLoad = init;
  74. const models = {
  75. pig: { url: 'resources/models/animals/Pig.gltf' },
  76. cow: { url: 'resources/models/animals/Cow.gltf' },
  77. llama: { url: 'resources/models/animals/Llama.gltf' },
  78. pug: { url: 'resources/models/animals/Pug.gltf' },
  79. sheep: { url: 'resources/models/animals/Sheep.gltf' },
  80. zebra: { url: 'resources/models/animals/Zebra.gltf' },
  81. horse: { url: 'resources/models/animals/Horse.gltf' },
  82. knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
  83. };
  84. {
  85. const gltfLoader = new GLTFLoader(manager);
  86. for (const model of Object.values(models)) {
  87. gltfLoader.load(model.url, (gltf) =&gt; {
  88. model.gltf = gltf;
  89. });
  90. }
  91. }
  92. function init() {
  93. // TBD
  94. }
  95. </pre>
  96. <p>This code will load all the models above and the <a href="/docs/#api/en/loaders/managers/LoadingManager"><code class="notranslate" translate="no">LoadingManager</code></a> will call
  97. <code class="notranslate" translate="no">init</code> when done. We'll use the <code class="notranslate" translate="no">models</code> object later to let us access the
  98. loaded models so the <a href="/docs/#examples/loaders/GLTFLoader"><code class="notranslate" translate="no">GLTFLoader</code></a> callback for each individual model attaches
  99. the loaded data to that model's info.</p>
  100. <p>All the models with all their animation are currently about 6.6meg. That's a
  101. pretty big download. Assuming your server supports compression (the server this
  102. site runs on does) it's able to compress them to around 1.4meg. That's
  103. definitely better than 6.6meg bit it's still not a tiny amount of data. It would
  104. probably be good if we added a progress bar so the user has some idea how much
  105. longer they have to wait.</p>
  106. <p>So, let's add an <a href="/docs/#api/en/loaders/managers/LoadingManager#onProgress"><code class="notranslate" translate="no">onProgress</code></a> callback. It will be
  107. called with 3 arguments, the <code class="notranslate" translate="no">url</code> of the last loaded object and then the number
  108. of items loaded so far as well as the total number of items.</p>
  109. <p>Let's setup some HTML for a loading bar</p>
  110. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  111. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  112. + &lt;div id="loading"&gt;
  113. + &lt;div&gt;
  114. + &lt;div&gt;...loading...&lt;/div&gt;
  115. + &lt;div class="progress"&gt;&lt;div id="progressbar"&gt;&lt;/div&gt;&lt;/div&gt;
  116. + &lt;/div&gt;
  117. + &lt;/div&gt;
  118. &lt;/body&gt;
  119. </pre>
  120. <p>We'll look up the <code class="notranslate" translate="no">#progressbar</code> div and we can set the width from 0% to 100%
  121. to show our progress. All we need to do is set that in our callback.</p>
  122. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const manager = new THREE.LoadingManager();
  123. manager.onLoad = init;
  124. +const progressbarElem = document.querySelector('#progressbar');
  125. +manager.onProgress = (url, itemsLoaded, itemsTotal) =&gt; {
  126. + progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
  127. +};
  128. </pre>
  129. <p>We already setup <code class="notranslate" translate="no">init</code> to be called when all the models are loaded so
  130. we can turn off the progress bar by hiding the <code class="notranslate" translate="no">#loading</code> element.</p>
  131. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function init() {
  132. + // hide the loading bar
  133. + const loadingElem = document.querySelector('#loading');
  134. + loadingElem.style.display = 'none';
  135. }
  136. </pre>
  137. <p>Here's a bunch of CSS for styling the bar. The CSS makes the <code class="notranslate" translate="no">#loading</code> <code class="notranslate" translate="no">&lt;div&gt;</code>
  138. the full size of the page and centers its children. The CSS makes a <code class="notranslate" translate="no">.progress</code>
  139. area to contain the progress bar. The CSS also gives the progress bar
  140. a CSS animation of diagonal stripes.</p>
  141. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#loading {
  142. position: absolute;
  143. left: 0;
  144. top: 0;
  145. width: 100%;
  146. height: 100%;
  147. display: flex;
  148. align-items: center;
  149. justify-content: center;
  150. text-align: center;
  151. font-size: xx-large;
  152. font-family: sans-serif;
  153. }
  154. #loading&gt;div&gt;div {
  155. padding: 2px;
  156. }
  157. .progress {
  158. width: 50vw;
  159. border: 1px solid black;
  160. }
  161. #progressbar {
  162. width: 0;
  163. transition: width ease-out .5s;
  164. height: 1em;
  165. background-color: #888;
  166. background-image: linear-gradient(
  167. -45deg,
  168. rgba(255, 255, 255, .5) 25%,
  169. transparent 25%,
  170. transparent 50%,
  171. rgba(255, 255, 255, .5) 50%,
  172. rgba(255, 255, 255, .5) 75%,
  173. transparent 75%,
  174. transparent
  175. );
  176. background-size: 50px 50px;
  177. animation: progressanim 2s linear infinite;
  178. }
  179. @keyframes progressanim {
  180. 0% {
  181. background-position: 50px 50px;
  182. }
  183. 100% {
  184. background-position: 0 0;
  185. }
  186. }
  187. </pre>
  188. <p>Now that we have a progress bar let's deal with the models. These models
  189. have animations and we want to be able to access those animations.
  190. Animations are stored in an array by default be we'd like to be able to
  191. easily access them by name so let's setup an <code class="notranslate" translate="no">animations</code> property for
  192. each model to do that. Note of course this means animations must have unique names.</p>
  193. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function prepModelsAndAnimations() {
  194. + Object.values(models).forEach(model =&gt; {
  195. + const animsByName = {};
  196. + model.gltf.animations.forEach((clip) =&gt; {
  197. + animsByName[clip.name] = clip;
  198. + });
  199. + model.animations = animsByName;
  200. + });
  201. +}
  202. function init() {
  203. // hide the loading bar
  204. const loadingElem = document.querySelector('#loading');
  205. loadingElem.style.display = 'none';
  206. + prepModelsAndAnimations();
  207. }
  208. </pre>
  209. <p>Let's display the animated models.</p>
  210. <p>Unlike the <a href="load-gltf.html">previous example of loading a glTF file</a>
  211. This time we probably want to be able to display more than one instance
  212. of each model. To do this, instead of adding
  213. the loaded gltf scene directly like we did in <a href="load-gltf.html">the article on loading a glTF</a>,
  214. we instead want to clone the scene and in particular we want to clone
  215. it for skinned animated characters. Fortunately there's a utility function,
  216. <code class="notranslate" translate="no">SkeletonUtils.clone</code> we can use to do this. So, first we need to include
  217. the utils.</p>
  218. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from '/build/three.module.js';
  219. import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';
  220. import {GLTFLoader} from '/examples/jsm/loaders/GLTFLoader.js';
  221. +import * as SkeletonUtils from '/examples/jsm/utils/SkeletonUtils.js';
  222. </pre>
  223. <p>Then we can clone the models we just loaded</p>
  224. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function init() {
  225. // hide the loading bar
  226. const loadingElem = document.querySelector('#loading');
  227. loadingElem.style.display = 'none';
  228. prepModelsAndAnimations();
  229. + Object.values(models).forEach((model, ndx) =&gt; {
  230. + const clonedScene = SkeletonUtils.clone(model.gltf.scene);
  231. + const root = new THREE.Object3D();
  232. + root.add(clonedScene);
  233. + scene.add(root);
  234. + root.position.x = (ndx - 3) * 3;
  235. + });
  236. }
  237. </pre>
  238. <p>Above, for each model, we clone the <code class="notranslate" translate="no">gltf.scene</code> we loaded and we parent that
  239. to a new <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>. We need to parent it to another object because when
  240. we play animations the animation will apply animated positions to the nodes
  241. in the loaded scene which means we won't have control over those positions.</p>
  242. <p>To play the animations each model we clone needs an <a href="/docs/#api/en/animation/AnimationMixer"><code class="notranslate" translate="no">AnimationMixer</code></a>.
  243. An <a href="/docs/#api/en/animation/AnimationMixer"><code class="notranslate" translate="no">AnimationMixer</code></a> contains 1 or more <a href="/docs/#api/en/animation/AnimationAction"><code class="notranslate" translate="no">AnimationAction</code></a>s. An
  244. <a href="/docs/#api/en/animation/AnimationAction"><code class="notranslate" translate="no">AnimationAction</code></a> references an <a href="/docs/#api/en/animation/AnimationClip"><code class="notranslate" translate="no">AnimationClip</code></a>. <a href="/docs/#api/en/animation/AnimationAction"><code class="notranslate" translate="no">AnimationAction</code></a>s
  245. have all kinds of settings for playing then chaining to another
  246. action or cross fading between actions. Let's just get the first
  247. <a href="/docs/#api/en/animation/AnimationClip"><code class="notranslate" translate="no">AnimationClip</code></a> and create an action for it. The default is for
  248. an action to play its clip in a loop forever.</p>
  249. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const mixers = [];
  250. function init() {
  251. // hide the loading bar
  252. const loadingElem = document.querySelector('#loading');
  253. loadingElem.style.display = 'none';
  254. prepModelsAndAnimations();
  255. Object.values(models).forEach((model, ndx) =&gt; {
  256. const clonedScene = SkeletonUtils.clone(model.gltf.scene);
  257. const root = new THREE.Object3D();
  258. root.add(clonedScene);
  259. scene.add(root);
  260. root.position.x = (ndx - 3) * 3;
  261. + const mixer = new THREE.AnimationMixer(clonedScene);
  262. + const firstClip = Object.values(model.animations)[0];
  263. + const action = mixer.clipAction(firstClip);
  264. + action.play();
  265. + mixers.push(mixer);
  266. });
  267. }
  268. </pre>
  269. <p>We called <a href="/docs/#api/en/animation/AnimationAction#play"><code class="notranslate" translate="no">play</code></a> to start the action and stored
  270. off all the <code class="notranslate" translate="no">AnimationMixers</code> in an array called <code class="notranslate" translate="no">mixers</code>. Finally
  271. we need to update each <a href="/docs/#api/en/animation/AnimationMixer"><code class="notranslate" translate="no">AnimationMixer</code></a> in our render loop by computing
  272. the time since the last frame and passing that to <a href="/docs/#api/en/animation/AnimationMixer.update"><code class="notranslate" translate="no">AnimationMixer.update</code></a>.</p>
  273. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let then = 0;
  274. function render(now) {
  275. + now *= 0.001; // convert to seconds
  276. + const deltaTime = now - then;
  277. + then = now;
  278. if (resizeRendererToDisplaySize(renderer)) {
  279. const canvas = renderer.domElement;
  280. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  281. camera.updateProjectionMatrix();
  282. }
  283. + for (const mixer of mixers) {
  284. + mixer.update(deltaTime);
  285. + }
  286. renderer.render(scene, camera);
  287. requestAnimationFrame(render);
  288. }
  289. </pre>
  290. <p>And with that we should get each model loaded and playing its first animation.</p>
  291. <p></p><div translate="no" class="threejs_example_container notranslate">
  292. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/game-load-models.html"></iframe></div>
  293. <a class="threejs_center" href="/manual/examples/game-load-models.html" target="_blank">click here to open in a separate window</a>
  294. </div>
  295. <p></p>
  296. <p>Let's make it so we can check all of the animations.
  297. We'll add all of the clips as actions and then enable just one at
  298. a time.</p>
  299. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const mixers = [];
  300. +const mixerInfos = [];
  301. function init() {
  302. // hide the loading bar
  303. const loadingElem = document.querySelector('#loading');
  304. loadingElem.style.display = 'none';
  305. prepModelsAndAnimations();
  306. Object.values(models).forEach((model, ndx) =&gt; {
  307. const clonedScene = SkeletonUtils.clone(model.gltf.scene);
  308. const root = new THREE.Object3D();
  309. root.add(clonedScene);
  310. scene.add(root);
  311. root.position.x = (ndx - 3) * 3;
  312. const mixer = new THREE.AnimationMixer(clonedScene);
  313. - const firstClip = Object.values(model.animations)[0];
  314. - const action = mixer.clipAction(firstClip);
  315. - action.play();
  316. - mixers.push(mixer);
  317. + const actions = Object.values(model.animations).map((clip) =&gt; {
  318. + return mixer.clipAction(clip);
  319. + });
  320. + const mixerInfo = {
  321. + mixer,
  322. + actions,
  323. + actionNdx: -1,
  324. + };
  325. + mixerInfos.push(mixerInfo);
  326. + playNextAction(mixerInfo);
  327. });
  328. }
  329. +function playNextAction(mixerInfo) {
  330. + const {actions, actionNdx} = mixerInfo;
  331. + const nextActionNdx = (actionNdx + 1) % actions.length;
  332. + mixerInfo.actionNdx = nextActionNdx;
  333. + actions.forEach((action, ndx) =&gt; {
  334. + const enabled = ndx === nextActionNdx;
  335. + action.enabled = enabled;
  336. + if (enabled) {
  337. + action.play();
  338. + }
  339. + });
  340. +}
  341. </pre>
  342. <p>The code above makes an array of <a href="/docs/#api/en/animation/AnimationAction"><code class="notranslate" translate="no">AnimationAction</code></a>s,
  343. one for each <a href="/docs/#api/en/animation/AnimationClip"><code class="notranslate" translate="no">AnimationClip</code></a>. It makes an array of objects, <code class="notranslate" translate="no">mixerInfos</code>,
  344. with references to the <a href="/docs/#api/en/animation/AnimationMixer"><code class="notranslate" translate="no">AnimationMixer</code></a> and all the <a href="/docs/#api/en/animation/AnimationAction"><code class="notranslate" translate="no">AnimationAction</code></a>s
  345. for each model. It then calls <code class="notranslate" translate="no">playNextAction</code> which sets <code class="notranslate" translate="no">enabled</code> on
  346. all but one action for that mixer.</p>
  347. <p>We need to update the render loop for the new array</p>
  348. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-for (const mixer of mixers) {
  349. +for (const {mixer} of mixerInfos) {
  350. mixer.update(deltaTime);
  351. }
  352. </pre>
  353. <p>Let's make it so pressing a key 1 to 8 will play the next animation
  354. for each model</p>
  355. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">window.addEventListener('keydown', (e) =&gt; {
  356. const mixerInfo = mixerInfos[e.keyCode - 49];
  357. if (!mixerInfo) {
  358. return;
  359. }
  360. playNextAction(mixerInfo);
  361. });
  362. </pre>
  363. <p>Now you should be able to click on the example and then press keys 1 through 8
  364. to cycle each of the models through their available animations.</p>
  365. <p></p><div translate="no" class="threejs_example_container notranslate">
  366. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/game-check-animations.html"></iframe></div>
  367. <a class="threejs_center" href="/manual/examples/game-check-animations.html" target="_blank">click here to open in a separate window</a>
  368. </div>
  369. <p></p>
  370. <p>So that is arguably the sum-total of the three.js portion of this
  371. article. We covered loading multiple files, cloning skinned models,
  372. and playing animations on them. In a real game you'd have to do a
  373. ton more manipulation of <a href="/docs/#api/en/animation/AnimationAction"><code class="notranslate" translate="no">AnimationAction</code></a> objects.</p>
  374. <p>Let's start making a game infrastructure</p>
  375. <p>A common pattern for making a modern game is to use an
  376. <a href="https://www.google.com/search?q=entity+component+system">Entity Component System</a>.
  377. In an Entity Component System an object in a game is called an <em>entity</em> that consists
  378. of a bunch of <em>components</em>. You build up entities by deciding which components to
  379. attach to them. So, let's make an Entity Component System.</p>
  380. <p>We'll call our entities <code class="notranslate" translate="no">GameObject</code>. It's effectively just a collection
  381. of components and a three.js <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>.</p>
  382. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function removeArrayElement(array, element) {
  383. const ndx = array.indexOf(element);
  384. if (ndx &gt;= 0) {
  385. array.splice(ndx, 1);
  386. }
  387. }
  388. class GameObject {
  389. constructor(parent, name) {
  390. this.name = name;
  391. this.components = [];
  392. this.transform = new THREE.Object3D();
  393. parent.add(this.transform);
  394. }
  395. addComponent(ComponentType, ...args) {
  396. const component = new ComponentType(this, ...args);
  397. this.components.push(component);
  398. return component;
  399. }
  400. removeComponent(component) {
  401. removeArrayElement(this.components, component);
  402. }
  403. getComponent(ComponentType) {
  404. return this.components.find(c =&gt; c instanceof ComponentType);
  405. }
  406. update() {
  407. for (const component of this.components) {
  408. component.update();
  409. }
  410. }
  411. }
  412. </pre>
  413. <p>Calling <code class="notranslate" translate="no">GameObject.update</code> calls <code class="notranslate" translate="no">update</code> on all the components.</p>
  414. <p>I included a name only to help in debugging so if I look at a <code class="notranslate" translate="no">GameObject</code>
  415. in the debugger I can see a name to help identify it.</p>
  416. <p>Some things that might seem a little strange:</p>
  417. <p><code class="notranslate" translate="no">GameObject.addComponent</code> is used to create components. Whether or not
  418. this a good idea or a bad idea I'm not sure. My thinking was it makes
  419. no sense for a component to exist outside of a gameobject so I thought
  420. it might be good if creating a component automatically added that component
  421. to the gameobject and passed the gameobject to the component's constructor.
  422. In other words to add a component you do this</p>
  423. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gameObject = new GameObject(scene, 'foo');
  424. gameObject.addComponent(TypeOfComponent);
  425. </pre>
  426. <p>If I didn't do it this way you'd instead do something like this</p>
  427. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gameObject = new GameObject(scene, 'foo');
  428. const component = new TypeOfComponent(gameObject);
  429. gameObject.addComponent(component);
  430. </pre>
  431. <p>Is it better that the first way is shorter and more automated or is it worse
  432. because it looks out of the ordinary? I don't know.</p>
  433. <p><code class="notranslate" translate="no">GameObject.getComponent</code> looks up components by type. That has
  434. the implication that you can not have 2 components of the same
  435. type on a single game object or at least if you do you can only
  436. look up the first one without adding some other API.</p>
  437. <p>It's common for one component to look up another and when looking them up they
  438. have to match by type otherwise you might get the wrong one. We could instead
  439. give each component a name and you could look them up by name. That would be
  440. more flexible in that you could have more than one component of the same type but it
  441. would also be more tedious. Again, I'm not sure which is better.</p>
  442. <p>On to the components themselves. Here is their base class.</p>
  443. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// Base for all components
  444. class Component {
  445. constructor(gameObject) {
  446. this.gameObject = gameObject;
  447. }
  448. update() {
  449. }
  450. }
  451. </pre>
  452. <p>Do components need a base class? JavaScript is not like most strictly
  453. typed languages so effectively we could have no base class and just
  454. leave it up to each component to do whatever it wants in its constructor
  455. knowing that the first argument is always the component's gameobject.
  456. If it doesn't care about gameobject it wouldn't store it. I kind of feel like this
  457. common base is good though. It means if you have a reference to a
  458. component you know you can find its parent gameobject always and from its
  459. parent you can easily look up other components as well as look at its
  460. transform.</p>
  461. <p>To manage the gameobjects we probably need some kind of gameobject manager. You
  462. might think we could just keep an array of gameobjects but in a real game the
  463. components of a gameobject might add and remove other gameobjects at runtime.
  464. For example a gun gameobject might add a bullet gameobject every time the gun
  465. fires. A monster gameobject might remove itself if it has been killed. We then
  466. would have an issue that we might have code like this</p>
  467. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">for (const gameObject of globalArrayOfGameObjects) {
  468. gameObject.update();
  469. }
  470. </pre>
  471. <p>The loop above would fail or do un-expected things if
  472. gameobjects are added or removed from <code class="notranslate" translate="no">globalArrayOfGameObjects</code>
  473. in the middle of the loop in some component's <code class="notranslate" translate="no">update</code> function.</p>
  474. <p>To try to prevent that problem we need something a little safer.
  475. Here's one attempt.</p>
  476. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class SafeArray {
  477. constructor() {
  478. this.array = [];
  479. this.addQueue = [];
  480. this.removeQueue = new Set();
  481. }
  482. get isEmpty() {
  483. return this.addQueue.length + this.array.length &gt; 0;
  484. }
  485. add(element) {
  486. this.addQueue.push(element);
  487. }
  488. remove(element) {
  489. this.removeQueue.add(element);
  490. }
  491. forEach(fn) {
  492. this._addQueued();
  493. this._removeQueued();
  494. for (const element of this.array) {
  495. if (this.removeQueue.has(element)) {
  496. continue;
  497. }
  498. fn(element);
  499. }
  500. this._removeQueued();
  501. }
  502. _addQueued() {
  503. if (this.addQueue.length) {
  504. this.array.splice(this.array.length, 0, ...this.addQueue);
  505. this.addQueue = [];
  506. }
  507. }
  508. _removeQueued() {
  509. if (this.removeQueue.size) {
  510. this.array = this.array.filter(element =&gt; !this.removeQueue.has(element));
  511. this.removeQueue.clear();
  512. }
  513. }
  514. }
  515. </pre>
  516. <p>The class above lets you add or remove elements from the <code class="notranslate" translate="no">SafeArray</code>
  517. but won't mess with the array itself while it's being iterated over. Instead
  518. new elements get added to <code class="notranslate" translate="no">addQueue</code> and removed elements to the <code class="notranslate" translate="no">removeQueue</code>
  519. and then added or removed outside of the loop.</p>
  520. <p>Using that here is our class to manage gameobjects.</p>
  521. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class GameObjectManager {
  522. constructor() {
  523. this.gameObjects = new SafeArray();
  524. }
  525. createGameObject(parent, name) {
  526. const gameObject = new GameObject(parent, name);
  527. this.gameObjects.add(gameObject);
  528. return gameObject;
  529. }
  530. removeGameObject(gameObject) {
  531. this.gameObjects.remove(gameObject);
  532. }
  533. update() {
  534. this.gameObjects.forEach(gameObject =&gt; gameObject.update());
  535. }
  536. }
  537. </pre>
  538. <p>With all that now let's make our first component. This component
  539. will just manage a skinned three.js object like the ones we just created.
  540. To keep it simple it will just have one method, <code class="notranslate" translate="no">setAnimation</code> that
  541. takes the name of the animation to play and plays it.</p>
  542. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class SkinInstance extends Component {
  543. constructor(gameObject, model) {
  544. super(gameObject);
  545. this.model = model;
  546. this.animRoot = SkeletonUtils.clone(this.model.gltf.scene);
  547. this.mixer = new THREE.AnimationMixer(this.animRoot);
  548. gameObject.transform.add(this.animRoot);
  549. this.actions = {};
  550. }
  551. setAnimation(animName) {
  552. const clip = this.model.animations[animName];
  553. // turn off all current actions
  554. for (const action of Object.values(this.actions)) {
  555. action.enabled = false;
  556. }
  557. // get or create existing action for clip
  558. const action = this.mixer.clipAction(clip);
  559. action.enabled = true;
  560. action.reset();
  561. action.play();
  562. this.actions[animName] = action;
  563. }
  564. update() {
  565. this.mixer.update(globals.deltaTime);
  566. }
  567. }
  568. </pre>
  569. <p>You can see it's basically the code we had before that clones the scene we loaded,
  570. then sets up an <a href="/docs/#api/en/animation/AnimationMixer"><code class="notranslate" translate="no">AnimationMixer</code></a>. <code class="notranslate" translate="no">setAnimation</code> adds a <a href="/docs/#api/en/animation/AnimationAction"><code class="notranslate" translate="no">AnimationAction</code></a> for a
  571. particular <a href="/docs/#api/en/animation/AnimationClip"><code class="notranslate" translate="no">AnimationClip</code></a> if one does not already exist and disables all
  572. existing actions.</p>
  573. <p>The code references <code class="notranslate" translate="no">globals.deltaTime</code>. Let's make a globals object</p>
  574. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const globals = {
  575. time: 0,
  576. deltaTime: 0,
  577. };
  578. </pre>
  579. <p>And update it in the render loop</p>
  580. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let then = 0;
  581. function render(now) {
  582. // convert to seconds
  583. globals.time = now * 0.001;
  584. // make sure delta time isn't too big.
  585. globals.deltaTime = Math.min(globals.time - then, 1 / 20);
  586. then = globals.time;
  587. </pre>
  588. <p>The check above for making sure <code class="notranslate" translate="no">deltaTime</code> is not more than 1/20th
  589. of a second is because otherwise we'd get a huge value for <code class="notranslate" translate="no">deltaTime</code>
  590. if we hide the tab. We might hide it for seconds or minutes and then
  591. when our tab was brought to the front <code class="notranslate" translate="no">deltaTime</code> would be huge
  592. and might teleport characters across our game world if we had code like</p>
  593. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">position += velocity * deltaTime;
  594. </pre>
  595. <p>By limiting the maximum <code class="notranslate" translate="no">deltaTime</code> that issue is prevented.</p>
  596. <p>Now let's make a component for the player.</p>
  597. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Player extends Component {
  598. constructor(gameObject) {
  599. super(gameObject);
  600. const model = models.knight;
  601. this.skinInstance = gameObject.addComponent(SkinInstance, model);
  602. this.skinInstance.setAnimation('Run');
  603. }
  604. }
  605. </pre>
  606. <p>The player calls <code class="notranslate" translate="no">setAnimation</code> with <code class="notranslate" translate="no">'Run'</code>. To know which animations
  607. are available I modified our previous example to print out the names of
  608. the animations</p>
  609. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function prepModelsAndAnimations() {
  610. Object.values(models).forEach(model =&gt; {
  611. + console.log('-------&gt;:', model.url);
  612. const animsByName = {};
  613. model.gltf.animations.forEach((clip) =&gt; {
  614. animsByName[clip.name] = clip;
  615. + console.log(' ', clip.name);
  616. });
  617. model.animations = animsByName;
  618. });
  619. }
  620. </pre>
  621. <p>And running it got this list in <a href="https://developers.google.com/web/tools/chrome-devtools/console/javascript">the JavaScript console</a>.</p>
  622. <pre class="prettyprint showlinemods notranslate notranslate" translate="no"> -------&gt;: resources/models/animals/Pig.gltf
  623. Idle
  624. Death
  625. WalkSlow
  626. Jump
  627. Walk
  628. -------&gt;: resources/models/animals/Cow.gltf
  629. Walk
  630. Jump
  631. WalkSlow
  632. Death
  633. Idle
  634. -------&gt;: resources/models/animals/Llama.gltf
  635. Jump
  636. Idle
  637. Walk
  638. Death
  639. WalkSlow
  640. -------&gt;: resources/models/animals/Pug.gltf
  641. Jump
  642. Walk
  643. Idle
  644. WalkSlow
  645. Death
  646. -------&gt;: resources/models/animals/Sheep.gltf
  647. WalkSlow
  648. Death
  649. Jump
  650. Walk
  651. Idle
  652. -------&gt;: resources/models/animals/Zebra.gltf
  653. Jump
  654. Walk
  655. Death
  656. WalkSlow
  657. Idle
  658. -------&gt;: resources/models/animals/Horse.gltf
  659. Jump
  660. WalkSlow
  661. Death
  662. Walk
  663. Idle
  664. -------&gt;: resources/models/knight/KnightCharacter.gltf
  665. Run_swordRight
  666. Run
  667. Idle_swordLeft
  668. Roll_sword
  669. Idle
  670. Run_swordAttack
  671. </pre><p>Fortunately the names of the animations for all the animals match
  672. which will come in handy later. For now we only care the that the
  673. player has an animation called <code class="notranslate" translate="no">Run</code>.</p>
  674. <p>Let's use these components. Here's the updated init function.
  675. All it does is create a <code class="notranslate" translate="no">GameObject</code> and add a <code class="notranslate" translate="no">Player</code> component to it.</p>
  676. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const globals = {
  677. time: 0,
  678. deltaTime: 0,
  679. };
  680. +const gameObjectManager = new GameObjectManager();
  681. function init() {
  682. // hide the loading bar
  683. const loadingElem = document.querySelector('#loading');
  684. loadingElem.style.display = 'none';
  685. prepModelsAndAnimations();
  686. + {
  687. + const gameObject = gameObjectManager.createGameObject(scene, 'player');
  688. + gameObject.addComponent(Player);
  689. + }
  690. }
  691. </pre>
  692. <p>And we need to call <code class="notranslate" translate="no">gameObjectManager.update</code> in our render loop</p>
  693. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let then = 0;
  694. function render(now) {
  695. // convert to seconds
  696. globals.time = now * 0.001;
  697. // make sure delta time isn't too big.
  698. globals.deltaTime = Math.min(globals.time - then, 1 / 20);
  699. then = globals.time;
  700. if (resizeRendererToDisplaySize(renderer)) {
  701. const canvas = renderer.domElement;
  702. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  703. camera.updateProjectionMatrix();
  704. }
  705. - for (const {mixer} of mixerInfos) {
  706. - mixer.update(deltaTime);
  707. - }
  708. + gameObjectManager.update();
  709. renderer.render(scene, camera);
  710. requestAnimationFrame(render);
  711. }
  712. </pre>
  713. <p>and if we run that we get a single player.</p>
  714. <p></p><div translate="no" class="threejs_example_container notranslate">
  715. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/game-just-player.html"></iframe></div>
  716. <a class="threejs_center" href="/manual/examples/game-just-player.html" target="_blank">click here to open in a separate window</a>
  717. </div>
  718. <p></p>
  719. <p>That was a lot of code just for an entity component system but
  720. it's infrastructure that most games need.</p>
  721. <p>Let's add an input system. Rather than read keys directly we'll
  722. make a class that other parts of the code can check <code class="notranslate" translate="no">left</code> or <code class="notranslate" translate="no">right</code>.
  723. That way we can assign multiple ways to input <code class="notranslate" translate="no">left</code> or <code class="notranslate" translate="no">right</code> etc..
  724. We'll start with just keys</p>
  725. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// Keeps the state of keys/buttons
  726. //
  727. // You can check
  728. //
  729. // inputManager.keys.left.down
  730. //
  731. // to see if the left key is currently held down
  732. // and you can check
  733. //
  734. // inputManager.keys.left.justPressed
  735. //
  736. // To see if the left key was pressed this frame
  737. //
  738. // Keys are 'left', 'right', 'a', 'b', 'up', 'down'
  739. class InputManager {
  740. constructor() {
  741. this.keys = {};
  742. const keyMap = new Map();
  743. const setKey = (keyName, pressed) =&gt; {
  744. const keyState = this.keys[keyName];
  745. keyState.justPressed = pressed &amp;&amp; !keyState.down;
  746. keyState.down = pressed;
  747. };
  748. const addKey = (keyCode, name) =&gt; {
  749. this.keys[name] = { down: false, justPressed: false };
  750. keyMap.set(keyCode, name);
  751. };
  752. const setKeyFromKeyCode = (keyCode, pressed) =&gt; {
  753. const keyName = keyMap.get(keyCode);
  754. if (!keyName) {
  755. return;
  756. }
  757. setKey(keyName, pressed);
  758. };
  759. addKey(37, 'left');
  760. addKey(39, 'right');
  761. addKey(38, 'up');
  762. addKey(40, 'down');
  763. addKey(90, 'a');
  764. addKey(88, 'b');
  765. window.addEventListener('keydown', (e) =&gt; {
  766. setKeyFromKeyCode(e.keyCode, true);
  767. });
  768. window.addEventListener('keyup', (e) =&gt; {
  769. setKeyFromKeyCode(e.keyCode, false);
  770. });
  771. }
  772. update() {
  773. for (const keyState of Object.values(this.keys)) {
  774. if (keyState.justPressed) {
  775. keyState.justPressed = false;
  776. }
  777. }
  778. }
  779. }
  780. </pre>
  781. <p>The code above tracks whether keys are up or down and you can check
  782. if a key is currently pressed by checking for example
  783. <code class="notranslate" translate="no">inputManager.keys.left.down</code>. It also has a <code class="notranslate" translate="no">justPressed</code> property
  784. for each key so that you can check the user just pressed the key.
  785. For example a jump key you don't want to know if the button is being
  786. held down, you want to know did the user press it now.</p>
  787. <p>Let's create an instance of <code class="notranslate" translate="no">InputManager</code></p>
  788. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const globals = {
  789. time: 0,
  790. deltaTime: 0,
  791. };
  792. const gameObjectManager = new GameObjectManager();
  793. +const inputManager = new InputManager();
  794. </pre>
  795. <p>and update it in our render loop</p>
  796. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(now) {
  797. ...
  798. gameObjectManager.update();
  799. + inputManager.update();
  800. ...
  801. }
  802. </pre>
  803. <p>It needs to be called after <code class="notranslate" translate="no">gameObjectManager.update</code> otherwise
  804. <code class="notranslate" translate="no">justPressed</code> would never be true inside a component's <code class="notranslate" translate="no">update</code> function.</p>
  805. <p>Let's use it in the <code class="notranslate" translate="no">Player</code> component</p>
  806. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const kForward = new THREE.Vector3(0, 0, 1);
  807. const globals = {
  808. time: 0,
  809. deltaTime: 0,
  810. + moveSpeed: 16,
  811. };
  812. class Player extends Component {
  813. constructor(gameObject) {
  814. super(gameObject);
  815. const model = models.knight;
  816. this.skinInstance = gameObject.addComponent(SkinInstance, model);
  817. this.skinInstance.setAnimation('Run');
  818. + this.turnSpeed = globals.moveSpeed / 4;
  819. }
  820. + update() {
  821. + const {deltaTime, moveSpeed} = globals;
  822. + const {transform} = this.gameObject;
  823. + const delta = (inputManager.keys.left.down ? 1 : 0) +
  824. + (inputManager.keys.right.down ? -1 : 0);
  825. + transform.rotation.y += this.turnSpeed * delta * deltaTime;
  826. + transform.translateOnAxis(kForward, moveSpeed * deltaTime);
  827. + }
  828. }
  829. </pre>
  830. <p>The code above uses <a href="/docs/#api/en/core/Object3D.transformOnAxis"><code class="notranslate" translate="no">Object3D.transformOnAxis</code></a> to move the player
  831. forward. <a href="/docs/#api/en/core/Object3D.transformOnAxis"><code class="notranslate" translate="no">Object3D.transformOnAxis</code></a> works in local space so it only
  832. works if the object in question is at the root of the scene, not if it's
  833. parented to something else <a class="footnote" href="#parented" id="parented-backref">1</a></p>
  834. <p>We also added a global <code class="notranslate" translate="no">moveSpeed</code> and based a <code class="notranslate" translate="no">turnSpeed</code> on the move speed.
  835. The turn speed is based on the move speed to try to make sure a character
  836. can turn sharply enough to meet its target. If <code class="notranslate" translate="no">turnSpeed</code> so too small
  837. a character will turn around and around circling its target but never
  838. hitting it. I didn't bother to do the math to calculate the required
  839. turn speed for a given move speed. I just guessed.</p>
  840. <p>The code so far would work but if the player runs off the screen there's no
  841. way to find out where they are. Let's make it so if they are offscreen
  842. for more than a certain time they get teleported back to the origin.
  843. We can do that by using the three.js <a href="/docs/#api/en/math/Frustum"><code class="notranslate" translate="no">Frustum</code></a> class to check if a point
  844. is inside the camera's view frustum.</p>
  845. <p>We need to build a frustum from the camera. We could do this in the Player
  846. component but other objects might want to use this too so let's add another
  847. gameobject with a component to manage a frustum.</p>
  848. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class CameraInfo extends Component {
  849. constructor(gameObject) {
  850. super(gameObject);
  851. this.projScreenMatrix = new THREE.Matrix4();
  852. this.frustum = new THREE.Frustum();
  853. }
  854. update() {
  855. const {camera} = globals;
  856. this.projScreenMatrix.multiplyMatrices(
  857. camera.projectionMatrix,
  858. camera.matrixWorldInverse);
  859. this.frustum.setFromProjectionMatrix(this.projScreenMatrix);
  860. }
  861. }
  862. </pre>
  863. <p>Then let's setup another gameobject at init time.</p>
  864. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function init() {
  865. // hide the loading bar
  866. const loadingElem = document.querySelector('#loading');
  867. loadingElem.style.display = 'none';
  868. prepModelsAndAnimations();
  869. + {
  870. + const gameObject = gameObjectManager.createGameObject(camera, 'camera');
  871. + globals.cameraInfo = gameObject.addComponent(CameraInfo);
  872. + }
  873. {
  874. const gameObject = gameObjectManager.createGameObject(scene, 'player');
  875. gameObject.addComponent(Player);
  876. }
  877. }
  878. </pre>
  879. <p>and now we can use it in the <code class="notranslate" translate="no">Player</code> component.</p>
  880. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Player extends Component {
  881. constructor(gameObject) {
  882. super(gameObject);
  883. const model = models.knight;
  884. this.skinInstance = gameObject.addComponent(SkinInstance, model);
  885. this.skinInstance.setAnimation('Run');
  886. this.turnSpeed = globals.moveSpeed / 4;
  887. + this.offscreenTimer = 0;
  888. + this.maxTimeOffScreen = 3;
  889. }
  890. update() {
  891. - const {deltaTime, moveSpeed} = globals;
  892. + const {deltaTime, moveSpeed, cameraInfo} = globals;
  893. const {transform} = this.gameObject;
  894. const delta = (inputManager.keys.left.down ? 1 : 0) +
  895. (inputManager.keys.right.down ? -1 : 0);
  896. transform.rotation.y += this.turnSpeed * delta * deltaTime;
  897. transform.translateOnAxis(kForward, moveSpeed * deltaTime);
  898. + const {frustum} = cameraInfo;
  899. + if (frustum.containsPoint(transform.position)) {
  900. + this.offscreenTimer = 0;
  901. + } else {
  902. + this.offscreenTimer += deltaTime;
  903. + if (this.offscreenTimer &gt;= this.maxTimeOffScreen) {
  904. + transform.position.set(0, 0, 0);
  905. + }
  906. + }
  907. }
  908. }
  909. </pre>
  910. <p>One more thing before we try it out, let's add touchscreen support
  911. for mobile. First let's add some HTML to touch</p>
  912. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  913. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  914. + &lt;div id="ui"&gt;
  915. + &lt;div id="left"&gt;&lt;img src="../resources/images/left.svg"&gt;&lt;/div&gt;
  916. + &lt;div style="flex: 0 0 40px;"&gt;&lt;/div&gt;
  917. + &lt;div id="right"&gt;&lt;img src="../resources/images/right.svg"&gt;&lt;/div&gt;
  918. + &lt;/div&gt;
  919. &lt;div id="loading"&gt;
  920. &lt;div&gt;
  921. &lt;div&gt;...loading...&lt;/div&gt;
  922. &lt;div class="progress"&gt;&lt;div id="progressbar"&gt;&lt;/div&gt;&lt;/div&gt;
  923. &lt;/div&gt;
  924. &lt;/div&gt;
  925. &lt;/body&gt;
  926. </pre>
  927. <p>and some CSS to style it</p>
  928. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#ui {
  929. position: absolute;
  930. left: 0;
  931. top: 0;
  932. width: 100%;
  933. height: 100%;
  934. display: flex;
  935. justify-items: center;
  936. align-content: stretch;
  937. }
  938. #ui&gt;div {
  939. display: flex;
  940. align-items: flex-end;
  941. flex: 1 1 auto;
  942. }
  943. .bright {
  944. filter: brightness(2);
  945. }
  946. #left {
  947. justify-content: flex-end;
  948. }
  949. #right {
  950. justify-content: flex-start;
  951. }
  952. #ui img {
  953. padding: 10px;
  954. width: 80px;
  955. height: 80px;
  956. display: block;
  957. }
  958. </pre>
  959. <p>The idea here is there is one div, <code class="notranslate" translate="no">#ui</code>, that
  960. covers the entire page. Inside will be 2 divs, <code class="notranslate" translate="no">#left</code> and <code class="notranslate" translate="no">#right</code>
  961. both of which are almost half the page wide and the entire screen tall.
  962. In between there is a 40px separator. If the user slides their finger
  963. over the left or right side then we need up update <code class="notranslate" translate="no">keys.left</code> and <code class="notranslate" translate="no">keys.right</code>
  964. in the <code class="notranslate" translate="no">InputManager</code>. This makes the entire screen sensitive to being touched
  965. which seemed better than just small arrows.</p>
  966. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class InputManager {
  967. constructor() {
  968. this.keys = {};
  969. const keyMap = new Map();
  970. const setKey = (keyName, pressed) =&gt; {
  971. const keyState = this.keys[keyName];
  972. keyState.justPressed = pressed &amp;&amp; !keyState.down;
  973. keyState.down = pressed;
  974. };
  975. const addKey = (keyCode, name) =&gt; {
  976. this.keys[name] = { down: false, justPressed: false };
  977. keyMap.set(keyCode, name);
  978. };
  979. const setKeyFromKeyCode = (keyCode, pressed) =&gt; {
  980. const keyName = keyMap.get(keyCode);
  981. if (!keyName) {
  982. return;
  983. }
  984. setKey(keyName, pressed);
  985. };
  986. addKey(37, 'left');
  987. addKey(39, 'right');
  988. addKey(38, 'up');
  989. addKey(40, 'down');
  990. addKey(90, 'a');
  991. addKey(88, 'b');
  992. window.addEventListener('keydown', (e) =&gt; {
  993. setKeyFromKeyCode(e.keyCode, true);
  994. });
  995. window.addEventListener('keyup', (e) =&gt; {
  996. setKeyFromKeyCode(e.keyCode, false);
  997. });
  998. + const sides = [
  999. + { elem: document.querySelector('#left'), key: 'left' },
  1000. + { elem: document.querySelector('#right'), key: 'right' },
  1001. + ];
  1002. +
  1003. + const clearKeys = () =&gt; {
  1004. + for (const {key} of sides) {
  1005. + setKey(key, false);
  1006. + }
  1007. + };
  1008. +
  1009. + const handleMouseMove = (e) =&gt; {
  1010. + e.preventDefault();
  1011. + // this is needed because we call preventDefault();
  1012. + // we also gave the canvas a tabindex so it can
  1013. + // become the focus
  1014. + canvas.focus();
  1015. + window.addEventListener('pointermove', handleMouseMove);
  1016. + window.addEventListener('pointerup', handleMouseUp);
  1017. +
  1018. + for (const {elem, key} of sides) {
  1019. + let pressed = false;
  1020. + const rect = elem.getBoundingClientRect();
  1021. + const x = e.clientX;
  1022. + const y = e.clientY;
  1023. + const inRect = x &gt;= rect.left &amp;&amp; x &lt; rect.right &amp;&amp;
  1024. + y &gt;= rect.top &amp;&amp; y &lt; rect.bottom;
  1025. + if (inRect) {
  1026. + pressed = true;
  1027. + }
  1028. + setKey(key, pressed);
  1029. + }
  1030. + };
  1031. +
  1032. + function handleMouseUp() {
  1033. + clearKeys();
  1034. + window.removeEventListener('pointermove', handleMouseMove, {passive: false});
  1035. + window.removeEventListener('pointerup', handleMouseUp);
  1036. + }
  1037. +
  1038. + const uiElem = document.querySelector('#ui');
  1039. + uiElem.addEventListener('pointerdown', handleMouseMove, {passive: false});
  1040. +
  1041. + uiElem.addEventListener('touchstart', (e) =&gt; {
  1042. + // prevent scrolling
  1043. + e.preventDefault();
  1044. + }, {passive: false});
  1045. }
  1046. update() {
  1047. for (const keyState of Object.values(this.keys)) {
  1048. if (keyState.justPressed) {
  1049. keyState.justPressed = false;
  1050. }
  1051. }
  1052. }
  1053. }
  1054. </pre>
  1055. <p>And now we should be able to control the character with the left and right
  1056. cursor keys or with our fingers on a touchscreen</p>
  1057. <p></p><div translate="no" class="threejs_example_container notranslate">
  1058. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/game-player-input.html"></iframe></div>
  1059. <a class="threejs_center" href="/manual/examples/game-player-input.html" target="_blank">click here to open in a separate window</a>
  1060. </div>
  1061. <p></p>
  1062. <p>Ideally we'd do something else if the player went off the screen like move
  1063. the camera or maybe offscreen = death but this article is already going to be
  1064. too long so for now teleporting to the middle was the simplest thing.</p>
  1065. <p>Lets add some animals. We can start it off similar to the <code class="notranslate" translate="no">Player</code> by making
  1066. an <code class="notranslate" translate="no">Animal</code> component.</p>
  1067. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Animal extends Component {
  1068. constructor(gameObject, model) {
  1069. super(gameObject);
  1070. const skinInstance = gameObject.addComponent(SkinInstance, model);
  1071. skinInstance.mixer.timeScale = globals.moveSpeed / 4;
  1072. skinInstance.setAnimation('Idle');
  1073. }
  1074. }
  1075. </pre>
  1076. <p>The code above sets the <a href="/docs/#api/en/animation/AnimationMixer.timeScale"><code class="notranslate" translate="no">AnimationMixer.timeScale</code></a> to set the playback
  1077. speed of the animations relative to the move speed. This way if we
  1078. adjust the move speed the animation will speed up or slow down as well.</p>
  1079. <p>To start we could setup one of each type of animal</p>
  1080. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function init() {
  1081. // hide the loading bar
  1082. const loadingElem = document.querySelector('#loading');
  1083. loadingElem.style.display = 'none';
  1084. prepModelsAndAnimations();
  1085. {
  1086. const gameObject = gameObjectManager.createGameObject(camera, 'camera');
  1087. globals.cameraInfo = gameObject.addComponent(CameraInfo);
  1088. }
  1089. {
  1090. const gameObject = gameObjectManager.createGameObject(scene, 'player');
  1091. globals.player = gameObject.addComponent(Player);
  1092. globals.congaLine = [gameObject];
  1093. }
  1094. + const animalModelNames = [
  1095. + 'pig',
  1096. + 'cow',
  1097. + 'llama',
  1098. + 'pug',
  1099. + 'sheep',
  1100. + 'zebra',
  1101. + 'horse',
  1102. + ];
  1103. + animalModelNames.forEach((name, ndx) =&gt; {
  1104. + const gameObject = gameObjectManager.createGameObject(scene, name);
  1105. + gameObject.addComponent(Animal, models[name]);
  1106. + gameObject.transform.position.x = (ndx + 1) * 5;
  1107. + });
  1108. }
  1109. </pre>
  1110. <p>And that would get us animals standing on the screen but we want them to do
  1111. something.</p>
  1112. <p>Let's make them follow the player in a conga line but only if the player gets near enough.
  1113. To do this we need several states.</p>
  1114. <ul>
  1115. <li><p>Idle:</p>
  1116. <p>Animal is waiting for player to get close</p>
  1117. </li>
  1118. <li><p>Wait for End of Line:</p>
  1119. <p>Animal was tagged by player but now needs to wait for the animal
  1120. at the end of the line to come by so they can join the end of the line.</p>
  1121. </li>
  1122. <li><p>Go to Last:</p>
  1123. <p>Animal needs to walk to where the animal they are following was, at the same time recording
  1124. a history of where the animal they are following is currently.</p>
  1125. </li>
  1126. <li><p>Follow</p>
  1127. <p>Animal needs to keep recording a history of where the animal they are following is while
  1128. moving to where the animal they are following was before.</p>
  1129. </li>
  1130. </ul>
  1131. <p>There are many ways to handle different states like this. A common one is to use
  1132. a <a href="https://www.google.com/search?q=finite+state+machine">Finite State Machine</a> and
  1133. to build some class to help us manage the state.</p>
  1134. <p>So, let's do that.</p>
  1135. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class FiniteStateMachine {
  1136. constructor(states, initialState) {
  1137. this.states = states;
  1138. this.transition(initialState);
  1139. }
  1140. get state() {
  1141. return this.currentState;
  1142. }
  1143. transition(state) {
  1144. const oldState = this.states[this.currentState];
  1145. if (oldState &amp;&amp; oldState.exit) {
  1146. oldState.exit.call(this);
  1147. }
  1148. this.currentState = state;
  1149. const newState = this.states[state];
  1150. if (newState.enter) {
  1151. newState.enter.call(this);
  1152. }
  1153. }
  1154. update() {
  1155. const state = this.states[this.currentState];
  1156. if (state.update) {
  1157. state.update.call(this);
  1158. }
  1159. }
  1160. }
  1161. </pre>
  1162. <p>Here's a simple class. We pass it an object with a bunch of states.
  1163. Each state as 3 optional functions, <code class="notranslate" translate="no">enter</code>, <code class="notranslate" translate="no">update</code>, and <code class="notranslate" translate="no">exit</code>.
  1164. To switch states we call <code class="notranslate" translate="no">FiniteStateMachine.transition</code> and pass it
  1165. the name of the new state. If the current state has an <code class="notranslate" translate="no">exit</code> function
  1166. it's called. Then if the new state has an <code class="notranslate" translate="no">enter</code> function it's called.
  1167. Finally each frame <code class="notranslate" translate="no">FiniteStateMachine.update</code> calls the <code class="notranslate" translate="no">update</code> function
  1168. of the current state.</p>
  1169. <p>Let's use it to manage the states of the animals.</p>
  1170. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// Returns true of obj1 and obj2 are close
  1171. function isClose(obj1, obj1Radius, obj2, obj2Radius) {
  1172. const minDist = obj1Radius + obj2Radius;
  1173. const dist = obj1.position.distanceTo(obj2.position);
  1174. return dist &lt; minDist;
  1175. }
  1176. // keeps v between -min and +min
  1177. function minMagnitude(v, min) {
  1178. return Math.abs(v) &gt; min
  1179. ? min * Math.sign(v)
  1180. : v;
  1181. }
  1182. const aimTowardAndGetDistance = function() {
  1183. const delta = new THREE.Vector3();
  1184. return function aimTowardAndGetDistance(source, targetPos, maxTurn) {
  1185. delta.subVectors(targetPos, source.position);
  1186. // compute the direction we want to be facing
  1187. const targetRot = Math.atan2(delta.x, delta.z) + Math.PI * 1.5;
  1188. // rotate in the shortest direction
  1189. const deltaRot = (targetRot - source.rotation.y + Math.PI * 1.5) % (Math.PI * 2) - Math.PI;
  1190. // make sure we don't turn faster than maxTurn
  1191. const deltaRotation = minMagnitude(deltaRot, maxTurn);
  1192. // keep rotation between 0 and Math.PI * 2
  1193. source.rotation.y = THREE.MathUtils.euclideanModulo(
  1194. source.rotation.y + deltaRotation, Math.PI * 2);
  1195. // return the distance to the target
  1196. return delta.length();
  1197. };
  1198. }();
  1199. class Animal extends Component {
  1200. constructor(gameObject, model) {
  1201. super(gameObject);
  1202. + const hitRadius = model.size / 2;
  1203. const skinInstance = gameObject.addComponent(SkinInstance, model);
  1204. skinInstance.mixer.timeScale = globals.moveSpeed / 4;
  1205. + const transform = gameObject.transform;
  1206. + const playerTransform = globals.player.gameObject.transform;
  1207. + const maxTurnSpeed = Math.PI * (globals.moveSpeed / 4);
  1208. + const targetHistory = [];
  1209. + let targetNdx = 0;
  1210. +
  1211. + function addHistory() {
  1212. + const targetGO = globals.congaLine[targetNdx];
  1213. + const newTargetPos = new THREE.Vector3();
  1214. + newTargetPos.copy(targetGO.transform.position);
  1215. + targetHistory.push(newTargetPos);
  1216. + }
  1217. +
  1218. + this.fsm = new FiniteStateMachine({
  1219. + idle: {
  1220. + enter: () =&gt; {
  1221. + skinInstance.setAnimation('Idle');
  1222. + },
  1223. + update: () =&gt; {
  1224. + // check if player is near
  1225. + if (isClose(transform, hitRadius, playerTransform, globals.playerRadius)) {
  1226. + this.fsm.transition('waitForEnd');
  1227. + }
  1228. + },
  1229. + },
  1230. + waitForEnd: {
  1231. + enter: () =&gt; {
  1232. + skinInstance.setAnimation('Jump');
  1233. + },
  1234. + update: () =&gt; {
  1235. + // get the gameObject at the end of the conga line
  1236. + const lastGO = globals.congaLine[globals.congaLine.length - 1];
  1237. + const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
  1238. + const targetPos = lastGO.transform.position;
  1239. + aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed);
  1240. + // check if last thing in conga line is near
  1241. + if (isClose(transform, hitRadius, lastGO.transform, globals.playerRadius)) {
  1242. + this.fsm.transition('goToLast');
  1243. + }
  1244. + },
  1245. + },
  1246. + goToLast: {
  1247. + enter: () =&gt; {
  1248. + // remember who we're following
  1249. + targetNdx = globals.congaLine.length - 1;
  1250. + // add ourselves to the conga line
  1251. + globals.congaLine.push(gameObject);
  1252. + skinInstance.setAnimation('Walk');
  1253. + },
  1254. + update: () =&gt; {
  1255. + addHistory();
  1256. + // walk to the oldest point in the history
  1257. + const targetPos = targetHistory[0];
  1258. + const maxVelocity = globals.moveSpeed * globals.deltaTime;
  1259. + const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
  1260. + const distance = aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed);
  1261. + const velocity = distance;
  1262. + transform.translateOnAxis(kForward, Math.min(velocity, maxVelocity));
  1263. + if (distance &lt;= maxVelocity) {
  1264. + this.fsm.transition('follow');
  1265. + }
  1266. + },
  1267. + },
  1268. + follow: {
  1269. + update: () =&gt; {
  1270. + addHistory();
  1271. + // remove the oldest history and just put ourselves there.
  1272. + const targetPos = targetHistory.shift();
  1273. + transform.position.copy(targetPos);
  1274. + const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
  1275. + aimTowardAndGetDistance(transform, targetHistory[0], deltaTurnSpeed);
  1276. + },
  1277. + },
  1278. + }, 'idle');
  1279. + }
  1280. + update() {
  1281. + this.fsm.update();
  1282. + }
  1283. }
  1284. </pre>
  1285. <p>That was big chunk of code but it does what was described above.
  1286. Hopefully of you walk through each state it will be clear.</p>
  1287. <p>A few things we need to add. We need the player to add itself
  1288. to the globals so the animals can find it and we need to start the
  1289. conga line with the player's <code class="notranslate" translate="no">GameObject</code>.</p>
  1290. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function init() {
  1291. ...
  1292. {
  1293. const gameObject = gameObjectManager.createGameObject(scene, 'player');
  1294. + globals.player = gameObject.addComponent(Player);
  1295. + globals.congaLine = [gameObject];
  1296. }
  1297. }
  1298. </pre>
  1299. <p>We also need to compute a size for each model</p>
  1300. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function prepModelsAndAnimations() {
  1301. + const box = new THREE.Box3();
  1302. + const size = new THREE.Vector3();
  1303. Object.values(models).forEach(model =&gt; {
  1304. + box.setFromObject(model.gltf.scene);
  1305. + box.getSize(size);
  1306. + model.size = size.length();
  1307. const animsByName = {};
  1308. model.gltf.animations.forEach((clip) =&gt; {
  1309. animsByName[clip.name] = clip;
  1310. // Should really fix this in .blend file
  1311. if (clip.name === 'Walk') {
  1312. clip.duration /= 2;
  1313. }
  1314. });
  1315. model.animations = animsByName;
  1316. });
  1317. }
  1318. </pre>
  1319. <p>And we need the player to record their size</p>
  1320. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Player extends Component {
  1321. constructor(gameObject) {
  1322. super(gameObject);
  1323. const model = models.knight;
  1324. + globals.playerRadius = model.size / 2;
  1325. </pre>
  1326. <p>Thinking about it now it would probably have been smarter
  1327. for the animals to just target the head of the conga line
  1328. instead of the player specifically. Maybe I'll come back
  1329. and change that later.</p>
  1330. <p>When I first started this I used just one radius for all animals
  1331. but of course that was no good as the pug is much smaller than the horse.
  1332. So I added the difference sizes but I wanted to be able to visualize
  1333. things. To do that I made a <code class="notranslate" translate="no">StatusDisplayHelper</code> component.</p>
  1334. <p>I uses a <a href="/docs/#api/en/helpers/PolarGridHelper"><code class="notranslate" translate="no">PolarGridHelper</code></a> to draw a circle around each character
  1335. and it uses html elements to let each character show some status using
  1336. the techniques covered in <a href="align-html-elements-to-3d.html">the article on aligning html elements to 3D</a>.</p>
  1337. <p>First we need to add some HTML to host these elements</p>
  1338. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  1339. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  1340. &lt;div id="ui"&gt;
  1341. &lt;div id="left"&gt;&lt;img src="../resources/images/left.svg"&gt;&lt;/div&gt;
  1342. &lt;div style="flex: 0 0 40px;"&gt;&lt;/div&gt;
  1343. &lt;div id="right"&gt;&lt;img src="../resources/images/right.svg"&gt;&lt;/div&gt;
  1344. &lt;/div&gt;
  1345. &lt;div id="loading"&gt;
  1346. &lt;div&gt;
  1347. &lt;div&gt;...loading...&lt;/div&gt;
  1348. &lt;div class="progress"&gt;&lt;div id="progressbar"&gt;&lt;/div&gt;&lt;/div&gt;
  1349. &lt;/div&gt;
  1350. &lt;/div&gt;
  1351. + &lt;div id="labels"&gt;&lt;/div&gt;
  1352. &lt;/body&gt;
  1353. </pre>
  1354. <p>And add some CSS for them</p>
  1355. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#labels {
  1356. position: absolute; /* let us position ourself inside the container */
  1357. left: 0; /* make our position the top left of the container */
  1358. top: 0;
  1359. color: white;
  1360. width: 100%;
  1361. height: 100%;
  1362. overflow: hidden;
  1363. pointer-events: none;
  1364. }
  1365. #labels&gt;div {
  1366. position: absolute; /* let us position them inside the container */
  1367. left: 0; /* make their default position the top left of the container */
  1368. top: 0;
  1369. font-size: large;
  1370. font-family: monospace;
  1371. user-select: none; /* don't let the text get selected */
  1372. text-shadow: /* create a black outline */
  1373. -1px -1px 0 #000,
  1374. 0 -1px 0 #000,
  1375. 1px -1px 0 #000,
  1376. 1px 0 0 #000,
  1377. 1px 1px 0 #000,
  1378. 0 1px 0 #000,
  1379. -1px 1px 0 #000,
  1380. -1px 0 0 #000;
  1381. }
  1382. </pre>
  1383. <p>Then here's the component</p>
  1384. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const labelContainerElem = document.querySelector('#labels');
  1385. class StateDisplayHelper extends Component {
  1386. constructor(gameObject, size) {
  1387. super(gameObject);
  1388. this.elem = document.createElement('div');
  1389. labelContainerElem.appendChild(this.elem);
  1390. this.pos = new THREE.Vector3();
  1391. this.helper = new THREE.PolarGridHelper(size / 2, 1, 1, 16);
  1392. gameObject.transform.add(this.helper);
  1393. }
  1394. setState(s) {
  1395. this.elem.textContent = s;
  1396. }
  1397. setColor(cssColor) {
  1398. this.elem.style.color = cssColor;
  1399. this.helper.material.color.set(cssColor);
  1400. }
  1401. update() {
  1402. const {pos} = this;
  1403. const {transform} = this.gameObject;
  1404. const {canvas} = globals;
  1405. pos.copy(transform.position);
  1406. // get the normalized screen coordinate of that position
  1407. // x and y will be in the -1 to +1 range with x = -1 being
  1408. // on the left and y = -1 being on the bottom
  1409. pos.project(globals.camera);
  1410. // convert the normalized position to CSS coordinates
  1411. const x = (pos.x * .5 + .5) * canvas.clientWidth;
  1412. const y = (pos.y * -.5 + .5) * canvas.clientHeight;
  1413. // move the elem to that position
  1414. this.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
  1415. }
  1416. }
  1417. </pre>
  1418. <p>And we can then add them to the animals like this</p>
  1419. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Animal extends Component {
  1420. constructor(gameObject, model) {
  1421. super(gameObject);
  1422. + this.helper = gameObject.addComponent(StateDisplayHelper, model.size);
  1423. ...
  1424. }
  1425. update() {
  1426. this.fsm.update();
  1427. + const dir = THREE.MathUtils.radToDeg(this.gameObject.transform.rotation.y);
  1428. + this.helper.setState(`${this.fsm.state}:${dir.toFixed(0)}`);
  1429. }
  1430. }
  1431. </pre>
  1432. <p>While we're at it lets make it so we can turn them on/off using lil-gui like
  1433. we've used else where</p>
  1434. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from '/build/three.module.js';
  1435. import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';
  1436. import {GLTFLoader} from '/examples/jsm/loaders/GLTFLoader.js';
  1437. import * as SkeletonUtils from '/examples/jsm/utils/SkeletonUtils.js';
  1438. +import {GUI} from '/examples/jsm/libs/lil-gui.module.min.js';
  1439. </pre>
  1440. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const gui = new GUI();
  1441. +gui.add(globals, 'debug').onChange(showHideDebugInfo);
  1442. +showHideDebugInfo();
  1443. const labelContainerElem = document.querySelector('#labels');
  1444. +function showHideDebugInfo() {
  1445. + labelContainerElem.style.display = globals.debug ? '' : 'none';
  1446. +}
  1447. +showHideDebugInfo();
  1448. class StateDisplayHelper extends Component {
  1449. ...
  1450. update() {
  1451. + this.helper.visible = globals.debug;
  1452. + if (!globals.debug) {
  1453. + return;
  1454. + }
  1455. ...
  1456. }
  1457. }
  1458. </pre>
  1459. <p>And with that we get the kind of start of a game</p>
  1460. <p></p><div translate="no" class="threejs_example_container notranslate">
  1461. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/game-conga-line.html"></iframe></div>
  1462. <a class="threejs_center" href="/manual/examples/game-conga-line.html" target="_blank">click here to open in a separate window</a>
  1463. </div>
  1464. <p></p>
  1465. <p>Originally I set out to make a <a href="https://www.google.com/search?q=snake+game">snake game</a>
  1466. where as you add animals to your line it gets harder because you need to avoid
  1467. crashing into them. I'd also have put some obstacles in the scene and maybe a fence or some
  1468. barrier around the perimeter.</p>
  1469. <p>Unfortunately the animals are long and thin. From above here's the zebra.</p>
  1470. <div class="threejs_center"><img src="../resources/images/zebra.png" style="width: 113px;"></div>
  1471. <p>The code so far is using circle collisions which means if we had obstacles like a fence
  1472. then this would be considered a collision</p>
  1473. <div class="threejs_center"><img src="../resources/images/zebra-collisions.svg" style="width: 400px;"></div>
  1474. <p>That's no good. Even animal to animal we'd have the same issue</p>
  1475. <p>I thought about writing a 2D rectangle to rectangle collision system but I
  1476. quickly realized it could really be a lot of code. Checking that 2 arbitrarily
  1477. oriented boxes overlap is not too much code and for our game with just a few
  1478. objects it might work but looking into it after a few objects you quickly start
  1479. needing to optimize the collision checking. First you might go through all
  1480. objects that can possibly collide with each other and check their bounding
  1481. spheres or bounding circles or their axially aligned bounding boxes. Once you
  1482. know which objects <em>might</em> be colliding then you need to do more work to check if
  1483. they are <em>actually</em> colliding. Often even checking the bounding spheres is too
  1484. much work and you need some kind of better spacial structure for the objects so
  1485. you can more quickly only check objects possibly near each other.</p>
  1486. <p>Then, once you write the code to check if 2 objects collide you generally want
  1487. to make a collision system rather than manually asking "do I collide with these
  1488. objects". A collision system emits events or calls callbacks in relation to
  1489. things colliding. The advantage is it can check all the collisions at once so no
  1490. objects get checked more than once where as if you manually call some "am I
  1491. colliding" function often objects will be checked more than once wasting time.</p>
  1492. <p>Making that collision system would probably not be more than 100-300 lines of
  1493. code for just checking arbitrarily oriented rectangles but it's still a ton more
  1494. code so it seemed best to leave it out.</p>
  1495. <p>Another solution would have been to try to find other characters that are
  1496. mostly circular from the top. Other humanoid characters for example instead
  1497. of animals in which case the circle checking might work animal to animal.
  1498. It would not work animal to fence, well we'd have to add circle to rectangle
  1499. checking. I thought about making the fence a fence of bushes or poles, something
  1500. circular but then I'd need probably 120 to 200 of them to surround the play area
  1501. which would run into the optimization issues mentioned above.</p>
  1502. <p>These are reasons many games use an existing solution. Often these solutions
  1503. are part of a physics library. The physical library needs to know if objects
  1504. collide with each other so on top of providing physics they can also be used
  1505. to detect collision.</p>
  1506. <p>If you're looking for a solution some of the three.js examples use
  1507. <a href="https://github.com/kripken/ammo.js/">ammo.js</a> so that might be one.</p>
  1508. <p>One other solution might have been to place the obstacles on a grid
  1509. and try to make it so each animal and the player just need to look at
  1510. the grid. While that would be performant I felt that's best left as an exercise
  1511. for the reader 😜</p>
  1512. <p>One more thing, many game systems have something called <a href="https://www.google.com/search?q=coroutines"><em>coroutines</em></a>.
  1513. Coroutines are routines that can pause while running and continue later.</p>
  1514. <p>Let's make the main character emit musical notes like they are leading
  1515. the line by singing. There are many ways we could implement this but for now
  1516. let's do it using coroutines.</p>
  1517. <p>First, here's a class to manage coroutines</p>
  1518. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function* waitSeconds(duration) {
  1519. while (duration &gt; 0) {
  1520. duration -= globals.deltaTime;
  1521. yield;
  1522. }
  1523. }
  1524. class CoroutineRunner {
  1525. constructor() {
  1526. this.generatorStacks = [];
  1527. this.addQueue = [];
  1528. this.removeQueue = new Set();
  1529. }
  1530. isBusy() {
  1531. return this.addQueue.length + this.generatorStacks.length &gt; 0;
  1532. }
  1533. add(generator, delay = 0) {
  1534. const genStack = [generator];
  1535. if (delay) {
  1536. genStack.push(waitSeconds(delay));
  1537. }
  1538. this.addQueue.push(genStack);
  1539. }
  1540. remove(generator) {
  1541. this.removeQueue.add(generator);
  1542. }
  1543. update() {
  1544. this._addQueued();
  1545. this._removeQueued();
  1546. for (const genStack of this.generatorStacks) {
  1547. const main = genStack[0];
  1548. // Handle if one coroutine removes another
  1549. if (this.removeQueue.has(main)) {
  1550. continue;
  1551. }
  1552. while (genStack.length) {
  1553. const topGen = genStack[genStack.length - 1];
  1554. const {value, done} = topGen.next();
  1555. if (done) {
  1556. if (genStack.length === 1) {
  1557. this.removeQueue.add(topGen);
  1558. break;
  1559. }
  1560. genStack.pop();
  1561. } else if (value) {
  1562. genStack.push(value);
  1563. } else {
  1564. break;
  1565. }
  1566. }
  1567. }
  1568. this._removeQueued();
  1569. }
  1570. _addQueued() {
  1571. if (this.addQueue.length) {
  1572. this.generatorStacks.splice(this.generatorStacks.length, 0, ...this.addQueue);
  1573. this.addQueue = [];
  1574. }
  1575. }
  1576. _removeQueued() {
  1577. if (this.removeQueue.size) {
  1578. this.generatorStacks = this.generatorStacks.filter(genStack =&gt; !this.removeQueue.has(genStack[0]));
  1579. this.removeQueue.clear();
  1580. }
  1581. }
  1582. }
  1583. </pre>
  1584. <p>It does things similar to <code class="notranslate" translate="no">SafeArray</code> to make sure that it's safe to add or remove
  1585. coroutines while other coroutines are running. It also handles nested coroutines.</p>
  1586. <p>To make a coroutine you make a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*">JavaScript generator function</a>.
  1587. A generator function is preceded by the keyword <code class="notranslate" translate="no">function*</code> (the asterisk is important!)</p>
  1588. <p>Generator functions can <code class="notranslate" translate="no">yield</code>. For example</p>
  1589. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function* countOTo9() {
  1590. for (let i = 0; i &lt; 10; ++i) {
  1591. console.log(i);
  1592. yield;
  1593. }
  1594. }
  1595. </pre>
  1596. <p>If we added this function to the <code class="notranslate" translate="no">CoroutineRunner</code> above it would print
  1597. out each number, 0 to 9, once per frame or rather once per time we called <code class="notranslate" translate="no">runner.update</code>.</p>
  1598. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const runner = new CoroutineRunner();
  1599. runner.add(count0To9);
  1600. while(runner.isBusy()) {
  1601. runner.update();
  1602. }
  1603. </pre>
  1604. <p>Coroutines are removed automatically when they are finished.
  1605. To remove a coroutine early, before it reaches the end you need to keep
  1606. a reference to its generator like this</p>
  1607. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gen = count0To9();
  1608. runner.add(gen);
  1609. // sometime later
  1610. runner.remove(gen);
  1611. </pre>
  1612. <p>In any case, in the player let's use a coroutine to emit a note every half second to 1 second</p>
  1613. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Player extends Component {
  1614. constructor(gameObject) {
  1615. ...
  1616. + this.runner = new CoroutineRunner();
  1617. +
  1618. + function* emitNotes() {
  1619. + for (;;) {
  1620. + yield waitSeconds(rand(0.5, 1));
  1621. + const noteGO = gameObjectManager.createGameObject(scene, 'note');
  1622. + noteGO.transform.position.copy(gameObject.transform.position);
  1623. + noteGO.transform.position.y += 5;
  1624. + noteGO.addComponent(Note);
  1625. + }
  1626. + }
  1627. +
  1628. + this.runner.add(emitNotes());
  1629. }
  1630. update() {
  1631. + this.runner.update();
  1632. ...
  1633. }
  1634. }
  1635. function rand(min, max) {
  1636. if (max === undefined) {
  1637. max = min;
  1638. min = 0;
  1639. }
  1640. return Math.random() * (max - min) + min;
  1641. }
  1642. </pre>
  1643. <p>You can see we make a <code class="notranslate" translate="no">CoroutineRunner</code> and we add an <code class="notranslate" translate="no">emitNotes</code> coroutine.
  1644. That function will run forever, waiting 0.5 to 1 seconds and then creating a game object
  1645. with a <code class="notranslate" translate="no">Note</code> component.</p>
  1646. <p>For the <code class="notranslate" translate="no">Note</code> component first lets make a texture with a note on it and
  1647. instead of loading a note image let's make one using a canvas like we covered in <a href="canvas-textures.html">the article on canvas textures</a>.</p>
  1648. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeTextTexture(str) {
  1649. const ctx = document.createElement('canvas').getContext('2d');
  1650. ctx.canvas.width = 64;
  1651. ctx.canvas.height = 64;
  1652. ctx.font = '60px sans-serif';
  1653. ctx.textAlign = 'center';
  1654. ctx.textBaseline = 'middle';
  1655. ctx.fillStyle = '#FFF';
  1656. ctx.fillText(str, ctx.canvas.width / 2, ctx.canvas.height / 2);
  1657. return new THREE.CanvasTexture(ctx.canvas);
  1658. }
  1659. const noteTexture = makeTextTexture('♪');
  1660. </pre>
  1661. <p>The texture we create above is white each means when we use it
  1662. we can set the material's color and get a note of any color.</p>
  1663. <p>Now that we have a noteTexture here's the <code class="notranslate" translate="no">Note</code> component.
  1664. It uses <a href="/docs/#api/en/materials/SpriteMaterial"><code class="notranslate" translate="no">SpriteMaterial</code></a> and a <a href="/docs/#api/en/objects/Sprite"><code class="notranslate" translate="no">Sprite</code></a> like we covered in
  1665. <a href="billboards.html">the article on billboards</a> </p>
  1666. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Note extends Component {
  1667. constructor(gameObject) {
  1668. super(gameObject);
  1669. const {transform} = gameObject;
  1670. const noteMaterial = new THREE.SpriteMaterial({
  1671. color: new THREE.Color().setHSL(rand(1), 1, 0.5),
  1672. map: noteTexture,
  1673. side: THREE.DoubleSide,
  1674. transparent: true,
  1675. });
  1676. const note = new THREE.Sprite(noteMaterial);
  1677. note.scale.setScalar(3);
  1678. transform.add(note);
  1679. this.runner = new CoroutineRunner();
  1680. const direction = new THREE.Vector3(rand(-0.2, 0.2), 1, rand(-0.2, 0.2));
  1681. function* moveAndRemove() {
  1682. for (let i = 0; i &lt; 60; ++i) {
  1683. transform.translateOnAxis(direction, globals.deltaTime * 10);
  1684. noteMaterial.opacity = 1 - (i / 60);
  1685. yield;
  1686. }
  1687. transform.parent.remove(transform);
  1688. gameObjectManager.removeGameObject(gameObject);
  1689. }
  1690. this.runner.add(moveAndRemove());
  1691. }
  1692. update() {
  1693. this.runner.update();
  1694. }
  1695. }
  1696. </pre>
  1697. <p>All it does is setup a <a href="/docs/#api/en/objects/Sprite"><code class="notranslate" translate="no">Sprite</code></a>, then pick a random velocity and move
  1698. the transform at that velocity for 60 frames while fading out the note
  1699. by setting the material's <a href="/docs/#api/en/materials/Material#opacity"><code class="notranslate" translate="no">opacity</code></a>.
  1700. After the loop it the removes the transform
  1701. from the scene and the note itself from active gameobjects.</p>
  1702. <p>One last thing, let's add a few more animals</p>
  1703. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function init() {
  1704. ...
  1705. const animalModelNames = [
  1706. 'pig',
  1707. 'cow',
  1708. 'llama',
  1709. 'pug',
  1710. 'sheep',
  1711. 'zebra',
  1712. 'horse',
  1713. ];
  1714. + const base = new THREE.Object3D();
  1715. + const offset = new THREE.Object3D();
  1716. + base.add(offset);
  1717. +
  1718. + // position animals in a spiral.
  1719. + const numAnimals = 28;
  1720. + const arc = 10;
  1721. + const b = 10 / (2 * Math.PI);
  1722. + let r = 10;
  1723. + let phi = r / b;
  1724. + for (let i = 0; i &lt; numAnimals; ++i) {
  1725. + const name = animalModelNames[rand(animalModelNames.length) | 0];
  1726. const gameObject = gameObjectManager.createGameObject(scene, name);
  1727. gameObject.addComponent(Animal, models[name]);
  1728. + base.rotation.y = phi;
  1729. + offset.position.x = r;
  1730. + offset.updateWorldMatrix(true, false);
  1731. + offset.getWorldPosition(gameObject.transform.position);
  1732. + phi += arc / r;
  1733. + r = b * phi;
  1734. }
  1735. </pre>
  1736. <p></p><div translate="no" class="threejs_example_container notranslate">
  1737. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/game-conga-line-w-notes.html"></iframe></div>
  1738. <a class="threejs_center" href="/manual/examples/game-conga-line-w-notes.html" target="_blank">click here to open in a separate window</a>
  1739. </div>
  1740. <p></p>
  1741. <p>You might be asking, why not use <code class="notranslate" translate="no">setTimeout</code>? The problem with <code class="notranslate" translate="no">setTimeout</code>
  1742. is it's not related to the game clock. For example above we made the maximum
  1743. amount of time allowed to elapse between frames to be 1/20th of a second.
  1744. Our coroutine system will respect that limit but <code class="notranslate" translate="no">setTimeout</code> would not.</p>
  1745. <p>Of course we could have made a simple timer ourselves</p>
  1746. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class Player ... {
  1747. update() {
  1748. this.noteTimer -= globals.deltaTime;
  1749. if (this.noteTimer &lt;= 0) {
  1750. // reset timer
  1751. this.noteTimer = rand(0.5, 1);
  1752. // create a gameobject with a note component
  1753. }
  1754. }
  1755. </pre>
  1756. <p>And for this particular case that might have been better but as you add
  1757. more and things you'll get more and more variables added to your classes
  1758. where as with coroutines you can often just <em>fire and forget</em>.</p>
  1759. <p>Given our animal's simple states we could also have implemented them
  1760. with a coroutine in the form of</p>
  1761. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// pseudo code!
  1762. function* animalCoroutine() {
  1763. setAnimation('Idle');
  1764. while(playerIsTooFar()) {
  1765. yield;
  1766. }
  1767. const target = endOfLine;
  1768. setAnimation('Jump');
  1769. while(targetIsTooFar()) {
  1770. aimAt(target);
  1771. yield;
  1772. }
  1773. setAnimation('Walk')
  1774. while(notAtOldestPositionOfTarget()) {
  1775. addHistory();
  1776. aimAt(target);
  1777. yield;
  1778. }
  1779. for(;;) {
  1780. addHistory();
  1781. const pos = history.unshift();
  1782. transform.position.copy(pos);
  1783. aimAt(history[0]);
  1784. yield;
  1785. }
  1786. }
  1787. </pre>
  1788. <p>This would have worked but of course as soon as our states were not so linear
  1789. we'd have had to switch to a <code class="notranslate" translate="no">FiniteStateMachine</code>.</p>
  1790. <p>It also wasn't clear to me if coroutines should run independently of their
  1791. components. We could have made a global <code class="notranslate" translate="no">CoroutineRunner</code> and put all
  1792. coroutines on it. That would make cleaning them up harder. As it is now
  1793. if the gameobject is removed all of its components are removed and
  1794. therefore the coroutine runners created are no longer called and it will
  1795. all get garbage collected. If we had global runner then it would be
  1796. the responsibility of each component to remove any coroutines it added
  1797. or else some other mechanism of registering coroutines with a particular
  1798. component or gameobject would be needed so that removing one removes the
  1799. others.</p>
  1800. <p>There are lots more issues a
  1801. normal game engine would deal with. As it is there is no order to how
  1802. gameobjects or their components are run. They are just run in the order added.
  1803. Many game systems add a priority so the order can be set or changed.</p>
  1804. <p>Another issue we ran into is the <code class="notranslate" translate="no">Note</code> removing its gameobject's transform from the scene.
  1805. That seems like something that should happen in <code class="notranslate" translate="no">GameObject</code> since it was <code class="notranslate" translate="no">GameObject</code>
  1806. that added the transform in the first place. Maybe <code class="notranslate" translate="no">GameObject</code> should have
  1807. a <code class="notranslate" translate="no">dispose</code> method that is called by <code class="notranslate" translate="no">GameObjectManager.removeGameObject</code>?</p>
  1808. <p>Yet another is how we're manually calling <code class="notranslate" translate="no">gameObjectManager.update</code> and <code class="notranslate" translate="no">inputManager.update</code>.
  1809. Maybe there should be a <code class="notranslate" translate="no">SystemManager</code> which these global services can add themselves
  1810. and each service will have its <code class="notranslate" translate="no">update</code> function called. In this way if we added a new
  1811. service like <code class="notranslate" translate="no">CollisionManager</code> we could just add it to the system manager and not
  1812. have to edit the render loop.</p>
  1813. <p>I'll leave those kinds of issues up to you.
  1814. I hope this article has given you some ideas for your own game engine.</p>
  1815. <p>Maybe I should promote a game jam. If you click the <em>jsfiddle</em> or <em>codepen</em> buttons
  1816. above the last example they'll open in those sites ready to edit. Add some features,
  1817. Change the game to a pug leading a bunch of knights. Use the knight's rolling animation
  1818. as a bowling ball and make an animal bowling game. Make an animal relay race.
  1819. If you make a cool game post a link in the comments below.</p>
  1820. <div class="footnotes">
  1821. [<a id="parented">1</a>]: technically it would still work if none of the parents have any translation, rotation, or scale <a href="#parented-backref">§</a>.
  1822. </div>
  1823. </div>
  1824. </div>
  1825. </div>
  1826. <script src="/manual/resources/prettify.js"></script>
  1827. <script src="/manual/resources/lesson.js"></script>
  1828. </body></html>