0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Javascriptでゲームプログラムの下地を作る

Last updated at Posted at 2023-02-06

Javascript でゲームプログラムの下地を作る

キー入力を抽象化しておおむね60fpsの描画を行う最小限のソースコードを作成します

ソースコードはここ
https://github.com/ikuo0/js-game_template

デモンストレーションはここ
WASD 又は 十字キー の操作で青いなにかが上下左右に動きます
https://ikuo0.github.io/js-game_template/

入力処理

JavaScriptのキー入力はイベント駆動ですのでフレーム毎にキー入力を取得するという関数がありません

こういうことがしたいがJavaScript標準のAPIだけではできないので困る

    // 毎フレーム呼ばれる関数
    function GameMain() {
        var input = getInput();// 毎フレームキー状態を取得したい
        /* なんらかの処理 */
    }

キーイベントが発生するたびに変数に入力を設定して入力状態の逐次取得を実現する

    self.keys = new Array(256);

    // 押されたら1をセット
    addEventListener("keydown", function(e) {
        self.keys[e.keyCode] = 1;
        e.preventDefault();
    }, false);

    // 離されたら0をセット
    addEventListener("keyup", function(e) {
        self.keys[e.keyCode] = 0;
    }, false);

    // 毎フレームの処理で self.keys を参照する事でキー入力を毎フレーム取得できる

※コメントより指摘 2023/2/7
e.keyCode は廃止され使用が推奨されていないので e.code を使いましょう
e.code について
https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/code

e.keyCode の非推奨について
https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/keyCode

だいぶロジックが変わってしまいそうですね

描画処理

requestAnimationFrame を使用するが、フレームが噛み合わない問題が出てくる
requestAnimationFrame 関数はリフレッシュレートに同期して呼び出される、そのため60fpsだったりゲーミングモニタのような環境では144fpsだったりと環境依存となってしまう
おおむね60fpsにするため 16.66ms を自前で計測してそのタイミングで描画するように記述する必要がある

    var mainLoop = function(timestamp) {
        while(1) {// CPU使用率高くなるが 144fpsと60fps の周期が上手く噛み合わず60fps未満となるのを防ぐため無限ループにて待つ
            if((performance.now() - t) > 16.66) {
                t = performance.now();
                var input = self.deviceToInput();
                g.main(input);
                
                var canvas = document.getElementById("main_canvas");
                var ctx = canvas.getContext("2d");
                Draw(ctx, g.getDraw());
                
                break;
            }
        }
        requestAnimationFrame(mainLoop);
    }

ソースコード

HTML

<html>
    <head>
        <meta charset="utf-8">
        <script src="./index.js"></script>
        <title>
            jsgame
        </title>
    </head>
    <body>
        <canvas id="main_canvas" width="800" height="600">
    </body>
</html>

JS


var CANVAS_WIDTH = 800;
var CANVAS_HEIGHT = 600;
var KEY_LEFT  = 0x0001;
var KEY_RIGHT = 0x0002;
var KEY_UP    = 0x0004;
var KEY_DOWN  = 0x0008;
var KEY_A     = 0x0100;
var KEY_B     = 0x0200;

function GameMain() {
    var self = this;
    
    self.loadImage = function(pathName) {
        var img = new Image();
        img.src = pathName;
        return img;
    };
    
    self.playerMove = function(k) {
        var r2 = 0.707106781188095;//1 / 1.41421356237;
        if(k & KEY_LEFT && k & KEY_UP) {
            return [-r2, -r2];
        } else if(k & KEY_LEFT && k & KEY_DOWN) {
            return [-r2, r2];
        } else if(k & KEY_RIGHT && k & KEY_UP) {
            return [r2, -r2];
        } else if(k & KEY_RIGHT && k & KEY_DOWN) {
            return [r2, r2];
        } else if(k & KEY_LEFT) {
            return [-1.0, 0.0];
        } else if(k & KEY_RIGHT) {
            return [1.0, 0.0];
        } else if(k & KEY_UP) {
            return [0.0, -1.0];
        } else if(k & KEY_DOWN) {
            return [0.0, 1.0];
        }
        return [0.0, 0.0];
    };
    
    self.playerOperation = function(input, x) {
        var me = this;
        var mv = me.playerMove(input);
        x.x += mv[0] * 8;
        x.y += mv[1] * 8;
        if(x.x < 0) {
            x.x = 0;
        }
        if(x.x > CANVAS_WIDTH) {
            x.x = CANVAS_WIDTH - 1;
        }
        if(x.y < 0) {
            x.y = 0;
        }
        if(x.y > CANVAS_HEIGHT) {
            x.y = CANVAS_HEIGHT - 1;
        }
    };
    
    self.X = {
        "img": self.loadImage("./circle.png"),
        "x": 100,
        "y": 100,
        "w": 128,
        "h": 128
    };
    self.main = function(input) {
        var me = this;
        me.playerOperation(input, me.X)
    };
    
    self.getDraw = function() {
        var me = this;
        var x = {
            "img": me.X.img,
            "x": me.X.x,
            "y": me.X.y,
            "w": me.X.w,
            "h": me.X.h,
        };
        return [x];
    };
}

function Draw(ctx, x) {
    ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    for(var i = 0; i < x.length; i += 1) {
        var ix = x[i];
        ctx.drawImage(ix.img, 0, 0, ix.w, ix.h, ix.x - ix.w / 2, ix.y - ix.h / 2, ix.w, ix.h);
    }
}

function MainLoop() {
    var self = this;
    self.keys = new Array(256);
    
    addEventListener("keydown", function(e) {
        self.keys[e.keyCode] = 1;
        e.preventDefault();
    }, false);
    
    addEventListener("keyup", function(e) {
        self.keys[e.keyCode] = 0;
    }, false);
    
    self.deviceToInput = function() {
        var me = this;
        var k = me.keys;
        var x = 0;
        if(k[37] || k[65]) {
            x |= KEY_LEFT;
        }
        if(k[38] || k[87]) {
            x |= KEY_UP;
        }
        if(k[39] || k[68]) {
            x |= KEY_RIGHT;
        }
        if(k[40] || k[83]) {
            x |= KEY_DOWN;
        }
        if(k[32] || k[0]) {
            x |= KEY_A;
        }
        if(k[13] || k[0]) {
            x |= KEY_B;
        }
        return x;
    }
    
    var g = new GameMain();
    var t = performance.now();
    var fpst = t;
    var framen = 0;
    var framerec = 0;
    var mainLoop = function(timestamp) {
        if((timestamp - fpst) > 1000) {
            fpst = performance.now();
            framerec = framen;
            framen = 0;
        }
        while(1) {
            if((performance.now() - t) > 16.66) {
                t = performance.now();
                var input = self.deviceToInput();
                g.main(input);
                
                var canvas = document.getElementById("main_canvas");
                var ctx = canvas.getContext("2d");
                Draw(ctx, g.getDraw());
                
                ctx.font = '20px sans-serif';
                ctx.fillText(String(framerec) + "fps", 0, 32);
                framen += 1;
                break;
            }
        }
        requestAnimationFrame(mainLoop);
    }
    mainLoop();
}


window.onload = function() {
    var g = new MainLoop();
}

実際の画面

こんな感じの画面が出れば動作成功です
WASD 又は 十字キー の操作で上下左右に動きます

image.png

問題点とか

検索して出てきたコードやら公式ドキュメント見ながら作ってみたけど期待通りの動作はしてるのでとりあえずはOK
CPU使用率が高くなる問題をなんとかしたいところ

以上です? メインループは改善点ありそうなのでまた更新するかもです

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?