ゲームエンジンやライブラリを使わずに、HTMLとJavaScriptだけでゲームを作ります。
以下のような人のためにあります。
ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは面倒くさい。
ある程度プログラミングの知識がある人を対象にしています。
本記事で作るもの
次に作るゲームのために、いったんこれまでやったことを整理して、2Dのドット絵キャラクターがアニメーションして動くゲームのテンプレートを用意します。
シンプルでも動作するプログラムのベースがあると、気軽に素早くゲームを作るとっかかりとして使えます。
これまで作ってきたゲームのスプライトシートの管理まわりの実装の整理や、ゲームパッド対応の成果の取り込みをしていきたいと思います。
キャラクターが上下左右に動きます。
キーボード: i ... 上, m ... 下, j ... 左, l ... 右
ゲームパッドの上下左右にも対応。
ソースコード
ファイル構成は以下のとおりです。ファイル数は多いですがソースコード行数は500行程度と、たいしたことはありません。
.
├── Chara.js
├── SpriteSheet.js
├── UserInput.js
├── World.js
├── index.html
├── index.js
└── spritesheet.png
スプライトシートの管理
スプライトシートは、N x Nのスプライト(ドット絵)を集めた一枚のpngファイルです。これまでスプライトは8×8の大きさのものしか作ってこなかったのですが、将来に向けて16×16とか16×24とかいろんなバリエーションの大きさにも対応できるようにしたいところです。
ひとつのスプライトシートpngファイル内に、いくつものスプライトの大きさのバリエーションがあるのは扱いづらいので、基本的にひとつのスプライトシート内のスプライトの大きさは固定(8×8か16×16など)です。
その上で、スプライトシートをJavaScriptのオブジェクトとして扱えるようにして、その中の任意の番号のスプライトを描画できるようにします。具体的には、class SpriteSheetを作ります。使い方は以下のとおりです。
const spsheet = new SpriteSheet(8, 8, "./spritesheet.png");
一つのスプライトの大きさ(幅、高さ)とpngファイル名をコンストラクタに指定すると、オブジェクトが生成されます。その後、
spsheet.drawSprite(context_bg, sprite_no, x, y, flip);
のように使います。class SpriteSheetのソースコードは下記の通りです。
export class SpriteSheet {
constructor(spr_w, spr_h, png_file) {
this.sprite_w = spr_w;
this.sprite_h = spr_h;
this.image_loaded = false;
// load the sprite sheet
this.image = new Image();
this.image.src = png_file;
this.onDecoded = this.onDecoded.bind(this);
this.image.decode()
.then( this.onDecoded )
.catch((error) => { console.error("failed to decode", erro); });
}
onDecoded() {
this.columns = Math.floor(this.image.width / this.sprite_w);
this.image_loaded = true;
}
drawSprite(context, sprite_no, x, y, flip=false) {
if (!this.image_loaded)
return;
const sw = this.sprite_w;
const sh = this.sprite_h;
const sx = (sprite_no % this.columns) * sw;
const sy = Math.floor(sprite_no / this.columns) * sh;
if (flip) {
context.save();
context.scale(-1,1);
context.drawImage(this.image, sx, sy, sw, sh, -x-sw, y, sw, sh);
context.restore();
} else {
context.drawImage(this.image, sx, sy, sw, sh, x, y, sw, sh);
}
}
}
スプライトシートの大きさは、読み込んだpngファイルのイメージ情報から取得できます。また、コンストラクタで渡されたスプライトの大きさ(幅spr_wと高さspr_h)を使えば、スプライトシートの区画の縦と横が何マスあるかは計算でわかります。
このclass SpriteSheetで管理するスプライトの番号づけのルールは下記の例に示す通りです。下記は横に8マスあるスプライトシートの場合です。
最上段の行の左端から0、1、2、、、と数えていきます。右端に届いたら、次の行に左端から続きを数えます。
スプライトシートの縦横の区画の数は自由です。上記の例の場合、16列 x 1行の構成にしても問題ありません。
入力イベントの管理
前回の記事では、Webブラウザからゲームパッドの入力イベントを受け付けるようにしました。
GamePadクラスを作り、キーボードイベントと同様に押された/離されたのトリガーでイベントを受けられるようにしました。
let gamepad = new GamePad(0) // 0番目のゲームパッドを対象とするオブジェクトを生成する
// ボタンが押された時のイベントハンドラーを登録する
gamepad.addEventListener("pressed", btnDownHandler);
// ボタンが離された時のイベントハンドラーを登録する
gamepad.addEventListener("released", btnUpHandler);
一方で、キーボードから通知されるのは入力されたキー(‘a’, ‘b’, ‘c’ …) の情報で、ゲームパッドの方はボタンの番号(0, 1, 2, …) です。入力イベントを使う側で、毎回いちいち両方を判断するのはプログラムの作りとしてはよろしくありません。どこかでまとめて統一した情報としてもらいたいところです。
そこで、class UserInputを作り、キーボードからのイベントとゲームパッドからのイベントをまとめて、入力状態を管理することにしました。下記がその実装部分です。
const keyMap = {
'j': 'left',
'l': 'right',
'i': 'up',
'm': 'down',
'c': 'A',
'x': 'Y',
'r': 'reset',
's': 'start',
}
const btnMap = {
0: 'B',
1: 'A',
2: 'Y',
3: 'X',
8: 'reset',
9: 'start',
12: 'up',
13: 'down',
14: 'left',
15: 'right',
}
・・・・
let gamepad = new GamePad(0);
export class UserInput {
constructor(doc) {
this.up = false;
this.down = false;
this.left = false;
this.right = false;
this.A = false;
this.B = false;
this.reset = false;
this.start = false;
this.prev_pressed = null;
this.available_inputs = Object.values(keyMap);
this.keyDownHandler = this.keyDownHandler.bind(this);
this.keyUpHandler = this.keyUpHandler.bind(this);
this.btnDownHandler = this.btnDownHandler.bind(this);
this.btnUpHandler = this.btnUpHandler.bind(this);
doc.addEventListener('keydown', this.keyDownHandler, false);
doc.addEventListener('keyup', this.keyUpHandler, false);
gamepad.addEventListener("pressed", this.btnDownHandler);
gamepad.addEventListener("released", this.btnUpHandler);
}
clearInputs() {
for (let prop of Object.values(keyMap)) {
this[prop] = false;
}
}
set_key_input(key, val) {
const prop = keyMap[key];
if (this.available_inputs.includes(prop)) {
this[prop] = val;
}
}
set_btn_input(index, val) {
const prop = btnMap[index];
if (this.available_inputs.includes(prop)) {
this[prop] = val;
}
}
setInputFilter(list) {
if (list==null)
list = Object.values(keyMap);
this.available_inputs = list;
}
keyDownHandler(event) {
this.set_key_input(event.key, true);
}
keyUpHandler(event) {
this.set_key_input(event.key, false);
}
btnDownHandler(event) {
this.set_btn_input(event.index, true);
}
btnUpHandler(event) {
this.set_btn_input(event.index, false);
}
}
UserInputクラスの中で、プロパティとして up, down, left, right, A, B という風に入力イベントを定義しているのがポイントです。キーボード/ゲームパッド入力は、それぞれ対応するイベント状態に反映します。
使い方は以下のとおりです。
// documentオブジェクトを渡してインスタンスを生成する。
export const input = new UserInput(document);
・・・
// 任意のところで入力状態を判断する。(キーボード、ゲームパッド両方の状態が反映された結果)
if (input.left) {
・・・
}
if (input.right) {
・・・
}
if (input.up) {
・・・
}
if (input.down) {
・・・
}
キャラクターのアニメーション処理
基本的なロジックは以前開設した内容と同じです。下記記事を参照してください。
まとめ
次に作るゲームのために、いったんこれまでやったことを整理して、2Dのドット絵キャラクターがアニメーションして動くゲームのテンプレートを用意しました。
スプライトシート管理と入力イベント周りの扱いを改善したので、その部分を主に解説しました。
これをベースに、次に1980年代の名作パズルを作ってみたいと思います。
コメント