フルスクラッチでシューティングゲームもどき作ってみる

ライブラリなどを一切使わずJavaScriptのみでフルスクラッチでシューティングゲームみたいなものを作ってみたいと思います。

canvasとは?

Canvasとは、ブラウザ上に図を描くために策定された仕様なんだそうです。図を描いたり線を描いたりすることは得意なようですが、ゲームを作ることは得意ではないみたいです。

なので私たちがゲームを作るにはゲーム用のプログラムを自分で作るか、もしくはゲームエンジンを使ったりしないと作れません。もどかしいですね。

とりあえずやってみる

まぁ、とりあえずやってみましょう(‘ω’)ノ

まずはHTMLファイルを準備。

<!DOCTYPE HTML>
<html lang="ja">
<head>
    <title>サンプル</title>
    <meta charset="utf-8">
    <script src="main.js"></script>
</head>
<body>
    <canvas id="canvas" style="background-color: black;"></canvas>
</body>
</html>

canvasタグがcanvasです。idもとりあえずcanvasにしときましょう。背景色はゲームらしく黒にしておきます。

ここで一つ要注意なのがcanvasのサイズの指定の仕方です。

canvasのサイズはcssでもjavascriptでも指定できるようになっているんですが、cssの方は表示エリアの指定になっていて、javascriptの方は解像度みたいな扱いになっています。

つまり表示エリアが300×300になっていて解像度が600×600になっていると300×300に収まるように縮小されます。これだとOKなんですが縦横の比率が違うように指定しますと変に伸びたり縮んだりしてしまいます。なので基本的にはcssでは幅だけ指定するのが良いと思います。

簡単なフレーム更新プログラム

サンプル画像

まずは簡単なフレーム更新プログラムを作ってみました。

sampleプログラム

var canvas, ctx;
var x = 0, y = 0, vx = 1, vy = 1;
//初期処理
function init(){
    canvas = document.getElementById("canvas");
    canvas.width = 640;
    canvas.height = 900;
    ctx = canvas.getContext("2d");//コンテキスト
    
    requestAnimationFrame(update);//フレーム処理のお知らせを送る
}
//四角を描く
function drawRect(x, y, w, h){
    ctx.fillStyle = "white";
    ctx.fillRect(x, y, w, h);
}
//画面描画処理
function render(){
    ctx.clearRect(0, 0, canvas.width, canvas.height);//画面をクリア(前の画面描画を削除)
    x += vx;
    y += vy;
    if(x >= canvas.width - 64){
        vx = -1;
    }else if(x <= 0){
        vx = 1;
    }
    if(y >= canvas.height - 64){
        vy = -1;
    }else if(y <= 0){
        vy = 1;
    }
    drawRect(x, y, 64, 64); 
}
//更新処理
function update(){
    render();
    requestAnimationFrame(update);//フレーム処理のお知らせを送る
}
//開始
window.onload = function(){
    init();
}

今回このなかで大事なのはrequestAnimationFrameというメソッドです。これはおよそ60FPSになるような一定の間隔で引数に渡した関数を呼び出してくれるようです。

ただし、requestAnimationFrameは一回こっきりしか働いてくれないようで、ずっと更新処理をし続けるには毎回requestAnimationFrameを呼ぶ必要があるようです。

画面が更新されていても何も表示されていなければわからないので四角形を表示させて移動させてみました。DVDプレーヤーのアレみたいなやつを作ってみました。角に行ったら盛り上がってください(外国で流行っているらしいです)。

仮のキャラを表示して作ってみる

シューティングサンプル

とりあえず画面の更新処理さえできてしまえばもうゲームは出来るはずです。

ってことで作ってみたのがこちらです → サンプルプログラム

かなり簡素ではありますが一応シューティングゲームになってます。まぁ弾は1発しか撃てないし敵も1匹ずつしか出てきませんが敵と弾の当たり判定をつけてあるのでちゃんとゲームになってますよね(*´ω`)

プログラムはこんな感じでやってみました。

var SCREEN_WIDTH = 640;//画面幅
var SCREEN_HEIGHT = 900;//画面高さ
var core, player, bullet, enemy;

class Chara{//キャラクタークラス
    constructor(w, h){
        this.x = 0;
        this.y = 0;
        this.width = w;
        this.height = h;
    }
    render(){
        drawRect(core.ctx, this.x, this.y, this.width, this.height);
    }
}
class Bullet extends Chara{//弾クラス
    constructor(w, h){
        super(w, h);
        this.y = -this.height;
    }
    move(){
        this.y -= 10;
        if(this.y < -this.height){
            this.x = player.x + (player.width - this.width) / 2; 
            this.y = player.y;
        }
    }
}
class Player extends Chara{//プレイヤークラス
    constructor(w, h){
        super(w, h);
        this.x = 300;
        this.y = 500;
    }    
}
class Enemy extends Chara{//敵クラス
    constructor(w, h){
        super(w, h);
        this.x = Math.floor(Math.random() * (SCREEN_WIDTH - this.width));
        this.y = -this.height;
    } 
    move(){
        this.y += 2;
        if(this.y > SCREEN_HEIGHT){
            this.x = Math.floor(Math.random() * (SCREEN_WIDTH - this.width));
            this.y = -this.height;
        }
    }
}
class Core{//ゲーム基幹クラス
    constructor(){
        this.canvas = document.getElementById("canvas");
        this.canvas.width = SCREEN_WIDTH;
        this.canvas.height = SCREEN_HEIGHT;
        this.ctx = this.canvas.getContext("2d");//コンテキスト
        
        this.canvas.addEventListener("mousedown", (e) => {//マウスダウン
            this.isMouseDown = true;
            player.px = e.x;
            player.py = e.y;
        });
        this.canvas.addEventListener("mouseup", (e) => {//マウスアップ
            this.isMouseDown = false;
        });
        this.canvas.addEventListener("mousemove", (e) => {//マウス移動
            if(!this.isMouseDown)return;
            player.x += e.x - player.px;
            player.y += e.y - player.py;
            player.px = e.x;
            player.py = e.y;
        });
        requestAnimationFrame(()=>{this.update();});//フレーム処理のお知らせを送る
    }
    update(){//更新処理
        requestAnimationFrame(()=>{this.update();});//フレーム処理のお知らせを送る
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);//画面をクリア(前の画面描画を削除)
        this.enterframe();//フレーム処理
        this.render();//描画処理
    }
    render(){//描画処理
        enemy.render();
        player.render();
        bullet.render();
    }
    enterframe(){//フレーム処理
        bullet.move();
        enemy.move();
        var r1 = bullet.width / 2;
        var r2 = enemy.width / 2;
        if(circleCollision(bullet.x + r1, bullet.y + r1, r1, enemy.x + r2, enemy.y + r2, r2)){
            enemy.y = SCREEN_HEIGHT;
            bullet.y = -100;
        }
    }
}
function drawRect(ctx, x, y, w, h){//四角を描く
    ctx.fillStyle = "white";
    ctx.fillRect(x, y, w, h);
}
function circleCollision(x1, y1, r1, x2, y2, r2){//当たり判定
    if((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2) <= (r1+r2) * (r1+r2)){
        return true;
    }
    return false;
}
//開始
window.onload = function(){
    core = new Core();
    enemy = new Enemy(64, 64);
    player = new Player(64, 64);
    bullet = new Bullet(16, 16);
}

プログラムの説明

キャラクターの部分はCharaというクラスから継承して少し楽してますね。また、ゲームの基幹的な部分もひとまとめにしてみました。

ゲームにするためには操作が必要なのでmousedown,  mouseup, mousemoveを使って操作できるようにしました。ただ、これはマウスでの操作にしか反応してくれません。スマホなどのタッチでの操作はまた別なんですよね(´Д`)どっちもOKな入力イベントないのかなぁ。。

あと、関係ないけど、画面に表示されているキャラは四角なのに当たり判定は円っていうね。まぁ細かいことは良いではないか(^^;)

画像を読み込んで表示する

シューティングサンプル

やっぱただの四角だと見た目につまらない。ゲームらしい画像を使わなけりゃ面白くないですよね。ってことで今回は画像を表示させてみます。

で、まず画像を用意していただきたいんですが、プレイヤー、敵、弾の3つ作成してください。大きさはプレイヤー、敵が64×64、弾が16×16です。それらをimagesフォルダに入れてください。

前回のサンプルの画像を入れ替えたのがこちらです→サンプル

では作ってきましょう(・∀・)

ゲームに使う画像の読み込み処理

まずは使う画像をASSETSオブジェクトにまとめて入れておきます。それぞれの画像の名前とファイルの場所を入れます。ASSETSという言い方は最近のトレンドらしいんですが、まぁ使うデータのことみたいです。

var ASSETS = {
    'img_player': 'images/player.png',
    'img_enemy': 'images/enemy.png',
    'img_bullet': 'images/bullet.png',
};

で、画像の読み込みなんですが、まず画像を扱うにはImageオブジェクトを作成してそこに画像を読み込んで使います。

var image = new Image();
image.src = '画像ファイルの場所';
image.onload = function() {//読み込んだよ
  
}

基本、上記のような感じで書きます。onloadメソッドは画像の読み込みが完了したら発生するイベントです。大きな画像だったりすると読み込むのに時間がかかったりするので必要な処理は読み込みが終わってからする必要があります。

ただ、ゲームだとたくさん画像を扱うので全部読み込んだかどうかの確認が必要です。

で、Coreクラスに以下のような処理をつけました。引数のdataはASSETSです。画像はすべてCoreクラスのassetsオブジェクトに読み込みます。

preload(data){
    var length = Object.keys(data).length;
    var count = 0;
    for(let key in data) {
        this.assets[key] = new Image();
        this.assets[key].src = data[key];
        this.assets[key].onload = ()=>{
            if(++count == length){//全部読み込んだ
                this.onload();//読み込み完了後の処理
            }
        }
    }
}

画像の枚数をカウントしておいて、読み込みの完了した数が画像の枚数と同じになればCoreクラスのonloadメソッドを実行します。

で、それを追加したCoreクラスがこんな感じです

class Core{//ゲーム基幹クラス
    constructor(){
        this.canvas = document.getElementById("canvas");
        this.canvas.width = SCREEN_WIDTH;
        this.canvas.height = SCREEN_HEIGHT;
        this.ctx = this.canvas.getContext("2d");//コンテキスト
        this.assets = [];//アセット保存用配列
        
        this.canvas.addEventListener("mousedown", (e) => {//マウスダウン
            this.isMouseDown = true;
            player.px = e.x;
            player.py = e.y;
        });
        this.canvas.addEventListener("mouseup", (e) => {//マウスアップ
            this.isMouseDown = false;
        });
        this.canvas.addEventListener("mousemove", (e) => {//マウス移動
            if(!this.isMouseDown)return;
            player.x += e.x - player.px;
            player.y += e.y - player.py;
            player.px = e.x;
            player.py = e.y;
        });
    }
    //画像のロード
    preload(data){
        var length = Object.keys(data).length;
        var count = 0;
        for(let key in data) {
            this.assets[key] = new Image();
            this.assets[key].src = data[key];
            this.assets[key].onload = ()=>{
                if(++count == length){//全部読み込んだ
                    this.onload();//読み込み完了後の処理
                }
            }
        }
    }
    update(){//更新処理
        requestAnimationFrame(()=>{this.update();});//フレーム処理のお知らせを送る
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);//画面をクリア(前の画面描画を削除)
        this.enterframe();//フレーム処理
        this.render();//描画処理
    }
    render(){//描画処理
        enemy.render();
        player.render();
        bullet.render();
    }
    enterframe(){//フレーム処理
        bullet.move();
        enemy.move();
        var r1 = bullet.width / 2;
        var r2 = enemy.width / 2;
        if(circleCollision(bullet.x + r1, bullet.y + r1, r1, enemy.x + r2, enemy.y + r2, r2)){
            enemy.y = SCREEN_HEIGHT;
            bullet.y = -100;
        }
    }
}

読み込み完了後にゲームが始動

画像の読み込みが完了したらゲームプログラムが動き始めるようにします。

core.onloadメソッドに読み込み完了後の処理を記述します。ここで初めてrequestAnimationFrameを使います。この後は自動で更新処理が行われるようになります。

//開始
window.onload = function(){
    core = new Core();
    core.preload(ASSETS);
    core.onload = function(){
        enemy = new Enemy(64, 64);
        player = new Player(64, 64);
        bullet = new Bullet(16, 16);
        requestAnimationFrame(()=>{core.update();});//フレーム処理のお知らせを送る
    }
}

画像を描画する

もっとも大事な画像を描画する部分を忘れてましたね(^^;)

Charaクラスのレンダーメソッドに画像を描画するdrawImageメソッドを記述します。

class Chara{//キャラクタークラス
    constructor(w, h){
        this.x = 0;
        this.y = 0;
        this.width = w;
        this.height = h;
    }
    render(){
        core.ctx.drawImage(this.image, 0, 0, this.width, this.height, this.x, this.y, this.width, this.height); 
    }
}

キャラクターはすべてこのCharaクラスを継承しているのでこの一か所変更するだけでOKです。

これで画像を使ったゲームになります。

 

☆筆者が全力で作ったレトロ風シューティングゲーム

 

★PixiJSで簡易ゲームエンジン作りました

◇ Posted by いんわん