2:プレイヤーを歩かせる
古都さん作シューティングゲームを元にして
(2019.1.11執筆)
前回に触れた、Javascriptお役立ちコラムを執筆されてる古都さんは、blogで独自にシューティングゲームのJavaScriptを公開されてます。⇒ JavaScriptで弾幕シューティングゲームをフルスクラッチで作ってみよう、その1
操作キャラが矢印キーで動いてるのは何となく体感。
⇒ 古都さんのデモページで確認
これらのソースコードは膨大なJSライブラリと違い、かなりシンプルな構成です。
シンプル=仕組みを理解しやすい、応用しやすい、というメリットが有ります。
初心者の私がプログラミングを体感する上で、シンプルさ以上に重要な要素はない。つまり、解説を見ながら一つ一つ仕組みを読解していけば、少しずつ自分なりのアレンジを加えていけるのではないか?と、考えました。後にコードの改変許可を頂けたので、実践過程を記しておきます。古都さんありがとうです!
⇒ JavaScriptで弾幕シューティングゲームをフルスクラッチで作ってみよう、その1
上記のソースコードを元に、プレイヤーが歩くまでの動作を組み込みます。

プレイヤーの画像はぴぽやさんの素材から、この子を使うことにします。。 この記事は備忘録です。既存のコードをどのように取り入れて、応用するかな? を、この文章で追体験してもらえると記事を残した甲斐があります。
自機のソースコードの仕組みを理解する
初期のゲームファイルは、engine.js(仕組みの中核部分)とdanmaku.js(シーンやキャラの設定部分)の2つで成り立っているようです。 ひとまずdanmaku.jsに記述されてる自機(class Fighterで定義)の部分を見ていきましょう。danmaku.js > Fighterクラス
class Fighter extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(0, 0, 16, 16));
const hitArea = new Rectangle(8, 8, 2, 2);
super(x, y, sprite, hitArea);
this.speed = 2;
}
update(gameInfo, input) {
if(input.getKey('ArrowUp')) { this.y -= this.speed; }
if(input.getKey('ArrowDown')) { this.y += this.speed; }
if(input.getKey('ArrowRight')) { this.x += this.speed; }
if(input.getKey('ArrowLeft')) { this.x -= this.speed; }
}
}
さっそく意味が分かりませんね(' '*);; ここで諦めます。終了....(o _ o。)
...いや、さっきのページに解説載ってるやん。見るよ、見るぞ。
⇒ JavaScriptで弾幕シューティングゲームをフルスクラッチで作ってみよう、その1
うん、よく分かんないね。
どうやら自機の記述そのものを理解するには、この内部に書かれてる「Sprite」やら「Rectangle」やら「SpriteActor」が何なのか? 記載されてるのがゲームエンジンの中核部分「engine.js」、そう「engine.js」の中身を追っていく必要があるようです。
engine.jsを開いて、上記の記述を抜粋します。
engine.js > Rectangle
class Rectangle {
constructor(x, y, width, height) {//現在位置(x座標、y座標)と、大きさ(幅、高さ)を持つオブジェクトの定義
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
hitTest(other) {
const horizontal = (other.x < this.x + this.width) &&
(this.x < other.x + other.width);//他オブジェクトとのx軸接触判定
const vertical = (other.y < this.y + this.height) &&
(this.y < other.y + other.height);//他オブジェクトとのy軸接触判定
return (horizontal && vertical);//x軸&&y軸両方重なるか接触判定する・・・trueかfalseの値で使う
}
}
矩形(くけい)は重要な要素です。描画や当たり判定など様々なところで使用します。座標と幅・高さを持つだけのシンプルな(sbfl.netより)
Rectangle...こういった存在のようです。
色んな判定や、範囲、位置情報に用いるのでしょう。Rectangle要素はゲーム中で実体を持つ、ほぼ全てに用いられているようです。
engine.js > Sprite
class Sprite {//Rectangleに画像を当てはめる。画像名と、画像のどの部分を使うか?の定義)
constructor(image, rectangle) {
this.image = image;
this.rectangle = rectangle;
}
get _rectangle(){return this.rectangle;}
set _rectangle(newRectangle){this.rectangle = newRectangle;}
}
1枚の画像と、範囲を表すRectangleオブジェクトを受け取ります。範囲を受け取るのは、画像1枚につき1キャラクターとは限らないからです。自機、敵、ショットなどを1枚の画像に収めてしまうのが普通です。その中から一部だけを切り取って使用します(sbfl.netより)
Spriteは、先ほどのRectangle要素に、画像を当てはめるための記述のようです。自機の画像を変更する場合は、Sprite要素を弄るとよいことが分かります。
engine.js > SpriteActor
class SpriteActor extends Actor {//画像を当てはめたActor
constructor(x, y, sprite, hitArea, tags=[]) {
super(x, y, hitArea, tags);
this.sprite = sprite;//画像
this.width = sprite.rectangle.width;//画像の幅
this.height = sprite.rectangle.height;//画像の高さ
}
render(target) {//オーバーライドしたrenderメソッドでは、canvasのdrawImageメソッドを使って自身を描画
const context = target.getContext('2d');
const rect = this.sprite.rectangle;
context.drawImage(this.sprite.image,
rect.x, rect.y,
rect.width, rect.height,
this.x, this.y,
rect.width, rect.height);
}
isOutOfBounds(boundRect) {//isOutOfBoundsメソッドはRectangleオブジェクトの外であるかどうかを判定
const actorLeft = this.x;
const actorRight = this.x + this.width;
const actorTop = this.y;
const actorBottom = this.y + this.height;
const horizontal = (actorRight < boundRect.x || actorLeft > boundRect.width);
const vertical = (actorBottom < boundRect.y || actorTop > boundRect.height);
return (horizontal || vertical);
}
}
SpriteActorは色々すっ飛ばしたので分かりません(o _ o。)
しかし此処までの段階で、一つ前のSpriteを弄ればプレイヤー画像を変更できることが分かりました。
画像ファイルの当てはめ方を理解する
では、danmaku.jsに戻って、自機(Fighter)のコードを再確認します。danmaku.js > Fighterクラス
class Fighter extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(0, 0, 16, 16));
const hitArea = new Rectangle(8, 8, 2, 2);
super(x, y, sprite, hitArea);
this.speed = 2;
}
update(gameInfo, input) {
if(input.getKey('ArrowUp')) { this.y -= this.speed; }
if(input.getKey('ArrowDown')) { this.y += this.speed; }
if(input.getKey('ArrowRight')) { this.x += this.speed; }
if(input.getKey('ArrowLeft')) { this.x -= this.speed; }
}
}
画像の差し替えにSpriteを弄る...つまりこのFighterクラスの3段目に注目ですね。
const sprite = new Sprite(assets.get('sprite'), new Rectangle(0, 0, 16, 16));↑ これこれ。
この部分を試しに以下のように変更するよ。
const sprite = new Sprite(assets.get('player'), new Rectangle(0, 0, 32, 32));

用意したキャラチップは"32×32"で1人前なので、領域分はnew Rectangle(0, 0, 32, 32)にします。 ...で、肝心の「chara_player.png」はきちんと読み込めるのか?
未だ、読み込めるわけないんですよね〜。(' '*) どうやって新しい画像を読み込ませるか? それには初期の画像「sprite.png」がどのように扱われているかを追っていく必要があります。 .jsファイルの中身を再度確認してみましょう。。。
ここで「sprite.png」の文字列を検索!
。。。するとdanmaku.jsの最後の方に、それらしき記述を発見できます。
danmaku.js > sprite.png
assets.addImage('sprite', 'sprite.png');
assets.loadAll().then((a) => {
const game = new DanamkuStgGame();
document.body.appendChild(game.screenCanvas);
game.start();
});
何やら「assets」という文字列でsprite.pngを読み込ませているようですね。
ということは、新しい画像の文字列とURLをコードを追加して...
assets.addImage('player', 'chara_player.png');
このようにまとめれば...
assets.addImage('sprite', 'sprite.png');
assets.addImage('player', 'chara_player.png');
assets.loadAll().then((a) => {
const game = new DanamkuStgGame();
document.body.appendChild(game.screenCanvas);
game.start();
});

⇒ ここまでのデモを開く
シンプルなコードのはずなのに、幾つもの要素を理解してようやくプレイヤー画像を差し替えられました。 正面向きで片足を前に出したまま移動する、シュールな感じです。。
ひとまず休憩...
コードを解読するのにここまで掛かりましたが、作業は2行追加しただけ。 如何にプログラミングが難しいか、思い起こされそうな気します...先長い。
こっから移動方向に向きを変えたり、キーを押してる間に歩く動作を適応させたり課題は残ります...
キャラクターの向きを歩く方向にチェンジする
const sprite = new Sprite(assets.get('player'), new Rectangle(0, 0, 32, 32));先ほど手を加えた上のコードでは、正面片足立ちが表示されました。

12個あるうち、一番左上の画像がデモで表示されてましたね。再確認。
じゃあRectangle(0, 0, 32, 32)の部分を、試しにRectangle(32, 0, 32, 32)にしてみます。
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 0, 32, 32));

今度は、正面で真っ直ぐ立つようになりました。
では次にRectangle(32, 32, 32, 32)を試してみましょう。
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 32, 32, 32));

先ほどのpngファイルと照らしあわせてみると、Sprite(〜)内にあるnew Rectangle(x, y, width, height)の意味がだんだん掴めてきます。
xを、この画像の場合は32ずつ加算すると、右のマスの画像に。
yを、この画像の場合は32ずつ加算すると、下のマスの画像に変化する、ということ。
1マスのキャラの大きさが32×32ピクセルの単位width=32、height=32...だったので。 つまり画像ファイルの左上から、右xピクセル、下yピクセルを起点に、幅=width、高さ=heightの範囲を切り取って画像を呼び出す!のが、Sprite(〜)内のnew Rectangle(x, y, width, height)の意味だと理解できたのでした。
ここまで分かってしまえば、後はnew Rectangle(x, y, width, height)の数値を加減すれば向きを変えられる!という発想になります。
ひとまず直立姿勢のRectangle(x=32, y, 32, 32)を起点としましょう。
↓下向き時は、y=0 ...つまりnew Rectangle(32, 0, 32, 32)
←左向き時は、y=32 ...つまりnew Rectangle(32, 32, 32, 32)
→右向き時は、y=64 ...つまりnew Rectangle(32, 64, 32, 32)
↑上向き時は、y=96 ...つまりnew Rectangle(32, 96, 32, 32)
となるように、自機のコード(キー入力部分)を改変してみましょう。
danmaku.js > Fighterクラス修正前
class Fighter extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('player'), new Rectangle(0, 0, 32, 32));
const hitArea = new Rectangle(8, 8, 2, 2);
super(x, y, sprite, hitArea);
this.speed = 2;
}
update(gameInfo, input) {
if(input.getKey('ArrowUp')) { this.y -= this.speed; }
if(input.getKey('ArrowDown')) { this.y += this.speed; }
if(input.getKey('ArrowRight')) { this.x += this.speed; }
if(input.getKey('ArrowLeft')) { this.x -= this.speed; }
}
}
これを、このように書き換える。
danmaku.js > Fighterクラス修正後
class Fighter extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 32, 32, 32));
const hitArea = new Rectangle(8, 8, 2, 2);
super(x, y, sprite, hitArea);
this.speed = 2;
}
update(gameInfo, input) {
if(input.getKey('ArrowUp')) { this.y -= this.speed; this.sprite.rectangle.y = 96;}
if(input.getKey('ArrowDown')) { this.y += this.speed; this.sprite.rectangle.y = 0;}
if(input.getKey('ArrowRight')) { this.x += this.speed; this.sprite.rectangle.y = 64;}
if(input.getKey('ArrowLeft')) { this.x -= this.speed; this.sprite.rectangle.y = 32;}
}
}
⇒ デモを開いてみましょうちゃんと進む方向に合わせて向きを変えています!!!素晴らしい。
ここまでの追加分は4行...理解は果てしなく大変なのに、作業する時は一瞬だよ。
実際には、めちゃくちゃ試行錯誤してるので、もっと掛かってますが...
ここで判ったのは、何かの要素の値を入れ替えたい場合、this.sprite.rectangle.y = 〜といったように、.(ピリオド)で要素を辿って「値」を代入できるということですね。
最後に、キーを押しっぱなしの間は、sprite.rectangle()内のxが±32する記述です!これでちゃんと歩く動作になるよ。実現できればね。
歩く動作を組み込むsprite.rectangle(x)を切り替える
さて、では歩く動作の原理を考えてみましょう。 何らかの矢印キー↑↓←→を押している間、「sprite.rectangle.x」の値がこのように変わります。32 ⇒ 64 ⇒ 32 ⇒ 0 ⇒ 32 ⇒ 64 ⇒ 32 ⇒ 0 ⇒ 以下繰り返し。。。
(32 ⇒ 0 ⇒ 32 ⇒ 64 ⇒ 32 ⇒ 0 ⇒ 32 ⇒ 64 ⇒ 以下繰り返し。。でもイイ)
一定の距離を進んだら、値を切り替えるようにするかな。
その一定距離を測るための要素を一つ追加してみます。。。Fighterクラス内に、他の書き方に添って。
Fighterクラス内に追加
this.speed = 2; this.walkCount = 0;
最初からあるthis.speed = 2の下に、this.walkCount = 0;という要素を加えてみました。
歩いてる間は、this.speed =2 で順次移動している。
このことを利用して、矢印キー↑↓←→を押している間は歩行距離が順次加算されていくコードを描きます。
Fighterクラス内の update(gameInfo, input) 内に追加
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {this.walkCount += this.speed;}
this.walkCount += this.speed;という計算式にした。if()でいずれかの矢印キーが押されてる間に適応。
あとは、this.walkCountが一定値を超える度に、歩行モーションを切り替えるようにします。 条件式ifを用いると良いのかな??? 色々試行錯誤する。
同じくupdate(gameInfo, input) 内に追加
if(this.walkCount > 0) { this.sprite.rectangle.x = 64; } if(this.walkCount > 16) { this.sprite.rectangle.x = 32; } if(this.walkCount > 32) { this.sprite.rectangle.x = 0; } if(this.walkCount > 48) { this.sprite.rectangle.x = 32; this.walkCount = -15; }この計算式を考えるのにだいたい6時間くらい掛かった。まだまだ要領をつかめん...
今回16刻みでやってますが、12刻みでも20刻みでも、値を加減して歩くモーションスピードを調整します。
移動キーを放した際に、直立姿勢になるコードも加えますか。バランスとる仕上げ。
if(input.getKeyUp('ArrowUp') || input.getKeyUp('ArrowDown') || input.getKeyUp('ArrowRight') || input.getKeyUp('ArrowLeft')) {this.sprite.rectangle.x = 32; this.walkCount = 0;}
ふぅうう、作業時間ばかり経過している(o _ o。)
ようやく完成形が見えてきました。
danmaku.js > Fighterクラス歩く完成形
class Fighter extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 32, 32, 32));
const hitArea = new Rectangle(8, 8, 2, 2);
super(x, y, sprite, hitArea);
this.speed = 2;
this.walkCount = 0;
}
update(gameInfo, input) {
if(input.getKey('ArrowUp')) { this.y -= this.speed; this.sprite.rectangle.y = 96;}
if(input.getKey('ArrowDown')) { this.y += this.speed; this.sprite.rectangle.y = 0;}
if(input.getKey('ArrowRight')) { this.x += this.speed; this.sprite.rectangle.y = 64;}
if(input.getKey('ArrowLeft')) { this.x -= this.speed; this.sprite.rectangle.y = 32;}
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {this.walkCount += this.speed;}
if(this.walkCount > 0) { this.sprite.rectangle.x = 64; }
if(this.walkCount > 16) { this.sprite.rectangle.x = 32; }
if(this.walkCount > 32) { this.sprite.rectangle.x = 0; }
if(this.walkCount > 48) { this.sprite.rectangle.x = 32; this.walkCount = -15; }
if(input.getKeyUp('ArrowUp') || input.getKeyUp('ArrowDown') || input.getKeyUp('ArrowRight') || input.getKeyUp('ArrowLeft')) {this.sprite.rectangle.x = 32; this.walkCount = 0;}
}
}

⇒ 完成形のデモを開く
。。。ここまで丸2日費やした。 労力とリターンが見合ってるかどうかは判らんけど、とりあえず歩く動作が完成!
おめでとー、やったね(' '*)
こんなふうにして、無事にゲーム作りの第一歩を迎えることができました。
ご成長ありがとうございました。javascriptとかプログラム初心者ではあるものの、これまでHTML+CSSの経験もあるからどうかのう、誰かの励みになれば◎。
つづいては...NPCとの会話イベントだろうか...???
ぼちぼちやっていくか(o _ o。)
⇒ JavaScriptでゲーム作り「3:プレイヤーの向きに応じてアクションを起こす」
すぺしゃるさんくす
https://sbfl.net/古都さん
JavaScriptで作る弾幕STGの基礎(フレームワーク)を使わせていただいてます。感謝!

ぴぽやさんもありがとう、キャラチップをお借りしています。
尚、ここまでの差分ファイルは、プレイヤー画像chara_player.pngの追加と、danmaku.jsの修正のみです。
danmaku.js修正後のソースコード
'use strict';
class Title extends Actor {
constructor(x, y) {
const hitArea = new Rectangle(0, 0, 0, 0);
super(x, y, hitArea);
}
render(target) {
const context = target.getContext('2d');
context.font = '25px sans-serif';
context.fillStyle = 'white';
context.fillText('弾幕STG', this.x, this.y);
}
}
class Fighter extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 32, 32, 32));
const hitArea = new Rectangle(8, 8, 2, 2);
super(x, y, sprite, hitArea);
this.speed = 2;
this.walkCount = 0;
}
update(gameInfo, input) {
if(input.getKey('ArrowUp')) { this.y -= this.speed; this.sprite.rectangle.y = 96; }
if(input.getKey('ArrowDown')) { this.y += this.speed; this.sprite.rectangle.y = 0; }
if(input.getKey('ArrowRight')) { this.x += this.speed; this.sprite.rectangle.y = 64; }
if(input.getKey('ArrowLeft')) { this.x -= this.speed; this.sprite.rectangle.y = 32; }
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {this.walkCount += this.speed;}
if(this.walkCount > 0) { this.sprite.rectangle.x = 64; }
if(this.walkCount > 16) { this.sprite.rectangle.x = 32; }
if(this.walkCount > 32) { this.sprite.rectangle.x = 0; }
if(this.walkCount > 48) { this.sprite.rectangle.x = 32; this.walkCount = -15; }
if(input.getKeyUp('ArrowUp') || input.getKeyUp('ArrowDown') || input.getKeyUp('ArrowRight') || input.getKeyUp('ArrowLeft')) {this.sprite.rectangle.x = 32; this.walkCount = 0;}
}
}
class DanmakuStgMainScene extends Scene {
constructor(renderingTarget) {
super('メイン', 'black', renderingTarget);
const fighter = new Fighter(150, 300);
this.add(fighter);
}
}
class DanmakuStgTitleScene extends Scene {
constructor(renderingTarget) {
super('タイトル', 'black', renderingTarget);
const title = new Title(100, 200);
this.add(title);
}
update(gameInfo, input) {
super.update(gameInfo, input);
if(input.getKeyDown(' ')) {
const mainScene = new DanmakuStgMainScene(this.renderingTarget);
this.changeScene(mainScene);
}
}
}
class DanamkuStgGame extends Game {
constructor() {
super('弾幕STG', 300, 400, 60);
const titleScene = new DanmakuStgTitleScene(this.screenCanvas);
this.changeScene(titleScene);
}
}
assets.addImage('sprite', 'sprite.png');
assets.addImage('player', 'chara_player.png');
assets.loadAll().then((a) => {
const game = new DanamkuStgGame();
document.body.appendChild(game.screenCanvas);
game.start();
});