同人ゲームの作り方 - るてんのお部屋の場合

第3回 画面の作り方

画面の作り方として、どういう画面を選ぶかという話をします。

● 画面サイズ

画面サイズは、毎回頭を悩まされる問題です。PCとスマホ両対応にしようとすると、かなり難易度が高いです。

自分の中で決定版はなく、どちらかを切り捨てるといきなり楽になります。

初心者のうちは、開発画面と同じ、PC向けのゲームを作るとよいのではないかと思います。なんなら、画面サイズを変えられなくするのも、一つの手だと思います。

● HTML5+Canvas

HTML5が登場して、ピクセル描画が行えるCanvasと、音声を複数制御して鳴らせるSoundが加わりました。そのおかげで、HTMLとJavaScriptを使って、ゲームを一通り作れる環境が整いました。

それ以降、私はこれらの技術を利用して、色々とゲームを開発しています。簡単なゲームなら、これだけの技術で完成させることができます。

● Canvasのリファレンス

Canvasのリファレンスとしては、以下のサイトが非常によくまとまっています。

サンプルも豊富なので、一通り読めばCanvasの2D描画はマスターできます。

● サンプルコード

ファイル構成は、以下の通りです。index.htmlを開くと、4:3の比率の描画領域がブラウザ内に現れます。クリックするごとに、前のクリック位置から線を引くことができます。

ChromeやFirefoxといったモダンブラウザ用のサンプルコードです。IEには対応していません。

index.html
css/main.css
js-game/game.canvas.js
js-game/game.core.js
js-game/game.view.js
js-lib/jquery-3.3.1.min.js
js-main/main.js

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, user-scalable=0" />
    <title></title>
    <link rel="stylesheet" href="css/main.css">

    <!-- /* jQuery, other... */ -->
    <script>
        (function() {
            // Electron or Browser
            const pJQ = './js-lib/jquery-3.3.1.min.js';
            if (window.require) {return window.jQuery = window.$ = require(pJQ)}
            document.writeln(`<script src="${pJQ}"></${``}script>`);
        })();
    </script>

    <!-- /* Lib. Game */ -->
    <script src="js-game/game.canvas.js"></script>
    <script src="js-game/game.core.js"></script>
    <script src="js-game/game.view.js"></script>

    <!-- /* Lib. Main */ -->

    <!-- /* Start Main */ -->
    <script src="js-main/main.js"></script>

  </head>
  <body>
    <div id="app" class="app"></div>
  </body>
</html>

css/main.css
html, body, .app {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
    position: absolute;
}
body {
    background: #000;
}
#app canvas {
    image-rendering: pixelated;
    position: absolute;
}
* {
    -webkit-tap-highlight-color:rgba(0,0,0,0);
}


js-game/game.canvas.js
/**
 * @license com.crocro.game v4.0.0
 * (c) 2013-2018 Masakazu Yanai https://crocro.com/
 * License: MIT / Date: 2018-03-15
 */

'use strict';

// ライブラリ用のオブジェクトの作成
((o, p) => p.split('.').forEach(k => o = !o[k] ? o[k]={} : o[k]))
(window, 'com.crocro.game.canvas');

//------------------------------------------------------------
(function() {
    // ショートカットの作成
    const game = window.com.crocro.game;
    const _t = game.canvas;

    //------------------------------------------------------------
    // キャンバスの生成
    _t.genCnvs = function(w, h) {
        const $cnvs = $(`<canvas width="${w}" height="${h}">`);
        const cnvs = $cnvs[0];
        const cntx = cnvs.getContext('2d');
        return {$cnvs: $cnvs, cnvs: cnvs, cntx: cntx, w: w, h: h};
    };

    //------------------------------------------------------------
    // キャンバス情報
    _t.CnvsInf = function(o) {
        // デフォルト値
        this.w  = 512;      // 16*8*4
        this.h  = 384;      // 16*8*3
        this.n  = 2;        // レイヤー枚数
        this.bg = '#000';   // 背景色

        // 引数の反映
        for (let k in o) {this[k] = o[k]}
    };

    //------------------------------------------------------------
    // キャンバス配列の初期化
    //   arguments
    //      cnvsInf - _t.CnvsInf
    //   return
    //      cnvsArr = [{$cnvs, cnvs, cntx, w, h, bg}, ...]
    _t.initCnvsArr = function(cnvsInf) {
        // 変数の初期化
        const cnvsArr = [];
        const $app = $('#app');

        // レイヤー枚数のキャンバスを初期化
        for (let i = 0; i < cnvsInf.n; i ++) {
            const c = _t.genCnvs(cnvsInf.w, cnvsInf.h);
            c.bg = cnvsInf.bg;      // 背景色を追加
            c.cntx.imageSmoothingEnabled = false;   // ドット絵用
            $app.append(c.cnvs);    // DOMに追加
            cnvsArr.push(c);        // 配列に追加
        }
        return cnvsArr;
    };

    //------------------------------------------------------------
    // キャンバス配列のクリア
    //   一番下は塗り潰し、それ以外は削除。
    _t.clearCnvsArr = function(cnvsArr) {
        cnvsArr.forEach((c, i) => {
            if (i == 0) {
                c.cntx.fillStyle = c.bg;
                c.cntx.fillRect(0, 0, c.w, c.h);
            } else {
                c.cntx.clearRect(0, 0, c.w, c.h);
            }
        });
    };

})();

js-game/game.core.js
/**
 * @license com.crocro.game v4.0.0
 * (c) 2013-2018 Masakazu Yanai https://crocro.com/
 * License: MIT / Date: 2018-03-15
 */

'use strict';

// ライブラリ用のオブジェクトの作成
((o, p) => p.split('.').forEach(k => o = !o[k] ? o[k]={} : o[k]))
(window, 'com.crocro.game.core');

//------------------------------------------------------------
(function() {
    // ショートカットの作成
    const game = window.com.crocro.game;
    const _t = game.core;

    //------------------------------------------------------------
    // 変数の初期化
    _t.ua = {};
    _t.ua.pc = ! window.navigator.userAgent.match(
        /iphone|ipod|ipad|android|windows Phone/i);

    //------------------------------------------------------------
    // 範囲内か判定
    //   cX - check X
    //   cY - check Y
    //   x, y, w, h - 矩形領域
    _t.inRng = function(cX, cY, x, y, w, h) {
        if (cX < x || x + w <= cX) {return false}
        if (cY < y || y + h <= cY) {return false}
        return true;
    };

    // 範囲内か判定 - 矩形
    _t.inRngRct = function(cX, cY, rct) {
        if (cX < rct.x || rct.x + rct.w <= cX) {return false}
        if (cY < rct.y || rct.y + rct.h <= cY) {return false}
        return true;
    };

})();

js-game/game.view.js
/**
 * @license com.crocro.game v4.0.0
 * (c) 2013-2018 Masakazu Yanai https://crocro.com/
 * License: MIT / Date: 2018-03-15
 */

'use strict';

// ライブラリ用のオブジェクトの作成
((o, p) => p.split('.').forEach(k => o = !o[k] ? o[k]={} : o[k]))
(window, 'com.crocro.game.view');

//------------------------------------------------------------
(function() {
    // ショートカットの作成
    const game = window.com.crocro.game;
    const _t = game.view;

    // 変数の初期化
    _t.cnvsInf = null;  // キャンバス情報
    _t.$idApp  = null;  // $('#app')
    _t.$clsApp = null;  // $('.app')
    _t.appRct  = {};    // アプリ矩形
    _t.tapArr  = [];    // タップ配列 [{id, fnc, rct: {x, y, w, h}}, ...]

    //------------------------------------------------------------
    // ビューの初期化
    _t.init = function(cnvsInf) {
        // 変数の初期化
        _t.cnvsInf = cnvsInf;
        _t.$idApp  = $('#app');
        _t.$clsApp = $('.app');

        // 実行初期化
        _t.initTap();       // タップの初期化
        _t.autoResize();    // 画面サイズの自動変更
    };

    //------------------------------------------------------------
    // アプリ矩形の計算
    _t.calcAppRct = function() {
        // Windowサイズの取得
        const winW = window.innerWidth;
        const winH = window.innerHeight;
        const cW = _t.cnvsInf.w;
        const cH = _t.cnvsInf.h;

        // App矩形の計算
        _t.appRct.w = Math.min(winW, winH * cW / cH) | 0;
        _t.appRct.h = Math.min(winH, winW * cH / cW) | 0;
        _t.appRct.x = (winW - _t.appRct.w) / 2 | 0;
        _t.appRct.y = (winH - _t.appRct.h) / 2 | 0;
    };

    //------------------------------------------------------------
    // アプリを画面にフィット
    _t.fitApp = function() {
        // キャンバスのサイズを調整
        _t.$idApp.find('canvas')
        .width(_t.appRct.w).height(_t.appRct.h);

        // .appのサイズと位置を調整
        _t.$clsApp
        .css({left: _t.appRct.x, top: _t.appRct.y})
        .width(_t.appRct.w).height(_t.appRct.h);
    };

    //------------------------------------------------------------
    // 画面サイズの自動変更
    _t.autoResize = function() {
        // 変数の初期化
        let tmrId = null;
        const fncResize = () => {
            _t.calcAppRct();    // アプリ矩形の計算
            _t.fitApp();        // アプリを画面にフィット
        };

        // 遅延付き実行(短時間の連続実行を避ける)
        $(window).resize(() => {
            if (tmrId !== null) {clearTimeout(tmrId)}
            tmrId = setTimeout(fncResize, 50);
        });

        // 初回実行
        fncResize();
    };

    //------------------------------------------------------------
    //||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    //------------------------------------------------------------
    // タップの初期化
    //   _t.tapArr に対する処理をタップ イベントに登録する。
    _t.initTap = function() {
        // タップ イベントの登録
        _t.onTap((x, y) => {
            // タップ配列への処理
            for (let i = _t.tapArr.length - 1; i >= 0; i --) {
                // 変数の初期化
                const o = _t.tapArr[i];

                // 矩形が存在して範囲外なら飛ばす
                if (o.rct !== null && ! game.core.inRngRct(x, y, o.rct)) {continue}

                // 実行と継続確認
                const res = o.fnc(x, y, o);     // 実行
                if (res !== true) {break}       // 明示的にtrueでないなら終了
            }
        });
    };

    //------------------------------------------------------------
    // タップ イベントの登録
    _t.onTap = function(cb) {
        _t.$idApp.mousedown(e => {
            // 変数の初期化
            const eX = e.clientX;
            const eY = e.clientY;

            // 画面範囲内か確認
            if (! game.core.inRngRct(eX, eY, _t.appRct)) {return}

            // 変数の初期化
            const cW = _t.cnvsInf.w;
            const cH = _t.cnvsInf.h;

            // イベント位置の計算
            let eCX = ((eX - _t.appRct.x) * cW / _t.appRct.w) | 0;
            let eCY = ((eY - _t.appRct.y) * cH / _t.appRct.h) | 0;

            // コールバック
            cb(eCX, eCY);
        });
    };

    //------------------------------------------------------------
    //||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    //------------------------------------------------------------
    // タップの追加
    //   fnc - (x, y, {id, rct, fnc})
    //   rct が null の場合は全面
    _t.addTap = function(id, rct, fnc) {
        _t.tapArr.push({id: id, rct: rct, fnc: fnc});
    };

    // タップの削除(前方一致)
    _t.delTap = function(id) {
        _t.tapArr = _t.tapArr.filter(o => o.id.indexOf(id) === -1);
    };

    // タップの全削除
    _t.delAllTap = function() {
        _t.tapArr = [];
    };

})();

js-main/main.js
/**
 * @license com.crocro.game Sample
 * (c) 2018 Masakazu Yanai https://crocro.com/
 * License: MIT / Date: 2018-03-15
 */

'use strict';

// 開始
$(function() {
    // ショートカットの作成
    const game = window.com.crocro.game;

    //------------------------------------------------------------
    // レイヤー情報の初期化
    const layerBg   = 0;
    const layerDraw = 1;
    const layerMax  = 2;

    // キャンバス情報とキャンバス配列の初期化
    const cnvsInf = new game.canvas.CnvsInf({n: layerMax, bg: '#ffc'});
    const cnvsArr = game.canvas.initCnvsArr(cnvsInf);

    // 画面の初期化
    game.canvas.clearCnvsArr(cnvsArr);  // キャンバスのクリア
    game.view.init(cnvsInf);            // 表示の初期化

    // タップ位置の描画
    let oldX, oldY;
    game.view.addTap('tapAndDraw', null, (x, y) => {
        // 描画の設定
        const cntx = cnvsArr[layerDraw].cntx;
        cntx.fillStyle   = 'red';
        cntx.strokeStyle = 'red';
        cntx.lineWidth   = 2;

        // 描画
        if (oldX === undefined) {
            // 初回時処理 - 初期位置描画
            cntx.fillRect(x - 5, y - 5, 10, 10);
        } else {
            // 2回目以降 - 線を引く
            cntx.beginPath();
            cntx.moveTo(oldX, oldY);
            cntx.lineTo(x, y);
            cntx.stroke();
        }

        // タップ位置の記録
        oldX = x;
        oldY = y;
    });
});

初Steamゲーム 8bit風RTS『TinyWar high-speed』発売中です。
その他の同人ゲームや同人技術書はこちら。
作成:2018/03/16  更新:2018/03/27  [Permalink]
カテゴリー
基本情報