「ゲーム作りって大変そう。RPGのWebアプリを作ってみたいけどどうやるの?」そんな疑問に答えていきます!前回はマップをブラウザに表示するとこまででしたが、今回は主人公の表示・操作・アニメーション、それにマップがスクロールするところまで実際のJavaScriptの書き方について解説していきます。
前回の記事を見逃した方は「JavaScriptでRPGを作ろう!自作したマップをブラウザに表示しよう!」をご覧ください。
今回の記事の完成版はこちらで確認できます。[デモ]
HTML解説
前回のHTMLと変わらないので割愛します。前回の記事を見逃した方は「JavaScriptでRPGを作ろう!自作したマップをブラウザに表示しよう!」をご覧ください。
キャラチップの準備
画像素材を無料配布している超大手サイトの「ぴぽや倉庫」さんから、サイズ32×32ピクセルのキャラクタチップをダウンロードしましょう。
ダウンロードしてきたら、dataフォルダにキャラクタチップファイルを置きます。
/(root)
┣━ /data
┃ ┣━ village.js <- JavaScriptマップファイル
┃ ┣━ CastleTown-C-1.png <- マップチップ 1つ目
┃ ┣━ pipo-map001_at-sabaku.png <- マップチップ 2つ目
┃ ┣━ :
┃ ┗━ pipo-charachip029.png <- キャラクタチップ [今回用意したもの]
┣━ index.html
┣━ main.js
┗━ p5.tiledmap.js
JavaScript解説
変数・定数定義
const IMG_CHAR_W = 32; // キャラクタ幅
const IMG_CHAR_H = 32; // キャラクタ高さ
// インスタンス
var pc; // プレイヤー
キャラクタの幅・高さを定数定義しておきます。また、プレイヤーキャラクタのインスタンスも定義しておきましょう。
Characterクラス
Characterクラスは初期化とキャラクタのアニメーションだけ定義しておきます。後述するPCクラスや次回の記事で取り上げる予定のNPCクラスなどから継承するために利用します。
コンストラクタ
////////////////////////////////////////////////////////
// 初期化
constructor(img, d = 0) {
this.img = img; // キャラクタチップ
this.direction = d; // 向き
}
this.img | キャラクタチップを格納する。 |
this.direction | キャラクタの向きを格納する(0->正面 1->右向き 2->左向き 3->背面)。 |
キャラクタ描画(drawCharacter)メソッド
////////////////////////////////////////////////////////
// キャラクタ描画
drawCharacter(cx, cy) {
// アニメーション
var dx = floor(frameCount/15)%4;
if(dx == 3) dx = 1; // 0->1->2->1
imageMode(CENTER);
image(this.img,
cx, cy,
IMG_CHAR_W, IMG_CHAR_H,
dx * IMG_CHAR_W, this.direction * IMG_CHAR_H,
IMG_CHAR_W, IMG_CHAR_H);
}
メソッドの仮引数は次の通りです。
仮引数:cx | キャラクタの描画位置(X座標) |
仮引数:cy | キャラクタの描画位置(Y座標) |
4~6行目ではアニメーションを計算しています。p5.jsでは60FPS(1秒間に60フレーム描画)なので、frameCountを15で割って(4等分)描画する画像を割り出しています。frameCountには現在のフレームカウントが格納されています。
8行目では描画する画像の位置を画像の中央に指定しています。
9~13行目では実際に画像を描画しています。
第1引数:img | 描画する画像を指定する。 |
第2引数:dx | 描画X座標を指定する。 |
第3引数:dy | 描画Y座標を指定する。 |
第4引数:dWidth | 描画幅を指定する。 |
第5引数:dHeight | 描画高さを指定する。 |
第6引数:sx | 描画する画像のサブセクションのX座標を指定する。 |
第7引数:sy | 描画する画像のサブセクションのY座標を指定する。 |
第8引数:sWidth | 描画する画像のサブセクションの幅を指定する。 |
第9引数:sHeight | 描画する画像のサブセクションの高さを指定する。 |
PlayerCharacterクラス
前述したCharacterクラスを継承して定義します。
コンストラクタ
////////////////////////////////////////////////////////
// 初期化
constructor(img) {
super(img);
}
親クラスのコンストラクタを呼び出します。
キャラクタ描画(draw)メソッド
////////////////////////////////////////////////////////
// キャラクタ描画
draw() {
// 描画
this.drawCharacter(width / 2, height / 2);
}
キャラクタを描画するメソッドです。親クラスのdrawCharacter()メソッドを呼び出します。プレイヤーは画面の中心に描画したいので、引数にはwidth/2とheight/2を指定します。
width | キャンバスの幅が格納されている。 |
height | キャンバスの高さが格納されている。 |
インスタンスの作成
// キャラクタクラス
pc = new PlayerCharacter(loadImage("data/pipo-charachip029.png"));
preload()関数でプレイヤーキャラクタクラスのインスタンスを作成します。
プレイヤーキャラクタの描画
// プレイヤー描画
pc.draw();
draw()関数でpc.draw()メソッドを呼び出して、プレイヤーキャラクタを描画します。
途中経過をデモで確認
ここまでのコードでプレイヤーが画面中央に表示されるようになりました。デモで確認してみてください。[デモ]
PCの向きとマップのスクロール
続いてプレイヤーキャラクタの向きとスクロールについて実装を加えていきます。
PlayerCharacterクラスの改造
this.x;
this.y;
PlayerCharacterクラスのコンストラクタにメンバ変数を定義します。
ここから先は、PlayerCharacterクラスのdraw()メソッドに追記していく処理の解説です。
// 歩く速さ
let walkSpeed = 0.05;
歩く速さを定義します。1フレームあたり0.05なので1秒間で3マス進ませます。
this.x = round(x*100)/100;
this.y = round(y*100)/100;
xとyにはプレイヤーの座標(マス数)が格納されています。ここでは小数点第3位以降を切り捨てた値を、this.xとthis.yに格納します。
// 向き決定
if (this.x%1 == 0 && this.y%1 == 0 && keyIsPressed) {
if(keyCode == LEFT_ARROW) {
this.direction = 1;
x -= walkSpeed;
}
if(keyCode == RIGHT_ARROW) {
this.direction = 2;
x += walkSpeed;
}
if(keyCode == UP_ARROW) {
this.direction = 3;
y -= walkSpeed;
}
if(keyCode == DOWN_ARROW) {
this.direction = 0;
y += walkSpeed;
}
}
「this.x%1==0」はX方向への移動をしていないことを調べています。「this.y%1==0」も同様にY方向を調べています。keyIsPressedはキー押下されているかを調べています。つまり移動していない時(マスの中心にいる状態)、かつキー押下された場合、このifブロックに入る訳です。
矢印キーが押下された時、this.directionにプレイヤーの向きをセットし移動を開始します。
// 1マス進める
if (this.x%1 != 0) {
if(this.direction == 1) x -= walkSpeed;
if(this.direction == 2) x += walkSpeed;
}
if (this.y%1 != 0) {
if(this.direction == 3) y -= walkSpeed;
if(this.direction == 0) y += walkSpeed;
}
移動中の場合はこの処理に入ります。「this.x%1!=0」はX方向に移動中かを調べています。「this.y%1!=0」も同様にY方向を調べています。1マス進むまでwalkSpeed分移動を進めます。
完成ソースコード
以上で今回の解説はおわりです。完成したソースをブラウザで確認してみましょう。ブラウザで動作確認するにはWebサーバー環境である必要があります。あるいはローカルWebサーバーを立ち上げて確認してみてください。
フォルダ構成
/(root)
┣━ /data
┃ ┣━ village.js <- JavaScriptマップファイル
┃ ┣━ CastleTown-C-1.png <- マップチップ 1つ目
┃ ┣━ pipo-map001_at-sabaku.png <- マップチップ 2つ目
┃ ┣━ :
┃ ┗━ pipo-charachip029.png <- キャラクタチップ [今回用意したもの]
┣━ index.html
┣━ main.js
┗━ p5.tiledmap.js
HTML
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title>RPGサンプル</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.min.js" integrity="sha512-NxocnqsXP3zm0Xb42zqVMvjQIktKEpTIbCXXyhBPxqGZHqhcOXHs4pXI/GoZ8lE+2NJONRifuBpi9DxC58L0Lw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="./p5.tiledmap.js"></script>
<script src="./data/village.js"></script>
<script src="./main.js"></script>
<style type="text/css">
body {
background-color: #666666;
}
h1 {
color: #ffffff;
}
main {
width: 640px;
height: 640px;
margin: 0 auto;
background-color: #333333;
}
</style>
</head>
<body>
<header>
<h1>RPGサンプル</h1>
</header>
<main></main>
<p style="text-align: center;">©2022 qoomei.com All rights reserved.</p>
</body>
</html>
main.js
const IMG_CHAR_W = 32; // キャラクタ幅
const IMG_CHAR_H = 32; // キャラクタ高さ
var tmap; // カレントマップ
var x, y; // キャラクタ位置
// インスタンス
var pc; // プレイヤー
////////////////////////////////////////////////////////////////////////////////
// プリロード
function preload() {
// 主人公の位置
x = 11;
y = 4;
// キャラクタクラス
pc = new PlayerCharacter(loadImage("data/pipo-charachip029.png"));
// デフォルトマップ
loadTiledMap("village", "data", mapLoaded);
}
////////////////////////////////////////////////////////////////////////////////
// セットアップ
function setup() {
let cnv = createCanvas(640, 640);
}
////////////////////////////////////////////////////////////////////////////////
// ドロー
function draw() {
background(tmap.getBackgroundColor());
tmap.draw(x, y);
// プレイヤー描画
pc.draw();
}
////////////////////////////////////////////////////////////////////////////////
// マップロード
function mapLoaded(newMap) {
tmap = newMap;
tmap.setPositionMode("MAP");
tmap.setDrawMode(CENTER);
}
////////////////////////////////////////////////////////////////////////////////
// キャラクタクラス
class Character {
////////////////////////////////////////////////////////
// 初期化
constructor(img, d = 0) {
this.img = img; // キャラクタチップ
this.direction = d; // 向き
}
////////////////////////////////////////////////////////
// キャラクタ描画
drawCharacter(cx, cy) {
// アニメーション
var dx = floor(frameCount/15)%4;
if(dx == 3) dx = 1; // 0->1->2->1
imageMode(CENTER);
image(this.img,
cx, cy,
IMG_CHAR_W, IMG_CHAR_H,
dx * IMG_CHAR_W, this.direction * IMG_CHAR_H,
IMG_CHAR_W, IMG_CHAR_H);
}
}
////////////////////////////////////////////////////////////////////////////////
// プレイヤークラス
class PlayerCharacter extends Character {
////////////////////////////////////////////////////////
// 初期化
constructor(img) {
super(img);
this.x;
this.y;
}
////////////////////////////////////////////////////////
// キャラクタ描画
draw() {
// 歩く速さ
let walkSpeed = 0.05;
// 描画
this.drawCharacter(width / 2, height / 2);
this.x = round(x*100)/100;
this.y = round(y*100)/100;
// 向き決定
if (this.x%1 == 0 && this.y%1 == 0 && keyIsPressed) {
if(keyCode == LEFT_ARROW) {
this.direction = 1;
x -= walkSpeed;
}
if(keyCode == RIGHT_ARROW) {
this.direction = 2;
x += walkSpeed;
}
if(keyCode == UP_ARROW) {
this.direction = 3;
y -= walkSpeed;
}
if(keyCode == DOWN_ARROW) {
this.direction = 0;
y += walkSpeed;
}
}
// 1マス進める
if (this.x%1 != 0) {
if(this.direction == 1) x -= walkSpeed;
if(this.direction == 2) x += walkSpeed;
}
if (this.y%1 != 0) {
if(this.direction == 3) y -= walkSpeed;
if(this.direction == 0) y += walkSpeed;
}
}
}
おわりに
主人公も登場しマップがスクロールすると、一気にゲームぽく見栄えが良くなったと思います。次回はNPCを登場させて、NPCが自由に歩き回る方法について解説していきたいと思います!