I am no longer working on COLT and currently contemplating what to do next. I have strong flash background, but this technology does not feel well. With WebGL technology exploding, on the other hand, I’ve decided to do old-school first-person shooter to see if this kind of work is something I want to do from now on, and get better idea of all the challenges involved.
The result is this sort of game where you pointlessly run around the platform and shoot monsters. Originally I thought I would complete it in 2-3 days, Ludum Dare style, but since I had no enough motivation to spend all my time on this, it took me whole month of random short coding sessions to “complete” it (and there are still some bugs to fix).
Another thing I hoped for was to write “how to make complete game with three.js” tutorial that all the internet would link to, as apparently there are no tutorials like that. It is easy to write “how to move the box” tutorial, but something more takes time that noone is really willing to spend. Well, the bad news are that I still can’t write the tutorial. I would need to make 10 more games like this before I can write one :) So the rest of this post is simply going to be adventure log, noting some tricks I used and some mistakes I made along the way.
The adventure log
Since it all sits in git repository, you can check out the game in any unfinished state you want and play around, implementing things differently and/or on your own from there, just like you would do with real tutorial. You will find necessary commands in the post, and if you use windows I suggest Tortoise GUI. All right, let’s go.
Any game, even if it is not real game, needs very real assets. In this case, I went for free-to-use assets available from post-quake era when many people were making open-sourced quake clones. I have found my old sandy game, extracted MD2 models and converted them with @oosmoxiecode‘s MD2 to JSON convertor. These included Dreadus shotgun, Edmundo Bordeu’s imp, jDoom shotgun and shells items. There were also some sounds and the skybox – enough to get it started.
The question now was, how do you load all these assets in the game? Looking at the way they load stuff in three.js examples, it was clear I needed something else to do it. I asked around, and the obvious but overlooked answer was jQuery. Seriously, it is really simple to wait for multiple files loading in parallel with jQuery. Except you still need to add extra code to load images. And sounds, too. And since we’re now in HTML5 la la land, there is no way to know when the audio has fully loaded (only when it is ready to play).
Once assets are there, 3D scene is created and inserted into the document (jQuery again) along with the skybox and the platform. The stage is set.
git clone email@example.com:makc/fps-three.js.git cd fps-three.js git checkout 258e7ba40f
ECS, basic physics, keyboard controls
Next, I had to decide game architecture and, after some reading, I have decided to try Entity-Component-System pattern. In this architecture, you have a bunch of no-op objects (called “entities”) that do nothing but hold various data bits (called “components”), and a bunch of ideally independent code that loops over entities to do various game stuff (called “systems”). Look it up for TL;DR. This pattern main promise is to make the game loop very simple – you just need to call every “system” once there.
The second thing to decide was what kind of physics engine to use. After careful consideration, however, I decided not to use physics engine :) Three.js comes with Raycaster object, which has everything to do the job – I can cast the ray downwards, check for intersections with the platform and modify object velocity accordingly. That is, either zero vertical velocity component (on the platform surface) to keep the object from falling through the platform, or increment it (in the air) to simulate gravity.
The key problem with self-coded physics was how to make it work with varying time step. Experiment after experiment, it became clear that simple calculations method I used is just so inherently shitty that it’s impossible. To work around it, I had to split the varying step into several short fixed steps and propagate the error to the next frame. For example, 19 and 21 ms frames would instead run 3 and 5 steps fixed to 5 ms correspondingly.
To sum up, in this commit I have single entity (hero prototype) with Motion component that holds its state of motion and Body component that has the reference to corresponding 3D object (in hero case it is the camera). Four systems are acting on this entity: keyboard controls system that alters Motion component of hero entity to move it around, physics system that alters Motion component again as described above, rendering system that places 3D objects according to Motion component data so that the scene would render correctly, and hero reset system that brings him back onto the platform when he falls off the edge. In other words, this commit is absolute minimum three.js platformer implementation.
Around the same time I have noticed that KeyboardEvent constants are available only in FF, so this commit was also my first bugfix.
git checkout c5bab0b915
There are three special areas on the platform that I wanted to fly the player across the level to the opposite side. Thanks to the work already done in previous commit, this was as easy as adding another system that manipulates player velocity in his Motion component.
git checkout 266053c03d
Again, adding another feature – the sound of hero footsteps – was as simple as creating 15 lines system that tracks the distance our player have walked. It almost looked like this ECS thing was good idea :)
git checkout e18fe6aee9
git checkout 2279951c08
It was time to place good stuff around. This time I have added two systems – one to decide when and what kind of item to spawn, another to decide where to place it on the platform. The idea was that I might be able to use this second system to spawn monsters later.
Note that all the items reuse same systems that were used with the player entity. The only modification I have made to physics was to move damping to Motion component to allow shotgun item to spin around indefinitely before it is picked up.
git checkout 217d613689
At this point main game script was approaching 450 lines and it became kind of hard to navigate in it. It needed to be somehow cut it in smaller parts. In retrospect, I just could have moved every system in its own file and include them all in html head tag, but for some reason I did it with require.js :S This resulted in having to explicitly specify code dependencies for no good reason from now on.
The problem I had there was that now, when all the systems were moved out of the main game closure, they could no longer access its variables (such as assets). To work around that, I have labeled some common code as “game” module and saved asset references there. Dirty solution that underlines how misguided my attempt to use require.js was.
git checkout 8209c574b3
Glowing plates under items, light pillars over glowing plates
Small items were hard to spot from the distance, so I came up with very original idea: every item should appear on top of blue glowing pentagram, and then some sort of light pillar marking the place. This innocent idea gave birth to the shittiest code in the game later on, because I have immediately jumped to implementation without thinking it through. Lesson to learn is that no amount of cool patterns like ECS will do thinking instead of you.
git checkout 70a987bfa4
Picking items up
The game has finally outgrown bare bones ECS implementation. I had to add the way to remove the entity. Also, since half of systems were only interested in player entity, I have added the way to terminate the loop over entities.
I found myself fighting with the glowing stuff again. Moving it from the scene level to item 3D object made it easy to remove when the item was picked up, but the pentagrams under shotguns were now spinning and I had to practice quaternion voodoo to counter the effect.
git checkout a56a936203
Can I haz shotgun?
Yes, I can :) Since I actually had the shotgun in there since first commit, it was a matter of 20 lines of code to show it when shotgun item was picked, and hide again when the player was reset.
git checkout 7f8bf788c1
Can I fire shotgun?
Suddenly things went difficult again. First, I have discovered that MorphAnimMesh animation API sucks. Not really surprising in the age of rigged models. So to play that firing animation I had to write really low level code abusing both three.js and jQuery. Second, this shotgun logic was now all over the code, so I tried to pull it into the single system, which means boolean hasShotgun flag was now promoted to Shotgun component with its own data. At the same time, two systems now had this new system as explicit dependency. Red flag, right? My reading of this is that the shotgun should have been separate module used by all three systems, but there was no practical reason to do it, so I have left it like that.
I have also found lots of very similar (if not exactly the same) frames in Dreadus shotgun animation, and removed them. Remember, kids, free art is worth what you paid for it.
git checkout dab0852889
Can I haz monsters?
Turns out that dual item creation scheme introduced in 217d613689 was really helpful, and I could leverage the same glowing stuff for monsters too, having them spawning over larger orange pentagrams. Also, since my monsters were just standing there doing nothing, I could actually use three.js animation API to animate them.
git checkout 60ab3379e3
Can I fire shotgun at monsters?
It was time for monsters to start dying when you fire at them. To that end shotgun system was now creating the entity with Shot component that was immediately consumed by another system that dealt damage to the monsters.
This part was actually very challenging to do. I did not want to simulate pellets, so I had to come up with some formula to describe the damage inflicted by the shotgun given its relative position and direction. After some trial an error I had this:
git checkout a9c69c12fa
Now it was time to animate monsters, and it was clear that jQuery hack I used with the shotgun does not scale. So I have asked myself, what kind of animation API would solve my problems in both cases? The shotgun had to only play its animation once, but monsters are a bit more complex: they had to play attack, pain and death animations once, but loop idle and walk animation. My solution was introducing “default” animation that AnimatedMD2Model would switch to after playing any animation once. This way, if you have idle set as default animation, and request it to play attack animation once, it will play attack once, then it will play idle once, then it will play idle once more, and so on.
git checkout 3aba72d54e
Introducing the map
All these commits, and my monsters were still just standing there. There were two problems to solve before I could make monsters move. First, and obvious one, was how do I keep them from falling off the platform? I could use three.js raycaster again to probe the platform before moving the monster, but it felt like asking for problems in the corners, and unnecessary performance hit. So instead I have decided to connect all respawn points into one graph and make monsters to walk along the edges. As a side effect of this change, I no longer had items spawning in the same location:
git checkout 39c8d2b8b8
Revisiting glowing plates effect
As I said, this effect came to be the shittiest code in the whole game. Once again it was in the way: if I were to move the monsters, this glowing stuff would follow them, because it was nested under monster models in the scenegraph. Obviously it was not what I wanted, so I had to redo this code once again. This time I gave it separate entities that were processed by physics system to sit tight on the platform and responded to their own addition/removal requests.
git checkout fe3d0e4bb2
Optimising physics loop, monster cloning
With glowing stuff being processed by physics system it became clear that the code is inefficient. Chrome profiler complained that V8 could not optimize the closure that ECS module was calling over and over during multiple fixed step iterations per frame. But I did not really need to iterate over entities, just their Motion components – and once I did that, Chrome profiler was happy.
While at premature optimisation, I thought why not fix AnimatedMD2Model clone() method too, so that monsters would spawn faster.
git checkout dd76138696
With every obstacle out of the way, I could finally implement the system to move monsters around and turn to the player when standing idle. It did not look right, though. Monsters would not turn to the player and keep walking when shot in the middle of the relocation. I guess I was too tired to fix the second problem, but I did address the first one by introducing UnderFire component and ignoring current destination for entities with this component in the system. Hacky.
git checkout 9e31f65968
Let monsters fight back
Watching me testing the game, my kid demanded that I stop killing monsters because they did no harm to me. It was clear that it’s time for them to harm the player. So I have spent ridiculous time prototyping plasma ball, and made them throw it at me at random times when they were not busy walking. This change made monster movement system a bit god-like, so I have changed its name to “control system” in acknowledgement :)
git checkout 71921112d7
So far it was not obvious how many shells the player had, or how much hit did he take. The game needed HUD. Three.js issue tracker has several issues debating the best way to create HUD, and HTML overlay seems to win in every debate. So I have followed the advice, overlaying the canvas with simple HTML HUD and tried to avoid DOM changes whenever possible. As a side effect, I have noticed and fixed the bug where the player could not fire his last shell.
By the way, DooM font I used has this awesome license:
FREEWARE:only for personal use [e.g: webpages],not for comercial use!! I MEAN THAT!!
I think I’m good here. Any lawers?
git checkout 529cd36f93
I have been waiting long time to perform this obvious optimisation :) In my case, raycasting does not really need to check anything but the top side of the platform. However, it operates on the entire mesh and there is no way around it. So, to make it work the way I wanted to, I had to create the mesh around semi-valid “filtered” top-side-only geometry and raycast that mesh. Results, according to Firefox profiler:
almost 35% before, and only 6% after
git checkout 6bd42b2bba