アクションRPG Day5 衝突

f:id:naosim:20181010064547p:plain 前回はアニメーションをやりました

naosim.hatenablog.jp

今回は壁との衝突です
Phaserの衝突は優秀で、プレイヤと壁それぞれに衝突する領域を定義したら後は勝手に計算してくれます

プレイヤーの衝突定義

プレイヤークラスのcreateメソッドをいじります
何も設定しないと判定領域はspriteのサイズ通り32x32になりますが、それだとちょっと大きいのでもう少し小さい領域にします

class Player {
  ...
  create() {
    this.sprite = this.scene.physics.add.sprite(32, 32, 'player');
    this.sprite.body.setSize(16, 24);
    this.sprite.body.offset.y = 8;
    this.sprite.setCollideWorldBounds(true);
  }
  ...
}

sprite.bodyとは物理演算に使うオブジェクトです sprite.bodyは物理演算にarcadeを使っているのでPhaser.Physics.Arcade. Bodyです

Phaser 3 API Documentation - Class: Body

this.sprite.body.setSize(16, 24);

ここで衝突の領域サイズを指定します
領域は中央寄せになります
今回は足が地についたところきっちり判定したいので、足から領域が始まるようにoffsetを指定します

this.sprite.body.offset.y = 8;

Groundの衝突定義

背景はtiledmapで作っていますが、mapの絵に衝突判定して欲しい部分と無視して欲しい部分があるのでそれを指定します Groundクラスのcreateをいじります

Ground {
  create() {
    const map = this.scene.make.tilemap({ data: this.level, tileWidth: 16, tileHeight: 16 });
    this.layer = map.createStaticLayer(0, map.addTilesetImage("ground"), 0, 0);
    this.layer.setCollisionBetween(6, 44);// 追加
  }
}

Phaser 3 API Documentation - Class: StaticTilemapLayer ポイントはsetCollisionBetween(6, 44)
衝突判定するインデックスを範囲指定します
指定の仕方は他にもあるのでいろいろやってみたら良さそう
実は今回はsetCollisionByExclusionの方が正しい気がしてきたし

プレイヤーとGroundの衝突判定をする

createメソッドに1行書くだけです

this.physics.add.collider(player.sprite, ground.layer);

衝突判定はこれで終わり
すげぇ...

以上

jsdo.it

プレイヤーが壁に当たるとちゃんと止まります
ゲームらしくなってきたな

アクションRPG Day4 アニメーション

f:id:naosim:20181010064547p:plain

前回はプレイヤーを表示しました

naosim.hatenablog.jp

ただ動かした時にアニメーションがなかったので今回はそれを追加します
アニメーションを追加ためにクマが「右方向に歩く絵」と「左方向に歩く絵」が必要なので、プレイヤーの画像を変更しました

http://jsrun.it/assets/W/0/B/U/W0BU0.png

preloadで読み込む画像の変更

プレイヤーの画像を変更します

this.load.spritesheet('player', 'http://jsrun.it/assets/W/0/B/U/W0BU0.png', { frameWidth: 32, frameHeight: 32 });

Player.create()でアニメーションを定義する

class Player {
  ...
  create() {
    this.sprite = this.scene.physics.add.sprite(32, 32, 'player');

    // ここにアニメーションの設定を追加する    
  }
  ...
}

上記のコメントの部分に下記の4つのアニメーションを追加します

  • 右向きに立ってる絵
  • 左向きに立ってる絵
  • 右向きに歩いている絵
  • 左向きに歩いている絵

右向きに立ってる絵

this.scene.anims.create({
  key: 'turn-right',
  frames: [ { key: 'player', frame: 4 } ],
  frameRate: 20
});

this.scene.anims.createのドキュメントはこれ
Phaser 3 API Documentation - Class: AnimationManager
引数: AnimationConfig
keyには適当な名前をつける
frameに渡す整数は画像をframeWidthとframeHeightで分割した状態で左上から数えた数
今回の画像の場合、右向きの絵は左から5番目で数値はゼロオリジンなので4を指定する

左向きに立ってる絵

考え方は右向きと同じ

this.scene.anims.create({
  key: 'turn-left',
  frames: [ { key: 'player', frame: 3 } ],
  frameRate: 20
});

右向きに歩いている絵

this.scene.anims.create({
  key: 'right',
  frames:this.scene.anims.generateFrameNumbers('player', { frames:[ 5, 4, 6, 4] }),
  frameRate: 10,
  repeat: -1
});

framesの指定でgenerateFrameNumbersを使う
generateFrameNumbers

framesで指定した順番に絵をアニメーション表示してくれる
このメソッドが何を返してるのかが気になったので、戻り値をログ出力してみたらこんなオブジェクトを返してた

[
  {key: "player", frame: 5},
  {key: "player", frame: 4},
  {key: "player", frame: 6},
  {key: "player", frame: 4}
]

要するにただのファクトリメソッドみたいw

frames: [5, 4, 6, 4].map(v => ({key:'player', frame: v}))

これでええやん
まぁいいや

左向きに歩いている絵

ほぼ一緒

this.scene.anims.create({
  key: 'left',
  frames:this.scene.anims.generateFrameNumbers('player', { frames:[2, 3, 1, 3] }),
  frameRate: 10,
  repeat: -1
});

createメソッドの最終形

class Player {
  ... 
  create() {
    this.sprite = this.scene.physics.add.sprite(32, 32, 'player');
    this.lastDirection = 'right';
    
    this.scene.anims.create({
      key: 'turn-right',
      frames: [ { key: 'player', frame: 4 } ],
      frameRate: 20
    });

    this.scene.anims.create({
      key: 'turn-left',
      frames: [ { key: 'player', frame: 3 } ],
      frameRate: 20
    });

    this.scene.anims.create({
      key: 'right',
      frames:this.scene.anims.generateFrameNumbers('player', { frames:[ 5, 4, 6, 4] }),
      frameRate: 10,
      repeat: -1
    });

    this.scene.anims.create({
      key: 'left',
      frames:this.scene.anims.generateFrameNumbers('player', { frames:[ 2, 3, 1, 3] }),
      frameRate: 10,
      repeat: -1
    });
  }

  ...
}

Player.update()でアニメーションを設定する

先ほど定義したアニメーションを動作に合わせて設定します

update () {
  if (this.cursors.left.isDown) {
    this.sprite.setVelocityX(-160);
    // 左向きに歩くアニメ
    this.sprite.anims.play('left', true);
    this.lastDirection = 'left';
  } else if (this.cursors.right.isDown) {
    this.sprite.setVelocityX(160);
    // 左向きに歩くアニメ
    this.sprite.anims.play('right', true);
    this.lastDirection = 'right';
  } else {
    this.sprite.setVelocityX(0);
    this.sprite.anims.play(this.lastDirection && this.lastDirection == 'left' ? 'turn-left' : 'turn-right');
  }

  if (this.cursors.up.isDown) {
    this.sprite.setVelocityY(-160);
  } else if(this.cursors.down.isDown) {
    this.sprite.setVelocityY(160);
  } else {
    this.sprite.setVelocityY(0);
  }
}

アニメーションを再生するplayメソッドの仕様はこれ
play(key [, ignoreIfPlaying] [, startFrame]) 今の実装ではupdateが呼ばれるたびに毎回playメソッドをコールするので、playメソッドの第2引数のignoreIfPlayingをtrueにしておきます
そうしないと例えば右ボタンを押してる時はupdateのたびに右・右・右...と実行されるので、画面上は右に歩くアニメの1フーレム目がずーっと表示されることになり、歩く感じになってくれません
ここらへんをフレームワークで吸収してくれるのは助かるなぁ

以上

jsdo.it

プレイヤーが左右に歩くアニメーションができました
次回は壁の衝突判定をやります

アクションRPG Day3 プレイヤー

f:id:naosim:20181010064547p:plain

前回はフィールドの表示をしました

naosim.hatenablog.jp

今回はプレイヤーを表示して移動させます
画像はいつものenchantのクマです

http://jsrun.it/assets/e/N/V/L/eNVLk.png

画像をpreloadで読み込む

フィールド読み込みの後に追加します

function preload() {
  // 背景画像の読み込み 16x16のマップチップの画像
  this.load.image('ground', 'http://jsrun.it/assets/K/e/w/N/KewNo.png');
  // プレイヤー 32x32
  this.load.spritesheet('player', 'http://jsrun.it/assets/e/N/V/L/eNVLk.png', { frameWidth: 32, frameHeight: 32 });
}

プレイヤーを表示する

プレイヤーもクラス化します

Playerクラスを使う側の実装

まずは使う側から

var player;// グローバルに定義
function create() {
  const ground = new Ground(this);
  ground.create();
  
  // キャラクタの生成
  player = new Player(this);
  player.create();
  player.cursors = this.input.keyboard.createCursorKeys();
}

function update() {
  // フレーム単位の処理
  player.update();
}

今回作るPlayerはcreateメソッドとupdateメソッドから利用するため、player変数をグローバルに作っておきます
(あんまり嬉しくないけどしょうがない)
そしてcreateメソッド内でplayerを生成し、テンキーでプレイヤーを移動させるためにcursorsをセットします
updateメソッドでplayer.update()を呼びます
player.update()の中で、カーソルの状態に応じてプレイヤーを移動させます

Playerクラス

プレイヤー画像の表示と移動をします

class Player {
  constructor(scene) {
    this.scene = scene;
    this.sprite = null;
    this.cursors = null;
  }

  create() {
    this.sprite = this.scene.physics.add.sprite(32, 32, 'player');
  }

  update () {
    if (this.cursors.left.isDown) {
      this.sprite.setVelocityX(-160);
    } else if (this.cursors.right.isDown) {
      this.sprite.setVelocityX(160);
    } else {
      this.sprite.setVelocityX(0);
    }

    if (this.cursors.up.isDown) {
      this.sprite.setVelocityY(-160);
    } else if(this.cursors.down.isDown) {
      this.sprite.setVelocityY(160);
    } else {
      this.sprite.setVelocityY(0);
    }
  }
}

createの中身

createメソッドでspriteを生成します
このタイミングで画面にプレイヤーが表示されます

this.scene.physics.add.sprite()

これはsprite生成のメソッドですが、ドキュメントによると
"Creates a new Arcade Sprite object with a Dynamic body." だそうです
物理演算対象のスプライトを生成するってことですかね
戻り値はSpriteです

Phaser 3 API Documentation - Class: Factory

Phaser 3 API Documentation - Class: Sprite

前回のGroundクラスではphyisicsを意識してなかったけど大丈夫か...?
まぁいいや

updateの中身

上下左右に押されたらその方向に速度をセットします
ただこの値の単位がわからない...
px/frameだとすると、1フレームごとに160px移動するから早すぎるし...
なので勝手にpx/secということにするw

今日はここまで
jsdo.it ここまででプレイヤーを画面に表示して移動させることができました
PCのテンキーで操作できます(スマホの人はごめんなさい)
ただ現段階では移動中のアニメーションや壁との衝突判定がないのでそれは次回以降実装します

phaserでアクションRPG Day2 フィールドの作成

f:id:naosim:20181010064547p:plain

前回phaserのセットアップをしました

naosim.hatenablog.jp

今日はフィールドを表示してみます

で...いきなり壁...
コードをcodepenで書いてたけどcodepenは画像を扱えなかった...
ワークアラウンドはあるけど本質的じゃないコードが入るといやなのでjsdo.itへお引越し
今回はでほぼ体力使い切った...

画像をpreloadで読み込む

まずはフィールドのマップチップ画像を読み込みます

http://jsrun.it/assets/K/e/w/N/KewNo.png

function preload() {
  // 背景画像の読み込み 16x16のマップチップの画像
  this.load.image('ground', 'http://jsrun.it/assets/K/e/w/N/KewNo.png');
  // thisはSceneクラス
  // https://photonstorm.github.io/phaser3-docs/Phaser.Scene.html
  
}

いきなりthisが出てきてビックリしますが、thisはSceneクラスです
Phaser 3 API Documentation - Class: Scene

Phaserのver2ではグローバル変数にgameオブジェクトをいれておいて使うのが定石ぽかったけど、ver3からはthisを使うんですねー
グローバル汚染が減ってよくなった

背景を表示する

次にGroundクラスを作ります
phaser系の記事を読むとだいたいcreateメソッドの中に直にいろいろ書いてますが、それだと辛くなってくるのでクラス化しておきます

Groundクラスを使う側の実装

function create() {
  const ground = new Ground(this);
  ground.create();
}

newしてcreateを呼んで終了
create()と同時に背景画像が表示される想定です
phaserはcreateと同時に表示する仕様がある意味特徴なのでそれに合わせます

他のフレームワーク(例えばenchantとか)だと、生成後にaddChild()すると表示されますが、phaserは一気に表示まで処理が進みます
強気だなぁと思うけど、慣れればまぁそんなもんって感じですね

Groundクラス

次にGroundクラスの実装です

class Ground {
  constructor(scene) {
      this.scene = scene;
      this.level = [
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  6],
        [  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6],
      ];
      this.layer = null
  }

  create() {
    const map = this.scene.make.tilemap({ data: this.level, tileWidth: 16, tileHeight: 16 });
    this.layer = map.createStaticLayer(0, map.addTilesetImage("ground"), 0, 0);
    // ここで背景画像が配置される
  }

ただの壁のある箱です
createStaticLayerを呼ぶとlayerが生成されて画面にフィールドが表示されます
ただ...createStaticLayerの意味がよくわからん...

Phaser 3 API Documentation - Class: Tilemap

他にもcreateDynamicLayerてのもある
何がStaticで何がDynamicなのか...物理演算的なやつかな... とりあえず今はStaticで実装しておきます

以上、

jsdo.it

ここまでで箱っぽいフィールドができました

phaserでアクションRPG Day1

f:id:naosim:20181010064547p:plain

アクションRPGが作りたくなった
あとゲームフレームワークのPhaserがバージョン3になってたので勉強も兼ねて開発経過をメモします

初期状態のおまじない

var config = {
  type: Phaser.AUTO,
  width: 400,
  height: 400,
  physics: {
    default: 'arcade',
    arcade: {
      debug: true// お好みで
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update
  },
  pixelArt: true// アンチエイリアスをoffにする
};
var game = new Phaser.Game(config);

function preload() {
  // assetの読み込み
}

function create() {
  // 初期化処理
}

function update() {
  // フレーム単位の処理
}

ここは「おまじない」だと思っていい
設定の仕方は以前はセッターを使ってたけど、ver3からはconfigオブジェクトにセットするように変更されたっぽい
その方が引数の拡張がしやすいからかな

設定のポイントはpixelArtを必ず設定すること

これを設定しないとアンチエイリアスが効いてピクセルが潰れてボケボケな絵になる
ファミコンぽいゲームを作りたいなら必ずpixelArtをtrueに設定しよう

その他の設定では、今回は物理的な衝突が使いたいのでarcadeという物理演算を使います arcadeは四角形の衝突等を適当に計算してくれます

あとhtml側はphserをCDNから読み込んでおけばok

<script src="//cdn.jsdelivr.net/npm/phaser@3.11.0/dist/phaser.js"></script>

便利だなぁ

See the Pen ActionRPG Day1 by なおしむ (@naosim) on CodePen.

実行すると真っ黒な画面が表示されました
とりあえず今日はここまで

問題: いい感じの日付取得

下記に月・日を入力するといい感じに日付(年月日)にしてくれる機能がある。
これと同じ機能のメソッドcreateDate()を完成させよ。
メソッドのインターフェースは↓この通り。

/**
 * @param {number} month 1-12
 * @param {number} dayOfMonth 1-31
 * @param {Date} currentDate
 * @return {Date}
 */
function createDate(month, dayOfMonth, currentDate) {
    //TODO: 実装
}

ただし言語はjsでなくてもなんでも良い。
また、monthやdayOfMonthに不正な値(マイナスなど)が入ることは考慮しなくて良い。

求められるスキル: 要件分析、実装

いい感じDate

月:
日:
現在:



結果:

先輩に「メソッドを実行して結果を変数に突っ込め」と言われたら2

前回の記事でメソッドを実行して変数に突っ込む時の思考を整理しました

naosim.hatenablog.jp

「そんなの出来て当然」と思った方も多いと思いますが、実はjavaはそんなに甘くないです
ってことで抜き打ちテスト!

以下の問いに答えよ

1. hogeメソッドを実行して変数に突っ込め

class Hoge {
public int hoge(String text) { /* 実装省略 */ }
}

2. hogeメソッドを実行して変数に突っ込め

class Hoge {
  public static int hoge(String text) { /* 実装省略 */ }
}

3. valueを取得して変数に突っ込め

class Hoge {
  @Getter
  private final String value;
  public String getDoubleValue() { return value + value; }
}

4. hogeメソッドを実行して変数に突っ込め

@Component
class Hoge {
public int hoge(String text) { /* 実装省略 */ }
}

5. invokeメソッドを実行して変数に突っ込め

※これはひっかけ問題です

@RestController
class Hoge {
  @RequestMapping(value = "/hello", method = RequestMethod.POST)
  public Map invoke() { /* 実装省略 */ }
}

解答

1

int hoge = new Hoge().hoge("あいうえお");

インスタンスに対してメソッドを呼べばOK

2

int hoge = Hoge.hoge("あいうえお");

クラスに対してメソッドを呼べばOK

3

ここからが本題
@Getterに注目できればOK @Getterlombokが提供しているアノテーションで、フィールドにつけると"getフィールド名()"の形式のメソッドを自動生成してくれる
今回の場合はvalueアノテーションが付いているのでgetValue()が生成されている
なので正解はこう

String value = new Hoge("あいうえお").getValue();

こんな感じでアノテーションが目に見えないメソッドを生成していることがある
アノテーションの意味を理解しよう
ちなみに今回の問題は@Getterを理解してないと
「問題はvalueを取れって言ってるけどprivateだから取れない。。お?getDoubleValue()てメソッドがあるぞ。とりあえずこれ使っとこと」

String value = new Hoge("あいうえお").getDoubleValue();

みたいなな違いが起きる
これはアノテーションを理解していない場合に現場でありがちな勘違い
気をつけよう

4

@Componentに注目できればOK
@Componentの場合はDIが絡んでいるのでインスタンス生成に@Autowiredを用いる
なのでどこかで生成してからメソッドを呼べばok

@Autowired
Hoge hoge;
int hoge = hoge.hoge("あいうえお");

ちなみに@Autowiredしてから使う系は @Componentの他にも@Service@Repositoryなどがある
いちおう文法上は普通にnewしても使えるけど、たぶんチームメンバーに怒られる

5

これはもはやRestApiなので普通にnewして呼ぶものじゃない
curlコマンドで呼ぶとか、webClient的なもので呼ぶとか、テストツールで呼ぶとか

まとめ

「メソッドを実行して結果を変数に突っ込め」と言われた時、その実現方法は状況に応じていろいろあることがわかった
とくにlombokやDIなどのアノテーションが絡むと状況判断が難しくなる
アノテーションに気をつけて正しい状況判断ができるようになろう