アクションRPG Day5 衝突
前回はアニメーションをやりました
今回は壁との衝突です
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);
衝突判定はこれで終わり
すげぇ...
以上
プレイヤーが壁に当たるとちゃんと止まります
ゲームらしくなってきたな
アクションRPG Day4 アニメーション
前回はプレイヤーを表示しました
ただ動かした時にアニメーションがなかったので今回はそれを追加します
アニメーションを追加ためにクマが「右方向に歩く絵」と「左方向に歩く絵」が必要なので、プレイヤーの画像を変更しました
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フーレム目がずーっと表示されることになり、歩く感じになってくれません
ここらへんをフレームワークで吸収してくれるのは助かるなぁ
以上
プレイヤーが左右に歩くアニメーションができました
次回は壁の衝突判定をやります
アクションRPG Day3 プレイヤー
前回はフィールドの表示をしました
今回はプレイヤーを表示して移動させます
画像はいつものenchantのクマです
画像を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 フィールドの作成
前回phaserのセットアップをしました
今日はフィールドを表示してみます
で...いきなり壁...
コードをcodepenで書いてたけどcodepenは画像を扱えなかった...
ワークアラウンドはあるけど本質的じゃないコードが入るといやなのでjsdo.itへお引越し
今回はでほぼ体力使い切った...
画像をpreloadで読み込む
まずはフィールドのマップチップ画像を読み込みます
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で実装しておきます
以上、
ここまでで箱っぽいフィールドができました
phaserでアクションRPG Day1
アクション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
前回の記事でメソッドを実行して変数に突っ込む時の思考を整理しました
「そんなの出来て当然」と思った方も多いと思いますが、実は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
@Getter
はlombokが提供しているアノテーションで、フィールドにつけると"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などのアノテーションが絡むと状況判断が難しくなる
アノテーションに気をつけて正しい状況判断ができるようになろう