ゲームプログラミング初心者向けの記事です。
本記事は「元祖落ちゲー」を、複数のステップに分けて作っていきます。
初めから完成品を作ろうとすると挫折しやすいので、まずは小さくて何かしら動くものを作り、後から少しずつ追加変更しながら完成に持っていく感じにしたいと思います。
ブラウザで動作するゲームを作ります。
ゲームエンジンやライブラリは使用しません。HTMLとJavascriptだけで実装します。
元祖落ちゲーって?
説明不要だと思いますし、記事であえて名前も出していませんが、Wikipediaをご参照ください。
ステップ1: 落下するブロックを作る
第一段階は、ゲーム表示領域の描画、ブロックの描画、ブロックの座標更新をする、というゲームのメインループの部分を作ります。
ソースコード
ソースコードは以下のindex.html、index.jsだけです。
<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;
const BLOCK_SIZE = 20
const blockShape = [1,1,0,0,
1,1,0,0]
function drawRect(x,y) {
context.strokeStyle = '#00f';
context.lineWidth = 2;
context.strokeRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
context.fillStyle = '#fff'
context.fillRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
function drawBlockShape(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
drawRect(x+i, y+j);
}
}
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
drawBlockShape(blockX, blockY, blockShape);
// ブロックを落下させる
blockY += 1;
if (blockY*BLOCK_SIZE > canvas.height) {
blockY = 0;
}
}
setInterval(update, 500);
動作画面
プログラムの構成
メインループ部分
setInterval()関数を使って、500msに一回update()関数を呼び出しています。
このゲームのメインループです。ただし、500msはあくまでステップ1のために設定した時間間隔で、この後の実装ではユーザ入力を扱うので、この数字は調整変更していきます。
setInterval(update, 500)
update()関数
ブロックを描画します(drawBlockShape()呼び出し)と、ブロックの座標を更新をします。
ステップ1の動作確認ではブロックを落下させます(blockYだけを更新)。
ブロックが画面下端の描画領域をはみ出た場合は、上端に戻すします。ここも、あくまでステップ1の動作確認用の実装です。
ブロックの描画
「元祖落ちゲー」のブロックの形状は7種類あります。この7種類のブロックの形状は4列x2行の配列で表現できます。
ステップ1では描画の確認をしたいだけなので、O typeだけ使います。
なお、データの定義は下記のように1次元配列にしましたが、別に2次元配列でも構いません。
const blockShape = [1,1,0,0,
1,1,0,0]
描画はdrawBlockShape関数で 4x2 の配列データを for ループで各要素を見ていって、
1であれば矩形を描画して、0であれば何も描画しない。ということで形状を書き分けています。
矩形(正方形)の描画はdrawRect()関数です。一辺の長さはBLOCK_SIZEで定義した20ドットです。つまり、20x20ドットの正方形を描画します。
落下ブロックは複数の矩形(正方形)で組み合わされます。ので、O typeの場合は、2x2の矩形なので、40ドット x 40ドットの大きさになります。
ステップ2: ブロックを積み上げられるようにする
次のステップは、落ちるブロックの下にブロックがあった場合に、積み上がるようにします。
ソースコード
index.htmlの内容同じなので省略します。index.jsを変更したものが下記になります。
const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
// ブロック
let blockX = 5;
let blockY = 0;
const BLOCK_SIZE = 20
const blockShape = [1,1,0,0,
1,1,0,0]
// 画面サイズ
const SCREEN_W = 12
const SCREEN_H = 22
let screen = Array(SCREEN_H*SCREEN_W);
function isBlockHit(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
if(screen[x+i + (y+j)*SCREEN_W] == 1)
return true;
}
}
}
return false;
}
function putBlock(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
screen[x+i + (y+j)*SCREEN_W] = 1;
}
}
}
}
function drawRect(x,y) {
context.strokeStyle = '#00f';
context.lineWidth = 2;
context.strokeRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
context.fillStyle = '#fff'
context.fillRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
function drawBlockShape(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
drawRect(x+i, y+j);
}
}
}
}
function drawScreen() {
for (j = 0; j < SCREEN_H; j++) {
for (i = 0; i < SCREEN_W; i++) {
if (screen[ i + j*SCREEN_W ]==1)
drawRect(i, j);
}
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
drawBlockShape(blockX, blockY, blockShape);
drawScreen();
// ブロックを落下させる
blockY += 1;
if (blockY*BLOCK_SIZE > canvas.height) {
blockY = 0;
}
// ブロックがブロックにぶつかったら積み上げる
if (isBlockHit(blockX, blockY+1, blockShape)) {
putBlock(blockX, blockY, blockShape); // ブロックを積み上げる
blockY = 0;
return;
}
}
// 画面を初期化
screen.fill(0);
// 画面の一番下のラインにブロックを置く
for (let i = 0; i < SCREEN_W; i++) {
screen[i + (SCREEN_H-1)*SCREEN_W] = 1;
}
setInterval(update, 500);
動作画面
プログラムの構成
画面上のブロックの存在判断
ブロックが存在するかどうかの判断には、画面上のどの位置にブロックがあるのかをデータとして保持しておく必要があります。
以下がそのデータ構造です。1か0の配列として持ちます。(ここでは1次元の配列にしていますが、2次元の配列で実装しても問題ありません。)
const SCREEN_W = 12
const SCREEN_H = 22
let screen = Array(SCREEN_H*SCREEN_W);
ここで、改めて画面サイズを定数SCREEN_W、SCREEN_Hとして持つようにしています。12マス x 22マスです。(矩形のサイズは20x20なので、解像度は240x440ドットです)
画面上の特定の位置(x,y)にブロックが存在するかどうかは、以下のように判断します。
screen[x + y*SCREEN_W] == 1
実際には落下ブロックの形にそって確認します。その実装が以下の関数です。
function isBlockHit(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
if(screen[x+i + (y+j)*SCREEN_W] == 1)
return true;
}
}
}
return false;
}
一番下のライン(初期化)
一番下のラインにブロックを敷き詰めることで、落下ブロックが画面外に出ることを防ぎます。
// 画面を初期化
screen.fill(0);
// 画面の一番下のラインにブロックを置く
for (let i = 0; i < SCREEN_W; i++) {
screen[i + (SCREEN_H-1)*SCREEN_W] = 1;
}
積み上げロジック
落下ブロックがこれ以上落ちないと判断されたら、その位置に落下ブロックが画面上に残ります。
画面上に残すのはscreen[]配列に1を入れるだけです。
screen[x + y*SCREEN_W] = 1
実際には、落下ブロックの形にそって処理をします。以下がその関数です。
function putBlock(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
screen[x+i + (y+j)*SCREEN_W] = 1;
}
}
}
}
なお、積み上げるかどうかの判断は、update()関数の中でやっています。
// ブロックがブロックにぶつかったら積み上げる
if (isBlockHit(blockX, blockY+1, blockShape)) {
putBlock(blockX, blockY, blockShape); // ブロックを積み上げる
blockY = 0;
return;
}
ステップ2の実装としては、以上でおしまいです。
ステップ3:落下ブロックを左右に動かす
このステップでは、ユーザが落下ブロックを操作できるようにします。
ブラウザーからキーの入力を受けるには、以下のようにEventListenerを登録します。
// 入力ハンドラー
document.addEventListener('keydown', keyDownHandler, false);
下記の関数をハンドラーとして登録します。ブラウザで何かキーが押されたら、この関数が呼ばれます。
function keyDownHandler(event) {
if (event.key === 'Left' || event.key === 'ArrowLeft') {
if(!isBlockHit(blockX-1, blockY, blockShape))
blockX -= 1;
}
if (event.key === 'Right' || event.key === 'ArrowRight') {
if (!isBlockHit(blockX+1, blockY, blockShape))
blockX += 1;
}
}
引数のeventには、入力されたキーイベントの内容が渡されるので、それを見て処理を振り分けます。
ここでは左右きーのみを扱いたいので、Left,ArrowLeft,Right,ArrowRightのみを見ます。
処理は単純に落下ブロックの座標blockXを変更するだけです。
左右の辺にブロックを敷き詰める
落下ブロックが左右に移動した時に、画面の外にはみ出ないようにブロックを敷き詰めます。
以下の処理を追加します。
for (let j = 0; j < SCREEN_H-1; j++) {
screen[j*SCREEN_W] = 1;
screen[(SCREEN_W-1) + j*SCREEN_W] = 1;
}
ソースコード
index.htmlは変更なしなので省略します。
const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
// ブロック
let blockX = 5;
let blockY = 0;
const BLOCK_SIZE = 20
const blockShape = [1,1,0,0,
1,1,0,0]
// 画面サイズ
const SCREEN_W = 12
const SCREEN_H = 22
let screen = Array(SCREEN_H*SCREEN_W);
// 入力ハンドラー
document.addEventListener('keydown', keyDownHandler, false);
function keyDownHandler(event) {
if (event.key === 'Left' || event.key === 'ArrowLeft') {
if(!isBlockHit(blockX-1, blockY, blockShape))
blockX -= 1;
}
if (event.key === 'Right' || event.key === 'ArrowRight') {
if (!isBlockHit(blockX+1, blockY, blockShape))
blockX += 1;
}
}
function isBlockHit(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
if(screen[x+i + (y+j)*SCREEN_W] == 1)
return true;
}
}
}
return false;
}
function putBlock(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
screen[x+i + (y+j)*SCREEN_W] = 1;
}
}
}
}
function drawRect(x,y) {
context.strokeStyle = '#00f';
context.lineWidth = 2;
context.strokeRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
context.fillStyle = '#fff'
context.fillRect( x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
function drawBlockShape(x, y, shape) {
for (j = 0; j < 2; j++) {
for (i = 0; i < 4; i++) {
if (shape[i+j*4]==1) {
drawRect(x+i, y+j);
}
}
}
}
function drawScreen() {
for (j = 0; j < SCREEN_H; j++) {
for (i = 0; i < SCREEN_W; i++) {
if (screen[ i + j*SCREEN_W ]==1)
drawRect(i, j);
}
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
drawBlockShape(blockX, blockY, blockShape);
drawScreen();
// ブロックを落下させる
blockY += 1;
if (blockY*BLOCK_SIZE > canvas.height) {
blockY = 0;
}
// ブロックがブロックにぶつかったら積み上げる
if (isBlockHit(blockX, blockY+1, blockShape)) {
putBlock(blockX, blockY, blockShape); // ブロックを積み上げる
blockY = 0;
return;
}
}
// 画面を初期化
screen.fill(0);
// 画面の一番下のラインにブロックを置く
for (let i = 0; i < SCREEN_W; i++) {
screen[i + (SCREEN_H-1)*SCREEN_W] = 1;
}
for (let j = 0; j < SCREEN_H-1; j++) {
screen[j*SCREEN_W] = 1;
screen[(SCREEN_W-1) + j*SCREEN_W] = 1;
}
setInterval(update, 500);
動作画面
まとめ
とりあえず、この記事では「元祖落ちゲー」の以下のロジックを実装しました。
- 特定の形のブロックが落下する
- ブロックが積み上がる
- キー入力で落下ブロックを左右に移動させる
まだ半分といったところです。それに、キー入力の反応がいまいちなので改善の余地ありです。
続きは別の機会に書きたいと思います。



