With the concept and design all decided on, it’s time to set up the structure of the project. The assignment has some requirements regarding this, namely that the HTML, CSS, and JS all be in their own dedicated files and that we have a README to explain the project. That gives the basic structure like this:
.
├── index.html
├── README.md
├── css
│ └── style.css
├── img
│ └── ...
├── js
│ └── main.js
└── .gitignore
However, there’s some extra stuff I want to add, such as a “/docs” folder for any extra information, such as the pseudocode. I also want to try working with JavaScript modules, to make everything more, well, modular.
For now, having built out the basic structure, I’m just going to throw the tags
into the <head>
of “index.html”:
<link rel="stylesheet" href="/css/main.css"/>
<script type="text/javascript" src="/js/main.js"></script>
The basics of JavaScript modules are pretty simple:
// in modules/module.js
export {something, somethingElse};
// in main.js
import {something, somethingElse} from "./modules/module.js";
Which I’ll set up for the three modules I intend to write: Physics.js, DOMRenderer.js, and Game.js. With the caveat that, for convenience, I will be using defaults and classes like so:
// in modules/Game.js
export default class Game { //...
// in main.js
import { Game } from "./modules/Game.js";
Trying this out didn’t work, I already know that modules only work on a sever,
so I have one running locally to test this, but I was getting an error saying
that import
doesn’t work outside of modules. This is slightly semantically
confusing because you export from modules, you import into other scripts, but
basically the <script>
tag needs to be updated with the attribute
type="module"
.
Fixing that produces a new problem, because the export is a default, there is
no export named Game
now! This is being caused by those curly brackets in
the import, because that’s for importing specific exports by name, but this is
just grabbing the default export so it should be:
import Game from "./modules/Game.js";
And now the modules are working as intended!
I want the code for this, and the comments, to be pretty legible and thorough, so I’m going to try to use the JSDoc standards for comments.1 So I’m reading the docs for modules, which is kind of vague, but there is a more specific page for ES 2015 Modules. Just declaring the module is pretty simple, but declaring the exports seems to be annoying, there’s a thread posted on GitHub that basically says you just need to space it out with lines to fit in the declarations, like so:2
/** @module modules/Game - Game logic module */
export default
/** Class representing the game itself */
class Game {
/** Initialize a game */
constructor(){
// ...
This feels a bit clunky, but it works3 and is still at least legible without
having to separate the class
and export
declarations, so I’ve thrown that
onto all the modules!
It’s here that I’m also realizing that I really need to separate the model and
controllers. Thinking back to the pseudocode, I had problems trying to figure
out how to handle having the physics engine separate from the game logic, and I
arbitrarily decided this should be done by passing the Game
into the
Physics
, but now I think that having a new module for GameData
would be
best (it would also open up a clearer path to loading and saving games in the
future). That should be everything for setting up the basic module structure,
done!
Now I want to test this on the public site, and I ran into a worse problem than expected! I messed up the JS and CSS links, since this is all published as a folder of una-ada.github.io, I need to use relative paths. Easy fix.
I’m not a huge fan of OOP in general, but this project just sort of feels like
an OOP project, so everything is going to be done with classes, as should have
been pretty clear from the previous section. What this really means here, is
that rather than using const
for constants in each module, I’ll be using
static
, which means they can be accessed as Class.property
but not
instance.property
.
class Physics {
/** @constant {number} G - Gravitational constant */
static G = 6.67e-11;
// ...
Regarding the JSDoc comments here, for objects and some similar constructs, documenting variables just requires dot notation:
/** @constant {Object} scale - Conversion rates for physics equations */
static scale = {
/** @constant {number} scale.space - Meters per pixel */
space: 0.30e7,
/** @constant {number} scale.time - Seconds per animation frame */
time: 0.50e3
};
As far as what constants get copied, for now I’ve just copied the scales
and G
from the original script.
Ok, I need to start getting some sort of I/O done for this, basically just a
proof of concept for user inputs. Per the pseudocode, the game state’s
properties regarding basic input are if the mouse is down, a cached position
where the mouse was put down, and the current mouse position. This will require
listeners for the mousedown
, mouseup
, and mousemove
events to update. So
that’s what I’ll do! First, the model:4
class GameData {
/** Create object to hold game states */
constructor(){
/** @var {Object} mouse - Data regarding the user's mouse */
this.mouse = {
/** @var {boolean} mouse.isDown - Is mouse currently down */
isDown: false,
/** @var {number[]} mouse.initialPosition - Position of mouse on down */
initialPosition: [0, 0],
/** @var {number[]} mouse.position - Current position of mouse */
position: [0, 0]
}
//...
Now, to add controllers for this. First, I’ll need to make sure that the
controller has access to the model, which is why it’s being passed into it, but
it also needs to save it. How do I document that model
here should be an
instance of GameData
? The documentation for @type
implies I just need
to do @var {GameData} model
but when I do tha— oh, I was just setting
that on the this.model
not on the parameter… wait, that also just says
model: any
, huh. Oh, right, modules! Ok:
class Game {
/**
* Initialize a game controller.
* @param {module:GameData.GameData} model
*/
constructor(model){
/** @var {module:GameData.GameData} model */
this.model = model;
}
// ...
Well, it still shows as any
, fuck. Oh, I’ve been over complicating things, I
need to import the module for it to check out properly. Sure, this is
technically just for documentation right now, but it could be very useful in
the future:
import GameData from "./GameData";
// ...
class Game {
/**
* Initialize a game controller.
* @param {GameData} model - A game model instance.
*/
constructor(model){
/** @var {GameData} model - Reference to the game's model */
this.model = model;
}
// ...
Finally, we can stop being on a detour and add some event listener functions,
like Game#handleMouseDown
. For now, I just want the basics, toggle
GameData#mouse.isDown
and save the position in
GameData#mouse.initialPosition
. Even before that, let’s make sure we can use
this! Throw together the function like:
class Game {
// ...
handleMouseDown(e){
console.log("BOOP!", e);
}
// ...
Then, need to make the document listen for that. Adding a quick little
.addEventListener
in main.js:
document.addEventListener("mousedown", game.handleMouseDown);
And now I’m getting a 404 in the console at GameData:1
? Confusing. Oh, the
import in Game.js is missing the “.js” part on "./GameData.js"
! Fixing
that, and everything works now!
Before moving on, let’s figure out how to document this. It might have
something to do with the @event
tag? There’s an answer on
StackOverflow about the .addEventListener
call, not quite what I’m thinking
of, plus it uses the document
namespace and I don’t think that’s a thing (I
did try it and it says any
). The MDN page on mousedown actually helped
less than that console.log
above, which reminded me this is a MouseEvent
,
which VS Code shows as a real type, so we’re cool.
Adding in the model updates, this now looks like:
/**
* Handle mouse down events
* @param {MouseEvent} e - Mouse down event
*/
handleMouseDown(e){
// Show mouse is down in model
this.model.mouse.isDown = true;
// Cache initial mouse position
this.model.mouse.initialPosition = [e.pageX, e.pageY];
// Save current mouse position
this.model.mouse.position = [e.pageX, e.pageY];
}
Something I went and forgot about, what is this
in an event listener?
Certainly not what I want it to be! So maybe I’ll bind it (uwu). Just update
the listeners’ callbacks to bind the controller:
document.addEventListener("mousedown", game.handleMouseDown.bind(game));
And there we go! User inputs all handled and updating the model!
Of course, I don’t particularly like the way I did all this. The positions are
just arrays of two numbers, which is questionably. Temporarily, I’ll make these
Objects of the form {x,y}
, and eventually will have classes for things like
positions and vectors. Just generally refactoring at this point before
moving on.
Following along with the pseudocode, on of the earliest steps is to append a rendering container to the DOM, for the renderer to push all its renderings onto. While I did write down that this would all be handled in DOMRenderer.js, I want to avoid having to copy things if I write a different Renderer, so I’m going to make a generic Renderer class first:
// In Renderer.js:
/** @module Renderer - Superclass for managing game view. */
export default class {
// ...
// In DOMRenderer.js:
/** @module DOMRenderer - Manages the game view (DOM). */
export default class extends Renderer {
/** Initialize a DOM-based renderer */
constructor() {
super();
// ...
Now, I’d like to define the container element in the superclass, but for
different rendering methods, a different element would be used (i.e. using
canvas would require a <canvas>
element). So this will be defined in the
subclass instead:
/** @var {HTMLElement} container - DOM Element holding all game views.*/
this.container = document.createElement("div");
The that can be appended to the DOM, so it can be used to actually render stuff later on:
const model = new GameData(),
view = new DOMRenderer(model),
// ...
document.body.append(view.container);
That about wraps it up for the basic setup stuff, to be honest. From here, things are pretty straightforward, so future installations in the blog series will be more about general concepts or problems I come across.
This is also helpful with using VS Code, because it will read the JSDoc comments and use them for tooltips when writing out function calls and the like! ↩︎
These were actually all originally block comments, but I updated them to single lines in the code, so I updated them here to take up less vertical space. ↩︎
Declaring modules like this with JSDoc in VS Code means that if you call an export of a module that you haven’t imported yet, it can automatically fix the issue by adding the import to the script! ↩︎
I got so caught up in the documentation here that I somehow forgot to
use isDown: false
rather than isDown = false
initially, but that’s
fixed now! ↩︎