ゲームプログラミング入門:キャラクターアニメーション

ゲーム

ゲームエンジンやライブラリを使わずに、HTMLとJavaScriptだけでゲームを作ります。

以下のような人のためにあります。

ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは覚えることが多すぎる。

ある程度プログラミングの知識がある人を対象にしています。

この記事で作るもの

ドット絵のキャラクターがアニメーションするデモを作ります。

荒いドット絵でしかも最小限のモーションであっても、アニメーションがあると雰囲気が出ます。実際に必要な実装もソースコードを見てもらえれば分かるように、たいしたことはありません。

このデモのアニメーションはモーションが少ないですが、それは単にドット絵を描くのが面倒なだけです。絵さえ頑張って描けば、仕組みを変えることなく、いくらでも精緻なモーションをさせることができます。

ソースコード

index.html, index.js を同じフォルダーに置いてください。さらに、ドット絵パターンが描かれたspritesheet.png を用意し、同じフォルダーにおく必要があります。index.htmlをブラウザからファイル読み込みすれば実行できます。

このデモのspritesheet.png

<html>
    <center>
        <canvas id="canvasBg" width="80" height="32" style="border:1px solid #000000; background-color: #000; display:none;"></canvas>
        <canvas id="gameCanvas" width="640" height="256" style="border:1px solid #000000; background-color: #000;"></canvas>
    <script type="text/javascript" src="index.js"></script>
</html>
// background canvas
const canvas_bg = document.getElementById('canvasBg');
const context_bg = canvas_bg.getContext('2d');

// display canvas
const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
context.imageSmoothingEnabled = false;

// スプライトシートのロード
const spriteSheet = new Image();
spriteSheet.src = "./spritesheet.png";

class Chara {
    constructor(x,y, anime_table) {
        this.x = x;
        this.y = y;
        this.frame_interval = 0;
        this.frame_index = 0;
        this.flip = false;
        this.anime_table = anime_table;
    }

    update() {
        this.anime_update();
    }

    anime_update() {
        let frames = this.anime_table.frames;
        let frame_interval = this.anime_table.frame_interval;

        if (this.frame_interval >= frame_interval) {
            this.frame_index = (this.frame_index+1) % frames.length;
            this.frame_interval = 0;
        }
        this.sprite = frames[this.frame_index];
        this.frame_interval++;
    }

    changeDirection() {
        this.flip = !this.flip;
    }

    draw() {
        drawSprite(this.sprite, this.x, this.y, this.flip);
    }
}

// キャラクターの生成
let chara1 = new Chara(20, 10, {frames: [0,1,2,3,4], frame_interval: 60});
chara1.changeDirection();
let chara2 = new Chara(50, 20, {frames: [8,9,10,11], frame_interval: 50});


function drawSprite(sprite_no, x, y, flip) {
    let sx = (sprite_no % 8) *8;
    let sy = Math.floor(sprite_no / 8)*8;
    if (flip) {
        context_bg.save();
        context_bg.scale(-1,1);
        context_bg.drawImage(spriteSheet, sx, sy, 8, 8, -x-8, y, 8, 8);
        context_bg.restore();
    } else {
        context_bg.drawImage(spriteSheet, sx, sy, 8, 8, x, y, 8, 8);
    }
}

function update() {
    context_bg.clearRect(0, 0, canvas_bg.width, canvas_bg.height);
    chara1.update();
    chara1.draw();
    chara2.update();
    chara2.draw();

    // 表示用に拡大する
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.drawImage(canvas_bg, 0, 0, canvas_bg.width, canvas_bg.height, 0, 0, canvas.width, canvas.height);
}

setInterval(update, 10);


スプライトシートの用意

まずは、ベースになるドット絵を用意します。ドット絵を作るのにグラフィックスエディタが必要ですが、筆者は無料のFirealpacaを使用しています。

自分で作らなくても、無料で公開しているドット絵を使うとか、販売している作品を買うのも手かもしれません。「ドット絵 キャラクター」でググると色々出てきます。

このデモで使っている spritesheet.png の中身は下記の通りです。(Firealpacaで開いたキャンバスの様子)。実際のサイズはかなり小さいので、ここには拡大表示した絵を貼っています。

spritesheet.png

8×8ドットのマスが 横8列、縦2行並んでいます。8×8ドットのキャラクターの絵をスプライトと呼び、これが複数並んだ画像をスプライトシートと呼びます。

スプライトシート上にスプライトをどのように並べるのかは、どのように管理したいかによるので、人それぞれです。

上記ではキャラクターの種類ごとに行を分けていますが、一行にできるだけ詰めて管理しても構いません。また、このシートでは8×8ドットのマスが8列ですが、これも特に制限があるわけではないので、1行16列としても構いません。

なお、右側を向いている絵しかないのに、デモでは左側にも向いている、と気づいた人もいると思います。後ほど簡単に述べますが、JavaScriptでイメージ描画時に向きを変えています。

スプライトの番号管理と描画

JavaScriptでスプライトを描画する際には、スプライトシート上のどのマスにある絵なのか指定できた方が便利です。

スプライトシートの8×8ドットのマスに番号をつけて管理します。このデモでは下記のように番号を振っています。

番号の振り方が決まったら、スプライトシートから8×8のドット絵を切り取る方法が決まります。
以下がその実装部分です。

function drawSprite(sprite_no, x, y, flip) {
    let sx = (sprite_no % 8) *8;
    let sy = Math.floor(sprite_no / 8)*8;
  ・・・
        context_bg.drawImage(spriteSheet, sx, sy, 8, 8, x, y, 8, 8);
  ・・・
}

% 演算子は割り算の余りを返し、Math.floor() は整数部を返す関数で、割り算の結果に使うと商を返します。

9番めの絵を例にとるとsx, syは以下のようになります。

sx = (9 % 8) * 8 = 1 * 8 = 8
sy = Math.floor(9 / 8) * 8 = 1 * 8 = 8

9番めの絵は、(8, 8) の座標から切り取られます。幅は 8、高さは8固定です。

絵の切り取りとキャンバスへの描画は drawImage() をという関数で行います。以下が仕様です。

context.drawImage(
                image,      // 画像オブジェクト
                sx,         // 画像内で切り取るX座標
                sy,         // 画像内で切り取るY座標
                sWidth,     // 切り取る幅
                sHeight,    // 切り取る高さ
                tx,         // Canvas上のX座標
                ty,         // Canvas上のY座標
                tWidth,     // 描画する幅
                tHeight     // 描画する高さ
            );

絵を左右反転に描画する方法

スプライトシートの絵は右向きのみで、左向きの絵を用意していません。キャラクターが左を向いた時は、切り取った絵を左右反転させて描画しています。

反転処理は、drawSprite() 関数の if (flip) { … } で囲んだ部分の実装です。これはJavaScriptの機能を使っていますが説明は割愛します。ひとまず、こういうものだと思ってもらえればOKです。詳細を知りたい人は、ググればいくらでも出てきますので、検索してみてください。

function drawSprite(sprite_no, x, y, flip) {
    let sx = (sprite_no % 8) *8;
    let sy = Math.floor(sprite_no / 8)*8;
    if (flip) {
        context_bg.save();
        context_bg.scale(-1,1);
        context_bg.drawImage(spriteSheet, sx, sy, 8, 8, -x-8, y, 8, 8);
        context_bg.restore();
    } else {
        context_bg.drawImage(spriteSheet, sx, sy, 8, 8, x, y, 8, 8);
    }
}

もちろん、スプライトシートに左右反転の絵を用意する、という方法でも実現できます。

アニメーションの実現方法

スプライトシートから任意のスプライトを切り出して描画することができるようになりました。後は、これを順番に切り替えて表示すれば、アニメーションになります。

一定間隔で表示を切り替えるのは、どうやるのでしょうか?
ベースはJavaScript のsetInterval()の関数です。

setInterval(update, 10);

これにより、update 関数が10ミリ秒ごとに繰り返し呼ばれます。(第2引数を20にすれば20m秒ごとになります。)

update()関数では、以下のように辿っていてclass Chara のanime_update()が呼ばれます。

update()
    → chara1.update()
           → chara1.anime_update()

anime_update()も10ミリ秒ごとに呼ばれます。anime_update()関数は、描画するスプライト番号を順に切り替えています。

    anime_update() {
        let frames = this.anime_table.frames;
        let frame_interval = this.anime_table.frame_interval;

        if (this.anime_count >= frame_interval) {
            this.anime_index = (this.anime_index+1) % frames.length;
            this.anime_count = 0;
        }
        this.sprite = frames[this.anime_index];
        this.anime_count++;
    }

実装を図にすると下記になります。

frames配列と、frame_interval はどこで定義しているのでしょか?
Charaのインスタンスを生成するときに与えています。

let chara1 = new Chara(20, 10, {frames: [0,1,2,3,4], frame_interval: 60});
let chara2 = new Chara(50, 20, {frames: [8,9,10,11], frame_interval: 50});

上記の 60 は、10ミリ秒の周期の60回、の意味になります。つまり、600ミリ秒に1回です。
このデモだとchara1は60周期に1回(600ミリ秒に1回)、キャラクターの絵が切り替わるというわけです。(chara2は50周期に一回)

興味ある人は、ここら辺の数字をいじって、どのようにアニメーションが変わるか、試してみてください。



canvasの2面持ちと拡大表示

実際に定義しているドット絵は 8×8ドットで、そのままの大きさで表示すると、ほぼ点になっていまい何が何だか分かりません。このデモでは、縦横それぞれ8倍に拡大して表示しています。

そのためにcanvasを2つ定義しています。

canvas_bg … 8×8ドットの絵をそのままの大きさで描画するためのもの。HTML上では非表示にしている。
gameCanvas … canvas_bgの8倍の大きさの領域を定義して、HTML上で表示している。

通常の描画は canvas_bg に対して行い、最後に canvas_bg 全体を gameCanvas に拡大コピーしています。以下のコードですね。

    // 表示用に拡大する
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.drawImage(canvas_bg, 0, 0, canvas_bg.width, canvas_bg.height, 0, 0, canvas.width, canvas.height)

アニメーションの次は

さて、簡単なドット絵キャラクターのアニメーションを実現する方法を見てきました。思った以上にシンプルだったではないでしょうか?

キャラクターの大きさが固定( 8×8ドット、16×16ドットなど)であれば、スプライトシートで管理できるので、アニメーションの実現はわりかし簡単です。

ドット絵を使った小さいゲームを作るのであれば、これで十分かと思います。

アニメーションができたあとは、キャラクターの状態や前後左右の移動と連動できれば、より表現力の高いゲームを作成できます。次回はそれを実装したいと思います。


コメント

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