ゲームエンジンやライブラリを使わずに、HTMLとJavaScriptだけでゲームを作ります。
以下のような人のためにあります。
ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは面倒くさい。
ある程度プログラミングの知識がある人を対象にしています。
この記事で作るもの
敵キャラである水色モンスターを導入します。
水色のモンスターがどこまでも追いかけてきます。捕まってもゲームオーパになることはありませんので、色々逃げてみてください。ブロックをレーザーで消せます。ブロックはしばらくすると自動修復します。
操作方法:
s … ゲーム開始
x … 左掘る、c … 右掘る
j … 上移動、m … 下移動、j … 左移動、l … 右移動
r … ゲームリセット
先に読んでおく記事
ソースコード
このデモのファイル構成は下記のとおりです。
.
├── Block.js
├── Chara.js
├── Enemy.js
├── World.js
├── index.html
├── index.js
└── spritesheet.png
前回からはEnemy.jsファイルが増えています。今回は主にEnemy.jsを解説します。
import {drawSprite} from "./index.js"
import {Chara} from "./Chara.js"
const STAY_HOLE_SEC = 3;
const State = {
STOP: 'STOP',
MOVE_LEFT : 'MOVE_LEFT',
MOVE_RIGHT: 'MOVE_RIGHT',
MOVE_UP : 'MOVE_UP',
MOVE_DOWN : 'MOVE_DOWN',
FALL: 'FALL',
STOP_LADDER: 'STOP_LADDER',
MOVE_BAR_LEFT : 'MOVE_BAR_LEFT',
MOVE_BAR_RIGHT : 'MOVE_BAR_RIGHT',
STOP_BAR : 'STOP_BAR',
IN_HOLE: 'IN_HOLE',
UP_HOLE: 'UP_HOLE',
}
const anime_table = {
STOP: {move_count: 0, frames: [50,51], frame_interval: 60},
MOVE_LEFT: {move_count: 8, frames: [50,52,53], frame_interval: 3},
MOVE_RIGHT: { move_count: 8, frames: [50,52,53], frame_interval: 3},
MOVE_UP: {move_count: 8, frames: [55,56], frame_interval: -1},
MOVE_DOWN: {move_count: 8, frames: [55,56], frame_interval: -1},
FALL: {move_count: 8, frames: [49,56], frame_interval: 2},
STOP_LADDER: {move_count: 8, frames: [55,56], frame_interval: -1},
MOVE_BAR_LEFT: {move_count: 8, frames: [48,49], frame_interval: -1},
MOVE_BAR_RIGHT: {move_count: 8, frames: [48,49], frame_interval: -1},
STOP_BAR: {move_count: 8, frames: [48], frame_interval: -1},
IN_HOLE: {move_count: 30*STAY_HOLE_SEC, frames: [50], frame_interval: 1},
UP_HOLE: {move_count: 8, frames: [50,52], frame_interval: 1},
};
export class Enemy extends Chara {
constructor(x,y, map, chara) {
super(x,y,map);
this.sprite = 50;
this.anime_table = anime_table;
this.chara = chara;
this.action_interval = 1;
this.action_wait_count = 0;
}
wait_for_action() {
if (this.action_wait_count < this.action_interval) {
this.action_wait_count++;
return true;
}
this.action_wait_count = 0;
return false;
}
update() {
if (this.wait_for_action())
return;
let action_func = `action_${this.state.toLowerCase()}`;
if (!this[action_func]()) {
if (this.can_fall()) {
this.change_state(State.FALL);
} else {
this.think_next_action();
}
}
this.anime_update();
}
think_next_action() {
let chara = this.chara;
if (chara.y == this.y) {
if (chara.x < this.x) {
this.check_move_left();
} else {
this.check_move_right();
}
return;
}
if (chara.y > this.y) {
if (this.check_move_down()) return;
} else {
if (this.check_move_up()) return;
}
switch(this.state) {
case State.MOVE_LEFT:
if (!this.check_move_left()) this.check_move_right();
break;
case State.MOVE_RIGHT:
if (!this.check_move_right()) this.check_move_left();
break;
default:
if (chara.x < this.x) {
this.check_move_left();
} else {
this.check_move_right();
}
}
}
check_move_down() {
if (this.map.isDigHole(this.x, this.y))
return false;
return super.check_move_down();
}
can_fall() {
if (this.map.isDigHole(this.x, this.y)) {
this.change_state(State.IN_HOLE);
return false;
}
return super.can_fall()
}
action_in_hole() {
let ret = this.count_move(0, 0);
if (!ret) {
// 動作完了したら穴を上る
this.change_state(State.UP_HOLE);
}
return true;
}
action_up_hole() {
let ret = this.count_move(0, -1);
if (!ret) {
// 動作完了したらCharaの位置を確認して移動する
if (this.chara.x < this.x) {
this.change_state(State.MOVE_LEFT);
} else {
this.change_state(State.MOVE_RIGHT);
}
}
return true;
}
}
CharaとEnemyクラスの継承関係
水色のモンスターはclass Enemyで実現しています。Enemyがゲーム内のフィールドでできる行動はプレイヤーとほぼ同じです。
違うのは、「プレイヤーの掘った穴は通り抜けない(ハマる)」という点だけです。
実装内容は class Charaとほぼ同じです。この時にclass Charaをまるごとコピーしてclass Enemyを作って変更するという手もあります。
が、同じ実装コードはソースコードの管理上、できるだけ一箇所だけにしたいところです。そこでオブジェクト指向言語JavaScriptの継承機能を使います。
継承の実装方法は extends を使います。
class Enemy extends Chara {
・・・
継承関係を図にすると以下のようになります。
EnemyはCharaを継承しているので、Charaの機能はそのまま使用できます。その上で、Charaとの差分の部分だけを追加で実装すれば良いのです。
本来なら、設計的にはプレイヤーのCharaクラスとEnemyクラスの共通部分だけを抽出して、別の親クラスを作って継承関係とすべきです。というのもCharaクラスにはEnemyクラスに必要のない、プレイヤーのキー入力用メソッドが定義されているからです。
とはいえ、このゲームはこれ以上登場キャラは増えないし、厳密にクラス分けして管理するメリットもないので、このままとしています。
プレイヤーを自動追尾するロジック
今回のデモで作ったフィールドだと、水色モンスターはプレイヤーをどこまでも追いかけていけます。なかなか悪くない動きだと思います。
実際にやっているのは、動きながらプレイヤーの方向に近づくように動作の状態を決めることです。
下記の関数内で、モンスターが次にどの方向に動くかを判断しています。
think_next_action() {
・・・
}
モンスターの思考ルーチンは、下記のとおりシンプルなものです。シンプル過ぎて拍子抜けするかもしれません。
・プレイヤーと同じ高さの場合は、プレイヤーの方向に動きます。
・プレイヤーと違う高さの場合、プレイヤーのいる方の上下に移動できるかチェックし、移動できる場合は移動します。
・それ以外の場合は、今の方向に行き止まりになるまで動き、行き止まりに当たったら反対方向に動きます
このアルゴリズムだと、プレイヤーと同じ高さで壁を挟んで対峙した場合は、水色モンスターは壁の向こう側に停滞する感じになると思います。
やろうと思えば、もっと賢くは作れるとは思いますが、ゲームが難しくなる可能性があります。いろいろなマップで試してみて、違和感がでるようなら調整すれば良いかなと思います。
穴に落ちて抜け出す動作
敵キャラがプレイヤーと動作が違う部分は、「プレイヤーの掘った穴は通り抜けない(ハマる)」という点です。穴にハマった後、しばらく時間が経って自力で脱出します。
以下の2つの状態を作る必要があります。
・穴にハマっている状態 (State.IN_HOLE)
・穴から上がる状態 (State.UP_HOLE)
状態遷移は下記の図のようになります。
実装では、これを実現するための状態を追加し、対応するアニメーションも定義します。
const State = {
・・・
IN_HOLE: 'IN_HOLE',
UP_HOLE: 'UP_HOLE',
}
const anime_table = {
・・・
IN_HOLE: {move_count: 30*STAY_HOLE_SEC, frames: [50], frame_interval: 1},
UP_HOLE: {move_count: 8, frames: [50,52], frame_interval: 1},
};
それぞれの状態の動作関数は下記のとおりです。
action_in_hole() {
let ret = this.count_move(0, 0);
if (!ret) {
// 動作完了したら穴を上る
this.change_state(State.UP_HOLE);
}
return true; // can_fall()チェックせずに直接次の状態のactionに移るためtrueとする
}
action_up_hole() {
let ret = this.count_move(0, -1);
if (!ret) {
// 動作完了したらCharaの位置を確認して移動する
if (this.chara.x < this.x) {
this.change_state(State.MOVE_LEFT);
} else {
this.change_state(State.MOVE_RIGHT);
}
}
return true; // can_fall()チェックせずに直接次の状態のactionに移るためtrueとする
}
次回は
さて、ついに敵キャラも導入しました。あと、残る要素は「金塊」です。
次回は、プレイヤーがフィールド上にある金塊を集めて、ゲームクリアするところまでを実装します。敵キャラに触れられたらゲームオーバーになる要素も追加します。
次回はいよいよゲームの完成です。
コメント