JavaScript
generator
pixi.js
pixijs
ゲームプログラミング

へにょりレーザーとは、タイトーSTGや東方project作品でよく見かけるぐにゃーっと曲がる謎レーザーの俗称です。

jyunsui-kyouki-modiki.gif

実行結果

これをゲーム等で一から実装するとなると結構大変ですが、pixi.jsには簡単に実装するためのクラスがありました。
それがPIXI.Rope(PIXI.mesh.Rope)なるクラスです。

これはテクスチャをムカデのように多関節化して、グネグネうねらせることのできるクラスです。公式のサンプルを見れば、どういう機能かは何となく分かると思います。

環境等

  • pixi.js v4.8.1
  • webGLが使えるブラウザ
  • es2015 classが使える・分かる
  • [推奨] generator functionが使える・分かる

基本:マウストラッカー

まず理解のため、新体操のリボンのごとく、マウスの軌跡に合わせて描画されるトラッカーエフェクトを作ってみます。

pixi-mouse-track.gif

実行結果

Ropeクラスを拡張して軌跡部分となるSnakeクラスを作成します。

// Snake.js

/**
 * @class Snake
 * @param {PIXI.Texture} texture
 * @param {number} [segmentNum] - 体節の数、0は無効
 */
class Snake extends PIXI.mesh.Rope {
  constructor(texture, segmentNum) {
    segmentNum = segmentNum || 10;

    /* 関節点を生成 */
    var points = [];
    for (var i = 0; i < segmentNum; i++) {
      points.push(new PIXI.Point(0, 0));
    }
    super(texture, points);
  }

  pushPoint(x, y) {
    // 先頭に新しいpoint追加
    this.points.unshift(new PIXI.Point(x, y));
    // 後ろの古いpointは取り除く
    this.points.pop();
  }

}

Snakeクラスは引数としてtextureとsegmentNumを取ります。
textureはそのまんまtextureクラスのことですが、segmentNumは体節数(関節の数)となります。増やすとより長く、キモい感じになります。

pushPointは先頭位置に指定したpointを追加し、押し出す形で最後尾のpointを除きます。こちらで適宜pointを追加することでテクスチャが蛇のように描画されます。

メインスクリプト

// main.js

window.onload = function() {
  /* アプリ土台作成 */
  var SCREEN_WIDTH = 512;
  var SCREEN_HEIGHT = 512;
  var app = new PIXI.Application(SCREEN_WIDTH, SCREEN_HEIGHT);
  document.body.appendChild(app.view);

  /* Graphicsからtexture生成 */
  var texture = new PIXI.Graphics()
  .beginFill(0xff22aa)
  .drawRect(0, 0, 1, 12) // 幅はあまり関係ないのでテキトー
  .endFill()
  .generateCanvasTexture();

  /* インスタンス生成 */
  var strip = new Snake(texture, 14);
  app.stage.addChild(strip);

  /* update */
  var mousePos = app.renderer.plugins.interaction.mouse.global;
  app.ticker.add(function(time) {
    /* マウス位置を毎フレーム追加する */
    strip.pushPoint(mousePos.x, mousePos.y);
  });
};

textureにはGraphicsクラスで動的に生成した幅1px 高さ12pxの矩形を使います。(幅に関してはどんな値にしても見た目は変わりません)

後は毎フレーム、マウス位置をpushPointしているだけで、先程のアニメーションのような動きになります。

本題:へにょりレーザー(CurvyLaser)の作成

Snakeクラスのtextureにそれっぽい画像を用意し、pushPointsでマウス位置ではなく、進行方向に応じたpointを追加することでへにょりレーザーっぽくなります。

テクスチャは右側を頭、左端がしっぽになります。(下の画像は自由に使ってもらって大丈夫です)
laser.png

動きのプログラムについて

では次にどうやってうまいことpointを毎フレームごとに追加し、それっぽい動きをさせるか?

色々方法は考えられますが、比較的コードの見通しがよくなりそうなのでgenerator functionを使ってみます。

SnakeクラスをベースにCurvyLaserクラスを作ります。

/* PIXI.Pointを拡張してベクトル回転機能をつける */
PIXI.Point.prototype.rotate = function(radian) {
  var preX = this.x;
  var preY = this.y;
  this.x = preX * Math.cos(radian) - preY * Math.sin(radian);
  this.y = preX * Math.sin(radian) + preY * Math.cos(radian);
}

/**
 * へにょりレーザークラス
 */
class CurvyLaser extends Snake {
  constructor(texture) {
    super(texture, 64);
    this.vector = new PIXI.Point(2, 0)
    this._currentGen  = this.move();
  }

  *move() {
    yield* this.goStraight(30);
    yield* this.turnAround(30, 180);
    yield* this.goStraight(300);
    return null;
  }

  *goStraight(duration) {
    let counter = 0;
    while (counter < duration) {
      let nextX = this.points[0].x + this.vector.x;
      let nextY = this.points[0].y + this.vector.y;
      yield this.pushPoint(nextX, nextY);
      counter++;
    }
  }

  *turnAround(duration, angle) {
    let counter = 0;
    while (counter < duration) {
      this.vector.rotate(angleDelta);
      let nextX = this.points[0].x + this.vector.x;
      let nextY = this.points[0].y + this.vector.y;
      yield this.pushPoint(nextX, nextY);
      counter++;
    }
  }

  tick() {
    if (this._currentGen) this._currentGen.next();
  }
}

pixi-henyori-sample.gif

上の例ではレーザーは出現位置から30フレーム右に真っすぐ進んだ後、30フレームかけて180度転回し、その後左に退場するような軌跡を描きます。

起点となるのがmoveというgenerator関数です。
moveの最初にgoStraightという、vectorで指定した方向にpointを追加してまっすぐ進むルーチンに入ります。このルーチンでは毎フレーム実行されるtick関数でジェネレーター関数のnextを実行するたび、一段階ずつwhile内のpushPoint処理が進みます。

内部カウンタ(counter)が、指定duration(フレーム数)を超えるとgoStraightルーチンを抜けて、次のturnAroundというルーチンに入ります。

ルーチンを全て抜けるとmoveの処理が終了し(_currentGenにnullが入り)、それ以上更新しなく(移動しなく)なります。
(ここで必要に応じてremove等する)

メインスクリプト

window.onload = function() {
  /* アプリ土台作成 */
  var SCREEN_WIDTH = 512;
  var SCREEN_HEIGHT = 512;
  var app = new PIXI.Application(SCREEN_WIDTH, SCREEN_HEIGHT);
  document.body.appendChild(app.view);

  /* textureのセットアップ */
  const texture = PIXI.Texture.fromImage('laser.png');

  /* laserの用意 */
  const laser = new CurvyLaser(texture);
  app.stage.addChild(laser);

  /* laserの更新 */
  app.ticker.add(function(time) {
    laser.tick();
  });
};

複数のレーザーを規則正しく並べたり、動きを複雑にしていくと記事冒頭のgifアニメのような感じになります。

当たり判定のとり方

最後に当たり判定ですが、「メッシュ内に指定した点が含まれているか」を判定してくれるこれまためっちゃ便利なcontainsPointというメソッドがあるので、これを利用するのが手軽です。

containsPointの引数にはPIXI.Pointインスタンス(position等)を渡します。

const laser = new CurvyLaser(texture);
const player = PIXI.Sprite.fromImage('assets/player.png')

if (laser.containsPoint(player.position)) {
  console.log('被弾!');
}

この場合、細かく設定ができなかったり、カスり判定(グレイズ)とかは検知できませんが...。
しっかりやるならpoint同士でつないだ線分との距離から判定するのがいいんじゃないすかね(雑)。

その他

  • へにょりレーザー楽しい(けど実際にゲームに出てくるのは超苦手)
  • 「○○秒(フレーム)かけてxxという処理をする」みたいなのは何らかのライブラリに頼ったほうが良いかもしれない。 (あまり詳しくないけど)それ系のライブラリの例としては弾幕プログラム用のbulletml.js等があります。