JavaScriptでゲーム作ろう

PixiJSのSpirteではJSONファイルを使ったフレームアニメーションのやり方があるんですが、JSONファイルを作成する正確な方法がいまいちわからず(作ってくれるドット絵ツールとかがあるらしいけど)、そもそも面倒くさいのでJSONファイルを使わない方法でフレームアニメーションできるようにします。

この方法でも問題なくバリバリに動くゲームを作ることができます ▷ 連射ソルジャー

pixi.jsのSpriteクラスの基本的な使い方

まずはpixi.jsのSpriteについて。

pixi.jsで画像を表示させるにはSpriteクラスを使います。

const img = new PIXI.Sprite(); 
img.texture = core.resources[Resource.Player].texture; 
this.addChild(img);

(※core.resource  = PIXI.Loader.shared.resourcesです。)

Spriteは表示したい画像をtextureに設定してやると画像が設定されます。

それをコンテナなど(上の例ではthisになってる部分)にaddChild()すると画面に表示されるようになります。

Spriteの主なプロパティについて

基本的にゲームで使う部分としては

  • x、y・・・addChildしたContainerなどの上の表示位置
  • scale・・・拡大縮小率。1が基準。xが幅、yが高さを変える
  • anchor・・・表示の際のスプライトの基準位置。(0, 0)だと画像の左上、(1, 1)で右下
  • rotation・・・回転。単位はラジアン
  • width、height・・・スプライトの幅、高さ
  • visible・・・trueで表示、falseで非表示
  • alpha・・・透明度。1で見える、0で見えなくなる

あたりが良く使うかと思います。またこれらのプロパティはほかのクラスともだいたい共通しています。

画像の表示はこれでできるんですが実はこれではゲームに使うにはちょっと困るんですよね。これは一枚の画像をそのまま表示するだけなのでちょっと使いづらいんです。

ということでenchant.jsなどで使われているスプライトを分割して表示するやり方を作ります。

画像を切り取って表示する

キャラクタ画像

一枚の画像をそのまま表示するだけでは非常に使い勝手が悪いので必要な部分を切り取って表示できるようにします。

画像を切り取るのはPIXI.Texture(“元画像テクスチャ”, “範囲”)で切り出すことができます。切り取った部分を新しいテクスチャとしてspriteに入れてやればその部分だけを表示できるようになります。

const img = new PIXI.Sprite(); 
const texture = core.resources[Resource.Player].texture; 
img.texture = new PIXI.Texture(texture, new PIXI.Rectangle(0, 0, 32, 32));
this.addChild(img);

PIXI.Rectangleは画像を切り取る範囲です。引数は画像内のx座標、y座標、幅、高さの順になっています。これで好きな場所から好きな大きさで切り取ることができます。

フレームアニメーションできるようにする

フレームアニメーションとはパラパラ漫画のような感じのものらしいです(言葉の意味とかあんまり分からん(^^;))。一枚の画像から切り取って順番に切り替えながら表示するやり方なんですがこれはenchant.jsと同じやり方なのでもしわからない方はこちらの解説記事を読んでいただくと何となくわかると思います。これと同じものを作っていきます。

ではenchant.js風なspriteを作っていきましょう。

用意する変数

ではまず今回使う変数はこちらになっております。

  • frameNumber 切り取る画像の番号
  • width 切り取る幅
  • height 切り取る高さ

例えば上の記事のように画像のサイズが横160px×縦96pxで使いたいキャラの大きさが32px×32pxだった場合5×3の15枚画像を切り取れます。そしてこれに0番から14番まで番号を付けます。変数frameNumberはその番号を指定するのに使います。

let frameNumber = 0;
const width = 32;
const height = 32;
const player = new PIXI.Sprite(); 
const texture = core.resources[Resource.Player].texture; 
const left = frameNumber % 5 * 32;
const top = (frameNumber / 5 | 0) * this.height;
player.texture = new PIXI.Texture(this._image, new PIXI.Rectangle(left, top, width, height));

画像は横が5枚分なのでframeNumberを5で割った余りに切り取り幅を掛けたのが切り取るx座標、割った数字(小数点以下は|0で取り除く)に高さを掛けたのがy座標になります。上の例ではleft, topと書いてますがキャラの座標と混同することがあるので私はこういう書き方にしています(^^;)

これでframeNumberの数字を変えてやれば好きな部分の画像を切り出すことができます。

ただしこれではframeNumberが15以上になった場合におかしくなります。だって画像は14番までしかないですからね。

ということで15以上にならないように

frameNumber = frameNumber % 15;

て感じの処理を入れておくと大きな数字が入った場合も問題なく動くので良いと思います。

Spriteを継承して新しいSpriteを作る

最後にこれからも使いやすいようにspriteを継承して新しいspriteを作ってしまいましょう(‘ω’)ノ

class EnchantSprite extends Sprite {
  constructor(width, height){
    super();
    this.width = width;
    this.height = height;
    this._frameNumber = 0;
  }
  //画像データの取得(いるかな?)
  get image(){
    return this._image;
  }
  //画像をセットしスプライトサイズに合わせて計算してテクスチャーをセット
  set image(data) {
    this._image = data;
    this.frameColumns = this._image.width / this.width | 0;
    this.frameRows = this._image.height / this.height | 0;
    this.frameMax = this.frameRows * this.frameColumns;
    this.setTexture();
  }
  //フレームナンバーを返す
  get frameNumber(){
    return this._frameNumber;
  }
  //フレームナンバーをセットし、画像をセットする
  set frameNumber(frameNumber) {
    this._frameNumber = frameNumber % this.frameMax;
    if(!this._image)return;//まだ画像がセットされてなければ抜ける
    const scaleX = this.scale.x;
    const scaleY = this.scale.y;
    this.scale.set(1, 1);//一度スケールを戻す必要がある(サイズが違うとおかしくなる)
    this.setTexture();
    this.scale.set(scaleX, scaleY);
  }
  //フレームナンバーに沿ったテクスチャーをセット
  setTexture() {
    const left = this.frameNumber % this.frameColumns * this.width;
    const top = (this.frameNumber / this.frameColumns | 0) * this.height;
    this.texture = new PIXI.Texture(this._image, new PIXI.Rectangle(left, top, this.width, this.height));
  }
}

使うときはこんな感じ。

const player = new Sprite(32, 32);
player.image = core.resources[Resource.Player].texture;
player.frameNumber = 10;

簡単に内容を説明するとこんな感じになってます。

image

get、setというものを使っています。あんま詳しくわかんないんですけど=で代入したときに関数みたいに処理してくれる便利なやつらしいです(^^;)なのでsetでテクスチャーを入れたときに画像のサイズと切り取るサイズから縦横の切り取れる数と最大フレーム数を出してます。で、setTextureで切り取った画像をセットしてます。

frameNumber

これもsetを使って数字が入れられたときに最大値のチェックをしてます。

setTexture()

ここで画像を切り取って切り取った画像をスプライトのテクスチャーにセットしています。

出来たけど欠点がある

というわけでenchant.js風のSpriteフレームアニメーションができるようになりました。

ただ、このやり方実はあんまり良くないっぽいんですよ。というのも実はこのやり方だと画像を移動させたりするときにノイズが入るんですよね。ノイズというのはちらつきというかなんか余計なものが入るんですよ(ノイズの原因は隣の画像がちょっと入り込むことみたい)。

で、その問題を解決してくれるやり方がjsonファイルを使ったやり方です。

これはスプライトシート作成時に画像(例えば32×32)の周りにあらかじめ数ピクセルのマージンを作って、それらの情報をjsonファイルに書いておいて、使うときにそのjsonファイルの情報をもとに切り出しているようです。

一応ドット絵のアプリなどではこれを自動で作成してくれるものもあるらしい(acespriteとか作れるらしいです)ですがやっぱちょっと面倒なので私は使っていないです(^^;)

 

一応このノイズの入る欠点は小数点以下を切り捨てることでも無くすことができます。それについてはこっちに書いてます(マップの話ですがやり方は同じです)。

ただ小数点以下を消しても拡大縮小・回転するとやっぱりノイズが入ります(-_-;)

 

あとこのやり方の最大の欠点は画像の切り出し位置を計算で出しているのでその分処理が多くなります。かなり多くのスプライトを使うゲームを作るならJSONファイルを使った方が良いと思います。

プログラムで分割してスプライトシートを作る

最終的なノイズ対応策として思いついた究極技が「スプライトシートを分割して作り直すプログラム作っとけば良くね?」です。

取り込んだ画像を一度タイルの大きさに分割してマージンを追加してまた一枚のスプライトシートを作成すればいいではないか!!

ということでやってみたのがこれ。

function createCustomSpriteSheet(texture, width, height, margin, app) {
  //サイズ計算
  const frameColumns = texture.width / width | 0;
  const frameRows = texture.height / height | 0;
  const frameMax = frameRows * frameColumns;
    
  const container = new PIXI.Container();
  app.stage.addChild(container);

  for(let i = 0; i < frameMax; i++){
    const left = i % frameColumns;
    const top = i / frameColumns | 0;
    const x = left * width;
    const y = top * height;
    const sprite = new PIXI.Sprite();
    sprite.texture = new PIXI.Texture(texture, new PIXI.Rectangle(x, y, width, height));
    sprite.position.set(x + (left + 1) * margin, y + (top + 1) * margin);
    container.addChild(sprite);  
  }
  const textureWidth = frameColumns * (width + margin) + margin;
  const textureHeight = frameRows * (height + margin) + margin;;
    
  //画像に変換
  const newTexture = new PIXI.RenderTexture(
    new PIXI.BaseRenderTexture({width: textureWidth, height: textureHeight})
  );
  app.renderer.render(container, newTexture);
    
  //コンテナを破棄
  container.destroy();

  return newTexture;
}

引数は取り込んだスプライトシートのテクスチャーとサイズ、、マージン、それとPIXI.Application(私のゲームエンジンではcore.app。変換で使う)。これでサイズごとにマージンが挿入された新しいテクスチャーが生成されます。

ただこのスプライトシートを使うにはEnchantSpriteも改造が必要です。

class EnchantSprite2 extends Sprite {
  constructor(width, height, margin){
    super();
    this.width = width;
    this.height = height;
    this.margin = margin;
    this._frameNumber = 0;
  }
  //画像データの取得(いるかな?)
  get image(){
    return this._image;
  }
  //画像をセットしスプライトサイズに合わせて計算してテクスチャーをセット
  set image(data) {
    this._image = data;
    this.frameColumns = this._image.width / (this.width + this.margin) | 0;
    this.frameRows = this._image.height / (this.height + this.margin) | 0;
    this.frameMax = this.frameRows * this.frameColumns;
    this.setTexture();
  }
  //フレームナンバーを返す
  get frameNumber(){
    return this._frameNumber;
  }
  //フレームナンバーをセットし、画像をセットする
  set frameNumber(frameNumber) {
    this._frameNumber = frameNumber % this.frameMax;
    if(!this._image)return;//まだ画像がセットされてなければ抜ける
    const scaleX = this.scale.x;
    const scaleY = this.scale.y;
    this.scale.set(1, 1);//一度スケールを戻す必要がある(サイズが違うとおかしくなる)
    this.setTexture();
    this.scale.set(scaleX, scaleY);
  }
  //フレームナンバーに沿ったテクスチャーをセット
  setTexture() {
    const left = this.frameNumber % this.frameColumns;
    const top = this.frameNumber / this.frameColumns | 0;
    const x = left * this.width + (left + 1) * this.margin;
    const y = top * this.height + (top + 1) * this.margin;
    this.texture = new PIXI.Texture(this._image, new PIXI.Rectangle(x, y, this.width, this.height));
  }
  //更新処理
  update(delta) {
    super.update(delta);
  }
}

この方法にも欠点があって「一つのサイズでしか分割できない」ことです。

違うサイズのキャラクターなどを使いたい場合は別のシートに分ける必要があります。

結局タイルマップでは意味が無かった

マージンを入れたことで隣の画像がノイズとして入ることは無くなりましたがタイルマップでは違うノイズが発生します。

今までと同様に小数点以下の数字が入るとタイル同士の間にノイズが発生するんですが、どうやらこれは文字通りの「隙間」が発生しているようです。どうしようもありません。

なのでタイルマップではenchant.js風のスプライトを使っても一緒です。

どのみち「小数点以下を分ける&戻す」という処理が必要になります。

もしくはマップを生成後一枚の画像にしてしまうかです。そうしてしまえば隙間は発生しなくなるし加工もできます。詳しくは下の記事に書いてあります。

あとがき

欠点さえ分かっていれば工夫してenchant.js風のやり方で問題なくゲームを作ることができます。

見た目に問題なければどんな手段を使おうが遊ぶ側にとっては同じですしね。