ゲームプログラミング初心者向けの記事です。
本記事は「元祖落ちゲー」を、複数のステップに分けて作っていきます。
前回までの記事
この記事は以下の記事の続きです。
[Javascript][ゲームプログラミング] 元祖落ちゲーを作る(その1)
ブロックの落下と積み上げロジック。
[Javascript][ゲームプログラミング] 元祖落ちゲーを作る(その2)
リファクタリング
本記事では
以下の処理を追加します。
- ブロックが横一列(ライン)揃ったら消す
- 落下ブロックの回転処理
消すブロックと消さないブロックを区別する
画面の最下辺に初期化でブロックを敷いたのと、左右にも壁の代わりにブロックを置きました。
これらのブロックは消したくないものなので、落下ブロック(積み上がるブロック)とは区別したいところです。
単純に、画面のバッファに埋め込む数値を別にしたいと思います。
0 ... 何もない空間
1 ... 積み上がるブロックを表す
2 ... 消えないブロックを表す
とします。
ブロックが横一列(ライン)揃ったら消す
ラインが揃っているかどうかのチェック
基本的には下記図に示す考え方で実現します。
一番下のラインからチェックして行って、一行分ブロックが詰まっているかをチェックします。
複数の行が当てはまる可能性があります。
実装
以下のようにシンプルに実装できます。
function lineIsComplete(line) {
for (i = 1; i < SCREEN_W-1; i++) {
if (getScreen(i,line) != 1)
return false;
}
return true;
}
あとは、すべてのラインを下からチェックすれば良いです。例えば以下のように実装します。
for (j = SCREEN_H-2; j >=0 ; j--) {
if (lineIsComplete(j)) {
・・・
}
}
揃ったラインを消す
ゲームの仕様としては、ブロックが横一列に揃ったラインは消えます。そして、上に積まれたブロックはまるごと一行したに落ちます。
これを実現する方法は、ゲームのエフェクトも含めて考えると、いろいろあります。
が、ここでは最もシンプルに下記図のような方法で実装します。
実装
実際の実装は下記です。
function copyLine(src, tgt) {
for (i = 1; i < SCREEN_W-1; i++) {
putScreen(i, tgt, getScreen(i, src));
}
}
function deleteLine(line) {
for (j = line-1; j >= 0; j--) {
copyLine(j,j+1);
}
}
copyLine()関数は、src行目をtgt行目にコピーします。
deleteLine()はline行よりも上の行を一行ずつ下にコピーします。
揃ったラインの削除の実装
下記関数で、ラインが揃ったかどうかのチェックと揃ったラインの削除の両方を行なっています。
function checkLineComplete() {
do {
deleted_line = false;
for (j = SCREEN_H-2; j >=0 ; j--) {
if (lineIsComplete(j)) {
deleteLine(j);
j++;
deleted_line = true;
}
}
} while (deleted_line);
}
落下ブロックの描画と回転
まずは、回転していない場合の、落ちるブロックの描画について説明します。
ブロックの形は4 x 2の配列のデータで表されます。0はなにもない、1はブロックのピースが存在することを示します。下記図のように横から順に要素を舐めていって、データが1だった場合に、画面の該当する座標に塗りつぶし矩形を描画します。
(x, y) がブロックの形状データの左上 (i=0, j=0)だとして、図のブロックの形だと、(i=1, j=0), (i=2, j=0), (i=0, j=1), (i=1, j=1)がピースが存在するので、(x+1, y), (x+2, y), (x, j+1), (x+1, y+1) の座標に矩形を描画する訳です。
下記が、ソースの実装部分です。drawFallBlock()関数の中の case 0: の部分です。
function drawBlockShape(x, y, shape, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
switch(rotate) {
case 0: drawRect(x + i, y + j, block.color); break;
・・・
}
}
}
}
}
左に90度回転
ブロックの回転は、ブロックのピースの矩形を描画する時のi, j の方向を変えることで実現します。左に90°回転したときは、下記図の通りです。
drawFallBlock()関数の中の、case 1: の部分です。描画する際の座標の計算が (x + j, y – i) となっているのがポイントです。
function drawBlockShape(x, y, shape, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
switch(rotate) {
・・・
case 1: drawRect(x + j, y - i, block.color); break;
・・・
}
}
}
}
}
左に180°回転、270°回転
drawFallBlock()関数の中の、case 2: 、case 3: の部分です。
function drawBlockShape(x, y, shape, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
switch(rotate) {
・・・
case 2: drawRect(x - i, y - j, block.color); break;
case 3: drawRect(x - j, y + i, block.color); break;
}
}
}
}
}
全ソースコード
<html>
<center>
<canvas id="gameCanvas" width="240" height="440" style="border:1px solid #000000; background-color: #999;"></canvas>
<script type="text/javascript" src="index.js"></script>
</html>
const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
// ブロック
let blockX = 5;
let blockY = 0;
let rotate = 0;
const BLOCK_SIZE = 20
const block = {
shape: [1,1,0,0,
0,1,1,0],
color: 'white'
}
// 画面サイズ
const SCREEN_W = 12
const SCREEN_H = 22
let screen = Array(SCREEN_H*SCREEN_W);
function putScreen(x, y, value) {
screen[x + y * SCREEN_W] = value;
}
function getScreen(x, y) {
return screen[x + y * SCREEN_W];
}
function initScreen() {
screen.fill(0);
// 画面の一番下のラインにブロックを置く
for (let i = 0; i < SCREEN_W; i++) {
putScreen(i, SCREEN_H-1, 2);
}
for (let j = 0; j < SCREEN_H-1; j++) {
putScreen(0, j, 2);
putScreen(SCREEN_W-1, j, 2);
}
}
// 入力ハンドラー
let input = undefined;
document.addEventListener('keydown', (e) => input = e.key, false);
function hasBlock(x,y) {
return getScreen(x,y) > 0;
}
function isBlockHit(x, y, shape, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
flg = false;
switch(rotate) {
case 0: flg = hasBlock(x + i, y + j); break;
case 1: flg = hasBlock(x + j, y - i); break;
case 2: flg = hasBlock(x - i, y - j); break;
case 3: flg = hasBlock(x - j, y + i); break;
}
if (flg) return true;
}
}
}
return false;
}
function putBlock(x, y, shape, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1)
switch(rotate) {
case 0: flg = putScreen(x+i, y+j, 1); break;
case 1: flg = putScreen(x+j, y-i, 1); break;
case 2: flg = putScreen(x-i, y-j, 1); break;
case 3: flg = putScreen(x-j, y+i, 1); break;
}
}
}
}
function drawRect(x,y, color) {
context.strokeStyle = '#00f';
context.lineWidth = 2;
context.strokeRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
context.fillStyle = color
context.fillRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
function drawBlockShape(x, y, shape, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
switch(rotate) {
case 0: drawRect(x + i, y + j, block.color); break;
case 1: drawRect(x + j, y - i, block.color); break;
case 2: drawRect(x - i, y - j, block.color); break;
case 3: drawRect(x - j, y + i, block.color); break;
}
}
}
}
}
function drawScreen() {
for (j = 0; j < SCREEN_H; j++) {
for (i = 0; i < SCREEN_W; i++) {
if (getScreen(i,j)==1)
drawRect(i, j, '#fff');
if (getScreen(i,j)==2)
drawRect(i, j, '#ddd');
}
}
}
function handleInputs() {
if (input === 'Left' || input === 'ArrowLeft') {
if (!isBlockHit(blockX - 1, blockY, block.shape, rotate))
blockX -= 1;
}
if (input === 'Right' || input === 'ArrowRight') {
if (!isBlockHit(blockX + 1, blockY, block.shape, rotate))
blockX += 1;
}
if (input === 'Down' || input === 'ArrowDown') {
if (!isBlockHit(blockX, blockY+1, block.shape, rotate))
blockY += 1;
}
if (input === 'x') {
if (!isBlockHit(blockX, blockY+1, block.shape, (rotate+ 1) % 4))
rotate = (rotate + 1) % 4; // 回転
}
input = undefined; // 入力をリセット
}
let count = 0;
function fallingBlock() {
count += 1;
if (count % 30 != 0)
return;
// ブロックを落下させる
blockY += 1;
if (blockY*BLOCK_SIZE > canvas.height) {
blockY = 0;
}
count = 0;
}
function lineIsComplete(line) {
for (i = 1; i < SCREEN_W-1; i++) {
if (getScreen(i,line) != 1)
return false;
}
return true;
}
function copyLine(src, tgt) {
for (i = 1; i < SCREEN_W-1; i++) {
putScreen(i, tgt, getScreen(i, src));
}
}
function deleteLine(line) {
for (j = line-1; j >= 0; j--) {
copyLine(j,j+1);
}
}
function checkLineComplete() {
do {
deleted_line = false;
for (j = SCREEN_H-2; j >=0 ; j--) {
if (lineIsComplete(j)) {
deleteLine(j);
j++;
deleted_line = true;
}
}
} while (deleted_line);
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
checkLineComplete();
handleInputs();
fallingBlock();
// ブロックがブロックにぶつかったら積み上げる
if (isBlockHit(blockX, blockY, block.shape, rotate)) {
putBlock(blockX, blockY-1, block.shape, rotate); // ブロックを積み上げる
blockX = 5; // ブロックの位置をリセット
blockY = 1;
rotate = 0;
}
drawBlockShape(blockX, blockY, block.shape, rotate);
drawScreen();
}
initScreen();
setInterval(update, 20);




