へにょりレーザーとは、タイトーSTGや東方project作品でよく見かけるぐにゃーっと曲がる謎レーザーの俗称です。
これを一から実装するとなると結構大変ですが、pixi.jsには簡単に実装するためのクラスがありました。
それがPIXI.Rope(PIXI.mesh.Rope)クラスです。
これはテクスチャをムカデのように多関節化して、グネグネうねらせることのできるクラスです。サンプルを見れば、どういう機能かは何となく分かると思います。
環境等
- pixi.js v4.8.1
- webGLが使えるブラウザ
- es2015 classが使える・分かる
- [推奨] generator functionが使える・分かる
基本:マウストラッカー
まず理解のため、新体操のリボンのごとくマウスの軌跡に合わせて描画されるトラッカーエフェクトを作ってみます。
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を追加することでへにょりレーザーっぽくなります。
テクスチャは右側を頭、左端がしっぽになります。(下の画像は自由に使ってもらって大丈夫です)
もしくはGraphicsクラスを使って動的に楕円を生成して使うこともできます。
動きのプログラムについて
では次にどうやってうまいこと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, degree) {
let counter = 0;
// 一度に回転する角度を計算
const turnUnit = degree * PIXI.DEG_TO_RAD / duration;
while (counter < duration) {
this.vector.rotate(turnUnit);
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();
}
}
上の例ではレーザーは出現位置から30フレーム右に真っすぐ進んだ後、30フレームかけて180度転回し、その後左に退場するような軌跡を描きます。
起点となるのがmoveというgenerator関数です。
moveの最初にgoStraightという、vectorで指定した方向にpointを追加してまっすぐ進むルーチンに入ります。このルーチンでは毎フレーム実行されるtick関数でジェネレーター関数のnextを実行するたび、一段階ずつwhile内のpushPoint処理が進みます。
内部カウンタ(counter)が、指定duration(フレーム数)を超えるとgoStraightルーチンを抜けて、次のturnAroundというルーチンに入ります。
ルーチンを全て抜けるとmoveの処理が終了し(_currentGenにnullが入り)、それ以上更新しなく(移動しなく)なります。
(ここは必要に応じてremoveChild等する)
メインスクリプト
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してから…」のような処理をgenerator functionに頼らずにやる場合は何らかのライブラリに頼る事になりそうです。
(あまり詳しくないけど)それ系のライブラリの例としては弾幕プログラム用のbulletml.js等があります。