3:プレイヤーの向きに応じてアクションを起こす
NPCと会話する記述に挑戦したい!
(2019.1.19執筆)
無事プレイヤーキャラを歩かせることができました、今回はNPCとの会話に挑戦する前提で、プレイヤーの動作を調整してみようと思います。内容的にも、古都さんのシューティングゲームの記事その2が活用できそうです。
参考 ⇒ JavaScriptで弾幕シューティングゲームをフルスクラッチで作ってみよう、その2
この記事を置き換えると、プレーヤーの弾がNPCに働きかけるアクション ⇒ NPCがアクションに触れたとき、会話イベントを発生させる。という発想に応用できそうですね。
前準備として、この記事ではプレイヤーが目の前の人物(オブジェクト)にアクションを働きかける流れを描いていきたいと思います。
danmaku.js 全体 ⇒ その2のカスタマイズ差分を合わせた記述
'use strict';
class TextLabel extends Actor {
constructor(x, y, text, size) {
const hitArea = new Rectangle(0, 0, 0, 0);
super(x, y, hitArea);
this.text = text;
this.size = size;
}
render(target) {
const context = target.getContext('2d');
context.font = `${this.size}px sans-serif`;
context.fillStyle = 'white';
context.fillText(this.text, this.x, this.y);
}
}
class Player 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._velocityX = 0;
this._velocityY = 0;
this.walkCount = 0;
this._interval = 5;
this._timeCount = 0;
}
update(gameInfo, input) {
this._velocityX = 0;
this._velocityY = 0;
const rect = this.sprite.rectangle;
//矢印キーを押した時に、プレーヤーの向きを変える。
if(input.getKey('ArrowUp')) { rect.y = 96; }
if(input.getKey('ArrowDown')) { rect.y = 0; }
if(input.getKey('ArrowRight')) { rect.y = 64; }
if(input.getKey('ArrowLeft')) { rect.y = 32; }
//キーを押した方向に移動させる
if(input.getKey('ArrowUp')) { this._velocityY = -this._speed; }
if(input.getKey('ArrowRight')) { this._velocityX = this._speed; }
if(input.getKey('ArrowDown')) { this._velocityY = this._speed; }
if(input.getKey('ArrowLeft')) { this._velocityX = -this._speed; }
this.x += this._velocityX;
this.y += this._velocityY;
//歩行カウントをすすめる
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {this.walkCount += this._speed;}
//歩行カウントが一定以上に達した時、歩くモーションを変化
if(this.walkCount > 0) { rect.x = 64; }
if(this.walkCount > 16) { rect.x = 32; }
if(this.walkCount > 32) { rect.x = 0; }
if(this.walkCount > 48) { rect.x = 32; this.walkCount = -15; }
//立ち止まった時に直立姿勢
if(input.getKeyUp('ArrowUp') || input.getKeyUp('ArrowDown') || input.getKeyUp('ArrowRight') || input.getKeyUp('ArrowLeft')) {rect.x = 32; this.walkCount = 0;}
const boundWidth = gameInfo.screenRectangle.width - this.width;
const boundHeight = gameInfo.screenRectangle.height - this.height;
const bound = new Rectangle(this.width, this.height, boundWidth, boundHeight);
if(this.isOutOfBounds(bound)) {
this.x -= this._velocityX;
this.y -= this._velocityY;
}
this._timeCount++;
const isFireReady = this._timeCount > this._interval;
if(isFireReady && input.getKey(' ')) {
const bullet = new Bullet(this.x, this.y);
this.spawnActor(bullet);
this._timeCount = 0;
}
}
}
class Bullet extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(0, 16, 16, 16));
const hitArea = new Rectangle(4, 0, 8, 16);
super(x, y, sprite, hitArea, ['playerBullet']);
this.speed = 6;
this.addEventListener('hit', (e) => {
if(e.target.hasTag('enemy')) { this.destroy(); }
});
}
update(gameInfo, input) {
this.y -= this.speed;
if(this.isOutOfBounds(gameInfo.screenRectangle)) {
this.destroy();
}
}
}
class Enemy extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(16, 0, 16, 16));
const hitArea = new Rectangle(0, 0, 16, 16);
super(x, y, sprite, hitArea, ['enemy']);
this.maxHp = 50;
this.currentHp = this.maxHp;
this.addEventListener('hit', (e) => {
if(e.target.hasTag('playerBullet')) {
this.currentHp--;
this.dispatchEvent('changehp', new GameEvent(this));
}
});
}
update(gameInfo, input) {
if(this.currentHp <= 0) {
this.destroy();
}
}
}
class EnemyHpBar extends Actor {
constructor(x, y, enemy) {
const hitArea = new Rectangle(0, 0, 0, 0);
super(x, y, hitArea);
this._width = 200;
this._height = 10;
this._innerWidth = this._width;
enemy.addEventListener('changehp', (e) => {
const maxHp = e.target.maxHp;
const hp = e.target.currentHp;
this._innerWidth = this._width * (hp / maxHp);
});
}
render(target) {
const context = target.getContext('2d');
context.strokeStyle = 'white';
context.fillStyle = 'white';
context.strokeRect(this.x, this.y, this._width, this._height);
context.fillRect(this.x, this.y, this._innerWidth, this._height);
}
}
class DanmakuStgMainScene extends Scene {
constructor(renderingTarget) {
super('メイン', 'black', renderingTarget);
const enemy = new Enemy(150, 100);
const hpBar = new EnemyHpBar(50, 20, enemy);
this.add(new Player(150, 200) );
this.add(enemy);
this.add(hpBar);
}
}
class DanmakuStgTitleScene extends Scene {
constructor(renderingTarget) {
super('タイトル', 'black', renderingTarget);
const title = new TextLabel(100, 200, '弾幕STG', 25);
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', 512, 384, 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();
});
⇒
ここまでのデモを開く前回までの記述をベースに、古都さんのシューティングゲーム記事その2のコード(途中まで)を組み合わせた上記のプログラム。。を元に、調整を始めます。
なおFighterクラスは、今回よりPlayerクラスに命名変更し、 前回調製した同クラス内のthis.sprite.rectangleの記述は「const rect =」で表記を簡略化してます。
加えてdanmaku.jsの最後の方に記述されている、danmakugameクラスでは、画面サイズを512×384(ピクセル)に設定⇒super('弾幕STG', 512, 384, 60);で表記した箇所...など、動作に影響ない数ヵ所を、自分の分かりやすい形に修正しました。
なぜか画面の幅を512pxより大きくすると、例えば650px↑とかにすると動作がつまずくんですよねぇ。 なんでかな。2進数、での計算限度を越えるんやろうか??? まぁイイや、512×384画面サイズで調整していきましょう。PC表示に合わせて幅4:高3の比率です。
プレイヤーの向く方にアクションを起こす
デモを開いてアクションを起こしてみると、上方向にしか行きません。 プレイヤーの向きが変わっても、アクションを働きかける方向が変わらないのを、まず何とかしてみましょうプレイヤーclass ⇒ dir「方向」の要素を付け加える
this._speed = 2;
this._velocityX = 0;
this._velocityY = 0;
this.walkCount = 0;
this._interval = 5;
this._timeCount = 0;
this._dir = 0;
this._dir = 0; というのを、プレーヤーの向きを現す数字として組み込んでみました。 上下左右斜め8方向に対応させる設計で、歩行をカスタマイズしていきます。
update(gameInfo, input) {
this._velocityX = 0;
this._velocityY = 0;
const rect = this.sprite.rectangle;
//矢印キーを押した時に、プレーヤーの向きを変える。
if(input.getKey('ArrowUp')) { rect.y = 96; this._dir = 0;} //上
if(input.getKey('ArrowRight')) { rect.y = 64; this._dir = 2;} //右
if(input.getKey('ArrowDown')) { rect.y = 0; this._dir = 4;} //下
if(input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 6; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 1;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 3;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 5;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 7;} //左上
//矢印キーを押してる間、歩行カウントをすすめて、向きの方角に移動する距離を求める
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {
this.walkCount += this._speed;
const sqrt_spead = this._speed / Math.sqrt(2); //斜め方向は、xとyの移動距離を2の平方根で割る。
switch (this._dir) {
case 0 : this._velocityY = -this._speed; break; //上方向へ this._dir = 0;のとき
case 1 : this._velocityY = -sqrt_spead; this._velocityX = sqrt_spead; break; //右上方向へ
case 2 : this._velocityX = this._speed; break; //右方向へ this._dir = 2;のとき
case 3 : this._velocityY = sqrt_spead; this._velocityX = sqrt_spead; break; //右下方向へ
case 4 : this._velocityY = this._speed; break; //下方向へ this._dir = 4;のとき
case 5 : this._velocityY = sqrt_spead; this._velocityX = -sqrt_spead; break; //左下方向へ
case 6 : this._velocityX = -this._speed; break; //左方向へ this._dir = 6;のとき
case 7 : this._velocityY = -sqrt_spead; this._velocityX = -sqrt_spead; break; //左上方向へ
default: break;
}
}
//移動を反映させる
this.x += this._velocityX;
this.y += this._velocityY;
このように、歩く挙動を調整しました。矢印キーに合わせて「this._dir=〜(プレイヤーの向き)」を設定し、何らかの矢印キーが押されている間、向きの方角にthis._spead分だけ移動するという仕組みです。 switch(プレーヤーの向き)の記述によって、移動する方角を振り分けています。
このように、プレイヤーの向き(this._dir)の値を活用することで、向きに合わせた動作を組み込めるようになりました。
(それと話は逸れますが、斜め移動で若干スピードを早く感じてたのも、斜めのベクトル距離を平方根...スピードをMath.sqrt(2)で割ったら、一定スピードになってくれました)
↓ では続いて、アクションを発生させる方向も考えてみましょう。
プレイヤーclass ⇒ if(isFireReady && input.getKey(' '))
this._timeCount++;
const isFireReady = this._timeCount > this._interval;
if(isFireReady && input.getKey(' ')) {
const bullet = new Bullet(this.x, this.y);
this.spawnActor(bullet);
this._timeCount = 0;
}
初期のプレーヤーアクションの記述、3段目の部分... const bullet = new Bullet(this.x, this.y);の部分に、this._dir要素も組み込めるよう追記。
const bullet = new Bullet(this.x, this.y, this._dir);
一方で、class Bulletのクラス設定側でも、this._dir要素を受け取れるよう記述を加えます。
class Bullet extends SpriteActor
class Bullet extends SpriteActor {
constructor(x, y, dir) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(0, 16, 16, 16));
const hitArea = new Rectangle(4, 0, 8, 16);
super(x, y, sprite, hitArea, ['playerBullet']);
this._speed = 6;
this._dir = dir;
this.addEventListener('hit', (e) => {
if(e.target.hasTag('enemy')) { this.destroy(); }
});
}
update(gameInfo, input) {
this.y -= this.speed;
if(this.isOutOfBounds(gameInfo.screenRectangle)) {
this.destroy();
}
}
}
Bullet の constructor(x, y)要素に dirを追加 ⇒ constructor(x, y, dir)
そしてdirを参照するための this._dir = dir; を追記しました。後は、Bulletの進む方向が this._dir の値によって変化する(switchする)ようupdate部分を調整してみます。
class Bullet ⇒ update(gameInfo, input)
update(gameInfo, input) {
const sqrt_spead = this._speed / Math.sqrt(2);
switch (this._dir) {
case 0 : this.y -= this._speed; break; //上方向
case 1 : this.y -= sqrt_spead; this.x += sqrt_spead break; //右上方向
case 2 : this.x += this._speed; break; //右方向
case 3 : this.y += sqrt_spead; this.x += sqrt_spead break; //右下方向
case 4 : this.y += this._speed; break; //下方向
case 5 : this.y += sqrt_spead; this.x -= sqrt_spead break; //左下方向
case 6 : this.x -= this._speed; break; //左方向
case 7 : this.y -= sqrt_spead; this.x -= sqrt_spead break; //左上方向
}
if(this.isOutOfBounds(gameInfo.screenRectangle)) {
this.destroy();
}
}
⇒
ここまでのデモを開くなんと、プレイヤーアクションが上下左右斜め8方向に反映されるようになりました。素晴らしい! ここまでは想定通りの動きです。
しかし斜め打ちで固定が難しいですね。
スペースキー(' ')を押してる間はプレイヤーの向きが変わらない設定にしますか。。
↓
プレイヤーclass ⇒ update(gameInfo, input) 内の修正
//矢印キーを押しただけの時(スペースが押されてない!時)に、プレーヤーの向きを変える。
if(!input.getKey(' ')) {
if(input.getKey('ArrowUp')) { rect.y = 96; this._dir = 0;} //上
if(input.getKey('ArrowRight')) { rect.y = 64; this._dir = 2;} //右
if(input.getKey('ArrowDown')) { rect.y = 0; this._dir = 4;} //下
if(input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 6; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 1;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 3;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 5;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 7;} //左上
}
⇒
ここまでのデモを開くよし向きが固定された。・・
そしてw(゚ロ゚)w 移動にバグが起こった。
しまった。。。
プレイヤー移動の記述に問題があるようです。矢印キーにかかわらず、向きの方向に進むのが固定されるからね。。 ....だんだん要素が増えてくるけど仕方ない。進む方向の要素も付け加えて修正します。。
プレイヤーclass ⇒ update(gameInfo, input) 内のさらなる修正
class Player extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 96, 32, 32));
const hitArea = new Rectangle(6, 4, 20, 28);
super(x, y, sprite, hitArea);
this._speed = 2;
this._velocityX = 0;
this._velocityY = 0;
this.walkCount = 0;
this._interval = 5;
this._timeCount = 0;
this._dir = 0; //プレイヤーの向き
this._dirGo = 0; //進む方角
}
update(gameInfo, input) {
this._velocityX = 0;
this._velocityY = 0;
const rect = this.sprite.rectangle;
const sqrt_spead = this._speed / Math.sqrt(2); //斜め移動は、xとyの移動距離を2の平方根で割る。
//矢印キーを押しただけの時に、プレーヤーの向きを変える。
if(!input.getKey(' ')) {
if(input.getKey('ArrowUp')) { rect.y = 96; this._dir = 0;} //上
if(input.getKey('ArrowRight')) { rect.y = 64; this._dir = 2;} //右
if(input.getKey('ArrowDown')) { rect.y = 0; this._dir = 4;} //下
if(input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 6; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 1;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 3;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 5;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 7;} //左上
}
//進む方角の設定
if(input.getKey('ArrowUp')) { this._dirGo = 0;} //上
if(input.getKey('ArrowRight')) { this._dirGo = 2;} //右
if(input.getKey('ArrowDown')) { this._dirGo = 4;} //下
if(input.getKey('ArrowLeft')) { this._dirGo = 6; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { this._dirGo = 1;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { this._dirGo = 3;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { this._dirGo = 5;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { this._dirGo = 7;} //左上
//矢印キーを押してる間、歩行カウントをすすめて、進む方角に応じた距離を計算
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {
this.walkCount += this._speed;
switch (this._dirGo) {
case 0 : this._velocityY = -this._speed; break; //上方向へ this._dir = 0;のとき
case 1 : this._velocityY = -sqrt_spead; this._velocityX = sqrt_spead; break; //右上方向へ=1
case 2 : this._velocityX = this._speed; break; //右方向へ this._dir = 2;のとき
case 3 : this._velocityY = sqrt_spead; this._velocityX = sqrt_spead; break; //右下方向へ=3
case 4 : this._velocityY = this._speed; break; //下方向へ this._dir = 4;のとき
case 5 : this._velocityY = sqrt_spead; this._velocityX = -sqrt_spead; break; //左下方向へ=5
case 6 : this._velocityX = -this._speed; break; //左方向へ this._dir = 6;のとき
case 7 : this._velocityY = -sqrt_spead; this._velocityX = -sqrt_spead; break; //左上方向へ=7
default: break;
}
}
//移動を反映させる
this.x += this._velocityX;
this.y += this._velocityY;
⇒
ここまでのデモを開くOK! プレイヤーの向きと進む方向を、それぞれ違う要素に独立させました。これで横歩きも後ろ歩きもできるようになったよ。すごいね! 将来的に(混乱状態?)とか導入するときも役に立ちそう?
ぶっ通しでコードと格闘、プログラムの勉強(switch文)など含めて丸2日。。。いったん休憩を入れましょう。
お茶でものみなされ。
その間、出来たやつで遊ぶもよし。わーい、弾がいっぱいでるー。
いんふぃにてぃ〜ばれっと弾なの〜〜〜〜♪ヽ(。◕ v ◕。)ノ~*:・'゚☆
。。。。
さて、攻撃弾が8方向に打てるようになっちゃいましたが、当初の目的を忘れてはなりません。ゴールはNPCとの会話。 必要なのはプレイヤーが目の前の人物に話しかけるアクションですよ。
ぼちぼち作り始めていきましょう。
目の前の人物にアクションを起こす新たな矩形を定義する
ではclass Bulletの記述を参考に、他者に働きかけるアクション...を発生させる矩形class PlayerActionを定義してみます。 class Playerの下に、新たに書き起こしてみましょう。class PlayerAction extends Actorを作成
class PlayerAction extends Actor {
constructor(x, y, dir) {
const hitArea = new Rectangle(0, 0, 32, 32);
super(x, y, hitArea, ['playerBullet']);
}
render(target) {//オーバーライドしたrenderメソッドで、アクション範囲を可視化してみる
const context = target.getContext('2d');
const rect = this.hitArea;
context.fillStyle = "rgba(255,255,255,0.7)"; //半透明の白で塗りつぶす
context.fillRect(
rect.x, rect.y,
rect.width, rect.height);
}
update(gameInfo, input) {
this.destroy();
}
}
解説。画像は必要ない。矩形だけあればいいので、SpriteActorではなくActorからの拡張クラスにします。
renderメソッドは、enjine.jsに記述されてあるSpriteActorの欄を参考に、描き足しました。アクション範囲を確認するためです。
判定は一瞬でいいので、次のupdate(gameInfo, input)で、自身を破棄します。
プレイヤーclass ⇒ if(isFireReady && input.getKey(' ')) の修正
続いてPlayerクラスの方は、スペースキーを押した時の動作(Bulletを発生させる部分)を、PlayerActionに置き換えます。
this._timeCount++;
const isFireReady = this._timeCount > this._interval;
if(isFireReady && input.getKey(' ')) {
const action = new PlayerAction(this.x, this.y, this._dir);
this.spawnActor(action);
this._timeCount = 0;
}
⇒
ここまでのデモを開くうん、いい感じですね。
これは単発で良さそうので、キーを押した時のみ⇒ input.getKeyDown(' ')のアクションにして、 あとはプレイヤーの向きによって、アクション範囲を考えたいところ。
...なんかthis._dirは新たなアクション要素に引き継がなくて良さそうですね。プレイヤー側で、向きに応じた挙動を調整してみましょうか。。
上向き時(this._dir=0)は、高さ分(32px)上にくるので、y座標が-32となる。
右向き時(this._dir=2)は、幅分(32px)右にくるので、x座標が+32となる。
下向き時(this._dir=4)は、高さ分(32px)下にくるので、y座標が+32。。。
というように、8方向をプログラムしていきましょう。
プレイヤーclass ⇒ const action = new PlayerAction(this.x, this.y);の修正
if(input.getKeyDown(' ')) {
switch(this._dir) {
case 0 : this.spawnActor( new PlayerAction(this.x, this.y - rect.height) ); break; //上
case 1 : this.spawnActor( new PlayerAction(this.x + rect.width / Math.sqrt(2), this.y - rect.height / Math.sqrt(2)) ); break; //右上
case 2 : this.spawnActor( new PlayerAction(this.x + rect.width, this.y) ); break; //右
case 3 : this.spawnActor( new PlayerAction(this.x + rect.width / Math.sqrt(2), this.y + rect.height / Math.sqrt(2)) ); break; //右下
case 4 : this.spawnActor( new PlayerAction(this.x, this.y + rect.height) ); break; //下
case 5 : this.spawnActor( new PlayerAction(this.x - rect.width / Math.sqrt(2), this.y + rect.height / Math.sqrt(2)) ); break; //左下
case 6 : this.spawnActor( new PlayerAction(this.x - rect.width, this.y) ); break; //左
case 7 : this.spawnActor( new PlayerAction(this.x - rect.width / Math.sqrt(2), this.y - rect.height / Math.sqrt(2)) ); break; //左上
}
}
あとは、PlayerActionクラスの、必要なかったdir要素を削除しておきます。 また、タグは[playerbullet](敵を攻撃するタグ)になってる箇所を[playerAction](アクションを起こすタグ)に変更しておきます。
class PlayerAction extends Actor {
constructor(x, y) {
const hitArea = new Rectangle(0, 0, 32, 32);
super(x, y, hitArea, ['playerAction']);
}
⇒
ここまでのデモを開くよし、想定通りの動きです! お見事。(タグを変更したから敵のHPは削れない...)
これでアクション動作が分かりやすい形で表現できました。
プレイヤー側の調整は終わりましたので、ようやくNPCキャラの作成に移れます。 かなり長くなったので、続きは次のページに。
補足と修正点 〜 三角関数でシンプルに表記
。。。後日、古都さんより改善案を頂きました。dirに角度(ラジアンで!)を突っ込んでおいて、 Math.sin(dir)とかMath.cos(dir)とかで三角関数が使えます。Math.PIっていう定数もあるのでπはこれを使えます。 プログラム的にはこの方がシンプルになるとは思います。たぶん。
だそうです。なるほど(o _ o。)
角度のラジアン表記については、解説サイトを見て学べる。
⇒ http://yarinaosinosansu.nomaki.jp/radian/index.html
this._dir(方向)を現す値を、角度(ラジアン)の表記にすれば、8方向にActorを出す記述をもっとシンプルにできるようです。 仕上げに、三角関数での記載を実践してみましょう。
this._dir と this._dirGoの値をラジアン表記に
class Player extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 96, 32, 32));
const hitArea = new Rectangle(6, 4, 20, 28);
super(x, y, sprite, hitArea);
this._speed = 2;
this._velocityX = 0;
this._velocityY = 0;
this.walkCount = 0;
this._interval = 5;
this._timeCount = 0;
this._dir = 90 * Math.PI/180; //プレイヤーの向き...角度(ラジアン)の単位。。角度°にπ/180をかけた値
this._dirGo = 90 * Math.PI/180; //進む方角...初期は上向き
}
update(gameInfo, input) {
this._velocityX = 0;
this._velocityY = 0;
const rect = this.sprite.rectangle;
//矢印キーを押しただけの時に、プレーヤーの向きを変える。
if(!input.getKey(' ')) {
if(input.getKey('ArrowUp')) { rect.y = 96; this._dir = 90 * Math.PI/180;} //上
if(input.getKey('ArrowRight')) { rect.y = 64; this._dir = 0 * Math.PI/180;} //右
if(input.getKey('ArrowDown')) { rect.y = 0; this._dir = -90 * Math.PI/180;} //下
if(input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 180 * Math.PI/180; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 45 * Math.PI/180;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = -45 * Math.PI/180;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = -135 * Math.PI/180;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 135 * Math.PI/180;} //左上
}
//進む方角の設定
if(input.getKey('ArrowUp')) { this._dirGo = 90 * Math.PI/180;} //上
if(input.getKey('ArrowRight')) { this._dirGo = 0 * Math.PI/180;} //右
if(input.getKey('ArrowDown')) { this._dirGo = -90 * Math.PI/180;} //下
if(input.getKey('ArrowLeft')) { this._dirGo = 180 * Math.PI/180; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { this._dirGo = 45 * Math.PI/180;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { this._dirGo = -45 * Math.PI/180;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { this._dirGo = -135 * Math.PI/180;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { this._dirGo = 135 * Math.PI/180;} //左上
角度に * Math.PI/180 を掛けると、ラジアン表記になります。右向きを0°、上向きを90°、左を180°、下が270(−90)°の流れにした場合、それぞれの角度に* Math.PI/180を付記した値がラジアン表記です。
↓ このラジアン表記を用いて、x軸の移動距離をMath.cos(ラジアン値)、y軸の移動距離をMath.sin(ラジアン値)、といった三角関数で簡単に表現することができるようです。
x,y軸の移動距離をラジアン表記を用いた三角関数で表記
//矢印キーを押してる間、歩行カウントをすすめて、進む方角に応じた移動距離を計算
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {
this.walkCount += this._speed;
this._velocityX += this._speed * Math.cos(this._dirGo);
this._velocityY += -this._speed * Math.sin(this._dirGo);
}
プレイヤーの移動表記、8方向の移動パターンで8行以上に及んだコードが、たった2行で済みました。
ラジアンを三角関数と合わせて使うと、一括で計算OK、x軸はプラス方向に、y軸はマイナス方向に、計算するみたいです。そして三角関数とラジアンを用いたコードは、プレイヤーアクションの方向にも活用できました。
アクションの方向も三角関数で表記
//スペースキーでアクション発生
if(input.getKeyDown(' ')) {
const actorX = this.x + rect.width * Math.cos(this._dir);
const actorY = this.y - rect.height * Math.sin(this._dir);
this.spawnActor( new PlayerAction(actorX, actorY) );
}
//zキーで射撃
this._timeCount++;
const isFireReady = this._timeCount > this._interval;
if(isFireReady && input.getKey('z')) {
const bullet = new Bullet(this.x, this.y, this._dir);
this.spawnActor(bullet);
this._timeCount = 0;
}
class Bullet extends SpriteActor {
constructor(x, y, dir) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(0, 16, 16, 16));
const hitArea = new Rectangle(4, 0, 8, 16);
super(x, y, sprite, hitArea, ['playerBullet']);
this._speed = 6;
this._dir = dir;
this.addEventListener('hit', (e) => {
if(e.target.hasTag('enemy')) { this.destroy(); }
});
}
update(gameInfo, input) {
this.y -= this._speed * Math.sin(this._dir);
this.x += this._speed * Math.cos(this._dir);
if(this.isOutOfBounds(gameInfo.screenRectangle)) {
this.destroy();
}
}
}
⇒
ここまでのデモを開くプレイヤーアクションと、z射撃の両方、記載しています。 該当部分、ものすっごく行数の削減になってます。約1/4です。 しかも16方向とか、32方向とか、果ては360方向までこの記述で対応できそうです。すさまじいですね。
方向を参照する時は、三角関数とラジアン表記が適する。というのを学びました。
ことさん、ありがとうございます><
次回、NPCとの会話(メッセージ編)につづきますよ。
⇒ JavaScriptでゲーム作り「4:NPCと会話する」
すぺしゃるさんくす
https://sbfl.net/古都さん
JavaScriptで作る弾幕STGの基礎(フレームワーク)を使わせていただいてます。感謝!

ぴぽやさんもありがとう、キャラチップをお借りしています。
尚、ここまでの差分ファイルは、danmaku.jsの修正のみです。
danmaku.js修正後のソースコード
'use strict';
class TextLabel extends Actor {
constructor(x, y, text, size) {
const hitArea = new Rectangle(0, 0, 0, 0);
super(x, y, hitArea);
this.text = text;
this.size = size;
}
render(target) {
const context = target.getContext('2d');
context.font = `${this.size}px sans-serif`;
context.fillStyle = 'white';
context.fillText(this.text, this.x, this.y);
}
}
class Player extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('player'), new Rectangle(32, 96, 32, 32));
const hitArea = new Rectangle(6, 4, 20, 28);
super(x, y, sprite, hitArea);
this._speed = 2;
this._velocityX = 0;
this._velocityY = 0;
this.walkCount = 0;
this._interval = 5;
this._timeCount = 0;
this._dir = 90 * Math.PI/180; //プレイヤーの向き...角度(ラジアン)の単位。。角度°にπ/180をかけた値
this._dirGo = 90 * Math.PI/180; //進む方角...初期は上向き
}
update(gameInfo, input) {
this._velocityX = 0;
this._velocityY = 0;
const rect = this.sprite.rectangle;
//矢印キーを押しただけの時に、プレーヤーの向きを変える。
if(!input.getKey(' ')) {
if(input.getKey('ArrowUp')) { rect.y = 96; this._dir = 90 * Math.PI/180;} //上
if(input.getKey('ArrowRight')) { rect.y = 64; this._dir = 0 * Math.PI/180;} //右
if(input.getKey('ArrowDown')) { rect.y = 0; this._dir = -90 * Math.PI/180;} //下
if(input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 180 * Math.PI/180; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 45 * Math.PI/180;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = -45 * Math.PI/180;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = -135 * Math.PI/180;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 135 * Math.PI/180;} //左上
}
//進む方角の設定
if(input.getKey('ArrowUp')) { this._dirGo = 90 * Math.PI/180;} //上
if(input.getKey('ArrowRight')) { this._dirGo = 0 * Math.PI/180;} //右
if(input.getKey('ArrowDown')) { this._dirGo = -90 * Math.PI/180;} //下
if(input.getKey('ArrowLeft')) { this._dirGo = 180 * Math.PI/180; } //左
if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { this._dirGo = 45 * Math.PI/180;} //右上
if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { this._dirGo = -45 * Math.PI/180;} //右下
if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { this._dirGo = -135 * Math.PI/180;} //左下
if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { this._dirGo = 135 * Math.PI/180;} //左上
//矢印キーを押してる間、歩行カウントをすすめて、進む方角に応じた移動距離を計算
if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {
this.walkCount += this._speed;
this._velocityX += this._speed * Math.cos(this._dirGo);
this._velocityY += -this._speed * Math.sin(this._dirGo);
}
//歩行カウントが一定以上に達した時、歩くモーションを変化
if(this.walkCount > 0) { rect.x = 64; }
if(this.walkCount > 16) { rect.x = 32; }
if(this.walkCount > 32) { rect.x = 0; }
if(this.walkCount > 48) { rect.x = 32; this.walkCount = -15; }
//立ち止まった時に直立姿勢
if(input.getKeyUp('ArrowUp') || input.getKeyUp('ArrowDown') || input.getKeyUp('ArrowRight') || input.getKeyUp('ArrowLeft')) {rect.x = 32; this.walkCount = 0;}
//移動を反映させる
this.x += this._velocityX;
this.y += this._velocityY;
const boundWidth = gameInfo.screenRectangle.width - this.width;
const boundHeight = gameInfo.screenRectangle.height - this.height;
const bound = new Rectangle(this.width, this.height, boundWidth, boundHeight);
if(this.isOutOfBounds(bound)) {
this.x -= this._velocityX;
this.y -= this._velocityY;
}
//スペースキーでアクション発生
if(input.getKeyDown(' ')) {
const actorX = this.x + rect.width * Math.cos(this._dir);
const actorY = this.y - rect.height * Math.sin(this._dir);
this.spawnActor( new PlayerAction(actorX, actorY) );
}
//zキーで射撃
this._timeCount++;
const isFireReady = this._timeCount > this._interval;
if(isFireReady && input.getKey('z')) {
const bullet = new Bullet(this.x, this.y, this._dir);
this.spawnActor(bullet);
this._timeCount = 0;
}
}
}
class PlayerAction extends Actor {
constructor(x, y) {
const hitArea = new Rectangle(0, 0, 32, 32);
super(x, y, hitArea, ['playerAction']);
}
render(target) {//オーバーライドしたrenderメソッドで、アクション範囲を可視化してみる
const context = target.getContext('2d');
const rect = this.hitArea;
context.fillStyle = "rgba(255,255,255,0.7)"; //半透明の白で塗りつぶす
context.fillRect(
rect.x, rect.y,
rect.width, rect.height);
}
update(gameInfo, input) {
this.destroy();
}
}
class Bullet extends SpriteActor {
constructor(x, y, dir) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(0, 16, 16, 16));
const hitArea = new Rectangle(4, 0, 8, 16);
super(x, y, sprite, hitArea, ['playerBullet']);
this._speed = 6;
this._dir = dir;
this.addEventListener('hit', (e) => {
if(e.target.hasTag('enemy')) { this.destroy(); }
});
}
update(gameInfo, input) {
this.y -= this._speed * Math.sin(this._dir);
this.x += this._speed * Math.cos(this._dir);
if(this.isOutOfBounds(gameInfo.screenRectangle)) {
this.destroy();
}
}
}
class Enemy extends SpriteActor {
constructor(x, y) {
const sprite = new Sprite(assets.get('sprite'), new Rectangle(16, 0, 16, 16));
const hitArea = new Rectangle(0, 0, 16, 16);
super(x, y, sprite, hitArea, ['enemy']);
this.maxHp = 50;
this.currentHp = this.maxHp;
this.addEventListener('hit', (e) => {
if(e.target.hasTag('playerBullet')) {
this.currentHp--;
this.dispatchEvent('changehp', new GameEvent(this));
}
});
}
update(gameInfo, input) {
if(this.currentHp <= 0) {
this.destroy();
}
}
}
class EnemyHpBar extends Actor {
constructor(x, y, enemy) {
const hitArea = new Rectangle(0, 0, 0, 0);
super(x, y, hitArea);
this._width = 200;
this._height = 10;
this._innerWidth = this._width;
enemy.addEventListener('changehp', (e) => {
const maxHp = e.target.maxHp;
const hp = e.target.currentHp;
this._innerWidth = this._width * (hp / maxHp);
});
}
render(target) {
const context = target.getContext('2d');
context.strokeStyle = 'white';
context.fillStyle = 'white';
context.strokeRect(this.x, this.y, this._width, this._height);
context.fillRect(this.x, this.y, this._innerWidth, this._height);
}
}
class DanmakuStgMainScene extends Scene {
constructor(renderingTarget) {
super('メイン', 'black', renderingTarget);
const enemy = new Enemy(150, 100);
const hpBar = new EnemyHpBar(50, 20, enemy);
this.add(new Player(150, 200) );
this.add(enemy);
this.add(hpBar);
}
}
class DanmakuStgTitleScene extends Scene {
constructor(renderingTarget) {
super('タイトル', 'black', renderingTarget);
const title = new TextLabel(100, 200, '弾幕STG', 25);
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', 512, 384, 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();
});