ct.jsは、分かりやすいIDEでカンタンに2Dゲームが作れるフレームワークです。
サンプルのシューティング探索ゲーム(DungeonCrawler_tutorial)にカメラ弾要素を追加します。カメラ弾なんて単語は存在しないんですが、
- Qボタンで武器切り替え
- 左上に選択中の武器を表示
- カメラ弾を発射するとカメラはその弾を追う
- カメラ弾は消滅するまで次の弾を撃てない(目が痛くなりそうなので)
- カメラ弾が消滅したらプレイヤーキャラ中心にカメラが戻る
といった仕様にしましょう。
新たな入力ボタンを追加
IDEの上部タブのProject > Actions and input methods > Add an action でToggleWeapons
というアクションをkeyboard.KeyQ
に対応させます。以上です。
UIに武器表示
Rooms > UI_InGame > 左上Room events > On Create に選択中の武器を表すテキストを追加します。ここで、ct.jsがpixi.jsを前提としたものらしいので、
this.weapon = new PIXI.Text('Normal 普通',
new PIXI.TextStyle({fontSize:10, fill: 'white'})); // めちゃくちゃボケる。
this.weapon.anchor.x = this.weapon.anchor.y = 0;
this.weapon.x = 4;
this.weapon.y = 16; // Heartが8x8なので 4+8=12 以上
this.addChild(this.weapon);
このようにPIXI.Text
で追加しました。
武器切り替えを実装
さて、武器切り替えを実装したいのですが、「選択中の武器が何か」はだれが持ってる情報でしょうか。本サンプル的には体力としてct.room.lives=3
と"UI_InGame"のoncreateで宣言しているので、ct.room
が持っているようです。UIはステージが切り替わるごとに読み込まれるので、体力もステージ切替ごとに全快します。武器の選択に関してはリセットしたくなかったので、Project > Custom scripts にて下記のように実装しました。
// グローバルな変数になる
const weapons = {
Normal:{id:1, string:"Normal"},
Cam:{id:2, string:"Cam"}
};
let selectedWeapon = weapons.Normal;
また、
if (ct.actions.ToggleWeapons.pressed) {
if(selectedWeapon.id == weapons.Normal.id){
selectedWeapon=weapons.Cam;
}else if(selectedWeapon.id == weapons.Cam.id){
selectedWeapon=weapons.Normal;
}
}
と"UI"にキー入力の処理を書き、"UI"のDraw(ondraw
)にthis.weapon.text = selectedWeapon.string;
と追記します。ここでLaunchして、Qキーを押すごとに左上の表示が切り替わることを確認しました。
UIを司る部分に「ゲームの持つ状態」を処理させるのは何か変な感じがしますが、ct.jsの親しみやすさと何かがトレードオフしてるんだと思います。私の経験が浅いので、全然ベストじゃないプラクティスで書いちゃってる気もします。
カメラ弾を実装
typeのBulletを右クリックしてDuplicateします。名前を"CamBullet"とします。テクスチャは適当にParticle_Redかなんかに差し替えます。こいつはカメラ弾なので発射直後から消滅までカメラが追います。
On Create にct.camera.follow = this;
を、
On Destroyにct.camera.follow = ct.types.list['Cat'][0];
を追記します。
ct.types.list['Cat'][0]
というのはこのゲームのtypeオブジェクトの中の1番目に生成された'Cat'ぐらいの意味です。Catというのはプレイヤーです。要は主人公にカメラを戻します1。
さて、あとはtypeのCatにこれを撃たせます。
// ここに弾を撃つ時のロジックが集中しそう
if (ct.actions.Shoot.pressed) { // bulletを生成、方向を指定 => 発砲
if(selectedWeapon.id == weapons.Normal.id){
const bullet = ct.types.copy('Bullet', this.x, this.y - 5);
bullet.direction = ct.u.pdn(this.x, this.y - 5, ct.mouse.x, ct.mouse.y);
}else if(selectedWeapon.id == weapons.Cam.id
&& !ct.types.exists(ct.types.list["CamBullet"][0])){ /** カメラ弾は1発 **/
const bullet = ct.types.copy('CamBullet', this.x, this.y - 5);
bullet.direction = ct.u.pdn(this.x, this.y - 5, ct.mouse.x, ct.mouse.y);
}
}
元々の弾を撃つロジックに追記しました。少しゴチャついているのですが、回避する方法はあるはずです。今回は探しませんが。
書きもらしていなければ、以上で終わりです。
- カメラをプレイヤーに戻すときに急に戻ってしまい、目が疲れる
- ↑カメラを下に少しシフトする処理も漏れている。共通化するべき
- カメラ弾が消えない事象が発生するとプレイが破綻する
らへんが課題です。
思ったこと
- イベントの流れを把握しておいたのは役に立った気がする
- IDEだけでは変更履歴が分からない?gitで管理しろということだろう
- 複雑なものを書き出すとパスタを茹でることになりそう
- 詳細な実装を隠してしまうという意見があり、その通りだと思った。
- ドキュメントも充実してるとは言えない。
- 分かりやすさのために詳細な実装を隠したいし、ドキュメントも簡潔に保ちたいから?
- 30分遊べるゲームを10分で作るなら最強って位置づけだと思う。
- 俺はすき
あと、"Project`s notepad"と"Global notepad" という機能があって、メモ帳かと思ったら違うっぽいです。そこで定義した変数が他で使えるので、グローバル変数を置くところかもしれません。ググった限りだれもこの機能の話してない...
-
typeクラスが生成したものがcopyという捉え方をするのがct.js流みたいですが、コードにcopyという単語は出てこないので正直ややこしい気がします。copyという単語を使わないか、いっそct.copies['Cat'][0]とか書かせればいいんじゃないかと思いましたがややこしいので黙ってます。 ↩