1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Javascript] ドット絵アニメーションの作り方

Last updated at Posted at 2025-07-23

筆者は趣味でレトロゲームのプログラミングをやります。ただし、ゲームを作るのが目的ではなく、プログラミングが目的だったりします。ひととおりゲームロジックを完成させたら満足して終わるタイプです。

ここで言っているレトロゲームとは何か?というと、80年代8bitパソコン/ファミコン時代のドット絵のゲーム達です。だいたいが16x16ドットのスプライト表示が主流の時代でした。

これからQiitaでもいくつか名作を紹介しながらゲームを作る記事を書こうかな、と思っていますが、何を作るにしてもドット絵のキャラクターのアニメーション表示が必要になってきます。

この記事では、Javascriptでドット絵のアニメーション表示部を作っていきます。
特定のゲームエンジンやライブラリは使いません。

使う道具

・言語:HTML(/CSS), Javascript
・エディタ:好きなもの
・ドット絵エディタ:好きなもの

ドット絵の表示方法

スプライトシートの用意

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

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

この記事に作るデモではドッド絵をpngファイルにしてJavascriptで読んで描画しています。
下記の通りです。(Firealpacaで開いたキャンバスの様子)。

スクリーンショット-2024-08-16-7.38.59.png

8x8ドットのマスが 横8列、縦2行並んでいます。全体で64x16ドットしかありません。
ひとつのマス(8x8ドット)の絵はスプライトと呼び、全体をスプライトシートと呼びます。

実際のサイズはかなり小さいので、ここには拡大表示した絵を貼っています。

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

プログラムの中でスプライトを描画するためには、スプライトシート上のどのマスにある絵なのか指定する必要があります。そこでシートのマスに番号をつけて管理します。ここでは下記のように番号を振ります。

スクリーンショット-2024-08-16-8.42.39.png

スプライトシート上にスプライトをどのように並べるのかは、特に決まりはありません。
自分が管理しやすいように並べればOKです。上記ではキャラクターの種類ごとに行を分けていますが、一行にできるだけ詰めて管理しても構いません。

また、このシートではマスが2行8列ですが、これも特に決まりがある訳ではなく、1行16列としても構いません。(ただし、シートの管理方法が変わったら、もちろんプログラムの方の変更が必要です)

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

描画方法

番号の振り方が決まったら、スプライトシートから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固定です。

スクリーンショット-2024-08-16-10.00.16.png

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

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

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

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

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

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);
    }
}

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

サンプル実装

以下のpngファイルと2つのファイルを同じフォルダーに入れてブラウザでindex.htmlを開いてください。

png画像ファイル

spritesheet.png

ソースコード

index.html
<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>
index.js_
// 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;

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);
    }
}

const spriteSheet = new Image();

spriteSheet.onload = () => {
    // スプライトの描画
    drawSprite(0, 10, 20, false);
    drawSprite(0, 20, 10, true);

    // バックグランドバッファを表示用バッファに拡大してコピーする
    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);
}

spriteSheet.src = "./spritesheet.png";

実行結果

ドット絵をアニメーションさせる

アニメーションの原理

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

スクリーンショット-2024-08-16-10.08.17.png

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

setInterval(update, 500); // 500msごとにupdate()関数を呼ぶ

一番シンプルに実装するとしたら、以下のようになると思います。

let sp_no = 0;
function update() {
    if (sp_no == 0) {
        drawSprite(0, 10, 20, false); // スプライト0番を描画する
        sp_no = 1;                    // 次の番号を1にする
    } else {
        drawSprite(1, 10, 20, false); // スプライト1番を描画する
        sp_no = 0;                     // 次の番号を0にする
    }
}
setInterval(update, 500); // 500msごとにupdate()関数を呼ぶ

サンプル実装

さきほどのスプライト表示サンプルの後半の部分を以下のソースに置き換えてみてください。

・・・
const spriteSheet = new Image();
spriteSheet.src = "./spritesheet.png";

let sp_no = 0;
function update() {
    // バックグランドバッファをクリアする
    context_bg.clearRect(0, 0, canvas_bg.width, canvas_bg.height);

    // スプライトの描画
    drawSprite(sp_no, 20, 10, true);
    sp_no = (sp_no+1) % 5;

    // バックグランドバッファを表示用バッファに拡大してコピーする
    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, 500);

アニメーション管理テーブルを用意する

実際のゲームではキャラクターの状態によってアニメーションを切り替える必要が出てきます。
この場合はテーブルを使ってアニメーション情報を管理するとよいです。

管理テーブル例

Javaの連想配列を使って下記のようなテーブルを持つとします。

{frames: [0,1,2,3,4], frame_interval: 30}

frames: はアニメーションで切り替えるスプライト番号を順番に入れた配列です。
frame_interval:はスプライト表示を切り替える間隔です。時間指定ではなくフレーム数とします。

フレーム数とはなんでしょう?

さきほど、setInterval(update, 500);で画面描画の間隔を設定していました。これが1フレームです。この例だと500msに一回画面更新をします。フレームレートは1/2秒になります。
さきほどは描画のデモなのでこの設定にしましたが、実際にはフレームレート1/2秒では使い物になりません。

一般的なゲームのフレームレートは1/60秒で、約16msに1回画面更新されます。以降、setInterval(update, 16);とします。

そうすると、frame_interval=30は、30フレームなので約1/2秒です。
スプライト表示は1/2秒ごとに切り替わります。別の言い方をすれば、アニメーションのフレームレートは1/2秒です。(画面更新のフレームレートは1/60秒ですが)

サンプル実装

以下のように2つのキャラクターがアニメーションするサンプルです。

chara-animation-basic.gif

以下のpngファイルと2つのファイルを同じフォルダーに入れてブラウザでindex.htmlを開いてください。

png画像ファイル

spritesheet.png

ソースコード

index.html
<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>
index.js
// 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: 30});
chara1.changeDirection();
let chara2 = new Chara(50, 20, {frames: [8,9,10,11], frame_interval: 25});


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, 16);

アニメーション管理部

Charaクラスの下記メソッドがアニメーションを切り替えているところです。

    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++;
    }

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

スクリーンショット-2024-08-16-15.04.18.png

まとめ

さて、ドット絵ゲームを開発するためのベースになるアニメーション機能を作ってきました。

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

個人的には8x8ドットのキャラクターでも表現力は十分です。(作っている分には楽しい)
みなさんも、よかったら遊んでみてください。

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?