2回にわけてミニマムに遊べるテトリス風の落ちものパズルゲームを作っていきます。ゲームエンジンやライブラリを使わずに、素のHTMLとJavaScriptだけでゲームを作ります。
以下のような人のためにあります。
ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは面倒くさい。
ある程度プログラミングの知識がある人を対象にしています。
先に読んでおくもの
この記事には前編があります。ここでは語らない、ブロックが落ちる、積み上がる、行が消えるなどの実装については、下記記事を読んでください。
この記事で作るもの
テトリスっぽいゲームが遊べるように完成させます。
- ブロックの種類の定義
- ブロックの回転
- ゲームの状態管理
を作っていきます。
ソースコード
前編と同じ部分、記事で説明している部分は一部省略しています。
const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
const SCREEN_W = 10
const SCREEN_H = 20
const BLOCK_SIZE = 20
const State = {
STANBY: 0,
STARTING: 1,
GAME: 2,
GAME_OVER: 3
}
let screen = Array(SCREEN_H*SCREEN_W);
// ブロック
let blockX = 5;
let blockY = 0;
let rotate = 0;
const blockShapeTable = [
// 省略
]
let blockType;
// ゲーム状態管理
let gameState = State.STANBY;
// 入力ハンドラー
document.addEventListener('keydown', keyDownHandler, false);
function keyDownHandler(event) {
if (event.key === 'Left' || event.key === 'ArrowLeft') {
if (!blockHit(blockX-1, blockY, blockType, rotate))
blockX -= 1;
}
if (event.key === 'Right' || event.key === 'ArrowRight') {
if (!blockHit(blockX+1, blockY, blockType, rotate))
blockX += 1;
}
if (event.key === 'd') {
if (!blockHit(blockX, blockY+1, blockType, rotate))
blockY += 1;
}
if (event.key === 'x') {
new_rotate = (rotate+1)%4;
if (!blockHit(blockX, blockY+1, blockType, new_rotate))
rotate = new_rotate;
}
if (event.key == 's') {
if (gameState == State.STANBY) gameState = State.STARTING;
}
if (gameState == State.GAME_OVER) gameState = State.STANBY;
}
function drawRect(x, y, color) {
if (color==0) {
context.fillStyle = 'black';
context.fillRect((x+1) * BLOCK_SIZE, (y+1) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
} else {
context.fillStyle = color;
context.fillRect((x+1) * BLOCK_SIZE, (y+1) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
context.strokeStyle = 'gray';
context.lineWidth = 1;
context.strokeRect((x+1) * BLOCK_SIZE, (y+1) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
}
function drawScreen() {
for (j = 0; j < SCREEN_H; j++) {
for (i = 0; i < SCREEN_W; i++) {
color = screen[ i + j*SCREEN_W ];
drawRect(i, j, color);
}
}
}
function hasBrick(x,y) {
if (x < 0 || x >= SCREEN_W || y >= SCREEN_H)
return true;
return 0!=screen[x + y*SCREEN_W];
}
function drawFallBlock(x, y, block, rotate) {
// 記事で説明しているので省略
}
function blockHit(x, y, block, rotate) {
// 記事で説明しているので省略
}
function putBlock(x, y, block) {
// 記事で説明しているので省略
}
function setBrick(x, y, block) {
screen[x + y*SCREEN_W] = block;
}
// ・・・省略
const WAIT_COUNT = 40;
let wait_count = 0;
function is_in_interval(count) {
wait_count += 1;
if (wait_count < count) {
return true;
}
wait_count = 0;
return false;
}
function update() {
if (is_in_interval(WAIT_COUNT))
return;
if (blockHit(blockX, blockY+1, blockType, rotate)) {
putBlock(blockX, blockY, blockType, rotate); // ブロックを積み上げる
// check Game Over
for (i = 0; i < SCREEN_W; i++) {
if (screen[i] != 0)
gameState = State.GAME_OVER;
}
initFallBlock();
return;
}
checkLineComplete();
blockY += 1;
}
function draw_text_center(text, color='#fff') {
x = BLOCK_SIZE;
y = canvas.height/2 - 40;
context.fillStyle = 'black';
context.fillRect(x, y, canvas.width-BLOCK_SIZE*2, 60);
context.fillStyle = color;
context.font = '16px Consolas';
context.textAlign = 'left';
text_w = context.measureText(text).width;
context.fillText(text, canvas.width/2-text_w/2, canvas.height/2);
}
function initFallBlock() {
blockX = 4;
blockY = 0;
rotate = 0;
bsi = Math.floor(Math.random()*7);
blockType = blockShapeTable[bsi];
}
function init() {
initFallBlock();
screen.fill(0);
rotate = 0;
}
function clearScreen() {
context.fillStyle = 'gray'
context.fillRect(0, 0, canvas.width, canvas.height);
}
function draw() {
switch (gameState) {
case State.STANBY:
clearScreen();
draw_text_center("Press 'S' Key to Start");
break;
case State.STARTING:
init();
gameState = State.GAME;
break;
case State.GAME:
clearScreen();
drawScreen();
drawFallBlock(blockX, blockY, blockType, rotate);
update();
break;
case State.GAME_OVER:
draw_text_center("Game Over!!", '#f00');
break;
}
}
setInterval(draw, 10);
ブロックの種類
ブロックの形は下記の図の通り、7種類あります。
4 x 2の配列で形を定義できますが、扱いやすいので1次元配列で実装しています。ソースコードの下記の部分です。
const blockShapeTable = [
{
shape: [1,1,0,0,
1,1,0,0], // O type
color: 'yellow'
},
{
shape: [1,1,1,1,
0,0,0,0], // I type
color: 'aqua'
},
{
shape: [0,1,1,0,
1,1,0,0], // S type
color: 'green'
},
{
shape: [1,1,0,0,
0,1,1,0], // Z type
color: 'red'
},
{
shape: [1,0,0,0,
1,1,1,0], // L type
color: 'orange'
},
{
shape: [0,0,1,0,
1,1,1,0], // J type
color: 'blue'
},
{
shape: [1,1,1,0,
0,1,0,0], // T type
color: 'purple'
}
]
ブロックの描画と回転
まずは、回転していない場合の、落ちるブロックの描画について説明します。
ブロックの形は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 drawFallBlock(x, y, block, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (block.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 drawFallBlock(x, y, block, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (block.shape[i+j*4]==1) {
switch(rotate) {
・・・
case 1: drawRect(x+j, y-i, block.color); break;
・・・
}
}
}
}
}
左に180°回転、270°回転
ソースコードでは、drawFallBlock()関数の中の、case 2: 、case 3: の部分です。
drawFallBlock()関数の全貌は下記のとおりです。
function drawFallBlock(x, y, block, rotate) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (block.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;
}
}
}
}
}
回転の中心をどこにするか?
ここでは実装を簡単にするために、回転の中心をブロックの形データの左上のピース(i=0, j=0)としました。実際、これでも遊べますが、ちょっと「回転がきれいじゃない」と感じる人もいるかと思います。O typeでも回転しちゃいますしね。
もう少し工夫して、形ごとに回転の中心を変えてみるということも考えられます。興味ある人は、いろいろ実装を変えてみてください。
回転した状態の当たり判定と積み上げ処理
落ちるブロックの描画と考え方は同じです。当たり判定の関数と積み上げ処理の関数は、前回の記事で紹介したものがベースになります。これに対して回転の方向の処理を追加する、だけです。
function blockHit(x, y, block, rotate) {
// ブロックの形データ4 x 2を見ていく
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (block.shape[i+j*4]==1) {
flg = false;
switch(rotate) {
case 0: flg = hasBrick(x+i, y+j); break;
case 1: flg = hasBrick(x+j, y-i); break;
case 2: flg = hasBrick(x-i, y-j); break;
case 3: flg = hasBrick(x-j, y+i); break;
}
if (flg) return true;
}
}
}
return false;
}
function putBlock(x, y, block) {
// ブロックの形データ4 x 2を見ていく
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (block.shape[i+j*4]==1) {
switch(rotate) {
case 0: flg = setBrick(x+i, y+j, block.color); break;
case 1: flg = setBrick(x+j, y-i, block.color); break;
case 2: flg = setBrick(x-i, y-j, block.color); break;
case 3: flg = setBrick(x-j, y+i, block.color); break;
}
}
}
}
}
ゲームの状態管理
一応、ゲームっぽくするために、最低限、以下の状態遷移を作っています。
もう少し演出を増やして、例えば行が消える時にチカチカとアニメーションさせる、とかさせたい場合には状態を増やす必要があります。
さて、この後は
元祖落ちゲーのテトリス以降、いろいろなバリエーションの落ちゲーが出ました。筆者はコラムス、ぷよぷよくらいしか知らないですが、ググると色々あります。これらのバリエーションを自分で作っていくのも楽しいと思います。
コメント