先日Phaserのチュートリアルを終えたのですが、スマホで作業をする上でやりづらさを感じました。
今回はゲームを作るのではなく、迷いなく見やすいコードを書くために色々と試行錯誤したことをまとめます。
普通にPCで作業をする方には、一部おかしなルールに思えるかもしれませんが、参考になれば幸いです。
問題点
コーディング中に以下の問題点が出てきました。
- 一つの行が長くなってくると、折り返しが増えて見にくくなる
- 関数が長くて一画面に収まらない
- タブ幅が広いと見にくい
- 引数の改行、カッコの位置などの書式ルールが統一されていない
今後も同様にスマホでコーディングを行うにあたり、見やすくなるようなルールがあった方が良いように思えました。
基本ルール
どんな言語、どんな環境においても共通で推奨されるルールというものがあります。
まずはそれらを適用していきます。
関数の分割
長い関数は処理のまとまりごとに別関数に分離します。
function create (){
// 足場
createPlatforms(this);
// プレイヤー
createPlayer(this);
// 星
createStars(this);
// ボム
bombs = this.physics.add.group();
// 衝突の設定
colliderSetting(this);
}
ゲーム内の要素ごとのcreateを個別で呼ぶようにしました。
プレイヤーのcreate処理を変えたい場合はcreatePlayer関数の中だけに注目すればよいので、修正がしやすくなります。
ローカル変数を使う
何度も呼び出しているメンバーはローカル変数にまとめることで、呼び出しを短くできます。
let physics = scene.physics;
// 星と足場
physics.add.collider(
stars,
platforms
);
毎回 scene.physics.add のように書かなくても済みます。
JavaScript固有のルール
カッコの位置や大文字/小文字の使い方は自由ですが、言語ごとに推奨のルールがあることが多いです。
JavaScriptに関してもそのようなものがあると思ったので調べてみました。
いくつか見てみた中ではWordPressのコード規約がまとまっていてわかりやすかったので、こちらを基本に考えようと思います。
連続したメソッド呼び出し
連続したメソッド呼び出しが長く、行を折り返す場合、1行に1呼び出しを書いてください。
これに従うと、例えば足場の生成処理は以下のようになります。
// 足場1
platforms
.create(300, 150, 'ground')
.setScale(0.5)
.refreshBody();
カッコの位置
僕が仕事でC++/C#のコードを書くとき、
void function()
{
// 中身
}
のように中カッコの位置を揃えるクセがあるのですが、WordPressルールでは同じ行に中カッコを置くことが推奨されているようです。
今のところ特に強いこだわりも無いので、ひとまず合わせておきます。
スマホ向けの特別ルール
スマホで作業をする都合、画面幅が狭いという制約があります。
このため一行をできるだけ短くする書き方が望ましいです。
読みやすさを妨げない範囲で、以下のルールを考えてみました。
タブ幅
タブはスペースではなく「本物のタブ」で入れるのが推奨されているのですが、
- スマホのキーボードでタブが使えない
- タブ幅が4文字分あると改行が増える
という問題が出たので「スペース2つ」で統一します。
引数の改行
引数が複数あると行が長くなりがちなので、PCのとき以上に積極的に改行するようにしました。
anims.create({
key: 'right',
frameRate: 10,
repeat: -1,
frames:
anims.generateFrameNumbers(
'dude',
{ start: 5, end: 8 }
),
});
ただし、start, endのようにひとまとまりで一行に収まる長さのものについては、改行するとかえって見にくくなるので、そのままにしています。
できあがったのがこちら!
以上をふまえて整理したコードが以下です。
//---------------------------
// グローバル変数
//---------------------------
// ゲーム設定
var config = {
type: Phaser.AUTO,
width: 300,
height: 400,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
},
scene: {
preload: preload,
create: create,
update: update
}
};
// ゲーム本体
var game = new Phaser.Game(config);
// プレイヤー
var player;
// 足場グループ
var platforms;
// 星グループ
var stars;
// ボムグループ
var bombs;
// スコア
var score = 0;
// スコア表示テキスト
var scoreText;
//---------------------------
// ロード
//---------------------------
function preload (){
this.load.image(
'star',
'img/20210425182551.png'
);
this.load.image(
'sky',
'img/20210425175550.png'
);
this.load.image(
'ground',
'img/20210425175525.png'
);
this.load.image(
'bomb',
'img/20210425182611.png'
);
this.load.spritesheet(
'dude',
'img/20210425182602.png',
{
frameWidth: 32,
frameHeight: 48
}
);
}
//---------------------------
// 足場の生成
//---------------------------
function createPlatforms(scene){
// 足場グループを生成
platforms = scene.physics.add.staticGroup();
// 足場1
platforms
.create(300, 150, 'ground')
.setScale(0.5)
.refreshBody();
// 足場2
platforms
.create(0, 250, 'ground')
.setScale(0.5)
.refreshBody();
// 地面
platforms.create(
150,
385,
'ground'
);
}
//---------------------------
// プレイヤーアニメーションの生成
//---------------------------
function createPlayerAnims(anims){
// 正面
anims.create({
key: 'turn',
frameRate: 20,
frames:
[{ key: 'dude', frame: 4 }],
});
// 左
anims.create({
key: 'left',
frameRate: 10,
repeat: -1,
frames:
anims.generateFrameNumbers(
'dude',
{ start: 0, end: 3 }
),
});
// 右
anims.create({
key: 'right',
frameRate: 10,
repeat: -1,
frames:
anims.generateFrameNumbers(
'dude',
{ start: 5, end: 8 }
),
});
}
//---------------------------
// プレイヤーの生成
//---------------------------
function createPlayer(scene){
// プレイヤー
player = scene.physics.add.sprite(
150,
200,
'dude'
);
player.setBounce(0.2);
player.setCollideWorldBounds(true);
// アニメーション
createPlayerAnims(scene.anims);
}
//---------------------------
// 星の生成
//---------------------------
function createStars(scene){
// グループの生成
stars = scene.physics.add.group({
key: 'star',
repeat: 4,
setXY: {
x: 13,
y: 0,
stepX: 68
}
});
// 星の物理挙動を設定
stars.children.iterate(
function (child) {
child.setBounceY(
Phaser.Math.FloatBetween(0.4, 0.8)
);
}
);
}
//---------------------------
// 新しいボムを生成する
//---------------------------
function createNewBomb(){
// プレイヤーから離れたランダムな場所
let x = (player.x < 150) ?
Phaser.Math.Between(150, 300) :
Phaser.Math.Between(0, 150);
// ボムの生成
let bomb = bombs.create(
x,
16,
'bomb'
);
bomb.setBounce(1);
bomb.setCollideWorldBounds(true);
bomb.setVelocity(
Phaser.Math.Between(-200, 200),
20
);
}
//---------------------------
// 衝突の設定
//---------------------------
function colliderSetting(scene){
let physics = scene.physics;
// 星と足場
physics.add.collider(
stars,
platforms
);
// プレイヤーと星
physics.add.overlap(
player,
stars,
collectStar,
null,
this
);
// プレイヤーと足場
physics.add.collider(
player,
platforms
);
// ボムと足場
physics.add.collider(
bombs,
platforms
);
// プレイヤーとボム
physics.add.collider(
player,
bombs,
hitBomb,
null,
scene
);
}
//---------------------------
// プレイヤーと星の衝突時に呼ばれる
//---------------------------
function collectStar (player, star){
// 星を消す
star.disableBody(true, true);
// スコアを加える
score += 10;
scoreText.setText(
'Score: ' + score
);
// 全ての星を取った時
if (stars.countActive(true) === 0){
// 星を復活させる
stars.children.iterate(
function (child) {
child.enableBody(
true,
child.x,
0,
true,
true
);
}
);
// 新しいボムを生成する
createNewBomb();
}
}
//---------------------------
// プレイヤーとボムの衝突時に呼ばれる
//---------------------------
function hitBomb (player, bomb){
this.physics.pause();
player.setTint(0xff0000);
player.anims.play('turn');
gameOver = true;
}
//---------------------------
// ロード後処理
//---------------------------
function create (){
// this = scene
// マウス入力を有効にする
this.input.mouse.capture = true;
// 背景
this.add.image(
300,
300,
'sky'
);
// スコア
scoreText = this.add.text(
16,
16,
'Score: 0',
{ fontSize: '32px', fill: '#000' }
);
// 足場
createPlatforms(this);
// プレイヤー
createPlayer(this);
// 星
createStars(this);
// ボム
bombs = this.physics.add.group();
// 衝突の設定
colliderSetting(this);
}
//---------------------------
// 更新処理
//---------------------------
function update (){
let pointer = this.input.activePointer;
if(pointer.isDown) {
// 左移動
if(pointer.x < 150) {
player.setVelocityX(-160);
player.anims.play('left',true);
}
// 右移動
else if(150 < pointer.x) {
player.setVelocityX(160);
player.anims.play('right',true);
}
// ジャンプ
if(
pointer.y < 200 &&
player.body.touching.down
){
player.setVelocityY(-330);
}
}
// 停止
else {
player.setVelocityX(0);
player.anims.play('turn');
}
}
正解はひとつではない
JavaScriptによらず、コーディングルールというのは、開発を効率よく進めるために考えられるものです。
ルールに従うことで開発効率が下がったり、不要なミスを生んだりしてしまうなら、ルールを見直すべきなのかもしれません。
今回僕が決めたルールも、今後運用しながら適宜見直していければと思います。
コメント