I mentioned in a previous section that the way I was saving positions (simply saving the values as properties in an Object, see below) may be better than the very rudimentary implementation with Arrays but it’s still quite unsatisfactory. So I’m going to work on that a bit!
position: {
x: 0,
y: 0,
},
This is terrible because I’m going to be doing a lot of math in the physics
engine, so having to access things like this is pretty annoying. What I need is
some sort of utility module that will contain a Point
class as well as a
Vector
class that extends it, at least:
/** @module Utils - A collection of utility classes. */
export class Point { /* ... */ }
export class Vector extends Point { /* ... */ }
How is a vector a subclass of a point? You might ask. Simple, where a point is defined by an array of positions of length $n$ in $n$-dimensional space, a vector is defined by an array of lengths of length $n$. The primary difference is that we can do much more with a these values in a vector, such as finding their magnitude, that we wouldn’t do with points.
Basically, with vectors we have these “lengths” which we can call components, but at some point we’re going to want the magnitude and direction. This is fundamentally equivalent to converting a point at the coordinates set by the components into polar coordinates from Cartesian coordinates. If we define this vector, let’s call it $\vec{a}$, by its components, we’d have
\[\vec{a} = a_x\hat{\imath} + a_y\hat{\jmath}, \tag1\]which we can then get the magnitude, $r$, from with the formula
\[a = \sqrt{a_x^2 + a_y^2}. \tag2\]Getting the direction, $\phi$, is a bit more complicated, because angles are just weird when working with different quadrants of the Cartesian plane:
\[\phi = \begin{cases} \arctan{\left(\frac{a_y}{a_x} \right)}, & \text{if $a_x\gt0$} \\ \arctan{\left(\frac{a_y}{a_x} \right)} + \pi, & \text{if $a_x\lt0$ and $a_y\ge0$} \\ \arctan{\left(\frac{a_y}{a_x} \right)} - \pi, & \text{if $a_x\lt0$ and $a_y\lt0$} \\ \frac{\pi}2, & \text{if $a_x=0$ and $a_y\gt0$} \\ -\frac{\pi}2, & \text{if $a_x=0$ and $a_y\lt0$} \\ \text{undefined}, & \text{if $a_x=0$ and $a_y=0$} \end{cases} \tag3\]Though, apparently, there’s a simpler way to do this using $r$. Despite my decent handle on trigonometry, I hadn’t considered this in the previous version of this project, but it seems like a good strategy:
\[\phi = \begin{cases} \arccos{\left(\frac{a_x}r \right)}, & \text{if $a_y\ge0$ and $r\neq0$} \\ -\arccos{\left(\frac{a_x}r \right)}, & \text{if $a_y\lt0$} \\ \text{undefined}, & \text{if $r=0$.} \end{cases} \tag4\]The reverse is rather trivial in comparison, since it’s the sort of thing trigonometric functions are designed to do:
\[\begin{align} a_x &= r\cos{\phi}, \tag{5a} \\ a_y &= r\sin{\phi}. \tag{5b} \\ \end{align}\]Because this is so much simpler, I’ll implement this first before dealing with the piecewise function stuff.
There is something I’d like to test out with this: using setters and getters.
Rather than having functions like Vector#getMagnitude
and
Vector#setMagnitude
, I’ll just have Vector#magnitude
defined by a getter
and a setter:
/** @type {number} */
get magnitude() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
set magnitude(magnitude) {
let direction = this.direction;
this.x = magnitude * Math.cos(direction);
this.y = magnitude * Math.sin(direction);
}
Setting the lengths along the axes is dependent on both magnitude and
direction, so to set either of them I first need to cache the other one. Which
is basically all there is to it. On the other hand, I would like to keep angles
within a single rotation for consistency, so setting direction
needs an extra
little bit of fixing up just in case:
set direction(direction) {
let magnitude = this.magnitude;
// Keep direction within [-pi, pi]
if(Math.abs(direction) > Math.PI) direction %= Math.PI;
this.x = magnitude * Math.cos(direction);
this.y = magnitude * Math.sin(direction);
}
I’m ignoring this for now, but I’m pretty sure these angles would not properly translate into CSS properties. I mean, sure, I can just set the angle in radian units, but that’s not the problem. The $y$-coordinates in CSS are all reflected over the $x$-axis because they are measured from the top rather than the bottom. I’ll need to get more testing done before I can determine a) if this is a real issue and b) if it should be addressed in the utility functions or in the rendering functions. I’m going to sleep now, so I’ll just push what I’ve gotten done so far here.
Good morning! I still haven’t written a getter for direction, which is really important! So here we go:
get direction() {
let magnitude = this.magnitude;
if(magnitude = 0) return NaN;
return (this.y < 0 ? -1 : 1) * Math.acos(Math.PI / magnitude);
}
I cut a lot of the annoying piecewise-ness of the function down with simply
returning NaN
if magnitude
($r$) is zero and using a ternary operator to
determine if the angle should be positive or negative. And that should be
everything for the math utilities (for now)!
Ok, so I thew together a little test for these utilities like so:
export default class Game {
// ...
handleMouseDown(e) {
// ...
let vec = new Vector(mouse.position.x, mouse.position.y);
console.log(vec.magnitude, (vec.direction / Math.PI));
// ...
and the first issue that became immediately obvious was that I fucked up with
the conditional in get Vector#direction
: magnitude = 0
should be magnitude
== 0
, but even after fixing that, the angles are all… uhh. Pretty much
always the same??? Oh! Fuck! Shit! I copied the formula down wrong the first
time, the return
should be:
return (this.y < 0 ? -1 : 1) * Math.acos(this.x / magnitude);
Also, no reason to bother with the whole / Math.PI
part, I tested the values
on transform: rotate(...rad);
declarations in CSS and they perfectly reflect
the direction of the vector in what is theoretically the first quadrant of the
plane. This still needs to be tested on the other quadrants tho, since that
tends to be when shit gets really weird with the reflected $y$-axis.
Before I get to use any of these fun math utilities in the physics, I need objects to manipulate, so onto some actual rendering I go. First, I want a way to make sure that everything is being rendered in the correct position, based on the bounds of the game view. Easy function (based on a StackOverflow answer):
/** @type {number[]} */
get bounds() {
const { top, left, height, width } = this.container.getBoundingClientRect();
return { top, left, height, width };
}
More importantly, creating a Celestial
class for game objects, what
properties does it need? For physics: position, velocity, mass, and a hitbox;
for rendering: position, size, and texture (things such as Elements will be set
in the Renderer); then some metadata like name and “birthday.” This class will
be a very basic list of declarations, essentially:
/** @module Celestial - Celestial game objects. */
export default class Celestial {
constructor(options) {
/*----- Metadata ---------------------------------------------------------*/
this.name = options.name || "Unnamed Celestial";
/** @var {number} birth Creation timestamp. */
this.birth = +new Date();
/*----- Physics ----------------------------------------------------------*/
this.position = options.positions || new Point(0, 0);
this.velocity = options.velocity || new Vector(0, 0);
this.mass = options.mass || 0;
/** @TODO Add HitBox class */
this.hitBox = null;
/*----- Rendering --------------------------------------------------------*/
this.size = options.size || 0;
this.texture = options.texture || null;
}
}
At the moment, I’m probably not going to be using the texture
property, I’ll
save that for when everything is more functional. It’s just about the last day
to work on this so I need to work on the minimum viable product (MVP), just
making sure everything functions to the minimum requirements of the project
before I do the Una thing and focus much more on the graphics than on the code.
Not going to make a whole new font for this project, probably.
That in mind, having added this module, I’m just going to try to recreate the original before adding in the new stuff. First, need an array for objects in the level:
/** @var {Celestial[]} scene Array of game objects in current scene. */
this.scene = [];
Then add in a couple instances in the main.js for now:
let saturn = new Celestial({
name: "Saturn",
mass: 5.683e26,
size: 102, // not to scale
}),
titan = new Celestial({
name: "Titan",
mass: 1.35e23,
position: new Point(0, -389), // not to scale
velocity: new Vector(15.26e3, 0),
size: 18, // not to scale
});
model.scene.push(saturn);
model.scene.push(titan);
Renderer#render()
Since I haven’t done any of the position
or size
scaling logic yet, these
are just going to be literal pixel values for now so I can test out the
rendering. So onto that rendering, first defining a Renderer#render
function,
default in the superclass to make sure there’s always something there:
class Renderer {
// ...
/** Render the game scene. */
render() {
console.error("Renderer#render() has not been overwritten by subclass!");
}
// ...
Then in our DOMRenderer
, we make sure that each object in the scene has an
Element to render in an overwriting DOMRenderer#render()
method:
/** Render the game scene. */
render() {
const scene = this.model.scene;
scene.forEach(
(obj) => obj.element || (obj.element = document.createElement("div"))
);
}
A quick test shows that this works fine! So moving on to putting these in
the scene by adding && this.container.append(obj.element)
into the loop. I
don’t like how these are just <div>
Elements, though, so going to create a
function to generate the appropriate Element instead:
/**
* Create a rendering Element for a Celestial and append it to the container.
* @arg {Celestial} celestial
* @returns {HTMLElement} The rendering Element for the Celestial.
*/
generateElement(celestial) {
const element = document.createElement("celestial"),
// Clean up the name for adding into a CSS class
cleanName = celestial.name.replace(/\s+/g, "-").toLowerCase();
element.classList.add(`gravity__celestial_${cleanName}`);
celestial.element = element;
this.container.append(celestial.element);
return element;
}
This properly adds the Elements into the container,^{1} and returns them
so I can use them inline like const elem = obj.element ||
this.generateElement(obj);
, but I’m going to need to make sure all the
elements are prepared with CSS before I can do anything further with them:^{2}
gravity {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
gravity > celestial {
position: absolute;
}
Now I can go in and apply all the position data to the Elements, which is pretty boring if I’m being honest! But I did it!:
render() {
const scene = this.model.scene,
bounds = this.bounds,
origin = {
x: bounds.width / 2,
y: bounds.height / 2,
};
scene.forEach(
/** @arg obj {Celestial} */
(obj) => {
// Make sure each Celestial has an Element
const elem = obj.element || this.generateElement(obj),
// Avoid repeating elem.style a whole bunch.
style = elem.style,
offset = 0 - obj.size / 2;
style.top = `${origin.y + obj.position.y + offset}px`;
style.left = `${origin.x + obj.position.x + offset}px`;
style.width = `${obj.size}px`;
style.height = `${obj.size}px`;
}
);
}
With the Renderer#render()
method done, I need it to actually animate. Which
is surprisingly simple, just loop it on each animation frame. I wish I had more
to say about this, I guess I’m also adding in a boolean to tell it to stop if I
want to stop it? Whatever, just adding in Renderer#loop()
:
class Renderer {
constructor(model) {
// ...
/** @var {boolean} running Should the renderer continue running? */
this.running = true;
// ...
/** Render on animationFrame */
loop() {
if (this.running) this.render();
requestAnimationFrame(this.loop);
}
A quick test showed some binding issues, so I guess I gotta do a little bit of manual binding:
requestAnimationFrame(this.loop.bind(this));
And now it works, cool!
As long as I’m making loops, I’ll throw in a little loop for the physics
engine. Remember, physics gets weird if you calculate it based on the animation
frames rather than a constant time, so this will be using
window.setInterval()
:
class Physics {
constructor(model) {
// ...
/** @var {number} _intervalId Holds the ID of the loop interval. */
this._intervalId = null;
// ...
/** @const {number} interval Interval time for physics loop. */
static interval = 1e3 / 120;
// ...
/** Calculate physics on a set interval. */
loop() {
this._intervalId = window.setInterval(
this.step.bind(this),
Physics.interval
);
}
/** Stop the physics engine loop. */
stop() {
window.clearInterval(this._intervalId);
}
/** Calculate the physics for objects in the scene and apply to model. */
step() {
// ...
Basic concept of this is to add the velocity vector multiplied by the time scale to the position vector of each object. I don’t have a utility for adding vectors or for multiplying them by scalars. So going to throw those into the Utils.js module:
class Point {
// ...
add(point) {
this.x += point.x;
this.y += point.y;
return this;
}
// ...
class Vector extends Point {
// ...
scale(scalar) {
this.magnitude = this.magnitude * scalar;
return this;
}
With those functions, I can add in the position updates pretty trivially:
/** Update `Celestial#position`s based on `Celestial#velocity` */
updatePositions() {
this.model.scene.forEach(
/** @arg obj {Celestial} */
(obj) => obj.position.add(obj.velocity.copy().scale(Physics.timeScale))
);
}
Testing this would show a huge flaw: the velocities are way too fast! Because
they’re based on the actual physics and not the scale of the view, which hasn’t
been implemented yet, so I need to scale the position rendering based on the
space scale… which was previously in Physics
but is really only important
for Renderer
:
export default class Renderer {
constructor(model) {
// ...
/** @var {number} scale Space scale (meters per pixel). */
this.scale = 0.3e7;
// ...
Then to create a function for calculating the view position based on the model
position, which I can do in the Renderer
superclass rather than the
DOMRenderer
subclass, because all renderers would need this. First, I need to
also add a scaling function for points, and, for convenience, modify the
Point#add()
function to accept a single argument and add that to both
components.^{3}
class Point {
// ...
/**
* Add another point to this point.
* @arg {Point|number} addend A point to add to this point
* @returns {Point} The updated point.
*/
add(addend) {
this.x += addend.x || addend;
this.y += addend.y || addend;
return this;
}
/**
* Multiply point components by a scalar
* @arg {number} scalar Multiplication scale
* @returns {Point} The updated point.
*/
scale(scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
}
Now I can actually get the position with the function specifically for that in
Renderer
and use that function in my DOMRenderer
:
// In Renderer.js
class Renderer {
// ...
getPosition(celestial) {
const bounds = this.bounds,
// View origin point (0, 0)
origin = new Point(
bounds.width / 2,
bounds.height / 2
),
// View position from scaled Celestial position
position = celestial.position.copy().scale(1 / this.scale),
// View offset position from Celestial origin
offset = celestial.size / 2;
// Return offset view position
return origin.add(position).subtract(offset);
}
// ...
// In DOMRenderer.js
class DOMRenderer extends Renderer {
// ...
render() {
const scene = this.model.scene;
scene.forEach(
(obj) => {
// ...
const position = this.getPosition(obj);
style.left = `${position.x}px`;
style.top = `${position.y}px`;
// ...
And, with this added in and the test object scales updated, everything works as expected.
Updating position based based on velocity is the most basic of basics when it comes to the physics, now I need to update the velocity based on acceleration. Technically, gravity is a force, so I should update acceleration based on force, but I’m just going to calculate the acceleration from the gravity and apply that directly to the velocity. A fun shortcut. So how do we calculate gravitational acceleration? Well, Newton’s law of universal gravitation is given as:
\[F = G\cdot\frac{m_1m_2}{r^2}, \tag6\]where $m_1$ and $m_2$ are the masses of two objects, $G$ is the gravitational
constant (already defined as Physics.G
in the module), and $r$ is the
distance between the two “point-like masses.” This “point-like masses” thing
can basically be interpreted as the distance between their centers of
mass; for convenience, I’m calling the Celestial#position
the center of
mass of the Celestial
instance. Then, to get the acceleration applied by
$m_2$ on $m_1$, we substitute Newton’s second law of motion, $F=ma$, in
for $F$ and simplify:
We can get this as a vector by simply multiplying it by the unit vector directed from $m_1$ to $m_2$, $\hat{\mathbf{r}}$:^{4}
\[\mathbf{a} = \frac{Gm_2}{r^2}\cdot\hat{\mathbf{r}}. \tag8 \label8\]Now to actually implement this! First, I’m going to want a way to get the
vector between two Point
s ($P_1$ and $P_2$), which is just subtracting the
components of $P_1$ from the components of $P_2$:
/**
* Get a copy of this Point as a Vector.
* @returns {Vector} Copy of this Point as a Vector.
*/
copyAsVector() {
return new Vector(this.x, this.y);
}
/**
* Get a Vector between two points.
* @param {Point} point
* @returns {Vector} A Vector between this Point and another Point.
*/
vectorTo(point) {
return point.copyAsVector().subtract(this);
}
Having added that, I can fairly easily use Eq. $\eqref{8}$ to get the acceleration between two instances of Celestial
:
/**
* Calculate gravitational acceleration between two Celestial instances
* @param {Celestial} m_1 Celestial to apply acceleration to.
* @param {Celestial} m_2 Celestial applying the acceleration.
* @returns {Vector} Gravitational acceleration Vector.
*/
gravitate(m_1, m_2) {
// Vector between m_1 and m_2
const between = m_1.position.vectorTo(m_2.position),
// Distance between m_1 and m_2
r = between.magnitude;
// Make `between` a unit vector
between.magnitude = 1;
return between.scale(Physics.G * m_2.mass / (r ** 2));
}
With the formula for gravitational acceleration implemented like this, I
just need to run it on each celestial for each other celestial, adding all the
accelerations up, then updating the velocity based on that sum. I want to try
doing this with Array iterator functions rather than for
loops. Because
this is a method running on each celestial, the outer “loop” will be a
.forEach()
, but the inner “loop” is a function, so it would be .reduce()
:
updateVelocities() {
this.model.scene.forEach(
/** @arg m_1 {Celestial} */
(m_1) =>
// Add to the velocity
m_1.velocity.add(
this.model.scene
// Total acceleration
.reduce(
/**
* @arg {Vector} acc Total acceleration (accumulator)
* @arg {Celestial} m_2 Celestial applying acceleration
*/
(acc, m_2) =>
// Add gravitational acceleration if not same celestial
m_1 === m_2 ? acc : acc.add(this.gravitate(m_1, m_2)),
// Initialize an zero acceleration vector
new Vector(0, 0)
)
// Velocity from acceleration
.scale(Physics.timeScale)
)
);
}
This looks fine, and it almost works. But my test moon keeps getting away, like it’s moving too fast? I don’t think it is, though? Oh, it definitely is, it’s set to a velocity of 15.26e3
when it should be 5.47e3
? Weird! Probably something to do with the scaling in the original JavaScript, but this works now so that’s it for the calculating gravity!
I never got around to this stuff in the original, but for game mechanics it’s absolutely necessary to have a way to check collisions. Doing this for arbitrary shapes is fucking impossible, I think. There’s probably a way to do it, but for the moment, I’m just going to use circles as the hit boxes, so the math is like:
\[(r_1+r_2)^2 \gt (x_1+x_2)^2 + (y_1+y_2)^2,\tag9\]where $r_1$ and $r_2$ are the radii of the two circles and $(x_1,y_1)$ and $(x_2,y_2)$ are the coordinated of their centers respectively. However, since I have all this vector math handled already, I can just check this using a vector, $\mathbf{v}$, that represents the position of $(x_2,y_2)$ relative to $(x_1,y_1)$:
\[r_1 + r_2 \gt \vert\mathbf{v}\rvert.\tag{10}\]Implementation time! First, make sure that I’m future proofing this a bit by creating a Celestial#hitBox
property that, at least for now, is simply a string describing the shape:
class Celestial {
constructor(options) {
// ...
/** @var {string} hitBox Shape of the hit box */
this.hitBox = options.hitBox || "CIRCLE";
// ...
Then make an Object for storing intersect check function. This is going to
exist purely because I don’t want to put in the effort of making a HitBox
Class at the moment, essentially. This way the functions can be accessed as
Physics.INTERSECT_CHECKS[type][type]
,^{5} so I implemented it as such:
static INTERSECT_CHECKS = {
CIRCLE: {
CIRCLE:
(a, b) => a.position.vectorTo(b.position).magnitude < a.size + b.size,
},
};
checkIntersection(a, b) {
return Physics.INTERSECT_CHECKS[a.hitBox][b.hitBox](a, b);
}
Okay, now time to do something about these collisions, for the time being, I’d
like to log them in the Celestial
object. Keeping a list of collisions like
this is more general and allows me to add different means of using the
information rather than simply writing a single handler right now. So, here’s
that:
// In Celestial.js
class Celestial {
constructor(options) {
// ...
/** @var {Object[]} collisions Log of collisions with other objects. */
this.collisions = [];
// ...
// In Physics.js
updateCollisions() {
this.model.scene.forEach(
/** @arg a {Celestial} */
(a) =>
this.model.scene.forEach(
/** @arg b {Celestial} */
(b) =>
a !== b &&
this.checkIntersection(a, b) &&
a.collisions.push({
time: +new Date(),
who: b,
})
)
);
}
But wait! Testing this by changing the color if the collision array is
longer than zero shows absolutely no changes! Why? It’s simple, the
Celestial#size
is in view scale, not model scale! What are the chances that
the objects just so happen to be within such a tiny range on any given
iteration anyway? Too low, also not what I want. So, all references to
Celestial#size
in a Renderer need to be divided by Renderer#scale
, and the
tests need to be scaled up.
That works better, but now they change before they actually hit! Because
Celestial#size
is a diameter not a radius, duh! God, how did I fuck that
up in the first place? Whatever, fixed that. And that’s everything for the basic physics… next to make a game out of it…
I have no idea how to document this in JSDoc, I would really like to
declare that Celestial#element
is now @type {HTMLElement}
but only
when DOMRenderer
adds it! ↩︎
I’m really tempted to write this out as actual SCSS so I can do some nesting and imports, but I don’t know if that’s allowed in this project or if I can do that on GitHub Pages without having a Jekyll Theme set… I probably can, but I’ll leave that for future work, I suppose. ↩︎
I forgot 0
is falsey the first time I wrote this, so I just did +=
addend.x || addend;
which would add and the Point to the x
value if
Point#x == 0
, so this needs to be a type check. ↩︎
Typically, one would define the attraction force that a larger mass, $M$, applies to a smaller mass, $m$, and so the force applied to $m$ would be defined using the negation of the unit vector pointing from $M$ to $m$: $-\hat{\mathbf{r}}$; however, since I’m just applying the force as acceleration to the velocity of $m_1$, I’m defining the vector $\hat{\mathbf{r}}$ in the opposite direction. ↩︎
Note that this is when I decided that, since I’m calling them “constants,” I will adopt a constants naming convention for static properties of classes. There was some refactoring involved. ↩︎