ゲームエンジンやライブラリを使わずに、素のHTMLとJavaScriptだけでゲームを作ります。
以下のような人のためにあります。
ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは面倒くさい。
ある程度プログラミングの知識がある人を対象にしています。
倉庫番とは何か?
1982年に発売された古典的なパズルゲームです。
プレイヤーは倉庫にある荷物を所定の場所に移動させます。箱を押すことはできますが、引くことはできません。一手間違った場所に箱を押してしまうと、もうにっちもさっちもいかなくなります。
この記事で作るもの
倉庫番のゲームの基本ルールを実装したデモを作ります。とりあえず動かしてみてください。
下記のデモでは、黄色の矩形が自分。茶色の矩形が箱。○が所定場所。
i … 上、m … 下、j … 左、l … 右
r … リセット
ソースコード
<html>
<center>
<canvas id="gameCanvas" width="640" height="480" style="border:1px solid #000000; background-color: #000;"></canvas>
<script type="text/javascript" src="index.js"></script>
</html>
const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
const SCREEN_W = 20
const SCREEN_H = 15
const BLOCK_SIZE = 32
const Const = {
WALL: 1,
BOX: 2,
POINT: 3,
PLAYER: 4,
}
const map = [
// 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 1
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 2
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 3
0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0, // 4
0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0, // 5
0,0,0,0,0,0,0,1,3,2,4,0,0,1,0,0,0,0,0,0, // 6
0,0,0,0,0,0,1,1,1,0,0,0,0,1,0,0,0,0,0,0, // 7
0,0,0,0,0,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0, // 8
0,0,0,0,0,0,1,3,2,0,0,1,0,0,0,0,0,0,0,0, // 9
0,0,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0, // 10
0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0, // 11
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 12
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 13
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 14
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 15
]
class Obj {
constructor(x,y) {
this.x = x;
this.y = y;
}
}
let player = new Obj(0,0);
let box_list = [];
// 入力ハンドラー
document.addEventListener('keydown', keyDownHandler, false);
function keyDownHandler(event) {
if (event.key === 'i') { // Up
movePlayer(0, -1);
}
if (event.key === 'm') { // Down
movePlayer(0, 1);
}
if (event.key === 'j') { // Left
movePlayer(-1, 0);
}
if (event.key == 'l') { // Right
movePlayer(1, 0);
}
if (event.key == 'r') { // Right
init();
}
}
function movePlayer(dx, dy) {
if (hitBox(player.x+dx, player.y+dy)) {
// 移動先に箱がある場合は、さらにその先が動けるかどうかを確認する
if (! hitObj(player.x+dx+dx, player.y+dy+dy, Const.WALL)
&& ! hitBox(player.x+dx+dx, player.y+dy+dy))
{
// 動ける場合は箱を移動させて自分も動く
moveBox(player.x+dx, player.y+dy, player.x+dx+dx, player.y+dy+dy);
player.x += dx;
player.y += dy;
}
}
else if (!hitObj(player.x+dx, player.y+dy, Const.WALL)) {
player.x += dx;
player.y += dy;
}
}
function hitObj(x, y, id) {
return id==map[x + y *SCREEN_W]
}
function hitBox(x, y) {
for (item of box_list) {
if (item.x == x && item.y == y)
return true;
}
return false;
}
function moveBox(sx, sy, tx, ty) {
for (item of box_list) {
if (item.x == sx && item.y == sy) {
item.x = tx;
item.y = ty;
}
}
}
function drawRect(x,y, obj) {
context.strokeStyle = '#fff';
context.lineWidth = 2;
context.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
if (obj==1) {
context.fillStyle = '#fff'
context.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
}
function drawScreen() {
for (j = 0; j < SCREEN_H; j++) {
for (i = 0; i < SCREEN_W; i++) {
obj = map[ i + j*SCREEN_W ];
if (obj == Const.POINT) {
drawPoint(i, j);
} else {
drawRect(i, j, obj);
}
}
}
}
function drawPlayer() {
context.fillStyle = 'yellow'
context.fillRect(player.x * BLOCK_SIZE, player.y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
function drawBox() {
for (const item of box_list) {
context.fillStyle = 'brown'
context.fillRect(item.x * BLOCK_SIZE, item.y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
}
function drawPoint(ix, iy) {
const radius = 8
x = ix * BLOCK_SIZE;
y = iy * BLOCK_SIZE;
context.beginPath();
context.arc(x+radius*2, y+radius*2, radius, 0, Math.PI * 2, false);
context.fillStyle = 'lightblue';
context.fill();
context.closePath();
}
function isGameClear() {
for (const item of box_list) {
if (map[item.x + item.y * SCREEN_W] != Const.POINT) {
return false;
}
}
return true;
}
function init() {
box_list = [];
for (y = 0; y < SCREEN_H; y++) {
for (x = 0; x < SCREEN_W; x++) {
obj = map[x + y*SCREEN_W];
switch (obj) {
case Const.PLAYER:
player.x = x;
player.y = y;
break;
case Const.BOX:
box_list.push(new Obj(x,y));
break;
}
}
}
}
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 = '20px Consolas';
context.textAlign = 'left';
text_w = context.measureText(text).width;
context.fillText(text, canvas.width/2-text_w/2, canvas.height/2);
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
drawScreen();
drawBox();
drawPlayer();
if (isGameClear()) {
draw_text_center("Game Clear!");
}
}
init();
setInterval(update, 10);
データの持ち方
プログラムにおいてデータの持ち方は最重要検討項目です。データ構造によってアルゴリズムが決まってきます。
このデモの実装では以下の種類のデータを持つことにします。
① マップデータ (20x15の大きさの配列)
② プレイヤーの座標 (x, y)
③ 荷物の座標(x,y) のリスト
① マップデータ (20x15の大きさの配列)
実装では扱いやすさの理由で1次元配列で定義しています。これは面のデータです。
const map = [
// 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 1
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 2
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 3
0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0, // 4
0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0, // 5
0,0,0,0,0,0,0,1,3,2,4,0,0,1,0,0,0,0,0,0, // 6
0,0,0,0,0,0,1,1,1,0,0,0,0,1,0,0,0,0,0,0, // 7
0,0,0,0,0,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0, // 8
0,0,0,0,0,0,1,3,2,0,0,1,0,0,0,0,0,0,0,0, // 9
0,0,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0, // 10
0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0, // 11
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 12
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 13
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 14
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 15
];
この中の数字は、1: 壁、2: 荷物、3: 収納場所、4: プレイヤー初期配置、になっています。
デモではステージがひとつのみなので、20 x 15の一面のみですが、複数のステージを追加した場合は、これを2次元配列などして管理します。
各ステージのデータを手で編集するのはしんどいので、本格的にゲームを作るなら制作用ツールを作る必要があるでしょう。
② プレイヤーの座標 (x, y)
以下が実装です。Objクラスは(x,y)座標を納めるために定義しています。荷物の座標と収納場所の座標でも同じものを使います。
class Obj {
constructor(x,y) {
this.x = x;
this.y = y;
}
}
let player = new Obj(0,0);
③ 荷物の座標(x,y) のリスト
ソースコードとしては下記です。
let box_list = [];
データの初期化
map データは、壁の配置と収納場所は固定ですが、荷物の場所、プレイヤーの場所はゲームが進行したら位置が変わります。
ゲームが始まる前に、map から初期配置の位置データを取り出して、player、box_list に座標データを入れる必要があります。
以下がその実装です。
const Const = {
WALL: 1,
BOX: 2,
POINT: 3,
PLAYER: 4,
}
・・・
function init() {
box_list = [];
point_list = [];
for (y = 0; y < SCREEN_H; y++) {
for (x = 0; x < SCREEN_W; x++) {
obj = map[x + y*SCREEN_W];
switch (obj) {
case Const.PLAYER:
player.x = x;
player.y = y;
break;
case Const.BOX:
box_list.push(new Obj(x,y));
break;
}
}
}
}
obj = map[x + y*SCREEN_W] で map からデータを取り出して、種類を判別して、座標の情報を格納しています。
基本ルール
基本ルールは、プレイヤーの操作の観点からは、下記のパターンだけです。
プレイヤーを上下左右のどれかの方向に動かす時に、
(1) ひとマス先に荷物があるかどうか?
(1-1) 荷物があった場合、さらのひとマス先に壁か荷物があるか?
(2) 箱がなかったら、ひとマス先に壁があるかどうか?
というのをチェックします。
ソースコードでは下記の関数で実装しています。この関数は上下左右全ての方向に対応しています。
function movePlayer(dx, dy) {
if (hitBox(player.x+dx, player.y+dy)) {
// 移動先に荷物がある場合は、さらにその先が動けるかどうかを確認する
if (! hitObj(player.x+dx+dx, player.y+dy+dy, Const.WALL)
&& ! hitBox(player.x+dx+dx, player.y+dy+dy))
{
// 動ける場合は荷物を移動させて自分も動く
moveBox(player.x+dx, player.y+dy, player.x+dx+dx, player.y+dy+dy);
player.x += dx;
player.y += dy;
}
}
else if (!hitObj(player.x+dx, player.y+dy, Const.WALL)) {
player.x += dx;
player.y += dy;
}
}
荷物を動かす
荷物を動かせるどうかはすでに、movePlayer() 関数の中で判断しているので、動かせる場合はただ単に、一つ先のマスに移動させるだけです。
下記の関数がその実装です。 sx, sy が移動元の座標、tx, ty が移動先の座標です。
function moveBox(sx, sy, tx, ty) {
for (item of box_list) {
if (item.x == sx && item.y == sy) {
item.x = tx;
item.y = ty;
}
}
}
ゲームクリアの判断
ゲームクリアの条件は「すべての荷物が収納場所に配置されているか」です。
これをそのまま実装すればよいので、とてもシンプルです。
function isGameClear() {
for (const item of box_list) {
if (map[item.x + item.y * SCREEN_W] != Const.POINT) {
return false;
}
}
return true;
}
やっているのは、box_listにあるすべての荷物の座標をなめて行って、mapのその位置が収納場所がどうかを確認しています。収納場所は、Const.POINTで定義していて、中身は数字の3です。
次回は
今回の実装は、倉庫番の全ルールは実装しているので、見た目を気にしなければ、これでも遊べます。(実際のパズルの要素として最重要な、面のデータがあれば)
とはいえ、画面に表示するのが矩形や丸だけでは雰囲気が出ないので、次回はちゃんとしたドット絵を表示させて、ゲームの完成としたいと思います。
コメント