Pixi.js でパララックススクローラーを作ってみる【反省】

反省

チュートリアルを見ながら作ってましたが、理解間違ってるところあるじゃないかなと思って、part1から勉強し直しました。
そして、やっぱりミスを発見しました。


pixi.jsの最初のkittykatattackさんのチュートリアル
GitHub - kittykatattack/learningPixi: A step-by-step introduction to making games and interactive media with the Pixi.js rendering engine.

で学んだことを生かして、Christopher Calebさんのチュートリアル
Building a Parallax Scroller with Pixi.js: Part 1

を改造しながら、勉強したいと思ってました。


しかし、中途半端な理解で新しいことを取り込もうとしたら、あまりきれいじゃないコード書いちゃって、間違ってるコードもありました。
普通に動作はしますが、余計なものも入ってました。しかもブログで公開しちゃいました。 チュートリアルとしては一番やっちゃいけないことをやってしまいました。 ご迷惑をかけてしまって、誠に申し訳ございません。

今できることは、 このシリーズの全コードも見直し、もう一回正しいコードを公開したいと思います。既に前に公開したコードを読んだ方、申し訳ないのですが、一緒にコードを見直していきましょう。

Pixi.js でパララックススクローラーを作ってみる【第三回】

前回の話

前回は初回で作ったもののリニューアルです。見た目や動き自体変わりがありません。
今回はチュートリアルのpart2の続きです。今まで作ってきたものをメソード化していきます。

Building a Parallax Scroller with Pixi.js: Part 2

続き

先ほどFarとMidはPIXI.extras.TilingSprite.prototypeから引き継いだので、今の全体像はこうなってます。
f:id:manmanrai:20170429202045p:plain そしてコードの全体はこんな感じです。

var Container = PIXI.Container,
    autoDetectRenderer = PIXI.autoDetectRenderer,
    loader = PIXI.loader;

var renderer = autoDetectRenderer(512, 384, {antialiasing: true}),
    stage = new Container();
document.body.appendChild(renderer.view);

loader
    .add(['images/bg-far.png', 'images/bg-mid.png'])
    .load(setup);
var far, mid;

function setup(){
  far = new Far();
  stage.addChild(far);

  mid = new Mid();
  stage.addChild(mid);

  requestAnimationFrame(update);
}

function update(){

  far.update();
  mid.update();

  renderer.render(stage);

  requestAnimationFrame(update);
}

function Far(){
    var texture = PIXI.Texture.fromImage("images/bg-far.png");
    PIXI.extras.TilingSprite.call(this,texture, 512, 256);
    this.position.x = 0;
    this.position.y = 0;
    this.tilePosition.x = 0;
    this.tilePosition.y = 0;
}

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
Far.prototype.update = function(){
    this.tilePosition.x -= 0.128;
};

function Mid(){
    var texture = PIXI.Texture.fromImage("images/bg-mid.png");
    PIXI.extras.TilingSprite.call(this,texture, 512, 256);
    this.position.x = 0;
    this.position.y = 128;
    this.tilePosition.x = 0;
    this.tilePosition.y = 0;
}

Mid.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
Mid.prototype.update = function(){
    this.tilePosition.x -= 0.64;
};

次はスクローラー自体をクラス化していきます。

function Scroller(stage) {
}

このクラス自体には二つの問題が存在しています。まず、rendererへに繋がってないこと、二つ目は、どのプロトタイプから引き継いでないこと。
MidとFarを利用してアウトプットできるように導入します。

function Scroller(stage) {

  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);
}

そしてupdateもScroller functionの後に追加します。

Scroller.prototype.update = function() {
  this.far.update();
  this.mid.update();
};

前のsetup functionをこのように書き換えられます。
midとfarに対しての操作じゃなくなるってことですね。

function setup(){
  //far = new Far();
  //stage.addChild(far);

  //mid = new Mid();
  //stage.addChild(mid);
  scroller = new Scroller(stage);

  requestAnimationFrame(update);
}

同じ今までmidとfarこの二つを操作してたupdate functionもこう書き換えます。

function update(){

  //far.update();
  //mid.update();
  scroller.update();

  renderer.render(stage);

  requestAnimationFrame(update);
}

ブラウザ開くと、動作変わりがないのかもう一回確認します。問題なければ次へ進みます。

Viewport表示域

まず、ScrollerのupdateをsetViewportXに書き換えます。

function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);
}

// Scroller.prototype.update = function() {
//   this.far.update();
//   this.mid.update();
// };
Scroller.prototype.setViewportX = function(viewportX) {
  this.far.setViewportX(viewportX);
  this.mid.setViewportX(viewportX);
};

次はFarとMidのupdateも書き換えます。

function Far() {
  var texture = PIXI.Texture.fromImage("resources/bg-far.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 0;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
 
  // 追加する
  this.viewportX = 0;
}

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

// Far.prototype.update = function() {
//   this.tilePosition.x -= 0.128;
// };

// 追加する
Far.DELTA_X = 0.128;
Far.prototype.setViewportX = function(newViewportX) {
  var distanceTravelled = newViewportX - this.viewportX;
  this.viewportX = newViewportX;
  this.tilePosition.x -= (distanceTravelled * Far.DELTA_X);
};

最後にupdate functionにも更新します。

function update() {
  // scroller.update();

  renderer.render(stage);

  requestAnimationFrame(update);
}

ブラウザを開くとアニメーション効果がなくなり、静止画面になってます。
そこでコンソール開きます。

scroller.setViewportX(50);

入れると、画像が少しずらしたこと分かります。

scroller.setViewportX(7000);

setViewportXをScrollerに任せる

先までx軸の値はMidとかFarで初期設定しましたが、今度からはscrollerで操作するように変えていきます。

function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);

  // 追加する
  this.viewportX = 0;
}

Scrollerのupdateにも

Scroller.prototype.setViewportX = function(viewportX) {
  // 追加する
  this.viewportX = viewportX;

  this.far.setViewportX(viewportX);
  this.mid.setViewportX(viewportX);
};

そして、Scrollerのprototypeもう一個追加します。

Scroller.prototype.getViewportX = function() {
  return this.viewportX;
};

次はupdate function内部で

function update(){

  // 追加する
  var newViewportX = scroller.getViewportX() + 5;
  scroller.setViewportX(newViewportX);
  // 追加する

  renderer.render(stage);
  requestAnimationFrame(update);
}

ブラウザ開くと結構早めなスピードで動いています。
最後にさっき追加したものをprototypeとしてScrollerの下に付けます。

Scroller.prototype.moveViewportXBy = function(units) {
  var newViewportX = this.viewportX + units;
  this.setViewportX(newViewportX);
};

そして、updateにそう書き換えます。

function update() {
  // var newViewportX = scroller.getViewportX() + 5;
  // scroller.setViewportX(newViewportX);
  scroller.moveViewportXBy(5);

  renderer.render(stage);

  requestAnimationFrame(update);
}

コード全体的に簡潔ように見えてきました。
part2意外に長かったのです、読むだけでも結構時間かかりました。

次はpart3向かって進みたいと思います。
ではまた次回で。

Pixi.js でパララックススクローラーを作ってみる【第二回】

前回の話

前回は二枚の画像を重ねさせ、動かしました。
今回はそれの進化型です。

Building a Parallax Scroller with Pixi.js: Part 2

構造の理解

前回に出てきた二種類スプライト、スプライトとタイリングスプライトなんですが、どちらもwidth, height, alpha, positionなどの設定できます。しかもどちらもaddChild function使って、containerの中に簡単に入れることできます。

承継(しょうけい)のルール通用しますので、それがこんな感じで引き継ぐらしいです。 f:id:manmanrai:20170429135631p:plain

承継を利用したプロトタイプ作成

前回tilingSpriteを作る時何回も重複したコードは:

// 画像を読み込む
var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");  

// タイリングスプライトとして貼り付ける
far = new PIXI.extras.TilingSprite(farTexture, 512, 256);

// タイリングスプライトの位置を決める
far.position.x = 0;
far.position.y = 0;

// タイルを貼る位置を決める
far.tilePosition.x = 0;
far.tilePosition.y = 0;

// コンテナに追加する
stage.addChild(far);

という流れでした。 この流れをプロトタイプとして、カスタマイズできるように独立させ

far = new Far();
stage.addChild(far);

これが今回の目標です。

スタート

まず、function Farを作成します。

function Far(texture, width, height) {
  PIXI.extras.TilingSprite.call(this, texture, width, height);
}

そして、このfunction Farの次に原型(どこから引き継いて来たなのか)を表記します。

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

それでsetupの中身をこういう風に書き換えられます。

  var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");

  // far = new PIXI.extras.TilingSprite(farTexture, 512, 256);
  far = new Far(farTexture, 512, 256);

  far.position.x = 0;
  far.position.y = 0;
  far.tilePosition.x = 0;
  far.tilePosition.y = 0;
  stage.addChild(far);

次は位置の設定もfunction Farに入れときます。

function Far(texture, width, height) {
  PIXI.extras.TilingSprite.call(this, texture, width, height);

  this.position.x = 0;
  this.position.y = 0;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}

そうしたら、

  var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");

  // far = new PIXI.extras.TilingSprite(farTexture, 512, 256);
  far = new Far(farTexture, 512, 256);

  // far.position.x = 0;
  // far.position.y = 0;
  // far.tilePosition.x = 0;
  // far.tilePosition.y = 0;
  stage.addChild(far);

コメントアウトした部分もいらないので、setupの中で3行で終わらせます。
最後に、このfarプロトタイプはbg-far.pngを呼び出すために存在するので、いっそう最初の読み込み入れれば良いです。
widthとheightも同時に指定します。

function Far(texture, width, height) {
  var texture = PIXI.Texture.fromImage("resources/bg-far.png");
  PIXI.extras.TilingSprite.call(this, texture,512, 256));

  this.position.x = 0;
  this.position.y = 0;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}

で、setup内に はこうなります。

  far = new Far();
  stage.addChild(far);

farを設置するコードは2行で済ませました。
次はmidにも同じようなことを。

function Mid() {
  var texture = PIXI.Texture.fromImage("resources/bg-mid.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 128;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}

Mid.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

setup function:

mid = new Mid();
stage.addChild(mid);

そして、update function内のコードもまとめて整理しましょう。

function update(){
  far.tilePosition.x -= 0.128;
  mid.tilePosition.x -= 0.64;

  renderer.render(stage);
  requestAnimationFrame(update);
}

そこのtilePositionをfunction Farの原型のところに追加します。

function Far(){
  // ここ省略します。
}
Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
Far.prototype.update = function() {
  this.tilePosition.x -= 0.128;
};

midにも同じ作業書き換えます。

最後の本体のupdate functionはこうなります。

function update() {
  far.update();
  mid.update();

  renderer.render(stage);

  requestAnimFrame(update);
}

f:id:manmanrai:20170429201417p:plain 動作確認してみたら、動き大丈夫そうですね。 チュートリアルのpart2意外に長くなりそうなので、一旦ここで切らせていただきます。
では、また次回にで。

Pixi.js でパララックススクローラーを作ってみる【第一回】

背景

Pixi.jsの公式サイトにはおすすめのチュートリアル幾つか載せています。

Tutorials - PixiJS v4

最近書き終えたばっかのこのシリーズ

pixi.js カテゴリーの記事一覧 - manmanrai’s diary

はこのチュートリアルを参照したものになります。

GitHub - kittykatattack/learningPixi: A step-by-step introduction to making games and interactive media with the Pixi.js rendering engine.

もっともっと練習したいので、チュートリアル第二弾を続けやりたいと思います!

ゴール

前後二枚画像が違うスピードで動くのは目標です。
ここのスクローラーとスクロールとは違い、ただ巻いてるに見えるとのことだと思います。
アニメーションという認識でも良いかと。

材料

参考してるサイトはこちらです。

Building a Parallax Scroller with Pixi.js: Part 1

画像の素材(2枚のみ)はこちらかからダウンロードできます。

http://www.yeahbutisitflash.com/pixi-parallax-scroller/tutorial-1/resources.zip

スタート

なんか書き方少し違いますので、自分に合うように調整しながら整理します。
まず、htmlとpixi.js(CDNで)などのを用意します。

<html>
  <head>
    <meta charset="UTF-8">
    <title>Endless Runner Game Demo</title>
  </head>
  <body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.4.3/pixi.min.js"></script>
  <script>
    // あとはここを書きます。
  </script>
  </body>
</html>

ここで準備できたら、scriptの中身入ります。
pixi.jsのベースを書きます。

var Container = PIXI.Container,
    autoDetectRenderer = PIXI.autoDetectRenderer,
    loader = PIXI.loader,
    resources = PIXI.loader.resources,
    TextureCache = PIXI.utils.TextureCache,
    Sprite = PIXI.Sprite;

var renderer = autoDetectRenderer(512, 384, {antialiasing: true}),
    stage = new Container();
document.body.appendChild(renderer.view);

renderer.render(stage);

この時点出力したcanvasの画面はこうなります。(真っ黒です。)
f:id:manmanrai:20170428141432p:plain 真っ黒なcanvas以外に何にも入ってません。

そして、チュートリアル用意してくれた背景画像を読み込みましょう。

var Container = PIXI.Container,
    autoDetectRenderer = PIXI.autoDetectRenderer,
    loader = PIXI.loader,
    resources = PIXI.loader.resources,
    TextureCache = PIXI.utils.TextureCache,
    Sprite = PIXI.Sprite;

var renderer = autoDetectRenderer(512, 384, {antialiasing: true}),
    stage = new Container();
document.body.appendChild(renderer.view);

// ここから追加する
loader
    .add('images/bg-far.png')
    .load(setup);

function setup(){
  var bgFar = new Sprite(resources["images/bg-far.png"].texture);
  stage.addChild(bgFar);

  renderer.render(stage);
  // ↑これをfunction setupの中に移動する
}
// ここまで追加する

背景入れたらこうなります。
f:id:manmanrai:20170428141826p:plain 元々画像の高さ足りないので、むしろこれで十分です。

次は二枚目画像を追加すると、

// 省略

// もう一枚をloaderの中に入れる
loader
    .add(['images/bg-far.png', 'images/bg-mid.png'])
    .load(setup);

function setup(){
  var bgFar = new Sprite(resources["images/bg-far.png"].texture);
  stage.addChild(bgFar);

  // 新しい一枚を呼び出す、高さを設定する
  var bgMid = new Sprite(resources["images/bg-mid.png"].texture);
  bgMid.y = 128;
  stage.addChild(bgMid);

  renderer.render(stage);
}

結果はこうなります。
f:id:manmanrai:20170428144123p:plain

基本ループ

動かします。

// 変数を外に出した
var bgFar, bgMid;

function setup(){
  bgFar = new Sprite(resources["images/bg-far.png"].texture);
  stage.addChild(bgFar);

  bgMid = new Sprite(resources["images/bg-mid.png"].texture);
  bgMid.y = 128;
  stage.addChild(bgMid);

  // 元々ここにあるrendererをupdate functionへ移動
  requestAnimationFrame(update);
}

function update(){
  // 左へ
  bgFar.position.x -= 0.128;
  bgMid.position.x -= 0.64;

  renderer.render(stage);

  requestAnimationFrame(update);
}

f:id:manmanrai:20170428150847p:plain 開くと画像どんどん左へずらしていく、最後二枚とも画面から消えていきます。

画像パターン化(タイル化)(TilingSpriteタイリングスプライト)

この二枚の画像実はフォトショップのパターンみたいな設計になってるんです。タイルみない横に貼っても繋ぎ目がない完璧なパターン(タイル)です。
でもcssみたいにrepeat-xにしたいのは難しい、最初の画像を読み込みの時にfunctionを変えなければいけません。

ひとまず、setup function内にはこうなります。

function setup(){
  // bgFar = new Sprite(resources["images/bg-far.png"].texture);
  var bgFar = PIXI.Texture.fromImage("images/bg-far.png");

  // PIXI.extras.TilingSprite(texture, width, height);
  far = new PIXI.extras.TilingSprite(bgFar, 512, 256);

  // 貼る時の位置
  far.tilePosition.x = 0;
  far.tilePosition.y = 0;
  stage.addChild(far);

  // bgMid = new Sprite(resources["images/bg-mid.png"].texture);
  var bgMid = PIXI.Texture.fromImage("images/bg-mid.png");

  // PIXI.extras.TilingSprite(texture, width, height);
  mid = new PIXI.extras.TilingSprite(bgMid, 512, 256);

  mid.y = 128;

  // 貼る時の位置
  mid.tilePosition.x = 0;
  mid.tilePosition.y = 0;
  stage.addChild(mid);

  requestAnimationFrame(update);
}

次、移動させるupdate functionも書き換えます。

function update(){
  // far.position.x -= 0.128;
  // mid.position.x -= 0.64;

  // タイルの位置
  far.tilePosition.x -= 0.128;
  mid.tilePosition.x -= 0.64;

  renderer.render(stage);

  requestAnimationFrame(update);
}

そしたら、動き始めました!!
f:id:manmanrai:20170428171503p:plain

最後に

Class: TilingSprite

公式ドキュメントでタイリングスプライトについて検索してみました。ご参考になった幸いでございます。

第二回はこれの進化バージョンを作りたいと思います。

2D WebGL renderer Pixi.js v4【連載最終回】

前回の話

最後のプロジェクト「宝探しゲーム」の下準備しました。
(setup function の中身)
続きは、play functionとend functionゲームの核心の部分です。
※ 自分わかるように整理したので、チュートリアルの順番少し違います。

play function

登場人物とモンスターなど用意完了しましたので、いざゲーム始まる時なにを用意すれば良いのか、リストアップしてみました。

  • 勇者の移動、移動の範囲制限
  • モンスターの移動、移動の範囲制限、壁ぶつかった時の反応
  • 勇者とモンスターぶつかったのか
  • 勇者と宝箱ぶつかったのか(宝ゲット?)
  • 勇者と扉ぶつかったのか(脱出成功?)
  • ゲーム成功か失敗かの判断

そして、整理してみましょう。

helper functionとしてまとめてplay functionから出した方が良いのは、

  • 範囲制限(限界に来たのかどうかの判断)
  • ぶつかってるかどうかの判断

では、コードに変えると

// 範囲制限(限界に来たのかどうかの判断)
// 入れる値は移動できる、移動してるスプライト(ここでは勇者とモンスター)と動ける範囲
function contain(sprite, container) {
  var collision = undefined;
  // 左
  if (sprite.x < container.x) {
    sprite.x = container.x;
    collision = "left";
  }
  // 上
  if (sprite.y < container.y) {
    sprite.y = container.y;
    collision = "top";
  }
  // 右
  if (sprite.x + sprite.width > container.width) {
    sprite.x = container.width - sprite.width;
    collision = "right";
  }
  // 下
  if (sprite.y + sprite.height > container.height) {
    sprite.y = container.height - sprite.height;
    collision = "bottom";
  }
  // 値を返す
  return collision;
}
// ぶつかってるかどうかの判断方程式、前回に詳しく説明したので、省略する
function hitTestRectangle(r1, r2) {
  var hit, combinedHalfWidths, combinedHalfHeights, vx, vy;

  hit = false;

  r1.centerX = r1.x + r1.width / 2; 
  r1.centerY = r1.y + r1.height / 2; 
  r2.centerX = r2.x + r2.width / 2; 
  r2.centerY = r2.y + r2.height / 2; 

  r1.halfWidth = r1.width / 2;
  r1.halfHeight = r1.height / 2;
  r2.halfWidth = r2.width / 2;
  r2.halfHeight = r2.height / 2;

  vx = r1.centerX - r2.centerX;
  vy = r1.centerY - r2.centerY;

  combinedHalfWidths = r1.halfWidth + r2.halfWidth;
  combinedHalfHeights = r1.halfHeight + r2.halfHeight;

  if (Math.abs(vx) < combinedHalfWidths) {
    if (Math.abs(vy) < combinedHalfHeights) {
      hit = true;
    } else {
      hit = false;
    }
  } else {
    hit = false;
  }

  return hit;
};
// わざ独立させなくても良いけど、一応チュートリアルには分けて書いた
function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

play function内部に入ります。

function play() {

  // キーボードeventからもらった数値移動させる
  explorer.x += explorer.vx;
  explorer.y += explorer.vy;

  // 勇者の移動範囲
  contain(explorer, {x: 28, y: 10, width: 488, height: 480});
  //contain(explorer, stage);

  // 勇者なにもぶつかってないという初期設定
  var explorerHit = false;

  // モンスター配列使って
  blobs.forEach(function(blob) {

    // 移動させる
    blob.y += blob.vy;

    // モンスターにも移動範囲制限かける、返ってくる値をキャッチする
    var blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});

    // 返ってきた値を判別し、進行方向を逆方向に変える
    if (blobHitsWall === "top" || blobHitsWall === "bottom") {
      blob.vy *= -1;
    }

    // 勇者とモンスターぶつかったら、さっきなにもぶつかってない値を変える
    if(hitTestRectangle(explorer, blob)) {
      explorerHit = true;
    }
  });

  // もし勇者モンスターにぶつかったら、
  if(explorerHit) {

    // 一瞬半透明なる
    explorer.alpha = 0.5;

    // HPも減る
    healthBar.outer.width -= 1;

  } else {

    // 半透明から戻す
    explorer.alpha = 1;
  }

  // もし勇者とぶつかったものは宝箱だったら、
  if (hitTestRectangle(explorer, treasure)) {

    // 宝箱を持って帰れるようにする(宝箱の位置を常に勇者の右下に)
    treasure.x = explorer.x + 8;
    treasure.y = explorer.y + 8;
  }

  // HP減りすぎるとゲーム終了、メッセージを失敗に変更
  if (healthBar.outer.width < 0) {
    state = end;
    message.text = "You lost!";
  }

  // (勇者&)宝箱とドアぶつかったら、ゲーム終了、メッセージを成功に変更
  if (hitTestRectangle(treasure, door)) {
    state = end;
    message.text = "You won!";
  } 
}

そして、end functionにでシーンの切り替え

function end() {
  gameScene.visible = false;
  gameOverScene.visible = true;
}

f:id:manmanrai:20170427143555p:plain

ゲーム完成です!!

補足:スプライトについて

スプライトの座標、visible、回転以外いろんなオプションあるみたいで、詳しく公式のドキュメントで調べられます。
リンクはこちらです。

Class: Sprite

基本、スプライトは継承のルール従ってます。

  • DisplayObject > Container > Sprite

これで終了になります!
お疲れ様でした。

シリーズ:2D WebGL renderer Pixi.js v4

manmanrai.hatenablog.com

参考サイト

pixi.jsの公式サイト(英語)

www.pixijs.com

参考しているチュートリアル(英語)

github.com