DEV Community

Isa Levine
Isa Levine

Posted on • Edited on

How I (Accidentally) Made a Game Engine from Scratch with Vanilla JS

UPDATE 7/29/19: "The Fire Mage" is now deployed on Heroku! Check it out here: https://the-fire-mage.herokuapp.com/

animated gif of “The Fire Mage” being played in browser
The game engine in action, with the proof-of-concept game "The Fire Mage"

Here's my Github link to the Javascript frontend, and here's the one to the Rails backend!
(I apologize that neither one has a Readme yet—read on to see that it's on my to-do list!)

Recently at Flatiron Seattle, we had a project to do a single-page application with a Javascript frontend and a Rails backend. I decided I also wanted to use the project to learn as much CSS as possible, along with practicing DOM manipulation. Feeling inspired by retro video games, I decided that I wanted to make a little real-time-strategy-esque game, in the vein of Warcraft II and Starcraft. My scope would be simple: have a unit, select it, tell it to move, the unit interacts with objects, and have some messages and animations to tie it all together.

gif of warcraft 2 being played

What I didn't realize at the start was that I needed to build a whole game engine to make all those little events happen!

Initially, I was offered help setting up Bootstrap and Canvas and Phaser as tools to help me make my game. But the more I looked at them, the less I felt I was pursuing my core mission. I half-assed-ly tried setting up Bootstrap, and took the minor difficulty I encountered to be a sign: I should build the entire game engine, from scratch, with vanilla Javascript.

In this blog post, I want to review some of the techniques and lessons I picked up in both Javascript and CSS as I was working.

CSS Grid

screenshot of
CSS grid in action.

Helpful links:
https://www.w3schools.com/css/css_grid.asp
https://hacks.mozilla.org/2017/10/an-introduction-to-css-grid-layout-part-1/
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout

When I gave up on Bootstrap, I was happy to find that CSS has a built-in grid function. Here’s a few things I learned about them:

Grid terminology: column, row, gap

The links above have terrific illustrations showing off this terminology and how it translates onto the page, but as a quick rundown:

Columns are columns.
Rows are rows.
Column-Gaps are the spaces between columns.
Row-Gaps are the spaces between rows.
Gap is shorthand for both column-gap and row-gap.

Each of these can be numbered and sized accordingly to create the desired grid.

Setting them up

To get a grid set up, create a CSS class for the grid container. Set the ‘display’ property to either ‘grid’ (for block-level) or ‘inline-grid’:



.grid-container {
  display: grid;
}


Enter fullscreen mode Exit fullscreen mode

Setting column/row size

There are a few ways to set the number of columns and rows as well as their sizes, but I found the most convenient way was to use the ‘repeat()’ method, in conjunction with the ‘grid-template-columns’ and ‘grid-template-rows’ attributes:



.container {
  position: absolute;
  display: inline-grid;
  grid-template-columns: repeat(20, 42px);
  grid-template-rows: repeat(12, 42px);
}


Enter fullscreen mode Exit fullscreen mode

In the above snippet, the repeat() method takes two arguments: the number of columns/rows, and the size of each. The code above produces a grid (this time as an inline element) with 20 columns and 12 rows, with each cell being 42x42 pixels.

Since my project had very finite dimensions, I coded most of the CSS using pixel counts. You can also use fractional notation to subdivide the remaining space inside the grid into equal parts—for instance, ‘3fr’ would take up 1/3 of the space, ‘2fr’ 1/2 the space, ‘1fr’ the entire space, etc. (There’s more nuance to setting up differently-sized columns/rows, but I’ll defer to the links above for that.)

Setting location with grid-column-start/end + grid-row-start/end

CSS Grid has a handy way to attach other elements to itself: specify the starting and ending column and row where you want it to go:



.item1 {
  grid-column-start: 1;
  grid-column-end: 3;
}


Enter fullscreen mode Exit fullscreen mode

(snippet from W3 Schools)

With the integers representing the column number from left to right, this will stretch your element to cover the columns starting at the top-left corner of the grid-column-start, and ending at the top-left corner of the grid-column-end. (Same goes for grid-row-start and grid-row-end.) The snippet above will stretch the element with class ‘item1’ to cover columns 1 and 2, and stop BEFORE column 3.

Project-specific usefulness

So, CSS is a great tool, but not a perfect one for my game engine’s purposes. Ultimately, the gaps between rows and columns needed to be eliminated for the final look of the game, and for elements on the grid’s level, I could only attach them to specific grid-cells—but not floating in-between them. As a result, I ended up only putting terrain images on the grid’s layer, as they are static and are (currently) not interacted with by units or items.

Javascript Classes

I’ve been hesitant to go all-in on Javascript classes, but this project helped me see the utility they provide. Part of my project’s requirements involved persisting some data, so I wanted to keep track of units’ and items’ locations on the game board. That way, I could reload the game if the browser refreshed, as long as the locations were saved to the database frequently enough.

Knowing that this location-memory would be critical in creating hitboxes and collision-detection, I decided to refactor all of my code (at that point, 2 or 3 days worth of DOM-manipulation) so that everything drawn for the game—the board, the layer-containers, the terrain images, the cells for units and items—were all class instances. It was an afternoon well-spent, because afterward I had several advantages:

My game’s class instances remembered their divs, and vice versa

Check out this code for the Cell class, which is extended to create Units and Items:



class Cell {
  constructor(containerQuery, position, onMap = true) {
    this.position = position;
    this.onMap = onMap

    this.div = div
    div.cell = this

    this.div.setAttribute('style', `left: ${this.position.left}px; top: ${this.position.top}px`)
  }
}


Enter fullscreen mode Exit fullscreen mode

Notice how each Cell has a .div attribute, and each div has a .cell attribute? I’m still not 100% sure if there’s an easier way to establish this connection, but it became important for me to have flexibility in grabbing units and items by their class instance or their div, so being able to call (div.cell) and (this.div) to get the correct data was very convenient. One example is this snippet from the endgame event, where the Item ‘tree’ has its class-list modified:



      treeCell.div.classList.add('slow-fadeout')
      treeCell.div.classList.add('special-effect')
      treeCell.div.classList.remove('item')


Enter fullscreen mode Exit fullscreen mode

The class instances remembered their divs’ positions on the board

I created a “position” attribute that pointed to an object with positions that could be used in HTML style, and built a helper method to translate the div’s location into that “position” attribute:



class Cell {
  constructor(containerQuery, position, onMap = true) {
    this.position = position;
  }
}

function positionCreator(div) {
  return {
    left: div.getBoundingClientRect().left,
    top: div.getBoundingClientRect().top,
    width: div.getBoundingClientRect().width,
    height: div.getBoundingClientRect().height
  }
}


Enter fullscreen mode Exit fullscreen mode

positionCreator() method courtesy of this amazing code by JR on JSFiddle.

Then, when I added functions to allow units to move, I included code to update the class instance’s position attribute based on its div’s current location, recalculated 20 times per second (every 50 milliseconds):



while (transitionOn) {
      let hitboxUpdater = setInterval(()=>{

        if (transitionOn === false) {
          clearInterval(hitboxUpdater);
          updateCells()
        }

        selectedUnit.cell.hitboxPosition = positionCreator(selectedUnit.cell.hitbox())

        let containerX = unitContainer.div.getBoundingClientRect().x
        let containerY = unitContainer.div.getBoundingClientRect().y
        selectedUnit.cell.position = positionCreator(selectedUnit)
        selectedUnit.cell.position.left -= containerX
        selectedUnit.cell.position.top -= containerY

        collider.checkContainerUnitCollision(selectedUnit, boardContainer)
        collider.checkItemUnitCollision(selectedUnit)
    }, 50)
    break;
 }


Enter fullscreen mode Exit fullscreen mode

While the transitionOn variable is ‘true’, this setInterval() function is updating the selectedUnit’s cell position based on its location within the game’s div, and then checks for collisions with both the game’s border and other cells.

Finally, adding console.logs to the functions (which for now are mostly gone or commented-out) gave me a handy readout of div locations in Chrome’s developer tools, which helped me with debugging while creating hitboxes and collision-detection.

Inheritance made it easy to build up and customize different in-game classes, like items and units

Okay okay, I know that prototyping is Javascript’s special thing, and that inheritance-vs-composition is a huge topic, but there were a couple small instances where inheritance really helped!

After I decided that I wanted units and items to be types of “Cells”, I made “Unit” and “Item” classes that extended Cell. This allowed me to debug and tweak one without affecting the other. Ultimately, there were only a couple differences, but it was good practice in DRY programming—afterall, only Units need inventories, not Items!



class Unit extends Cell {
  constructor(name, container, position, onMap) {
    super(container, position, onMap)
    this.name = name
    this.cellType = "unit"
    this.gameSessionId = currentGameSession.id

    this.inventory = []
  }


Enter fullscreen mode Exit fullscreen mode

I will say, however, that I'm excited to try a composition-focused approached in lieu of an inheritance-focused one the next time I have the chance!

Hitboxes, collision-detection, and collision events

hitboxes in counter-strike
Example of hitboxes from Counter-Strike

This was the crown-jewel of the project: creating a game engine that allows objects to interact through collisions. This was achieved by giving each interactive element a hitbox, and having functions to constantly check for hitbox collisions while elements were in motion (and thus potentially creating collisions).

Hitboxes - using CSS and helper functions to add them quickly

Part of the constructor for interactive elements was to create a sub-div with the “hitbox” class, thus giving them a smaller inner-div as their hitbox:



.cell > .hitbox {
  position: absolute;
  border-style: solid;
  border-width: 1px;
  /* border-color normally set to yellow to add visibility */
  border-color: transparent;
  width: 85%;
  height: 85%;
  left: 5%;
  top: 5.5%;
}


Enter fullscreen mode Exit fullscreen mode

When elements are moving and having their positions updated 20 times per second, their hitbox positions are also updated.

Collision-detection and collision events

I’ve included this link to JSFiddle before, but I’ll repeat it again: https://jsfiddle.net/jlr7245/217jrozd/3/ (thanks JR!!!)

This became my de facto goal: practice enough Javascript to intuitively understand and recreate this for my game. The snippet is an elegant vanilla JS code that moves divs around, and changes their color when a collision is detected. Collisions are detected by measuring each divs’ positions relative to each other. There are a few key points to this code:

1. this.position and posititionCreator()

JR’s code was what ultimately convinced me to refactor everything into Javascript classes. The elegance of this class and this function was something I knew I wanted to replicate myself:



class BaseDiv {
  constructor(position) {
    this.position = position;
  }
}

function positionCreator(currentDiv) {
  return {
    left: currentDiv.getBoundingClientRect().left,
    top: currentDiv.getBoundingClientRect().top,
    height: currentDiv.getBoundingClientRect().height,
    width: currentDiv.getBoundingClientRect().width
  };
}


Enter fullscreen mode Exit fullscreen mode

2. Measuring collision with four position conditionals

This code shows the conditionals checking for divs overlapping. Taken together, they determine whether two rectangular divs are touching or not:



if (currentDiv.position.left < this.moveableDiv.position.left + this.moveableDiv.position.width &&
currentDiv.position.left + currentDiv.position.width > this.moveableDiv.position.left &&
currentDiv.position.top < this.moveableDiv.position.top + this.moveableDiv.position.height &&
currentDiv.position.height + currentDiv.position.top > this.moveableDiv.position.top) {
    hasJustCollided = true;


Enter fullscreen mode Exit fullscreen mode

3. Storing all conditionals and logic/control flow in a “collider” variable

This was the final stroke of genius: create a variable that houses all the logic needed to detect a collision, and appropriately trigger the correct collision-event:



const collider = {
  moveableDiv: null,
  staticDivs: [],
  checkCollision: function() {
    let hasJustCollided = false;
    for (let i = 0; i < this.staticDivs.length; i++) {
      const currentDiv = this.staticDivs[i];
      if (currentDiv.position.left < this.moveableDiv.position.left + this.moveableDiv.position.width &&
      currentDiv.position.left + currentDiv.position.width > this.moveableDiv.position.left &&
      currentDiv.position.top < this.moveableDiv.position.top + this.moveableDiv.position.height &&
      currentDiv.position.height + currentDiv.position.top > this.moveableDiv.position.top) {
        hasJustCollided = true;
        if (!this.moveableDiv.ref.classList.contains('collision-state')) {
          this.moveableDiv.ref.classList.add('collision-state');
        }
      } else if (this.moveableDiv.ref.classList.contains('collision-state') && !hasJustCollided) {
          this.moveableDiv.ref.classList.remove('collision-state');
        }
    }
  },
};


Enter fullscreen mode Exit fullscreen mode

My challenges

With this beautiful code as a guide, I began building something similar piece-by-piece on top of my own code. Naturally, I encountered some challenges in adapting the snippet to my own code:

Unit-border collisions and unit-item collisions are very different!

In addition to the different sizes and types of collisions (afterall, units are always INSIDE the game’s borders, and thus are ALWAYS colliding according to the code above), border collisions required very different results—namely, preventing movement rather than triggering a new event.

When a unit collided with the game’s border, I wanted to stop the unit from moving further so they would stay inside the game. However, simply stopping the unit’s movement meant it got stuck—ultimately, my solution was to “bounce” the colliding unit away from the border by just a few pixels, so they could assign a new movement target without being stuck:



      let unitHitboxPosition = selectedUnit.cell.hitboxPosition
      let containerHitboxPosition = container.hitboxPosition

      // left side - extra-padding 8px, rebound 3px
      if (unitHitboxPosition.left <= containerHitboxPosition.left + 8) {
          console.log("BORDER COLLISION DETECTED!! (left)")
          selectedUnit.style.left = parseInt(getComputedStyle(selectedUnit).left.replace("px", "")) + 3 + "px"
        }
      // top side - extra-padding 10px, rebound 3px
      if (unitHitboxPosition.top <= containerHitboxPosition.top + 10) {
          console.log("BORDER COLLISION DETECTED!! (top)")
          selectedUnit.style.top = parseInt(getComputedStyle(selectedUnit).top.replace("px", "")) + 3 + "px"
        }
      // right side - extra-padding 7px, rebound -1px
      if (unitHitboxPosition.left + unitHitboxPosition.width >= containerHitboxPosition.left + containerHitboxPosition.width - 7) {
          console.log("BORDER COLLISION DETECTED!! (right)")
          selectedUnit.style.left = parseInt(getComputedStyle(selectedUnit).left.replace("px", "")) - 1 + "px"
        }
      // bottom side - extra-padding 10px, rebound -1px
      if (unitHitboxPosition.top + unitHitboxPosition.height >= containerHitboxPosition.top + containerHitboxPosition.height - 10) {
          console.log("BORDER COLLISION DETECTED!! (bottom)")
          selectedUnit.style.top = parseInt(getComputedStyle(selectedUnit).top.replace("px", "")) - 1 + "px"
        }


Enter fullscreen mode Exit fullscreen mode

Calculating collisions for discreet movements vs. fluid movements require different listeners

I’ve touched on this already, but the reason I had to recalculate unit positions and check for detections 20 times per second is due to the fluid movement that units do, as opposed to discreet jumps done in the original snippet (press an arrow key => move 5 pixels). By rechecking for collisions 20 times per second, collisions are likely to be caught fast enough to trigger events before the user notices the unit moving away from the collision.

What if some elements disappear from the board after a collision-event?

Another place that Javascript classes came in handy was the “onMap” attribute, which let me determine whether to render a cell on the board or not. To make the user experience feel more natural, I added some setTimeout() and CSS animations to those collision-events—that way, the user was seeing cool animations while the class attribute was being updated and the cell removed from the board.



function itemCollisionEvent(unitCell, itemCell) {

  if (itemCell === axeCell && unitCell === mageCell) {
    itemCell.onMap = false
    addItemToInventory(unitCell, axeCell.name)
    updateCells()
    displayTextMessage("Axe gained to your inventory!")

    itemCell.div.classList.remove('item')
    itemCell.div.classList.add('fadeout', 'special-effect')

  }
}


Enter fullscreen mode Exit fullscreen mode

I really appreciated the opportunity to practice making CSS animations and transitions that complimented the underlying code and provided a better user experience, rather than simply sitting on top of it! (Plus, it gave me a lot of appreciation for how much is happening during video game loading screens...)

Database and efficiency

I don’t have much to say about this, other than I specifically built some aspects as poorly as possibly to illustrate efficiency issues (and eventually get practice identifying ways to fix them). I wanted my game engine to not only remember unit and item locations upon refresh, but also remember the randomly-generated terrain (specifically, the integer at the end of the .png filename).

In retrospect, I now see that I could store this data as a single string of integers—but as I was creating the backend in Rails, I realized that I could experiment with the time-delays of making inefficient database calls. So instead, I coded it so that each new game immediately saves 240 lines into the Terrains table. Each of them contains only an image source url, and a game session id number for lookup—definitely inefficient!

Nonetheless, I gave myself two issues to address that I feel are microcosms of larger efficiency issues:

a. How could I design a user experience that feels smooth while querying the database and rendering the board?

When a game session is reloaded, 240 lines need to be retrieved from the database and used to redraw the map before the game starts.Ultimately, I ended up building the main menu’s transition-times around this, so that the menu hides the incomplete board while the database is being queried. This doesn’t solve the problem, but provides a smoother user experience that will work even when the underlying issue is resolved.

b. How could I efficiently destroy unused data in the database after a game session is finished?

Full disclosure, this is not functionality I have built in yet. The reason I have not deployed this on Heroku is because of database limitations—at one point, my database had over 120,000 lines just in the Terrains table! The necessity of efficiently cleaning this out became apparent after I was waiting endlessly for my seed file to delete all the current records (“endlessly” meaning four minutes exactly). This was a perfect illustration of the types of efficiency issues I began looking into during my last blog: after a certain threshold of operations to run, the increase in time became unmanageable. Truly, there’s no good time during a video game to make a player wait four whole minutes for anything!

This is another case where Javascript classes came to the rescue. Part of the endgame event is that the game session’s “complete” attribute is set to “true”, which will allow for easy identification for periodic queries to clean out the database. (My thinking is that the endgame animation is the perfect time for this to run in the background.) For games that are abandoned, I plan to use the database timestamps to clean out any game sessions that are expired, most likely 10 minutes after being created. I anticipate this pseudo-garbage-collection will spare players of those dreaded four minute wait-times.

skeleton drumming fingers, with caption “how long”

Next Steps

I do not consider this project finished! Even though it was a one-week assignment, I have been encouraged by Brian Pak to clean up and open source this engine. Here are my goals and next step related to that:

TO BE READY FOR OPEN-SOURCE:

  1. Clean up the code, add comments for clarity, and restore console.logs that generate useful debugging information (such as click-event positions).
  2. Finally write a Readme that describes how to create units, items, terrain, and collision events.
  3. Create a non-game-specific version of the engine—currently, the engine is inseparable from the proof-of-concept game I made for it, “The Fire Mage.”

TO EXPAND ON THE ENGINE:

  1. Add in the database pseudo-garbage-collection for completed and expired game sessions.
  2. Change how terrain data is saved into the database.
  3. Deploy a testable version on Heroku, and test in other browsers.
  4. (STRETCH GOAL) Use Rails’ Action Cable to enable multiplayer by allowing multiple browsers to access and update the same game session.
  5. (STRETCH GOAL) Add in basic attacking/combat functionality, in the style of the original Zelda (select item in inventory, trigger attack, render attack animation and collision events)

I hope you’ve found some useful tips for Javascript and CSS in here! Keep your eyes open for a follow-up post about open-sourcing this engine, and feel free to contribute feedback and suggestions on here or on Github directly! Once again, here's my Github link to the Javascript frontend, and here's the one to the Rails backend!

Top comments (7)

Collapse
 
flozero profile image
florent giraud

Hello i am actually trying to integrate your work in VueJS app and nodeJS / typescript backend api. I will come back when it's finish :)

Collapse
 
isalevine profile image
Isa Levine

Oooo please do! And please let me know if there's any clarification I can provide, I know the code's a little scatter-brained right now! :)

Collapse
 
flozero profile image
florent giraud

we can do some live coding if you want it could be faster :) let me now

Collapse
 
darcyrayner profile image
Darcy Rayner

This is really cool. If you want to dive off the deep end with this stuff, my all time favorite book on software engineering in general is called Game Engine Architecture. Word of warning though, it's a rabbit hole.

Collapse
 
nickitax profile image
Nick Shulhin

Awesome! Was always interested in a plain JS implementation of game logic 👍

Collapse
 
miclgael profile image
Michael Gale

Love this, thanks for the share!

Collapse
 
ananth profile image
AnanthNB

Noice!