3Dの知識なしで挑む!変態的CSSとJSでマイクラ風3Dエディターを作った!

2016/11/13

Christopher Pitt

111

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

3Dのゲームを作るなら、3Dの知識が必要。でもCSSを応用してJavaScriptと組み合わせれば、マインクラフト風の3D表現ができるかも……?

個人的には3Dゲームを構築したいとずっと思っていましたが、複雑な3Dプログラミングを学ぶ時間とエネルギーがありませんでした。しかし後に、学ぶ必要はないと分かったのです。

ある日あれこれ操作していたら、CSS変形(transform)を使えば3D環境をシミュレートできるのでは? と考えるようになりました。そして、HTMLとCSSを使って3Dの世界を作成する古い記事に出会ったのです。

そこで私は、Minecraft(マインクラフト)の世界(あるいは少なくともそのごく一部)をシミュレートしたいと思いました。Minecraftはブロックの破壊や設置ができるサンドボックスゲームです。私は同じような機能がほしいと思いました。それも、HTML、JavaScript、CSSを使って。

この記事では、私が学んだこと、それから、CSS変形をもっとクリエイティブに使う方法を紹介します。

注意:本記事のコードのほとんどがGithubにあります。コードはChromeの最新バージョンでテストしました。ほかのブラウザーでもまったく同じである保証はできませんが、コア概念は共通しています。

この記事で説明していることは、まだ完璧なものではありません。実際のサーバーにデザインを保存する方法を知りたい場合は、関連記事の『Modding Minecraft with PHP – Buildings from Code!』(日本版編注:翻訳記事を後日公開予定)を参照してください。リアルタイムでの操作やユーザー入力の対応に必要なMinecraftサーバーとのやりとり方法を説明しています。

すでに実行していること

私はWebサイトを制作するためにこれまでCSSを書いてきて、とても正しいCSSについてよく理解するようになりました。しかしその理解は2次元空間で作業するという仮定に基づいています。

次の例を検討します。

.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;
}

例にはキャンバスとなる要素があります。ページの左上から開始し、右下まで全体に広がります。その上にツール要素を追加しています。ページの左から25px、上から35pxの位置から開始し、幅は200px、高さは400pxあります。

このCSSに対応したdiv.toolsdiv.canvasをマークアップに追加すると、div.canvasdiv.toolsに完全に重ねることができます(それぞれに適用したz-indexのスタイルは除きます)。

2D平面は互いに重なることがあるので、このようなスタイルで要素を考えることには慣れているかもしれません。しかしその重なりは本質的には3Dです。lefttopz-indexも、xyzに名前を変更できます。すべての要素に1pxの固定された深さがあり、z-indexには潜在的なpx単位があると想定するならば、すでに3Dの面で思考していることになります。

3Dでの回転と平行移動を考えると混乱して、悪戦苦闘する人も少なくないでしょう。

変形の理論

CSS translateは、lefttopz-indexの制限を越えてこれら機能をAPIで再現するものです。translateを使って、以前のスタイルを次のように部分的に交換できます。

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

(基点を左から0px、上から0pxに想定して)lefttopのオフセットを定義する代わりに、明示的な基点を宣言できます。0 0を中心に使用し、全種類の変形をこの要素で実行できます。translate(35px, 25px)は要素を右へ35px、下へ25px移動します。左および/または上への要素の移動には、負の値を使用します。

変形の基点を定義する機能により、ほかにもおもしろいことを始められます。たとえば次のように、要素を回転しスケーリングできます。

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

すべての要素はtransform-originのデフォルトである50% 50% 0からスタートしますが、centerの値はxyz50%相当に設定します。要素を01の間の値にスケーリングし、度またはラジアンで(時計回りに)回転できます。そして、以下の2つの間で変形できます。

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

要素を反時計回りに回転させるには、負のdegまたはrad値を使います。

興味深いのは、これらの変形に3Dバージョンが使用できることです。

最新版にアップデートされ続けているブラウザーには、これらのスタイルにとても良いサポートがありますが、ベンダープレフィックスが必要かもしれません。CodePenにはきちんと整理された「autoprefix」オプションがありますが、同じことを達成するためにローカルコードにPostCSSのようなライブラリーを追加できます。

最初のブロック

それでは3D世界の作成を開始します。ブロックを置くスペースの作成から始めます。新しいファイル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="https://code.jquery.com/jquery-3.1.0.slim.min.js"></script>
    <script src="http://ricostacruz.com/jquery.transit/jquery.transit.min.js"></script>
    <script>
      // TODO
    </script>
  </body>
</html>

ここではpaddingを0pxにリセットし、bodyの幅と高さをフルにします。次に、さまざまなブロックを維持するために使う小さめのdiv.sceneを作成します。水平方向と垂直方向の中央に配置するためlefttopに負のmargin(widthheightの半分に等しい)を使うのと同様に、lefttop50%を使用します。そのあと、ブロックがある位置の斜視図が描けるように(3D回転を使用して)わずかに傾けます。

transform-style:preserve-3dに注目してください。その子要素もまた、3D空間で操作できるようになります。

結果は次のようになります。

では、シーンにブロック形状を追加します。block.jsというJavaScriptファイルを作成します。

"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
  }
}

各ブロックは6面体の3D形状となる必要があります。構文は、(1)ブロック全体を構築、(2)各表面を構築、(3)各表面の質感を構築、の3つのメソッドに分解できます。

これらの振る舞い(またはメソッド)はそれぞれ、ES6クラスに含まれます。データ構造とそれらを操作するメソッドをグループ化するには、きちんと整理された方法です。次のような従来の書き方のほうがよく知っているかもしれません。

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
}

少し違って見えるかもしれませんが、ほぼ同じです。短い構文のほかに、ES6クラスは、プロトタイプを拡張して上書きされたメソッドを呼び出すためのショートカットも提供しています。本題から少しそれますが。

次のようにボトムアップから作業しましょう。

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)`;
}

各表面(または面)は回転および変形されたdivで構成されます。要素の厚さを1px以上にはできませんが、すべての穴をカバーし、互いに平行な複数の要素を使用することで、深さをシミュレートできます。それが空洞であっても、ブロックに奥行きの錯覚を与えられます。

ブロックに奥行きの錯覚を与えるため、createFaceメソッドは面の位置がxyzの座標を受け取ります。また、いかなる設定でもcreateFaceを呼び出し、思いどおりに面を変形および回転ができるように各軸に回転をつけます。

それでは、基本的な形状を構築します。

build() {
  const size = 64;
  const x = this.x * size;
  const y = this.y * size;
  const z = this.z * size;

  const block = this.block = $(`<div class="block" />`)
    .css({
      transform: `
        translateX(${x}px)
        translateY(${y}px)
        translateZ(${z}px)
      `
    });

  $(`<div class="x-axis" />`)
    .appendTo(block)
    .css({
      transform: `
        rotateX(90deg)
        rotateY(0deg)
        rotateZ(0deg)
      `
    });

  $(`<div class="y-axis" />`)
    .appendTo(block)
    .css({
      transform: `
        rotateX(0deg)
        rotateY(90deg)
        rotateZ(0deg)
      `
    });

  $(`<div class="z-axis" />`)
    .appendTo(block);
}

私たちは単一のピクセル位置の視点で考えることに慣れていますが、Minecraftのようなゲームはもっと大きなスケールで動作します。すべてのブロックが大きく、個々のピクセルによって構成するのではなく、(ゲーム内の)座標系でブロックの位置を処理します。同じような考え方を取り込みましょう。

新しいブロックが1×2×3で作成されたら、それが0px × 64px × 128pxを意味するようにしたいと思います。デフォルトサイズによって各座標を乗算します(使用するテクスチャパックでのテクスチャのサイズなので、今回の場合は64pxです)。

このあと、div.blockを呼び出すコンテナのdivを作成します。その中に3つのdivを配置します。これらはブロックの軸を表示するもので、3Dレンダリングプログラムのガイドに似ています。また、次のようにブロックに新しいCSSをいくつか追加します。

.block {
  position: absolute;
  left: 0;
  top: 0;
  width: 64px;
  height: 64px;
  transform-style: preserve-3d;
  transform-origin: 50% 50% 50%;
}

.x-axis,
.y-axis,
.z-axis {
  position: absolute;
  left: 0;
  top: 0;
  width: 66px;
  height: 66px;
  transform-origin: 50% 50% 50%;
}

.x-axis {
  border: solid 2px rgba(255, 0, 0, 0.3);
}

.y-axis {
  border: solid 2px rgba(0, 255, 0, 0.3);
}

.z-axis {
  border: solid 2px rgba(0, 0, 255, 0.3);
}

このスタイリングは、いままで見てきたものと同様です。独自の3D空間で軸がレンダリングされるように、.blocktransform-style:preserve-3dを設定することを覚えておきます。それぞれに異なる色を与え、含まれているブロックよりもやや大きくして、ブロックに側面がある場合でも表示されるようにします。

それでは、新しいブロックを作成し、div.sceneに追加します。

let first = new Block(1, 1, 1);

$(".scene").append(first.block);

結果は次のようになります。

では、面を追加します。

this
  .createFace("top", 0, 0, size / 2, 0, 0, 0)
  .appendTo(block);

this
  .createFace("side-1", 0, size / 2, 0, 270, 0, 0)
  .appendTo(block);

this
  .createFace("side-2", size / 2, 0, 0, 0, 90, 0)
  .appendTo(block);

this
  .createFace("side-3", 0, size / -2, 0, -270, 0, 0)
  .appendTo(block);

this
  .createFace("side-4", size / -2, 0, 0, 0, -90, 0)
  .appendTo(block);

this
  .createFace("bottom", 0, 0, size / -2, 0, 180, 0)
  .appendTo(block);

3D視点における私の経験が限られているので、このコードは少しトライ&エラーが必要でした。各要素はdiv.z-axis要素とまったく同じ位置で開始されます。つまり、div.blockの垂直中心で上部に面します。

「top」要素は、ブロックの半分のサイズで「up」へ変形する必要がありますが、回転の必要はありませんでした。「bottom」要素については、(x軸またはy軸に沿って)180度回転させ、ブロックの半分の大きさで下へ移動する必要がありました。

同様の考え方を使って、残りのそれぞれの側面を回転および変形しました。また、次のように対応するCSSを追加する必要もありました。

.side {
  position: absolute;
  left: 0;
  top: 0;
  width: 64px;
  height: 64px;
  backface-visibility: hidden;
  outline: 1px solid rgba(0, 0, 0, 0.3);
}

backface-visibility:hiddenの追加は、要素の「bottom(底部)」側がレンダリングされるのを防ぎます。通常はどれほど回転させても、ミラーリングのみで同じように表示されます。裏側に面が隠れていても「top(上部)」側のみがレンダリングされます。これをオンにするときは注意してください。表面を正しく回転させないと、ブロックの側面が消えます。それが理由で、側面を90/270/-90/-270回転にしました。

それでは、ブロックをもう少しリアルにします。block.dirt.jsと呼ばれる新しいファイルを作成し、createTextureメソッドを上書きします。

"use strict"

const DIRT_TEXTURES = {
  "top": [
    "textures/dirt-top-1.png",
    "textures/dirt-top-2.png",
    "textures/dirt-top-3.png"
  ],
  "side": [
    "textures/dirt-side-1.png",
    "textures/dirt-side-2.png",
    "textures/dirt-side-3.png",
    "textures/dirt-side-4.png",
    "textures/dirt-side-5.png"
  ]
};

class Dirt extends Block {
  createTexture(type) {
    if (type === "top" || type === "bottom") {
      const texture = DIRT_TEXTURES.top.random();

      return `url(${texture})`;
    }

    const texture = DIRT_TEXTURES.side.random();

    return `url(${texture})`;
  }
}

Block.Dirt = Dirt;

Sphax PureBDCraftと呼ばれる人気のテクスチャパックを使用します。商用利用をしなければ、ダウンロードと使用は無料で、さまざまなサイズで提供されています。今回はx64版を使用します。

ブロックの側面および上部のテクスチャにルックアップテーブルの定義から始めます。テクスチャパックは底部に使用するテクスチャを指定しないので、上のテクスチャを再利用します。

テクスチャを必要とする側が「top(上部)」または「bottom(底部)」なら、「top(上部)」リストからランダムなテクスチャを取得します。それを定義するまでは、ランダムなメソッドは存在しません。

Array.prototype.random = function() {
  return this[Math.floor(Math.random() * this.length)];
};

同様に、側面にテクスチャを必要とする場合もランダムなものを取得します。テクスチャはシームレスなので、ランダム化がうまく動作します。

結果は次のようになります。

シーンを作成する

それでは、ブロックをインタラクティブにするには、どうすれば良いでしょうか。シーンを使って始めるのがおすすめです。すでにシーン内にブロックを配置してきたので、今度は動的な配置を有効にします。

ブロックの平坦な表面のレンダリングから始めます。

const $scene = $(".scene");

for (var x = 0; x < 6; x++) {
  for (var y = 0; y < 6; y++) {
    let next = new Block.Dirt(x, y, 0);
    next.block.appendTo($scene);
  }
}

これでブロックの追加を始めるための平坦な表面が得られました。表面にカーソルを移動したときに強調表示するようにします。

.block:hover .side {
  outline: 1px solid rgba(0, 255, 0, 0.5);
}

しかし何か奇妙なことが起こっています。

1478221282_image3.gif

表面が互いをランダムに切り抜き表示しているから奇妙に見えるのです。この問題を解決する良い方法はありませんが、ブロックを少しスケーリングすると防止できます。

const block = this.block = $(`<div class="block" />`)
  .css({
    transform: `
      translateX(${x}px)
      translateY(${y}px)
      translateZ(${z}px)
      scale(0.99)
    `
  });

1478221282_image2.gif

これで解決したように見えますが、より多くのブロックがシーンにあると、パフォーマンスに影響します。一度にたくさんの要素をスケーリングするときは、気を付けて進めてください。

では、ブロックとタイプの各面に属するタグを付けます。

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)
    })
    .data("block", this)
    .data("type", type);
}

このあと表面をクリックすると、座標の新しいセットを導き出して新しいブロックを作成できます。

functioncreateCoordinatesFrom(side, x, y, z){if(side =="top"){

function createCoordinatesFrom(side, x, y, z) {
  if (side == "top") {
    z += 1;
  }

  if (side == "side-1") {
    y += 1;
  }

  if (side == "side-2") {
    x += 1;
  }

  if (side == "side-3") {
    y -= 1;
  }

  if (side == "side-4") {
    x -= 1;
  }

  if (side == "bottom") {
    z -= 1;
  }

  return [x, y, z];
}

const $body = $("body");

$body.on("click", ".side", function(e) {
  const $this = $(this);
  const previous = $this.data("block");

  const coordinates = createCoordinatesFrom(
    $this.data("type"),
    previous.x,
    previous.y,
    previous.z
  );

  const next = new Block.Dirt(...coordinates);

  next.block.appendTo($scene);
});

createCoordinatesFromには、シンプルですが重要なタスクがあります。側面の種類および属するブロックの座標を考慮すると、createCoordinatesFromは座標の新しいセットを返す必要があります。新しいブロックが配置される場所です。

イベントリスナーも添付しました。クリックされる各div.sideにトリガーされます。これにより、側面が属するブロックを取得して次のブロックの座標の新しいセットを導き出します。ブロックを作成してシーンに追加します。

結果を見てみると、すばらしくインタラクティブであると分かります。

ゴーストを見せる

配置する前に、配置しようとしているブロックのアウトラインを見るのは役に立ちます。これは、「ゴーストが見える」と称されることがあります。

ゴーストを見るコードは、これまでのコードと非常によく似ています。

let ghost = null;

function removeGhost() {
  if (ghost) {
    ghost.block.remove();
    ghost = null;
  }
}

function createGhostAt(x, y, z) {
  const next = new Block.Dirt(x, y, z);

  next.block
    .addClass("ghost")
    .appendTo($scene);

  ghost = next;
}

$body.on("mouseenter", ".side", function(e) {
  removeGhost();

  const $this = jQuery(this);
  const previous = $this.data("block");

  const coordinates = createCoordinatesFrom(
    $this.data("type"),
    previous.x,
    previous.y,
    previous.z
  );

  createGhostAt(...coordinates);
});

$body.on("mouseleave", ".side", function(e) {
  removeGhost();
});

主な違いは、ゴーストブロックの単一のインスタンスを維持していることです。それぞれ新しいものが作成されると、古いものが削除されます。これには、いくつかの追加のスタイルが有効です。

.ghost {
  pointer-events: none;
}

.ghost .side {
  opacity: 0.6;
  pointer-events: none;
  -webkit-filter: brightness(1.5);
}

アクティブなままにします。ゴーストの要素に関連づけられたポインターイベントは、側面下部のmouseentermouseleaveイベントの効力を弱めます。ゴースト要素と相互に作用する必要はないので、これらのポインタイベントは無効にします。

この結果はすばらしいです。

視点を変える

インタラクティビティを追加すればするほど、なにが起こっているか確認するのが難しくなります。なんとかしたほうが良さそうです。なにが起こっているのか確認できるようにするには、ビューポートをズームし回転させられると良いのです。

では、ズームから始めます。多くのインターフェイス(とゲーム)は、マウスホイールをスクロールしてビューポートのズーム操作をします。異なるブラウザーはマウスホイールイベントを異なる方法で処理するので、抽象化されたライブラリーの使用は理にかなっています。

インストールされたら、イベントにフックできます。

let sceneTransformScale = 1;

$body.on("mousewheel", function(event) {
  if (event.originalEvent.deltaY > 0) {
    sceneTransformScale -= 0.05;
  } else {
    sceneTransformScale += 0.05;
  }

  $scene.css({
    "transform": `
      scaleX(${sceneTransformScale})
      scaleY(${sceneTransformScale})
      scaleZ(${sceneTransformScale})
    `
  });
});

1478221282_image4.gif

これで、マウスホイールをスクロールしてシーン全体の規模を制御できます。残念ながら、制御する瞬間は回転が上書きされます。調整はマウスでビューポートをドラッグするので、回転を考慮に入れる必要があります。

let sceneTransformX = 60;
let sceneTransformY = 0;
let sceneTransformZ = 60;
let sceneTransformScale = 1;

const changeViewport = function() {
  $scene.css({
    "transform": `
      rotateX(${sceneTransformX}deg)
      rotateY(${sceneTransformY}deg)
      rotateZ(${sceneTransformZ}deg)
      scaleX(${sceneTransformScale})
      scaleY(${sceneTransformScale})
      scaleZ(${sceneTransformScale})
    `
  });
};

この関数はシーンのスケール係数だけでなく、xyzの回転係数を考慮します。また、ズーム操作のイベントリスナーを変更する必要があります。

$body.on("mousewheel", function(event) {
  if (event.originalEvent.deltaY > 0) {
    sceneTransformScale -= 0.05;
  } else {
    sceneTransformScale += 0.05;
  }

  changeViewport();
});

シーンの回転を開始するには、次のイベントリスナーが必要です。

  1. ドラッグアクションが開始したときのイベントリスナー
  2. マウスを移動するときのイベントリスナー(ドラッグ中)
  3. ドラッグアクションが停止したときのイベントリスナー

対応するには、ちょっとしたコツが必要です。

Number.prototype.toInt = String.prototype.toInt = function() {
  return parseInt(this, 10);
};

let lastMouseX = null;
let lastMouseY = null;

$body.on("mousedown", function(e) {
  lastMouseX = e.clientX / 10;
  lastMouseY = e.clientY / 10;
});

$body.on("mousemove", function(e) {
  if (!lastMouseX) {
    return;
  }

  let nextMouseX = e.clientX / 10;
  let nextMouseY = e.clientY / 10;

  if (nextMouseX !== lastMouseX) {
    deltaX = nextMouseX.toInt() - lastMouseX.toInt();
    degrees = sceneTransformZ - deltaX;

    if (degrees > 360) {
        degrees -= 360;
    }

    if (degrees < 0) {
        degrees += 360;
    }

    sceneTransformZ = degrees;
    lastMouseX = nextMouseX;

    changeViewport();
  }

  if (nextMouseY !== lastMouseY) {
    deltaY = nextMouseY.toInt() - lastMouseY.toInt();
    degrees = sceneTransformX - deltaY;

    if (degrees > 360) {
        degrees -= 360;
    }

    if (degrees < 0) {
        degrees += 360;
    }

    sceneTransformX = degrees;
    lastMouseY = nextMouseY;

    changeViewport();
  }
});

$body.on("mouseup", function(e) {
  lastMouseX = null;
  lastMouseY = null;
});

mousedownで最初のマウスのxy座標を取得します。(ボタンが押されている状態なら)マウスが移動すると、スケーリングされた量だけsceneTransformZsceneTransformXを調整します。値が360度以上または0度以下になっても問題はありませんが、画面上でレンダリングしたい場合、見た目がひどくなります。

mousemoveイベントリスナー内部の計算結果は、リスナーがどれだけトリガーされるかで計算に時間がかかる可能性があります。画面上には潜在的に数百万のピクセルがあり、リスナーはそれぞれにマウスを移動するとトリガーされます。それが理由で、マウスボタンが押されていない場合は早期終了します。

マウスボタンが離されたたらmousemoveリスナーが計算を停止するようにlastMouseXlastMouseYの設定解除をします。lastMouseXだけを消去できますが、両方とも消すと、さらにすっきりします。

残念ながら、マウスダウンイベントはブロック側のクリックイベントを妨害することがあります。次のようにイベントのバブリングを防止することで、回避できます。

$scene.on("mousedown", function(e) {
  e.stopPropagation();
});

回転させます。

ブロックを削除する

ブロックを取り除く機能を追加して、実験を締めくくります。細かいことですが重要なことをいくつか実施します。

  1. hoverのボーダーカラーを緑から赤に変更
  2. ブロックゴーストの無効

追加(通常)モードまたは減算モードを示すbody classがあれば、CSSでの操作が簡単になります。

$body.on("keydown", function(e) {
  if (e.altKey || e.controlKey || e.metaKey) {
    $body.addClass("subtraction");
  }
});

$body.on("keyup", function(e) {
  $body.removeClass("subtraction");
});

修飾キー(altcontrolcommand)を使うと、このコードがbodysubtractionクラスがあるかを確認できます。次のクラスを使用するとさまざまな要素をターゲットにするのが簡単になります。

.subtraction .block:hover .side {
  outline: 1px solid rgba(255, 0, 0, 0.5);
}

.subtraction .ghost {
  display: none;
}

1478221282_image5.gif

異なるオペレーティングシステムは異なる修飾子を妨害するので、修飾キーの数をチェックしています。たとえば、controlKeyはUbuntuで動作するのに対し、altKeymetaKeyはMacOSで動作します。

減算モードのときにブロックをクリックする場合は、次のように削除する必要があります。

$body.on("click", ".side", function(e) {
  const $this = $(this);
  const previous = $this.data("block");

  if ($body.hasClass("subtraction")) {
    previous.block.remove();
    previous = null;
  } else {
    const coordinates = createCoordinatesFrom(
      $this.data("type"),
      previous.x,
      previous.y,
      previous.z
    );

    const next = new Block.Dirt(...coordinates);
    next.block.appendTo($scene);
  }
});

これは以前と同じ.sideクリックイベントリスナーですが、側面がクリックされたときに新しいブロックを追加する代わりに、減算モードになっていないかまず確認してください。減算モードの場合はクリックしたブロックがシーンから削除されます。

最終デモ

最終デモは実にすばらしい使い心地です。

Minecraftと同じくらい多くのブロックや相互作用をサポートするまでは長い道のりですが、これは良いスタートです。さらに重要なのは、高度な3D技術を学ぶことなく達成できたことです。これは、型にはまらない(そしてクリエイティブな)CSS変形の利用法です。

Minecraftのサーバーを利用する上でPHPの専門家である必要はありません。その知識を使ってできるすばらしいものを想像してみてください。

まだ本記事では完成していないということを忘れないでください。実際のサーバーにデザインを永続化する方法を知りたい場合は、関連記事の『Modding Minecraft with PHP – Buildings from Code!』を参照してください。リアルタイムでの操作やユーザー入力の対応に必要なMinecraftサーバーとのやりとり方法を解説します。

※本記事はPaul O’Brienが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Building a JavaScript 3D Minecraft Editor

[翻訳:柴田理恵/編集:Livit

Copyright © 2016, Christopher Pitt All Rights Reserved.

Christopher Pitt

Christopher Pitt

ライター兼プログラマーでSilverStripeに勤務しています。普段はアプリケーションアーキテクチャーに取り組んでいますが、ときどきコンパイラーやロボットを作ることもあります。

Loading...