この記事では、YoutuberであるAkichonさんの下記動画をベースに、JavaScript(JS)による自作テトリス製作時の気付き・トラブル事例を記載します。
- チャンネル名:Akichonプログラミング講座
- タイトル:プログラミング講座 第15回、第16回
【テトリスを作る(3)、(4)/JavaScript】
前回の下記記事にて、テトリスブロックを動かすことはできました。
今回はテトリスブロックの当たり判定をつけるところを採り上げます。
最初に注意点として、私はプログラミング初心者です。
そのため、この記事は初心者目線での気付き・トラブル事例の紹介が主体となります。
解説内容については、私なりに調べた内容を記載していますが、内容に誤りがある恐れもございますので、その点はご理解お願いいたします。
自作テトリスのソースコード(間違い有り)
今回紹介するソースコードには私が間違えた箇所を敢えて記載しています。
私の間違いが2箇所と、冒頭の動画で紹介されている間違い箇所が1箇所あります。
動画で紹介されている箇所については、動画の続編で修正されているため、この記事では取り扱いません。
さて、どこが間違っているかわかるでしょうか?
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Tetris</title>
5 </head>
6 <body>
7 <canvas id="can"></canvas>
8 <script>
9 //フィールドサイズ
10 const FIELD_W = 10;
11 const FIELD_H = 20;
12
13 // ブロック一つのサイズ(ピクセル)
14 const BLOCK_SIZE = 30;
15
16 //キャンバスサイズ
17 const SCREEN_W = BLOCK_SIZE * FIELD_W;
18 const SCREEN_H = BLOCK_SIZE * FIELD_H;
19
20 // テトロミノのサイズ
21 const TETRO_SIZE = 5;
22
23 let can = document.getElementById("can");
24 let con = can.getContext("2d");
25
26 can.width = SCREEN_W;
27 can.height = SCREEN_H;
28 can.style.border = "4px solid #555";
29
30 //テトロミノ本体
31 let tetro = [
32 [0, 0, 1, 0, 0],
33 [1, 1, 1, 1, 1],
34 [0, 0, 1, 0, 0],
35 [0, 1, 0, 1, 0],
36 [1, 0, 0, 0, 1]
37 ];
38 //テトロミノの座標
39 let tetro_x = 0;
40 let tetro_y = 0;
41
42 //フィールドの中身
43 let field = [];
44
45 init();
46 drawAll();
47
48 //テストブロック
49 field[5][8] = 1;
50
51 //初期化
52 function init() {
53 for(let y=0; y<FIELD_H; y++) {
54 field[y] = [];
55 for(let x=0; x<FIELD_W; x++) {
56 field[y][x] = 0;
57 }
58 }
59 }
60
61
62 //ブロック一つを描画
63 function drawBlock(x, y) {
64 let px = (tetro_x + x) * BLOCK_SIZE;
65 let py = (tetro_y + y) * BLOCK_SIZE;
66
67 con.fillStyle = "red";
68 con.fillRect(px, py, BLOCK_SIZE, BLOCK_SIZE);
69 con.strokeStyle = "black";
70 con.strokeRect(px, py, BLOCK_SIZE, BLOCK_SIZE);
71 }
72
73 //フィールドを表示する関数
74 function drawAll() {
75 con.clearRect(0, 0, SCREEN_W, SCREEN_H);
76
77 for(let y=0; y<FIELD_H; y++) {
78 for(let x=0; x<FIELD_W; x++) {
79 if(field[y][x] == 1) {
80 drawBlock(x, y);
81 }
82 }
83 }
84
85 for(let y=0; y<TETRO_SIZE; y++) {
86 for(let x=0; x<TETRO_SIZE; x++) {
87 if(tetro[y][x] == 1) {
88 drawBlock(tetro_x + x, tetro_y + y);
89 }
90 }
91 }
92 }
93
94 function checkMove(mx, my) {
95 for(let y=0; y<TETRO_SIZE; y++) {
96 for(let x=0; x<TETRO_SIZE; x++) {
97 let nx = tetro_x + mx + x;
98 let ny = tetro_y + my + y;
99 if(tetro[y][x]) {
100 if(field[ny][nx] ||
101 ny < 0 ||
102 nx < 0 ||
103 ny >= FIELD_H ||
104 nx >= FIELD_W) {
105 return false;
106 }
107 }
108 }
109 }
110 return true;
111 }
112
113 document.onkeydown = function(e) {
114
115 switch(e.keyCode) {
116 case 37: //左
117 if(checkMove(-1, 0))tetro_x--;
118 break;
119 case 38: //上
120 if(checkMove(0, -1))tetro_y--;
121 break;
122 case 39: //右
123 if(checkMove(1, 0))tetro_x++;
124 break;
125 case 40: //下
126 if(checkMove(0, 1))tetro_y++;
127 break;
128 case 32: //スペース
129 break;
130 }
131 drawAll();
132 }
133
134 </script>
135 </body>
136 </html>
実際にこのコードでブラウザに表示してみると、最初の表示で49行目のテストブロックが表示されていないことがわかります。
十字キーでテトリスブロック(テトロミノ)を移動させると、テストブロックが表示されます。
また、テトロミノを移動させるとき、1マス移動ではなく2マス移動となっています。
合わせて、テストブロックも移動してしまいます。
上記の動作から、プログラミング初心者でも、どこに問題があるか大体理解できるかもしれません。
まず最初の表示で、49行目のテストブロックが表示されない問題は、フィールドを表示する関数drawAll( ) がテストブロック field[5][8] = 1 よりも先に記載されていることが原因です。
コードは上から順に読まれていくため、drawAll( )でフィールドが描画された時点ではテストブロックは読み込まれていません。
キーボードの十字キーを押すことでonkeydown属性を読み込むと、恐らくソース全てが読み込まれることで、テストブロックが表示されるようになるものと考えます。
それでは、テストブロック field[5][8] = 1 を 関数drawAll( ) より上に記載すればよいという話になります。
しかし、注意したいのは、テストブロックの定義は関数init( ) 内の field[y][x] よりも後に記載しなければいけないということです。
field の配列を定義していないのに、いきなり 「field の配列はこうですよ」と指示されても「fieldって何?」というエラーが出るという話です。
そのため、関数init( ) と関数drawAll( ) の間または関数init( ) 内で field の定義をした後に、テストブロックの記載をすることが正しいです。
次に、テトロミノの移動が2マス移動になっている問題について、原因は64行目と65行目のコードにあります。
ブロック1つを描画する関数drawBlock( ) は引き渡された(x, y)座標を基に、(px, py)の座標にブロックを1つ描画する関数です。
しかし、64行目と65行目では、(px, py)座標の設定にテトロミノの座標 (tetro_x, tetro_y) が余分に追加されています。
テトロミノの座標は、キーボードの十字キーで移動させる際に増減しますので、本来の1マス移動に加えて、(tetro_x, tetro_y)分の十字キーの移動が更に追加されるため、2マス移動してしまう形になっています。
まとめ
今回のコードを書いた上で重要だと感じたことは下記の2点です。
チェックポイント
・ソースコードの読み込みの順序を理解する
・それぞれの関数で基準となる値(引数など)について理解する
この2点はオンライン学習などの教材に沿って学習しても、きちんと理解できないと考えています。
なぜなら、それらは初心者に理解しやすいように作られているため難易度が低く、応用性は身に付かないと考えるからです。
教材に頼らず、自分の力で自分が作りたい機能を実現しようとした際、上記の読み込み順序および関数の基準値の間違いによるエラーは出てくると思います。
そうした問題について、問題点を調査して解決していくことで、きちんとした理解が深まるものだと考えます。
実際、私のこのテトリス作成もアレンジは加えていますが、基本的には冒頭の動画の模写となりますので、今回のソースコードについて、きちんとした理解でできているとはいえない状態です。
しかし、模写をすることで経験と知識は身に付いている実感があるため、模写から自作へと段階的にステップアップしていくのは良いと考えます。
コメント