anarchy.website / Gravity Development Log, Part 2
Toggle Dark Mode

Framework and Basic Functionality

By Una Ada, June 01, 2021

III. Structure

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>

III-1. Modules

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.

III-2. Classes

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.

IV. Inputs

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.

V. Render Setup

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.

Footnotes

  1. 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! ↩︎

  2. 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. ↩︎

  3. 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! ↩︎

  4. 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! ↩︎