Tuesday, October 18, 2016

Building a JavaScript 3D Minecraft Editor

I've always wanted to build a 3D game. I've just never had the time and energy to learn the intricacies of 3D programming. Then I discovered I didn't need to...

While tinkering one day, I got to thinking that maybe I could simulate a 3D environment using CSS transformations. I stumbled across an old article about creating 3D worlds with HTML and CSS.

I wanted to simulate a Minecraft world (or a tiny part of it at least). Minecraft is a sandbox game, in which you can break and place blocks. I wanted the same kind of functionality, but with HTML, JavaScript, and CSS.

Come along as I describe what I learned, and how it can help you to be more creative with your CSS transformations!

Note: Most of the code for this tutorial can be found on Github. I've tested it in the latest version of Chrome. I can't promise it will look exactly the same in other browsers, but the core concepts are universal.

This is just half of the adventure. If you'd like to know how to persist the designs to an actual server, check out the sister post, PHP Minecraft Mod. There we explore ways to interact with a Minecraft server, to manipulate it in real time and respond to user input.

The Things We're Already Doing

I've written my fair share of CSS and I've come to understand it quite well, for the purpose of building websites. But that understanding is predicated on the assumption that I'm going to be working in a 2D space.

Let's consider an example:

.tools {
  position: absolute;
  left: 35px;
  top: 25px;
  width: 200px;
  height: 400px;
  z-index: 3;
}

.canvas {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
}

Here we have a canvas element, starting at the top left corner of the page, and stretching all the way to the bottom right. On top of that, we're adding a tools element. It starts 25px from the left and 35px from the top of the page, and measures 200px wide by 400px high.

Depending on the order div.tools and div.canvas are added to the markup, it's entirely possible that div.canvas could overlap div.tools. That is except for the z-index styles applied to each.

You're probably used to thinking of elements, styled in this way, as 2D surfaces with the potential to overlap each other. But that overlapping is essentially a third dimension. left, top, and z-index may as well be renamed to x, y, and z. So long as we assume every element has a fixed depth of 1px, and z-index has an implicit px unit, we're already thinking in 3D terms.

What some of us tend to struggle with are the concepts of rotation and translation in this third dimension...

The Theory of Transformations

CSS translations duplicate this familiar functionality, in an API that extends beyond the limitations top, left, and z-index place on us. It's possible to replace some of our previous styles with translations:

.tools {
  position: absolute;
  background: green;
  /*
    left: 35px;
    top: 25px;
  */
  transform-origin: 0 0;
  transform: translate(35px, 25px);
  width: 200px;
  height: 400px;
  z-index: 3;
}

Instead of defining left and top offsets (with an assumed origin of 0px from the left and 0px from the top), we can declare an explicit origin. We can perform all sorts of transformations on this element, for which use 0 0 as the centre. translate(35px, 25px) moves the element 35px to the right and 25px down. We can use negative values to move the element left and/or up.

With the ability to define an origin for our transformations, we can start to do other interesting things as well. For example, we can rotate and scale elements:

transform-origin: center;
transform: scale(0.5) rotate(45deg);

Every element starts with a default transform-origin of 50% 50% 0, but a value of center sets x, y, and z to the equivalent of 50%. We can scale our element to a value between 0 and 1, and rotate it (clockwise) by degrees or radians. And we can convert between the two with:

  • 45deg = (45 * Math.PI) / 1800.79rad
  • 0.79rad = (0.79 * 180) / Math.PI45deg

To rotate an element anti-clockwise, we just need to use a negative deg or rad value.

What's even more interesting, about these transformations, is that we can use 3D versions of them.

Evergreen browsers have pretty good support for these styles, though they may require vendor prefixes. CodePen has a neat "autoprefix" option, but you can add libraries like PostCSS to your local code to achieve the same thing.

The First Block

Let's begin to create our 3D world. We'll start by making a space in which to place our blocks. Create a new file, called index.html:

<!doctype html>
<html>
  <head>
    <style>
      html, body {
        padding: 0;
        margin: 0;
        width: 100%;
        height: 100%;
      }

      .scene {
        position: absolute;
        left: 50%;
        top: 50%;
        margin: -192px 0 0 -192px;
        width: 384px;
        height: 384px;
        background: rgba(100, 100, 255, 0.2);
        transform: rotateX(60deg) rotateZ(60deg);
        transform-style: preserve-3d;
        transform-origin: 50% 50% 50%;
      }
    </style>
  </head>
  <body>
    <div class="scene"></div>
    <script src="http://ift.tt/2erNXZg
jquery-3.1.0.slim.min.js"></script>
    <script src="http://ift.tt/2e3DEw3
jquery.transit/jquery.transit.min.js"></script>
    <script>
      // TODO
    </script>
  </body>
</html>

Here we stretch the body to the full width and height, resetting padding to 0px. Then we create a smallish div.scene, which we'll use to hold various blocks. We use 50% left and top, as well as a negative left and top margin (equal to half the width and height) to horizontally and vertically centre it. Then we tilt it slightly (using 3D rotation) so that we have a perspective view of where the blocks will be.

Notice how we define transform-style:preserve-3d. This is so that child elements can also be manipulated in a 3D space.

The result should look something like this:

See the Pen Empty Scene by SitePoint (@SitePoint) on CodePen.

Now, let's start to add a block shape to the scene. We'll need to create a new JavaScript file, called block.js:

"use strict"

class Block {
  constructor(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;

    this.build();
  }

  build() {
    // TODO: build the block
  }

  createFace(type, x, y, z, rx, ry, rz) {
    // TODO: return a block face
  }

  createTexture(type) {
    // TODO: get the texture
  }
}

Each block needs to be a 6-sided, 3D shape. We can break the different parts of construction into methods to (1) build the whole block, (2) build each surface, and (3) get the texture of each surface.

Each of these behaviours (or methods) are contained within an ES6 class. It's a neat way to group data structures and the methods that operate on them together. You may be familiar with the traditional form:

function Block(x, y, z) {
  this.x = x;
  this.y = y;
  this.z = z;

  this.build();
}

var proto = Block.prototype;

proto.build = function() {
  // TODO: build the block
};

proto.createFace = function(type, x, y, z, rx, ry, rz) {
  // TODO: return a block face
}

proto.createTexture = function(type) {
  // TODO: get the texture
}

This may look a little different, but it's much the same. In addition to shorter syntax, ES6 classes also provide shortcuts for extending prototypes and calling overridden methods. But I digress...

Let's work from the bottom up:

createFace(type, x, y, z, rx, ry, rz) {
  return $(`<div class="side side-${type}" />`)
    .css({
      transform: `
        translateX(${x}px)
        translateY(${y}px)
        translateZ(${z}px)
        rotateX(${rx}deg)
        rotateY(${ry}deg)
        rotateZ(${rz}deg)
      `,
      background: this.createTexture(type)
    });
}

createTexture(type) {
  return `rgba(100, 100, 255, 0.2)`;
}

Each surface (or face) consists of a rotated and translated div. We can't make elements thicker than 1px, but we can simulate depth by covering up all the holes and using multiple elements parallel to each other. We can give the block the illusion of depth, even though it is hollow.

To that end, the createFace method takes a set of coordinates: x, y, and z for the position of the face. We also provide rotations for each axis, so that we can call createFace with any configuration and it will translate and rotate the face just how we want it to.

Let's build the basic shape:

Continue reading %Building a JavaScript 3D Minecraft Editor%


by Christopher Pitt via SitePoint

No comments:

Post a Comment