SpringBoot + h2 + mybatisでHelloWorld
ハンズオン用のメモです
開発環境はIntellij、ビルドツールはgradleです
今回のゴール
API名 | 説明 | METHOD | URL | RESPONSE |
---|---|---|---|---|
HELLO | ただhelloを返す | GET | http://localhost:8080/hello | {"response":"hello"} |
LIST | DBのデータをリストで返す | GET | http://localhost:8080/list | {"response":["2018-11-28T07:29:19.596"]} |
INSERT | DBに挿入する | GET | http://localhost:8080/insert | {"response":{"id":2,"event_date_time":"2018-11-28T07:30:35.264"}} |
プロジェクトを新規作成する
File -> New -> Project
で作ります
Gradleを選択し、Javaにチェックを入れてNext
GroupIdに自分のパッケージ名をいれ、ArtifactIdにプロジェクト名をいれてNext
Use auto-importにチェックを入れます。そうするとimport文を自動的に最適化してくれます
あとはそのままでNext
次もNext
これで完了
いろいろロードされるのでしばし待つ
SpringBootでHello
build.gradleを編集する
buildscriptを追加する
springのプラグインを使いたいので、pluginsの上にbuildscript
を書く
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE")
}
}
pluginにspringを追加する
先ほどのbuildscriptを書いた上で以下を追加
plugins { id 'java' id 'eclipse' id 'idea' } apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management'
pluginsに追加したらうまく動かなかったのでapply plugin
にした
pluginsがダメな理由はよくわからない
dependenciesにspringを追加する
dependencies { compile("org.springframework.boot:spring-boot-starter-web") compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20' testCompile group: 'junit', name: 'junit', version: '4.12' }
最小構成としてはspring-boot-starter-web
だけで良い
個人的にlombokも使いたいので追加
build.gradle全体
全体はこんな感じ
buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") } } plugins { id 'java' id 'eclipse' id 'idea' } apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group 'com.naosim' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile("org.springframework.boot:spring-boot-starter-web") compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20' testCompile group: 'junit', name: 'junit', version: '4.12' }
Applicationにmain関数を書く
要するにメイン関数を記述します
パス: ./src/main/java/com/naosim/dddspringh2/Application.java
package com.naosim.dddspringh2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Applicationクラスのパッケージ名がとても重要です
なぜならこのパッケージ配下は後記述する@Autowired
が自動的に聞きますがパッケージ配下以外の場合は設定をしないと効かないから
なので何も考えないのであればApplicationクラスは出来るだけ上位のパッケージに作った方がいいです
ControllerにhelloのAPIを作成する
helloを返すだけのAPIを作ります
package com.naosim.dddspringh2; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController @AllArgsConstructor public class Controller { @RequestMapping(value = "/hello", method = RequestMethod.GET) public Map hello() { Map<String, Object> res = new HashMap<>(); res.put("response", "hello"); return res; } }
@RequestMapping
でURLのパスやメソッドを設定します
hello()
の戻り値をMapにすると自動的にJSON形式に変換してくれます
なのでこのAPIの挙動としては
GET http://localhost:8080/hello
を投げると
{"response":"hello"}
を返すカタチになります
アプリケーションを実行してみる
実行の仕方もたくさんあるので紹介します
intelljがたまにおかしくなるので、やり方は全部知っておいた方がいいです
intellijのgradleから実行する
intellijの右側のバーからgradle
を選択し、Tasks->application->bootRun
をダブルクリックすると実行されます
intellijの停止ボタンを押すと停止できます
停止せずに別のアプリを起動するとポートが被るために起動できないことがあるので、実行が終わったら必ず停止しましょう
Applicationクラスから実行する
Applicationクラスのソースコードを表示すると、クラス名とメソッド名の左に再生ボタンが表示されています
これをクリックし、Run'Application.main()'
を選択すると実行できます
ターミナルから実行する
ターミナルから
./gradlew bootRun
で実行できます
停止はcontrol + c
です
intellijがおかしくなったらこのやり方が最強です
たまにintellijだとエラーだけどターミナルからなら実行出来ることがありますが
この場合、ソースコードは間違ってないです
intellijの何かがうまくいってないだけです
intellijを再起動するなり、ネットワーク周りの設定を見直すなりしましょう
動作確認
前置きが長くなりましたが動作確認です アプリケーションを実行した状態で
http://localhost:8080/hello
にアクセスすると想定通りのjsonが表示されます
{"response":"hello"}
ここまででSpringBootでhelloを返すAPIができました
h2 + mybatis
ここから先はDBを扱う設定の話です
build.gradleにh2とmybatisを追加する
まずはbuild.gradle
にライブラリを追加します
dependencies { compile("org.springframework.boot:spring-boot-starter-web") compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20' compile group: 'com.h2database', name: 'h2', version: '1.4.197'// 追加 compile group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '1.3.2'// 追加 testCompile group: 'junit', name: 'junit', version: '4.12' }
application.ymlにh2の設定を書く
リソースにh2の設定を記述します
パス: ./src/main/resources/application.yml
spring.datasource.url: jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE spring.datasource.driverClassName: org.h2.Driver spring.datasource.username: sa spring.datasource.password: spring.h2.console.enabled: true
今回はインメモリの設定で書きました
もしもアプリケーションが終了してもデータが消えないようにしたいなら
spring.datasource.url
の値をこんな感じにすると良いです
spring.datasource.url=jdbc:h2:file:./target/db/testdb
最後の./target/db/testdb
がファイルの出力先になるので適宜変更してください
DBのセットアップ処理を書く
リソースにCreateTable文や初期状態のinsert文を書きます
schema.sql
schema.sql
起動時に最初に呼んでくれるsqlです
ここにCreateTable文を書きます
DROP TABLE IF EXISTS time_event; CREATE TABLE time_event ( id INT PRIMARY KEY AUTO_INCREMENT, event_date_time DATETIME );
DROP TABLE IF EXISTS
はテーブルがあったら一旦消すコマンドです
インメモリだから2回目の起動を意識する必要はないけど念のため決しておきます
data.sql
schema.sqlの次に呼んでくれるsqlです
ここにDBの初期状態を書いておきます
INSERT INTO time_event (event_date_time) VALUES (sysdate);
マッパーにDB操作のメソッドを書く
Event
後に記述するマッパーで利用するためのEventクラスを作ります
作成したテーブルのカラム名と本クラスのフィールド名を一致させるのがミソです
パス: ./src/main/java/com/naosim/dddspringh2/datasource/Event.java
package com.naosim.dddspringh2.datasource; import java.time.LocalDateTime; public class Event { public int id; public LocalDateTime event_date_time; }
DbMapper
マッパーでDBの操作を書きます
パス: ./src/main/java/com/naosim/dddspringh2/datasource/DbMapper.java
package com.naosim.dddspringh2.datasource; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.time.LocalDateTime; import java.util.List; @Mapper public interface DbMapper { @Select("SELECT * FROM time_event ORDER BY id") List<Event> select(); @Insert("INSERT INTO time_event (event_date_time) VALUES (#{datetime});") void insert(@Param("datetime")LocalDateTime localDateTime); }
interfaceで作ります
interfaceに@Mapper
を付けるとmybatisとして処理されます
select()の解説
@Select("SELECT * FROM time_event ORDER BY id") List<Event> select();
@Select
の中にSELECT文を書きます
戻り値は今回は複数返るのでListにしています
ジェネリクスで型を指定すると、mybatisがそのクラスをnewしてカラム名と同じ名前のフィールドに値を入れるところまで自動でやってくれます
ちなみに戻り値の数が1つだけの場合はListではなく戻りの型を指定します
この状態でSELECTが複数ヒットした場合は例外になります
必ず1つとわかっているならその記述の方が良いと思います
insert()の解説
@Insert("INSERT INTO time_event (event_date_time) VALUES (#{datetime});") void insert(@Param("datetime")LocalDateTime localDateTime);
@Insert
の中にInsert文を書きます
変数を使う場合は#{datetime}
のように書くとSQLインジェクション等をイイ感じに回避してくれます
変数名はメソッド引数の@Param
で指定した文字列と同じものを指定します
ControllerからMapperを使う
マッパーをDIする
@RestController @AllArgsConstructor public class Controller { @Autowired private final DbMapper dbMapper; ...
フィールドにDbMapper
を追加し、@Autowiredを付与します
これでController生成時にマッパーを生成してくれます
注意点としては、@Autowired
はDIによって生成されたものの中でだけ効くということ
たとえばControllerクラスを自分で
Controller c = new Controller();
System.out.print(c.dbMapper);
として場合、Controllerは自分で生成していてAutowiredが聴いていないためdbMapperはnullになります
それを防ぐ方法としては
「@Autowired
を付けるフィールドは必ずfinal
を付け、コンストラクタインジェクションにすること」
です
DIの方法にはコンストラクタインジェクションとフィールドインジェクションの二種類がありますが、前者の方がいいです
理由は...良い記事を見つけたのでリンクを貼っておきます
今回の例ではDbMapperにfinal
を付けて、Controllerクラスに@AllArgsConstructor
を書くことでコンストラクタインジェクションにしています
DB内のデータをリストで取得するAPIを作成する
上記のAutowiredでマッパーが使えるようになったのでそれを使ってデータ取得APIを作ります マッパーからデータを取得し、日付だけにして返します
@RequestMapping(value = "/list", method = RequestMethod.GET) public Map list() { Map<String, Object> res = new HashMap<>(); res.put("response", dbMapper.select().stream().map(v -> v.event_date_time).collect(Collectors.toList())); return res; }
この状態でhttp://localhost:8080/list
にアクセスすると
{"response":["2018-11-28T07:29:19.596"]}
のようなデータが取得できます
DBにデータを投入するAPIを作成する
現在日時をinsertするAPIを作成します インサートしたら、インサートした値をレスポンスで返します
@RequestMapping(value = "/insert", method = RequestMethod.GET) public Map insert() { dbMapper.insert(LocalDateTime.now()); // 返却値 List l = dbMapper.select(); Map<String, Object> res = new HashMap<>(); res.put("response", l.get(l.size() - 1)); return res; }
この状態でhttp://localhost:8080/insert
にアクセスすると
{"response":{"id":2,"event_date_time":"2018-11-28T07:30:35.264"}}
のようなデータが返ってきて、挿入できたことが確認できます
Controllerの全体はこんな感じ
package com.naosim.dddspringh2; import com.naosim.dddspringh2.datasource.DbMapper; import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @RestController @AllArgsConstructor public class Controller { @Autowired private final DbMapper dbMapper; @RequestMapping(value = "/hello", method = RequestMethod.GET) public Map hello() { Map<String, Object> res = new HashMap<>(); res.put("response", "hello"); return res; } @RequestMapping(value = "/list", method = RequestMethod.GET) public Map list() { Map<String, Object> res = new HashMap<>(); res.put("response", dbMapper.select().stream().map(v -> v.event_date_time).collect(Collectors.toList())); return res; } @RequestMapping(value = "/insert", method = RequestMethod.GET) public Map insert() { dbMapper.insert(LocalDateTime.now()); // 返却値 List l = dbMapper.select(); Map<String, Object> res = new HashMap<>(); res.put("response", l.get(l.size() - 1)); return res; } }
以上で、ゴール達成です
お疲れ様でした!
RAMLの導入を考える
夜中に眠れなくなってしまったので、なんとなくRAMLの導入について考える
RAMLとは
今私がいる環境
- java
- spring boot
- ddd
何が生成できるの?
YAMLから以下を生成できる
- API仕様書
- バリデーションチェック(可能性)
- スタブ作成
- テスト仕様書作成(可能性)
疑問: RAML => ソース
かソース => RAML
か?
RAML => ソースの場合
RAMLは設計書、ソースは製造物と考える
pros
- RAMLをしっかりレビューして、そこから差異なく製造することにより品質を担保できる
- コンポ図との相性が良い
cons
- コードを手で入力したら結局差異が出てバグる (Excel仕様書と差異なし)
- RAMLからコードを自動的に生成するのは大変そう
- RAMLをjavaに食わせたり出来んの?
- テストコードの自動生成ができればいいのかなぁ
ソース => RAMLの場合
ソースは設計書兼製造物と考える
RAMLはそこから出力される何か
pros
- RAMLとソースの差異がない
cons
- RAML生成までに時間がかかる(実装が必要なため)
- 既存APIへの変更の場合は内部実装も修正しないとRAMLを生成できない
- Requestクラスを毎回新規作成すれば良さそうだが差分がわかりづらくなる
- 仕様書早くくれ〜って他チームから言われると辛い
感想
メンテまで考えるとソース => RAML
は現実的ではなさそう
ただRAML => ソースも自動化をしておかないと旨味を活かせない
疑問: スタブ作成?既存APIはどうすんの?
APIのバージョン管理をしてないと無理
優先低い
こんなこといいな、できたらいいな
spockのテストパタン自動生成
RAMLに記述したテストからspockのwhereとwhenを自動生成する
setupとthenは自力で書く
設計書(RAML)、テストコード、製造の流れが作りやすくなりそう
特に結論もなく以上です
【SpringBoot】Formクラスのフィールドを値オブジェクトにする
真面目に調べたことがなかったのでメモ
@RestController public class Api { @RequestMapping("/") public String index(Form form) { return "Greetings from Spring Boot! " + form.name.value; } // privateでもok private static class Form { private NameForm name; // フォームではセッターで渡す public void setName(NameForm nameForm) { this.name = nameForm; } } // privateでもok private static class NameForm { private final String value; // 値オブジェクトへはコンストラクタで渡す public NameForm(String value) { this.value = value; } } }
ポイント
Formクラス
- メソッド名をAPIのキー名と同じにする
- セッターへは値オブジェクトを渡す
値オブジェクト (NameForm)
- 値はコンストラクタで渡す
その他
Formや値オブジェクトはプライベートクラスにしてもOK
googleドキュメントにmermaid.jsを書く
googleドキュメントはオンライン上で編集できてとても便利
そこにmermaid.jsも書きたい!ってことでやってみた
ドキュメントを書く
こんな感じ
これを読み込んでHTMLのページ上に図を表示します
スクリプトエディタでプログラムを書く
ツール > スクリプトエディタ
でエディタを開いてプログラムを書きます
//ドキュメントのID var DOC_ID = 'YOUR_DOCUMENT_ID'; // HTMLを表示する function doGet() { return HtmlService.createTemplateFromFile("index").evaluate(); } // ドキュメントの内容を取得する function getDocs() { var docs=DocumentApp.openById(DOC_ID); return { title: docs.getName(), text: docs.getBody().getText() } }
YOUR_DOCUMENT_ID
のところにドキュメントのIDを入れます
具体的にはドキュメントのURLの↓この部分です
https://docs.google.com/document/d/!!ココ!!/edit
HTMLを作る
ファイル > 新規作成 > HTMLファイル
を選択しindex.html
というファイルを作ります
ファイルの中身はこんな感じ
<!DOCTYPE html> <html> <head> <base target="_top"> <script src="https://unpkg.com/mermaid@7.1.2/dist/mermaid.js"></script> </head> <body> <h1 id="title"></h1> <div id="mermaid" class="mermaid"></div> <script> function update() { google.script.run .withSuccessHandler(function(result) {// 戻り値に対する処理 console.log(result); document.querySelector('#title').innerHTML = result.title; document.querySelector('.mermaid').innerHTML = result.text; mermaid.init();// mermaidを更新 }) .getDocs(); } update();// 初回実行 mermaid.initialize({startOnLoad:false}); </script> </body> </html>
公開する
公開 > ウェブアプリケーションとして導入
を選択して必要事項を埋める
アプリケーションにアクセスできるユーザ
を自分だけ
にすると自分以外見れないので安心です
作成したサイトのURLにアクセスすると図が表示される
キターー!
まとめ
mermaid.jsが手軽にかけるようになった
markdown + mermaid.jsが表示できるようにしたくなってきた!
開発は続く
アクション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のテンキーで操作できます(スマホの人はごめんなさい)
ただ現段階では移動中のアニメーションや壁との衝突判定がないのでそれは次回以降実装します