筆者は趣味でレトロゲームのプログラミングをやります。ただし、ゲームを作るのが目的ではなく、プログラミングが目的だったりします。ひととおりゲームロジックを完成させたら満足して終わるタイプです。
ここで言っているレトロゲームとは何か?というと、80年代8bitパソコン/ファミコン時代のドット絵のゲーム達です。だいたいが16x16ドットのスプライト表示が主流の時代でした。
これからQiitaでもいくつか名作を紹介しながらゲームを作る記事を書こうかな、と思っていますが、何を作るにしてもドット絵のキャラクターのアニメーション表示が必要になってきます。
この記事では、Javascriptでドット絵のアニメーション表示部を作っていきます。
特定のゲームエンジンやライブラリは使いません。
使う道具
・言語:HTML(/CSS), Javascript
・エディタ:好きなもの
・ドット絵エディタ:好きなもの
ドット絵の表示方法
スプライトシートの用意
まずは、ベースになるドット絵を用意します。ドット絵を作るのにグラフィックスエディタが必要ですが、筆者は無料のFirealpacaを使用しています。
自分で作らなくても、無料で公開しているドット絵を使うとか、販売している作品を買うのも手かもしれません。「ドット絵 キャラクター」でググると色々出てきます。
この記事に作るデモではドッド絵をpngファイルにしてJavascriptで読んで描画しています。
下記の通りです。(Firealpacaで開いたキャンバスの様子)。
8x8ドットのマスが 横8列、縦2行並んでいます。全体で64x16ドットしかありません。
ひとつのマス(8x8ドット)の絵はスプライトと呼び、全体をスプライトシートと呼びます。
実際のサイズはかなり小さいので、ここには拡大表示した絵を貼っています。
スプライトの番号管理と描画
プログラムの中でスプライトを描画するためには、スプライトシート上のどのマスにある絵なのか指定する必要があります。そこでシートのマスに番号をつけて管理します。ここでは下記のように番号を振ります。
スプライトシート上にスプライトをどのように並べるのかは、特に決まりはありません。
自分が管理しやすいように並べれば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固定です。
絵の切り取りとキャンバスへの描画は 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画像ファイル
ソースコード
<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;
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";
実行結果

ドット絵をアニメーションさせる
アニメーションの原理
スプライトシートから任意のスプライトを切り出して描画できたら、後はこれを順番に切り替えて表示すれば、アニメーションになります。
一定間隔で表示を切り替えるのは、どうやるのでしょうか?
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つのキャラクターがアニメーションするサンプルです。
以下のpngファイルと2つのファイルを同じフォルダーに入れてブラウザでindex.htmlを開いてください。
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: 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++;
}
実装を図にすると下記のようになります。
まとめ
さて、ドット絵ゲームを開発するためのベースになるアニメーション機能を作ってきました。
キャラクターの大きさが固定(8×8ドット、16×16ドットなど)であれば、複数のキャラクターを1枚のスプライトシートで管理できるので、アニメーションの実現はわりと簡単です。
個人的には8x8ドットのキャラクターでも表現力は十分です。(作っている分には楽しい)
みなさんも、よかったら遊んでみてください。