第五ステージの障害物の動作編
ご訪問いただき誠にありがとうございます。
今回は第五ステージの障害物の処理について解説いたします。
第五ステージは天井と床から先の尖った障害物が上下に動いて行く手を阻む、というステージです。上下の移動は一定間隔ですので、タイミングさえ合わせれば簡単にクリアできるステージだと思います。ただ一気には通り抜けられないため、いちいち安全な場所に止まりながら進まないといけないので多少イライラするステージになっていると思います。
実際の「避けゲー」で遊びたい方は以下よりアクセスしてください。
ソースコードはGitHub上にアップしていますので適宜参考にしてください。
第五ステージの制御
第五ステージはenemy7関数で制御しています。今回の障害物の形は三角形にしています。JavaScriptのキャンバス 2Dの標準APIには三角形を描くメソッドが提供されていませんので、moveTo()メソッド、lineTo()メソッド、closePath()メソッドを使って三角形のパスを作成しています。使い方は以下のような感じです。
- moveTo(x1, y1)・・・最初の座標(x1, y1)を指定
- lineTo(x2, y2)・・・(x1, y1)から(x2, y2)に線を引く
- lineTo(x3, y3)・・・(x2, y2)から(x3, y3)に線を引く
- closePath()・・・(x1, y1)と(x3, y3)のパスを閉じる
ちなみにこのままでパスを引いただけですので描画しません。描画するためにはfill()メソッドまたはstroke()メソッドを続けて呼び出しましょう。線だけを描画するときはstroke()、図形の中を塗りつぶす場合はfill()を使用します。
moveToメソッド
void ctx.moveTo(x, y);
新しいサブパスの始点を(x, y)に移します。
線を描くために紙の上にペンを置いた、とイメージしてもらえると分かりやすいでしょうか。
lineToメソッド
void ctx.lineTo(x, y);
現在のサブパスに対して、その終点から指定された(x, y)座標に向けて直線を追加します。
lineToメソッドを連続的に呼び出せば、引数で指定した座標を次々と線で結んでくれます。ただし、このメソッドを呼び出してもキャンバスに直接描画されることはない点に注意しましょう。
ではenemy7関数のアクティビティ図と実際のソースコードを見ていきます。
enemy7関数のアクティビティ図
enemy7関数のコード(avoider.jsから抜粋)
let ex7 = 500; //障害物のベースのx座標
let ey7 = 120; //障害物の初期の頂点の位置
let eh7 = 160; //隣の障害物との間隔
let em7 = 4; //上下移動の移動量
let et7 = 0; //第五ステージ用のカウンタ
function enemy7() {
//上段一番左の障害物の表示
ctx.beginPath();
ctx.moveTo(ex7+Rex,ey7);
ctx.lineTo(ex7-30+Rex,ey7-300);
ctx.lineTo(ex7+30+Rex,ey7-300);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
//上段左から二番目の障害物の表示
ctx.beginPath();
ctx.moveTo(ex7+eh7+Rex,ey7);
ctx.lineTo(ex7-30+eh7+Rex,ey7-300);
ctx.lineTo(ex7+30+eh7+Rex,ey7-300);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
//上段左から三番目の障害物の表示
ctx.beginPath();
ctx.moveTo(ex7+eh7*2+Rex,ey7);
ctx.lineTo(ex7-30+eh7*2+Rex,ey7-300);
ctx.lineTo(ex7+30+eh7*2+Rex,ey7-300);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
//下段一番左の障害物の表示
ctx.beginPath();
ctx.moveTo(ex7+eh7*0.5+Rex,ey7);
ctx.lineTo(ex7+eh7*0.5-30+Rex,ey7+300);
ctx.lineTo(ex7+eh7*0.5+30+Rex,ey7+300);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
//下段左から二番目の障害物の表示
ctx.beginPath();
ctx.moveTo(ex7+eh7*1.5+Rex,ey7);
ctx.lineTo(ex7+eh7*1.5-30+Rex,ey7+300);
ctx.lineTo(ex7+eh7*1.5+30+Rex,ey7+300);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
//下段左から三番目の障害物の表示
ctx.beginPath();
ctx.moveTo(ex7+eh7*2.5+Rex,ey7);
ctx.lineTo(ex7+eh7*2.5-30+Rex,ey7+300);
ctx.lineTo(ex7+eh7*2.5+30+Rex,ey7+300);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
et7++; //カウンタアップ
ey7=ey7+em7; //障害物管理用のy軸座標の増減
if (ey7==200||ey7==120) { //障害物の移動が下限か上限の位置になったとき
if (em7!=0) { //障害物が移動していたら
em7=0; //移動を止める
et7=0; //カウンタをクリアする
}
}
if (et7==50) { //カウンタクリア後、0.5秒経過時
if (ey7==200) { //障害物が下限に来た時
em7=-4; //障害物のy座標を上方向の移動量に切り替え
}else { //障害物が上限に来た時
em7=+4; //障害物のy座標を下方向の移動量に切り替え
}
}
//障害物が下に移動しているとき
if (ey7>=160) {
//上段の障害物の頂点と自キャラのx座標が重なった時
if ((x>=ex7-20&&x<=ex7+20)||(x>=ex7+eh7-20&&x<=ex7+eh7+20)||(x>=ex7+eh7*2-20&&x<=ex7+eh7*2+20)) {
gameover=true;
}
}
//障害物が上に移動しているとき
if (ey7<=160) {
//下段の障害物の頂点と自キャラのx座標が重なった時
if ((x>=ex7+eh7*0.5-20&&x<=ex7+eh7*0.5+20)||(x>=ex7+eh7*1.5-20&&x<=ex7+eh7*1.5+20)||(x>=ex7+eh7*2.5-20&&x<=ex7+eh7*2.5+20)) {
gameover=true;
}
}
}
- 8行目~54行目までは障害物の描画処理になっています。三角形の頂点が異なるだけで、全く同じ処理が6回繰り返されています。こういった場合は関数にまとめた方がいいですね。
- 56行目~70行目は障害物を動かす処理です。障害物の動作は「下に移動→0.5秒停止→上に移動→0.5秒停止→以降繰り返し」となっています。変数を駆使してこの動きを実現していますが、他の人がコードを見ても分かりにくいのではないかと思います。このようなときは状態を持ち、シーケンシャルに制御した方がシンプルで分かりやすいですね。
- 73行目~85行目は障害物と自キャラの当たり判定です。障害物は上下に動いているため、障害物のx座標だけでは当たり判定できませんので、障害物の先端が自キャラに当たっているかの条件も加えています。
リファクタリング
先ほどのコメントで言及した通り、リファクタリングした方がよい処理がありますので、今回もリファクタリングにチャレンジしてみたいと思います。リファクタリングの対象は以下です。
- 障害物の表示処理をまとめる
- 障害物の移動を状態遷移で制御する
では1つ1つ見ていきましょう。
障害物の表示処理をまとめる
障害物の表示処理をまとめるリファクタリングは割と簡単にできます。それぞれの処理で異なっている点は三角形の頂点の座標だけですので、頂点の3点の座標を引数で渡せばOKですね。3点の座標を引数でもらって三角形を描画する関数を定義してみます。
3点の座標をもらって三角形に描画する関数定義
function drawTriangle(x1, y1, x2, y2, x3, y3){
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
}
あとはそれぞれの処理を上記で定義した関数を呼び出す形に修正すれば完成です。
関数呼び出しに変更
//上段一番左の障害物の表示
drawTriangle(ex7+Rex, ey7, ex7-30+Rex, ey7-300, ex7+30+Rex, ey7-300);
//上段左から二番目の障害物の表示
drawTriangle(ex7+eh7+Rex, ey7, ex7-30+eh7+Rex, ey7-300, ex7+30+eh7+Rex, ey7-300);
//上段左から三番目の障害物の表示
drawTriangle(ex7+eh7*2+Rex, ey7, ex7-30+eh7*2+Rex, ey7-300, ex7+30+eh7*2+Rex, ey7-300);
//下段一番左の障害物の表示
drawTriangle(ex7+eh7*0.5+Rex, ey7, ex7+eh7*0.5-30+Rex, ey7+300, ex7+eh7*0.5+30+Rex, ey7+300);
//下段左から二番目の障害物の表示
drawTriangle(ex7+eh7*1.5+Rex, ey7, ex7+eh7*1.5-30+Rex, ey7+300, ex7+eh7*1.5+30+Rex, ey7+300);
//下段左から三番目の障害物の表示
drawTriangle(ex7+eh7*2.5+Rex, ey7, ex7+eh7*2.5-30+Rex, ey7+300, ex7+eh7*2.5+30+Rex, ey7+300);
ライン数は42行から15行まで圧縮できました。さらに他のステージでも三角形の障害物を作りたい場合もこの関数を再利用すれば簡単に三角形を描画できますね。もし三角形ごとに色を変えたい場合は引数を一つ追加すれば、色の設定も自由に行えます。どこまで関数にまかせる
障害物の移動を状態遷移で制御する
次は障害物の移動処理をリファクタリングしてみましょう。
システム開発の仕事ではプログラムは書いて終わり、ではなく、そのプログラムはそのあとも引き継がれることがほとんどですし、別のプロジェクトから流用されることもよくあります。つまり自分が書いたコードが多くの人の目に触れますので「自分だけが分かればいい」という道理は通りません。ときには「短くて難解なコード」よりも「長くても分かりやすいコード」の方がよい場合があります。仕事では超初心者から大ベテランまでさまざまなレベルのプログラマがコードを書いたり読んだりしますので、難解過ぎるコードだと初心者の人が対応できなくなってしまいます。開発現場では「シンプルかつ分かりやすいロジック」が一番求められます。
今回のロジックはいくつもの変数を使って制御していますので、コード自体は短いのですがパッと見て分かるとは言い難いコードだと思います。では分かりやすいコードを意識してリファクタリングしてみましょう。障害物の移動は4つのシーケンスで制御されています。障害物を一定の高さまで下に移動→0.5秒停止→障害物を一定の高さまで上に移動→0.5秒停止、このシーケンスを繰り返しています。1つ1つのシーケンスごとに処理を分けた方が分かりやすくなりそうですね。リファクタリングしたコード例がいかです。
リファクタリング後のコード
switch(state){
case 0:
ey7 += 4; //障害物のy座標を下方向に移動
if (ey7 >= 200) { //障害物の頂点が200pxを超えた?
et7=0; //カウンタクリア
state++; //次の状態(case 1)に遷移
}
break;
case 1:
et7++; //カウントアップ
if (et7 >= 50) { //0.5秒経過?
state++; //次の状態(case 2)に遷移
}
break;
case 2:
ey7 -= 4; //障害物のy座標を上方向に移動
if (ey7 <= 120) { //障害物の頂点が120pxより上になった?
et7=0; //カウンタクリア
state++; //次の状態(case 3)に遷移
}
break;
case 3:
et7++; //カウントアップ
if (et7 >= 50) { //0.5秒経過?
et7=0; //カウンタクリア
state = 0; //最初の状態(case 0)に戻る
}
break;
}
シーケンスの状態を持つ「state」というグローバル変数を新しく定義しました。また「em7」という変数は無くして、代わりにリテラルな数値で設定しています。
如何でしょうか?ライン数は15行から28行に増えていますが、シンプルで分かりやすいコードになったと思います。仮に仕様を変更し、もっとシーケンスを増やして複雑な動きにしたい場合ー例えば横に動かすパターンも追加する、といった仕様変更にも対応しやすくなりました。
今回は分かりやすさを重視してほとんどの数値はリテラルで記載していますが、仕事でコードを書くときはリテラルなコードは極力避けて定数化しましょう。
リファクタリング後のenemy7関数の全文を掲載しておきます。
リファクタリング後の全文
let ex7 = 500; //障害物のベースのx座標
let ey7 = 120; //障害物の初期の頂点の位置
let eh7 = 160; //隣の障害物との間隔
let et7 = 0; //第五ステージ用のカウンタ
let state = 0;
function enemy7() {
//上段一番左の障害物の表示
drawTriangle(ex7+Rex, ey7, ex7-30+Rex, ey7-300, ex7+30+Rex, ey7-300);
//上段左から二番目の障害物の表示
drawTriangle(ex7+eh7+Rex, ey7, ex7-30+eh7+Rex, ey7-300, ex7+30+eh7+Rex, ey7-300);
//上段左から三番目の障害物の表示
drawTriangle(ex7+eh7*2+Rex, ey7, ex7-30+eh7*2+Rex, ey7-300, ex7+30+eh7*2+Rex, ey7-300);
//下段一番左の障害物の表示
drawTriangle(ex7+eh7*0.5+Rex, ey7, ex7+eh7*0.5-30+Rex, ey7+300, ex7+eh7*0.5+30+Rex, ey7+300);
//下段左から二番目の障害物の表示
drawTriangle(ex7+eh7*1.5+Rex, ey7, ex7+eh7*1.5-30+Rex, ey7+300, ex7+eh7*1.5+30+Rex, ey7+300);
//下段左から三番目の障害物の表示
drawTriangle(ex7+eh7*2.5+Rex, ey7, ex7+eh7*2.5-30+Rex, ey7+300, ex7+eh7*2.5+30+Rex, ey7+300);
switch(state){
case 0:
ey7 += 4; //障害物のy座標を下方向に移動
if (ey7 >= 200) { //障害物の頂点が200pxを超えた?
et7=0; //カウンタクリア
state++; //次の状態に遷移
}
break;
case 1:
et7++; //カウントアップ
if (et7 >= 50) { //0.5秒経過?
state++; //次の状態に遷移
}
break;
case 2:
ey7 -= 4; //障害物のy座標を上方向に移動
if (ey7 <= 120) { //障害物の頂点が120pxより上になった?
et7=0; //カウンタクリア
state++; //次の状態に遷移
}
break;
case 3:
et7++; //カウントアップ
if (et7 >= 50) { //0.5秒経過?
et7=0; //カウンタクリア
state = 0; //最初の状態に戻る
}
break;
}
//障害物が下に移動しているとき
if (ey7>=160) {
//上段の障害物の頂点と自キャラのx座標が重なった時
if ((x>=ex7-20&&x<=ex7+20)||(x>=ex7+eh7-20&&x<=ex7+eh7+20)||(x>=ex7+eh7*2-20&&x<=ex7+eh7*2+20)) {
gameover=true;
gamewait = true;
}
}
//障害物が上に移動しているとき
if (ey7<=160) {
//下段の障害物の頂点と自キャラのx座標が重なった時
if ((x>=ex7+eh7*0.5-20&&x<=ex7+eh7*0.5+20)||(x>=ex7+eh7*1.5-20&&x<=ex7+eh7*1.5+20)||(x>=ex7+eh7*2.5-20&&x<=ex7+eh7*2.5+20)) {
gameover=true;
gamewait = true;
}
}
}
まとめ
今回も最後までご覧いただきありがとうございます。
今回は第五ステージの障害物の動きを見ていきました。第五ステージでは先端の尖った杭のような障害物が行く手を阻んでいます。
障害物は一定間隔で同じ動きを繰り返すだけですので処理としてはそれほど複雑ではありません。とはいえ制御する変数が増えてくると、他の人が見ると分かりにくいコードになってしまいがちです。プログラムは書いて動いたら終わりではなく、もう一度コードを見直す機会を持つことをお勧めします。コードを見直したときにもし「自分でも分かりにくい」とか「同じ処理を何度も書いている」といったところがあれば、おそらく改善の余地があると思います。積極的にリファクタリングしましょう。
次回はいよいよ最終ステージです。最終ステージは宝箱をゲットするのですが、そのあと大地がグラグラし、右から巨大な障害物が追いかけてきます。ステージを逆戻りするしか生き残る術はなさそうです。
次回のブログにもお付き合いいただけると嬉しいです。
【PR】FXを始めるなら【DMM FX】!