JavaScriptでRPGを作ろう!主人公のアニメーションとマップスクロールを解説

JavaScriptでRPGを作ろう!主人公のアニメーションとマップスクロールを解説

「ゲーム作りって大変そう。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行目では実際に画像を描画しています。

p5.js image関数
第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が自由に歩き回る方法について解説していきたいと思います!