JS:CanvasAPIでダンジョンを描画する

JavaScript

棒倒し法というアルゴリズムを使った2次元ダンジョンを描画するプログラムです。
JavaScriptの標準機能であるCanvasAPIを利用した描画方法です。

棒倒し法アルゴリズムついて知りたい方は以下で詳しく解説していますので、参考にしてください。

ソースコード

ソースコードファイルは以下の4つです。

index.htmlHTML部分
User.jsブロックサイズや色などの定義
Functions.jsダンジョン生成などの関数群
main.js起動時の処理と主な処理

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
    <title>CanvasAPIでダンジョンを描画する</title>
     
    <style>
        *{
            margin: 0;
            padding: 0;
            line-height: 0;
        }
 
        html, body, #wrapper{
            width: 100%;
            height: 100%;
        }
 
        body{
            font-family: sans-serif;
        }
 
        #canvas{
            display: block;
        }
    </style>
 
    <script src="User.js" type="text/javascript"></script>
    <script src="Functions.js" type="text/javascript"></script>
    <script src="main.js" type="text/javascript"></script>
</head>
<body>
    <div id="wrapper">
        <canvas id="canvas" width="" height=""></canvas>
    </div>
</body>
</html>

User.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
 * ユーザー定義
 */
 
// サイズ
const BLOCK_SIZE = 32;                    // 1ブロックのピクセルサイズ(32x32)
 
// 色
const STAGE_BG_COLOR = "peachpuff";        // ステージ背景色
 
// ブロック種別と色
const [SPACE, WALL] = [0, 1, 2, 3];
const BLOCK_COLOR = [STAGE_BG_COLOR, "sienna"];
 
// キャンバスを制御する変数
let canvas = null;                    // キャンバス(描画領域)
let g = null;                        // 描画対象
 
// ゲーム全体の設定や情報
let game = {
    rows: 0,            // ダンジョン行数(高さ)
    columns: 0,            // ダンジョン列数(幅)
    block_length: 0,    // ダンジョン行数×ダンジョン列数
    offsetX: 0,            // キャンバス左上からのステージ座標オフセット値(X座標)
    offsetY: 0,            // キャンバス左上からのステージ座標オフセット値(Y座標)
};
 
// ダンジョン情報(リサイズ時に設定される2次元配列)
let dungeon = null;

Functions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/*
 * 指定範囲の整数乱数を生成する関数(min~max)
 *
 * @param {int} min 乱数の最小値
 * @param {int} max 乱数の最大値
 *
 * @return {int} min~maxの範囲の整数乱数
 */
const randInt = (min, max) =>{
    return Math.floor(Math.random() * (max+1-min)+min);
};
 
/*
 * スマホ・タブレット⇔PCのチェック
 */
const isMobile = () =>{
    // スマホかPCかのチェック(iPhoneまたはAndroidとMobileを含む文字列か?で判断)
    if(navigator.userAgent.match(/iPhone|Android.+Mobile/)){
        console.log("端末チェック: Mobile");
        return true;    // スマホ端末判定
    }
    else{
        console.log("端末チェック: PC");
        return false;    // PC判定
    }
}
 
/*
 * m行n列の2次元配列を初期化する
 *
 * @param {int} m 配列(行)
 * @param {int} n 配列(列)
 * @param {variable} val 配列の初期化値(指定がない場合0埋め)
 *
 * @return {Array} valで埋めた[m][n]形式の2次元配列
 */
 
const generate2DArray = (m, n, val = 0) => {
    return [...Array(m)].map(_ => Array(n).fill(val));
}
 
/*
 * ダンジョン生成
 */
const generateDungeon = ()=> {
    // 2次元配列を生成
    let _dungeon = generate2DArray(game.rows, game.columns);
 
    // ダンジョンを空にする
    for(let y=0; y<game.rows; y++){
        for(let x=0; x<game.columns; x++){   
            _dungeon[y][x] = SPACE;
        }
    }
 
    // 柱を倒す方向(上右下左の順)
    const dx = [0, 1, 0, -1];
    const dy = [-1, 0, 1, 0];
     
    // ステージ配列に基づき、ブロック種別を設定
    for(let y=0; y<game.rows; y++){
        for(let x=0; x<game.columns; x++){
            // 周囲を壁にする
            if(y === 0 || y === game.rows-1 || x === 0 || x === game.columns-1){
                _dungeon[y][x] = WALL;
            }
            // 壁の内側に1つおきに柱を立てる
            else if(x >= 2 && x <= game.columns-1 && y >= 2 && y <= game.rows-1 && x%2 == 0 && y%2 == 0){
                _dungeon[y][x] = WALL;
 
                // 1つおきに立てた柱をランダム(上下左右いづれか)方向に倒す
                let r = 0;
                if(x === 2){    // 左端の柱は4方向(上右下左)
                    r = randInt(0, 3);
                }
                else{            // 左端以外の柱は3方向(上右下)
                    r = randInt(0, 2);
                }
 
                // 柱を設置
                _dungeon[y + dy[r]][x + dx[r]] = WALL;
            }
        }
    }
    return _dungeon;
}
 
/*
 * ダンジョン描画
 */
const drawDungeon = ()=> {
    // ステージ全体をクリア
    g.fillStyle = STAGE_BG_COLOR;
    g.fillRect(0, 0, canvas.width, canvas.height);
 
    // ステージ配列に基づき、ブロック画像を表示
    for(let y=0; y<game.rows; y++){
        for(let x=0; x<game.columns; x++){
            // ダンジョン配列からブロック種別を取得
            const blockType = dungeon[y][x];
 
            // ブロック種別により色を変更
            g.fillStyle = BLOCK_COLOR[blockType];
 
            if(blockType === WALL){                        // 壁の描画
                // ■描画
                g.fillRect(x * BLOCK_SIZE + game.offsetX, y * BLOCK_SIZE + game.offsetY, BLOCK_SIZE-1, BLOCK_SIZE-1);
            }
        }
    }
}

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*
 * CanvasAPIでダンジョンを描画する
 *
 *    CanvasAPIの機能のみでダンジョンを描画してみる(棒倒し法により迷路を作成)
 *    ブラウザ画面のリサイズイベントに応じて(画面を傾けたり、ウインドウサイズを変えたり)ダンジョンが再生成される
 *
 *    2024-12-12
 *
*/
 
/*
 * タッチした瞬間の処理(モバイル端末向け)
 */
const touchStartListener = (e) => {
    // 端末のデフォルト動作をキャンセル
    e.preventDefault();
};
 
/*
 * タッチして離した時の処理(モバイル端末向け)
 */
const touchEndListener = (e) => {
    // 端末のデフォルト動作をキャンセル
    e.preventDefault();
};
 
/*
 * キャンバスのサイズをウインドウに合わせて変更(リサイズ時に実行)
 */
const getSize = ()=>{
    // キャンバスのサイズを再設定
    canvas.width = wrapper.offsetWidth;
    canvas.height =  wrapper.offsetHeight;
 
    // ダンジョンの高さを再設定
    let height = Math.floor(canvas.height / BLOCK_SIZE) - 1;
 
    if(height % 2 == 0){    // 奇数にしておく
        height -= 1;
    }
 
    // ダンジョンの幅を再設定
    let width = Math.floor(canvas.width / BLOCK_SIZE) - 1;
     
    if(width % 2 == 0){    // 奇数にしておく
        width -= 1;
    }
 
    // ダンジョン行数/列数/ブロック長を設定
    game.rows = height;
    game.columns = width;
    game.block_length = game.rows * game.columns;
 
    console.log(`${game.rows}行 x ${game.columns}列 = ${game.block_length} ブロック`);
 
    // ダンジョン描画のオフセット値を再設定(ダンジョンが画面のなるべく中央になるように配置するため)
    game.offsetX = (canvas.width - game.columns * BLOCK_SIZE) / 2;
    game.offsetY = (canvas.height - game.rows * BLOCK_SIZE) / 2;
 
    // ダンジョン生成
    dungeon = generateDungeon();
    console.log("★ダンジョン生成完了!");
    console.log(dungeon);
}
 
window.addEventListener("resize", getSize, false);    // リサイズ時のイベントに設定
 
/*
 * ゲームのメイン処理
 */
const GameMain = () =>{
    // ダンジョン描画
    drawDungeon();
 
    // フレーム再描画
    requestAnimationFrame(GameMain);
}
 
/*
 * 起動時の処理
 */
window.addEventListener("load", async ()=>{
    // キャンバス取得
    canvas = document.querySelector("#canvas");
    g = canvas.getContext("2d");    // コンテキスト:描画対象
 
    // 端末サイズ取得
    getSize();
 
    // 入力イベント設定(スマホとPCで振り分ける)
    if(isMobile()){        // スマホならタッチイベントで設定
        canvas.addEventListener("touchstart", touchStartListener, false);
        canvas.addEventListener("touchend", touchEndListener, false);
    }
    else{                // PCならマウスイベントで設定(未使用)
        // canvas.addEventListener("mousemove", mouseMoveListener, false);
        // canvas.addEventListener("mousedown", mouseDownListener, false);
    }
 
    // ゲームループ開始
    GameMain();
});

少しだけ解説

棒倒し法のアルゴリズム部分については割愛します。こちらを参考にしてください。

起動時の処理

起動時(loadイベント発生時)にGameMain()アロー関数が呼ばれるようになっています。
ダンジョンを1回描画するだけなら必要ないのですが、ゲーム制作などに応用がきくようにrequestAnimationFrameを使ってゲームループ処理(GameMainを再起呼び出し)して1秒間に60回ほどダンジョンを描画するようにしています。

/*
* ゲームのメイン処理
*/

const GameMain = () =>{
// ダンジョン描画
drawDungeon();

// フレーム再描画
requestAnimationFrame(GameMain);
}

/*
* 起動時の処理
*/

window.addEventListener("load", async ()=>{
:
:
// ゲームループ開始
GameMain();
});

ダンジョンの幅と高さ

ダンジョンの幅と高さに関しては端末の(ブラウザの)描画領域サイズによって変化する仕組みになっています。

そのためresizeイベントを感知して、描画領域サイズが変更する度にダンジョンを再生成するようにしています。(⇒ main.jsにあるgetSize()アロー関数)

/*
* キャンバスのサイズをウインドウに合わせて変更(リサイズ時に実行)
*/

const getSize = ()=>{
// キャンバスのサイズを再設定
canvas.width = wrapper.offsetWidth;
canvas.height = wrapper.offsetHeight;

// ダンジョンの高さを再設定
let height = Math.floor(canvas.height / BLOCK_SIZE) - 1;

if(height % 2 == 0){ // 奇数にしておく
height -= 1;
}

// ダンジョンの幅を再設定
let width = Math.floor(canvas.width / BLOCK_SIZE) - 1;

if(width % 2 == 0){ // 奇数にしておく
width -= 1;
}

// ダンジョン行数/列数/ブロック長を設定
game.rows = height;
game.columns = width;
game.block_length = game.rows * game.columns;

console.log(`${game.rows}行 x ${game.columns}列 = ${game.block_length} ブロック`);

// ダンジョン描画のオフセット値を再設定(ダンジョンが画面のなるべく中央になるように配置するため)
game.offsetX = (canvas.width - game.columns * BLOCK_SIZE) / 2;
game.offsetY = (canvas.height - game.rows * BLOCK_SIZE) / 2;

// ダンジョン生成
dungeon = generateDungeon();
console.log("★ダンジョン生成完了!");
console.log(dungeon);
}

window.addEventListener("resize", getSize, false); // リサイズ時のイベントに設定

ダンジョン生成

ダンジョン生成は、Functions.jsにあるgenerateDungeon()アロー関数で行います。
2次元配列dungeon0(通路)1(壁)を代入することで数値上の仮想ダンジョンを表現します。

dungeon配列に別の数値、例えば2をモンスター、3をアイテムなどと当てはめると容易にダンジョン要素を増やすことが出来ると思います。

/*
* ダンジョン生成
*/

const generateDungeon = ()=> {
// 2次元配列を生成
let _dungeon = generate2DArray(game.rows, game.columns);

// ダンジョンを空にする
for(let y=0; y<game.rows; y++){
for(let x=0; x<game.columns; x++){
_dungeon[y][x] = SPACE;
}
}

// 柱を倒す方向(上右下左の順)
const dx = [0, 1, 0, -1];
const dy = [-1, 0, 1, 0];

// ステージ配列に基づき、ブロック種別を設定
for(let y=0; y<game.rows; y++){
for(let x=0; x<game.columns; x++){
// 周囲を壁にする
if(y === 0 || y === game.rows-1 || x === 0 || x === game.columns-1){
_dungeon[y][x] = WALL;
}
// 壁の内側に1つおきに柱を立てる
else if(x >= 2 && x <= game.columns-1 && y >= 2 && y <= game.rows-1 && x%2 == 0 && y%2 == 0){
_dungeon[y][x] = WALL;

// 1つおきに立てた柱をランダム(上下左右いづれか)方向に倒す
let r = 0;
if(x === 2){ // 左端の柱は4方向(上右下左)
r = randInt(0, 3);
}
else{ // 左端以外の柱は3方向(上右下)
r = randInt(0, 2);
}

// 柱を設置
_dungeon[y + dy[r]][x + dx[r]] = WALL;
}
}
}
return _dungeon;
}

ダンジョンの描画

ダンジョンの描画は、Functions.jsにあるdrawDungeon()アロー関数を使います。
CanvasAPIの描画機能であるfillStyleメソッド(塗りつぶし色)とfillRectメソッド(塗りつぶし矩形描画)を利用して塗りつぶされた正方形の描画を行い1ブロックを表現します。

2次元配列dungeonの値がWALL(=1)のときを壁とみなしfillRectを実行します。

/*
* ダンジョン描画
*/

const drawDungeon = ()=> {
// ステージ全体をクリア
g.fillStyle = STAGE_BG_COLOR;
g.fillRect(0, 0, canvas.width, canvas.height);

// ステージ配列に基づき、ブロック画像を表示
for(let y=0; y<game.rows; y++){
for(let x=0; x<game.columns; x++){
// ダンジョン配列からブロック種別を取得
const blockType = dungeon[y][x];

// ブロック種別により色を変更
g.fillStyle = BLOCK_COLOR[blockType];

if(blockType === WALL){ // 壁の描画
// ■描画
g.fillRect(x * BLOCK_SIZE + game.offsetX, y * BLOCK_SIZE + game.offsetY, BLOCK_SIZE-1, BLOCK_SIZE-1);
}
}
}
}

ダンジョンの見た目

User.jsでは、1ブロックの大きさやブロックの色などを変更できます。
適当にBLOCK_SIZESTAGE_BG_COLORBLOCK_COLORなどの定数の値を変えてみてください。

/*
* ユーザー定義
*/


// サイズ
const BLOCK_SIZE = 32; // 1ブロックのピクセルサイズ(32x32)

// 色
const STAGE_BG_COLOR = "peachpuff"; // ステージ背景色

// ブロック種別と色
const [SPACE, WALL] = [0, 1, 2, 3];
const BLOCK_COLOR = [STAGE_BG_COLOR, "sienna"];
:
:

ちなみに冒頭の画像のダンジョンはBLOCK_SIZE16としたものです。
BLOCK_SIZE最小で2まではディスプレイ上で認識できました。

BLOCK_COLORの2番目の要素が壁の色です。(初期値はsienna)
Webカラー(red, blue…)や16進カラー(#ffcc00など)が利用できます。

コメント

タイトルとURLをコピーしました