JavaScript|RPG戦闘画面の複数モンスター配置座標を計算

勘違いしながらどハマりしながら作り上げる爺の『JavaScript奮闘記』。爺のくせにゲームのプログラミングに興味を持ってしまいました。

ポク太郎です。

JavaScriptでのゲームのプログラミングに挑戦中。

今回は複数のモンスターとエンカウントした場合の敵配置座標の計算について。


今回のJavaScriptで行うのは配置座標と分布を持つ乱数

雛形にするのはあきちょんさんのソース

上記動画はシューティングの例ですが、表示部分を流用しやすかったのでこちらで。あきちょんさんのソース置き場はこちら

"use strict";

//デバッグ
const DEBUG = true;
let drawCount=0;
let fps=0;
let lastTime=Date.now();

//ゲームスピード
//const GAME_SPEED=1000/60;
const GAME_SPEED=1000/1;

//画面サイズ
const SCREEN_W = 480;
const SCREEN_H = 320;

//キャンバスサイズ
//実際に表示するのがCANVAS:アスペクト比同じに
const CANVAS_W = SCREEN_W *1.5;
const CANVAS_H = SCREEN_H *1.5;

//フィールドサイズ
const FIELD_W = SCREEN_W *2;
const FIELD_H = SCREEN_H *2;

//キャンバス
let can = document.getElementById("can");
let con = can.getContext("2d");
can.width  = CANVAS_W;
can.height = CANVAS_H;

//フィールド(仮想画面)
let vcan = document.createElement("canvas");
let vcon = vcan.getContext("2d");
vcan.width  = FIELD_W;
vcan.height = FIELD_H;

//カメラの座標
let camera_x = 0;
let camera_y = 0;

	//★★★★本記事で使用する設定	ここから↓↓↓↓↓↓↓↓
//戦闘画面パラ
const GND1 = SCREEN_H *2/3;		//戦闘画面のモンスター地面
const GND2 = SCREEN_H *1/3;		//戦闘画面のモンスター地面(ドラクエ5の鳥とか、現在未使用)
const OFFSET = SCREEN_W *1/8;		//戦闘画面の左右スペース
const W_DEKA = 70;			//これより幅のデカいモンスターはデカキャラと定義

//ファイルを読み込み
let monImage = new Image();
monImage.src = "img/monster_test01_trw120dot.png";
const	gMonsterName = [ "スライム", "ミノムン", "オルトロフォーク", "コーシュー", "マンティコア" , "メデューサ" ];//モンスター名称
const	gMonsterX = [ 38, 144, 253, 16, 130, 240 ];	//	モンスター画像の左上x座標
const	gMonsterY = [ 61, 80, 70, 226, 210, 168 ];	//	モンスター画像の左上y座標
const	gMonsterW = [ 44, 66, 80, 88, 94, 114 ];	//	モンスター画像の幅
const	gMonsterH = [ 37, 42, 89, 94, 110, 144 ];	//	モンスター画像の高さ
	//★★★★本記事で使用する設定	ここまで↑↑↑↑↑↑↑↑

//情報の表示
function putInfo()
{
	if(DEBUG)
	{
		drawCount++;
		if( lastTime +1000 <= Date.now() )
		{
			fps=drawCount;
			drawCount=0;
			lastTime=Date.now();
		}
		
		con.font="20px 'Impact'";
		con.fillStyle ="white";
		con.fillText("FPS :"+fps,20,20);
	//★★★★本記事で表示したデバッグ情報	ここから↓↓↓↓↓↓↓↓
		con.fillText("MON_NOW :"+mon_now,20,40);
		con.fillText("r_num :"+r_num,20,70);
	//★★★★本記事で表示したデバッグ情報	ここまで↑↑↑↑↑↑↑↑
	}
}

	//★★★★本記事で作成したグローバル変数	ここから↓↓↓↓↓↓↓↓
let n_mon = 10;				//1度にエンカウントするモンスターの最大値
let h_gaus = 0.15;			//正規分布の標準偏差(大→多数登場確率大)
let mon_now;//出現モンスター名
let r_num;//出現モンスター数
let r_gn;//グループメンバー数
let mon=[];//出現モンスター列
let i;
let j;
	//★★★★本記事で作成したグローバル変数	ここまで↑↑↑↑↑↑↑↑

function rand_gaus(h,min,max)//引数hは標準偏差
{
	//★★★★本記事の話題はこの関数	ここから↓↓↓↓↓↓↓↓
//正規分布に近い乱数…一様な乱数Math.random()に分布を持たせる
		let rs;
		let r=1.2*h*Math.sqrt(2)*Math.sqrt(-1*Math.log(Math.random()*Math.exp(-0.5))-1/Math.sqrt(2))
		rs=Math.floor( r*(max-min+1) )+min;
	if(rs<min){		rs=min;	}
	if(rs>max){		rs=max;	}
	if (isNaN(rs)) {	rs=min; }//何故かNaN発生する場合が
		return rs;
	//★★★★本記事の話題はこの関数	ここまで↑↑↑↑↑↑↑↑
}

function selmon(){
	//★★★★本記事の話題はこの関数	ここから↓↓↓↓↓↓↓↓
			r_num=rand_gaus(h_gaus,1,n_mon);//r_num:出現モンスター数

			let n=0;
		while(n<r_num){
			let ran=rand(0,gMonsterName.length-1);//ran:モンスター種
			r_gn=rand(1,n_mon);//r_gn:グループメンバー数

	//デカキャラ制限…2回連続で確率当たったときだけ&必ず個体で出現
		if(gMonsterW[ran]>W_DEKA){	let ran=rand(0,gMonsterName.length-1);	}
		if(gMonsterW[ran]>W_DEKA){	r_gn=1;	}

		for( j=1; j<=r_gn; j++ ){
			mon[n]=ran;
			n=n+1;		}

		}
	//ここまでモンスター選択、ここから表示座表計算
		let hx=[];
		let posx=[];

		for( i=1; i<=r_num; i++ ){
			posx[i]=(SCREEN_W-2*OFFSET)/(r_num+1)*i+OFFSET;		}

		for( i=0; i<posx.length; i++ ){
			n=mon[i];
			mon_now=gMonsterName[n];
			let sx = gMonsterX[n];
			let sy = gMonsterY[n];
			let sw = gMonsterW[n];
			let sh = gMonsterH[n];
			hx[i] = posx[i]-sw/2;
			let hy = GND1 - sh;

			vcon.drawImage( monImage,sx,sy,sw,sh,hx[i],hy,sw,sh);	}
	//★★★★本記事の話題はこの関数	ここまで↑↑↑↑↑↑↑↑
}

//描画の処理
function drawAll(){
	//描画の処理
	
	vcon.fillStyle="black";
	vcon.fillRect(camera_x,camera_y,SCREEN_W,SCREEN_H);
	
	// 自機の範囲 0 ~ FIELD_W
	// カメラの範囲 0 ~ (FIELD_W-SCREEN_W)

	//★★★★本記事の表示はここで指令	ここから↓↓↓↓↓↓↓↓
	selmon();
	//★★★★本記事の表示はここで指令	ここまで↑↑↑↑↑↑↑↑
	
//	camera_x = (jiki.x>>8)/FIELD_W * (FIELD_W-SCREEN_W);
//	camera_y = (jiki.y>>8)/FIELD_H * (FIELD_H-SCREEN_H);
	
	//仮想画面から実際のキャンバスにコピー
	con.drawImage( vcan ,camera_x,camera_y,SCREEN_W,SCREEN_H,
		0,0,CANVAS_W,CANVAS_H);
}

//ゲームループ
function gameLoop(){
	drawAll();
	putInfo();	}

//ゲームの初期化
function gameInit(){
	setInterval(gameLoop,GAME_SPEED);	}

//ゲームの開始
window.onload=function(){
gameInit();	};

//整数の乱数を取得
function rand(min,max){
	return Math.floor( Math.random()*(max-min+1) )+min;	}

あきちょんさんソースのブロック構成

プログラムの開始は//ゲームの開始部分のonload関数。gameInit()が呼び出されます。

gameInit()内で規定時間に1回gameLoop()が呼び出され、そこで画面描画drawAll()とデバッグ情報表示putInfo()

今回テストする部分を呼び出すのはその中。selmon()とデバッグ情報を表示して動作を確認します。また、今回のテスト内容の都合で1フレーム/秒で表示します。

画面への表示は
仮想画面.drawImage( モンスター画像,sx,sy,sw,sh,hx,hy,sw,sh);で仮想画面に描き出し。
●実際のキャンバスへキャンバス.drawImage(仮想画面,camera_x,camera_y,SCREEN_W,SCREEN_H,0,0,CANVAS_W,CANVAS_H);で転送。

今回行うのは複数敵配置の座表計算と分布を持った乱数発生

RPGの戦闘画面でのモンスター表示。パーティ戦闘での複数を表示するため、以下の図のように各モンスターの表示位置を計算します。

JavaScript~RPGパーティ敵の配置座標

また、エンカウントするモンスター数を乱数で決定しますが、Math.random()は一様な確率ですべての数値を発生するので、1体登場する確率と8体登場する確率が同じになってしまいます。

そこで、ほぼ正規分布で発生する乱数を作り、7体・8体が当たる確率は低く1体・2体が当たる確率を高くし自然な難易度調整をしやすくします。

JavaScript~正規分布を持った乱数発生

モンスターデータの読み込みと目指す仕様

お絵かき爺なので、わざわざオリジナルモンスターの絵を描いてデータを準備しました。テストなのでわざと無駄が生じる配置にして読み込んでいます。

JavaScriptで作るRPGのモンスターデータ
低級作品にありがちな“鳥山明概念をパクった時点でオリジナルになり得ず&そんな神レベル実現できないのに、無理して独自に準備する痛々しさ”は十分自覚しています。

ソースの設定部分にあるgMonsterXgMonsterYgMonsterWgMonsterHの4配列は画像を開いて手動で計測しました。

	//★★★★本記事で使用する設定	ここから↓↓↓↓↓↓↓↓
//戦闘画面パラ
const GND1 = SCREEN_H *2/3;		//戦闘画面のモンスター地面
const GND2 = SCREEN_H *1/3;		//戦闘画面のモンスター地面(ドラクエ5の鳥とか、現在未使用)
const OFFSET = SCREEN_W *1/8;		//戦闘画面の左右スペース
const W_DEKA = 70;			//これより幅のデカいモンスターはデカキャラと定義

//ファイルを読み込み
let monImage = new Image();
monImage.src = "img/monster_test01_trw120dot.png";
const	gMonsterName = [ "スライム", "ミノムン", "オルトロフォーク", "コーシュー", "マンティコア" , "メデューサ" ];//モンスター名称
const	gMonsterX = [ 38, 144, 253, 16, 130, 240 ];	//	モンスター画像の左上x座標
const	gMonsterY = [ 61, 80, 70, 226, 210, 168 ];	//	モンスター画像の左上y座標
const	gMonsterW = [ 44, 66, 80, 88, 94, 114 ];	//	モンスター画像の幅
const	gMonsterH = [ 37, 42, 89, 94, 110, 144 ];	//	モンスター画像の高さ
	//★★★★本記事で使用する設定	ここまで↑↑↑↑↑↑↑↑

グループ作成と複数モンスター表示

表示位置の計算の前に必要なことを忘→単なる乱数で当たったモンスターがバラバラに出てくるのは困ります。モンスターらもグループを作るので。(少なくともドラクエでは)

つまり、エンカウントしたモンスター数決定→モンスター種別を決定した後、それが何体連なるのかをまた別の乱数を発生させて決めないといけません。

そこで準備したのが変数r_numr_gnと遭遇したモンスター列mon[](モンスター種別を記憶)

エンカウント数r_numだけwhileループを回し、その中でグループ数r_gnを発生、カウントしていった結果エンカウント数に到達したらループを抜ける方法でmon[]を作成します。

ただし、デカキャラ定義したヤツはドカドカ出てくると変な感じだったので、ソースのような制限を加えました。

	//★★★★本記事で作成したグローバル変数	ここから↓↓↓↓↓↓↓↓
let n_mon = 10;				//1度にエンカウントするモンスターの最大値
let h_gaus = 0.15;			//正規分布の標準偏差(大→多数登場確率大)
let mon_now;//出現モンスター名
let r_num;//出現モンスター数
let r_gn;//グループメンバー数
let mon=[];//出現モンスター列
let i;
let j;
	//★★★★本記事で作成したグローバル変数	ここまで↑↑↑↑↑↑↑↑

function selmon(){
	//★★★★本記事の話題はこの関数	ここから↓↓↓↓↓↓↓↓
			r_num=rand_gaus(h_gaus,1,n_mon);//r_num:出現モンスター数

			let n=0;
		while(n<r_num){
			let ran=rand(0,gMonsterName.length-1);//ran:モンスター種
			r_gn=rand(1,n_mon);//r_gn:グループメンバー数

	//デカキャラ制限…2回連続で確率当たったときだけ&必ず個体で出現
		if(gMonsterW[ran]>W_DEKA){	let ran=rand(0,gMonsterName.length-1);	}
		if(gMonsterW[ran]>W_DEKA){	r_gn=1;	}

		for( j=1; j<=r_gn; j++ ){
			mon[n]=ran;
			n=n+1;		}

		}
	//ここまでモンスター選択、ここから表示座表計算
		let hx=[];
		let posx=[];

		for( i=1; i<=r_num; i++ ){
			posx[i]=(SCREEN_W-2*OFFSET)/(r_num+1)*i+OFFSET;		}

		for( i=0; i<posx.length; i++ ){
			n=mon[i];
			mon_now=gMonsterName[n];
			let sx = gMonsterX[n];
			let sy = gMonsterY[n];
			let sw = gMonsterW[n];
			let sh = gMonsterH[n];
			hx[i] = posx[i]-sw/2;
			let hy = GND1 - sh;

			vcon.drawImage( monImage,sx,sy,sw,sh,hx[i],hy,sw,sh);	}
	//★★★★本記事の話題はこの関数	ここまで↑↑↑↑↑↑↑↑
}

遭遇したモンスター列mon[]が出来上がったので、上で示した配置座標を計算します。

JavaScript~RPGパーティ敵の配置座標

ソースでは、図中の式をposx[]として計算しました。各モンスターは幅が異なるので、表示X座標も配列としてhx[]→=posx[i]-sw/2となります。

正規分布を持つ乱数の発生

上記ソース内でエンカウント数r_numを求める際に使ったのがrand_gaus()なる自作関数。

function rand_gaus(h,min,max){
	//★★★★本記事の話題はこの関数	ここから↓↓↓↓↓↓↓↓
//引数hは標準偏差	正規分布に近い乱数…一様な乱数Math.random()に分布を持たせる
		let rs;
		let r=1.2*h*Math.sqrt(2)*Math.sqrt(-1*Math.log(Math.random()*Math.exp(-0.5))-1/Math.sqrt(2))
		rs=Math.floor( r*(max-min+1) )+min;
	if(rs<min){		rs=min;	}
	if(rs>max){		rs=max;	}
	if (isNaN(rs)) {	rs=min; }//何故かNaN発生する場合が
		return rs;
	//★★★★本記事の話題はこの関数	ここまで↑↑↑↑↑↑↑↑
}

大昔に自分が導き出した式。どうやって導いたかは記憶になし。一様な発生乱数Math.random()をこの式に代入して噛ますとおおよそ正規分布を持った乱数に変換されます。

ただし、-∞~∞までの値が出力されるので、今回のような場合はminmaxに納まるようソース内の3条件により成形しています。なので正規分布からのずれが大きくなります。多少。

今、エンカウント最大数n_mon=10としてありますが、この乱数発生を使うとそー簡単に9体・10体に遭遇しなくなります。調整は変数h_gausにて標準偏差を設定。

h_gausに0.8や1.2などと代入すると、10体エンカウントの頻度が上がるので、マップエリアによる自然な難易度調整に使用できます。

出来上がった戦闘画面の複数敵表示機能

今、FPS=1としてあるので、1秒間に1回該当のルーチンが演算され画面が更新されます。

デカキャラが並んだり、グループが分離したりもしますが、それはドラクエでも発生するグループ分離と同じで、その低確率が当たっただけ。

意味も無くキャラクターまで自力で描いております。でもそうするとなんか凄く「ゲーム作ってるわ~」と自分で感じます。機能的な部分は全然なんですが。

後退せず、亀や牛歩のように歩んでいけるでしょうか。乞うご期待。

コメント

タイトルとURLをコピーしました