自作RPG~マップエディタもJavaScriptで作ればええやん

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

UnityUnrealエンジンに挫折し、ゲーム製作なんてJavaScriptで十分やん!と数カ月前に気付いたポク太郎です。

ドラクエ型のRPG製作を進める中、馴染みの開発環境でマップエディタを作成中でした→画像の表示部分が重すぎになり難航。上手な制御方法を探し回っておりました。

そんな中、マップエディタJavaScriptで作る方法を紹介したページ発見。

マップエディタJavaScriptで十分やん!

同じくRPG製作に奮闘する方の参考にでもなれば。てか、仕様を固めるための自分の整理用だったり。


マップエディタもJavaScriptで作りゃええやん!

WorldWideSoftwareさん~参考サイト①

見つけたのはワールドワイドソフトウェア有限会社さんが解説するこちらのページ。プログラミングに関する指導を請け負う業者さんです。特にゲーム系が得意の様子。

こちらでサンプルとして公開されたマップエディタをゴニョゴニョ改造したのが下で紹介するもの。改造点についてそれぞれ説明してあります。

いつものakichonさん~参考サイト②

こちらはakichonさん製作のシューティングの例

あきちょんさんのシューティングの例では、一旦仮想画面に作業上の描画を行い、1フレームごとに実際の表示画面へとコピーするもの。それによりスムーズな操作感覚が得られます。

ここでは上記マップエディタ自体も、“ゲームで使うフレーム処理”で表示するように作り変えました。それにより+50%程度のスピードで動くようになりました。

JavaScriptで作ったマップエディタ

ブラウザ全面で使用するものをブログ上に表示してるので、マウスのコロコロでページがスクロールしてしまいます。SHIFTキー+コロコロなら左右に動くのが分かるかと。
その他、表示幅によっては座標がトチ狂うかも。座表計算甘いので。


マップエディタ画面構成
チップ選択部 使用できるチップが並びます。
レイヤもどきの構成。現在は背景x16(2段)、地形x40(5段)、キャラx24(3段)、イベントx8(1段)。それぞれの末尾のチップは削除用です。
ボタン部 「Load」「Save」「仕上」ボタンが並びます。
「Load」…ダイアログが出るので保存データを選択します。何のチェックもしないので変なの読み込むとトチ狂います。
「Save」…保存ダイアログが出ます。
「仕上」…背景が存在する領域のみを切り出して保存、更にマップデータ(単なるcsv)がクリップボードにコピーされます。
ナビゲーション部 内部情報表示+ワールドマップを表示します。
マウス座標のタイル情報が表示されます。背景チップが置かれた部分はワールドマップに紫色で表示されます。
編集画面 指定のチップを実際に置いて作業する場所。
ブラウザの大きさに合わせタイルが並べられます。青+白線で描かれた枠は16×14マスです←ファミコン・スーファミで16×16ドットのキャラが並ぶ大きさ。
【操作方法】
マウス左クリック:チップを置く
  各レイヤ末尾のチップ置くと削除
  (SHIFT押しながら)水平・垂直に限定
マウスコロコロ:ワールドマップ内縦移動
  (SHIFT押しながら)ワールドマップ内横移動
上下左右:ワールドマップ内高速移動
Aキー:現在のチップで表示中の編集画面内を埋める
Bキー:現在のチップで表示中の編集画面内の空タイルを埋める

マップエディタ改造ポイント一覧

元にしたソースは先ほどのワールドワイドソフトウェア有限会社さんのページ下方、「◆マップエディタ&キャラ移動のソース一式」からダウンロードできます。

関数名・変数名は基本そのままのつもりですが、記憶にないほど全面変更してるので自信無し。

フレーム描画へ変更

akichonさんのシューティングの方式=フレームごとに仮想画面を全体へ転写する方法へ全面移行。

仮想画面は背景スクロールを考慮したものでマップエディタには無関係ですが、画像の重ね合わせの場合でも実際に描くのが1回/フレームで済むのでかなり軽くなります。

表示を、仮想画面:画面上のキャンバス=1:1にすればよいし、表示スケールを調整する場合でもこの方が分かりやすいし。

//ゲームの初期化
function gameInit(){
//MAPE初期化 今はクラス化した
	setInterval(gameLoop,GAME_SPEED);	}

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

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

//描画の処理
function drawAll(){
	vcon.fillStyle='white';
	vcon.fillRect(camera_x,camera_y,SCREEN_W,SCREEN_H);

	scale();		//リザーブ
	winsize();		//画面サイズ更新対応
	SideMake();		//チップ選択部描画
	MapNav();		//ナビ描画
	KeySence();		//key検出
	EditMake();		//配列データを編集画面に表示
	cursol();		//カーソル枠とファミコン枠表示
	putInfo();		//デバッグ情報表示

//	仮想画面から転送するようにしたら60数フレームから90数フレームに速度アップ
//	仮想画面から実際のキャンバスにコピー
	con.drawImage( vcan ,camera_x,camera_y,SCREEN_W,SCREEN_H,0,0,CANVAS_W,CANVAS_H);
}

画面構成を変更

チップ選択部分を左側(原点側)に固定し、表示倍率変更の混乱から逃亡。

“タイル”という定型サイズを並べるにあたり、固定幅が右側にあると何かと混乱するので。イベント駆動型でしかプログラムしなかった理由は“座表計算嫌い”。

ボタン列を上方に動かし低解像度時の考慮。表示が欠けると操作すらできないので。後、マウス座標にあるタイルの情報表示と、広大にしたワールドマップへの存在表示(紫印)。

ワールドマップ中央を編集スタート位置に。領域が足りなくなってよく困るので。なので、広大なワールドマップ中央から始め、「仕上」ボタンでトリミングする形としました。

「Load」「Save」は無条件にワールドマップ全域が対象。

レイヤもどきを実装

チップ種類を減らすためレイヤっぽいものを実装。
 背景:一番下に描くチップ。草原、砂漠、雪原、床等。
 地形:イベント無し・座標合致のみのイベントを持つもの。山・林・壁・看板と城・町・搭。
 キャラ:画像が同じでも別IDが必要なもの。扉・宝箱・タンス・人も。城もコッチにすべき?
 イベント:(仕様不確定)座標合致のイベント場所。ワールドマップへ戻る・隠しアイテム等。

次項の仕様決めで書くが、まだ固まってない部分。特にイベントの記録方法。圧倒的にチップが足りないので、チップ選択部をタブ切り替え式にし数量増やさないと、とかも考えてます。

また、チップはすべてクラスとして定義しました。

接頭語Hai:背景。クラス・マップ用2次元配列。
接頭語Trr:地形。クラス・マップ用2次元配列。
接頭語Chr:キャラ。クラス・マップ用2次元配列。
接頭語Evt:イベント。クラス・マップ用2次元配列。
//JSではマップチップなどの集合画像1枚だけ読み込み、その読み込み座標値を変数・クラス化、
//実際に表示する場面では、その変数・クラスを指定し集合画像から直接切り出す場合が多い。
//※(恐らく)ホームページ用なので大量画像のファイル名が鬱陶しいから。
//表示命令の関数をあらかじめ準備し、切り出し座標を持つ変数・クラスを渡すことで表示実現。

//背景クラス
class Haid{	constructor( x,y,w,h ){this.x = x;this.y = y;this.w = w;this.h = h;	}}
//地形クラス
class Trrd{	constructor( x,y,w,h ){this.x = x;this.y = y;this.w = w;this.h = h;	}}
//キャラクラス
class Chrd{	constructor( x,y,w,h ){this.x = x;this.y = y;this.w = w;this.h = h;	}}
//イベントクラス
class Evtd{	constructor( x,y,w,h ){this.x = x;this.y = y;this.w = w;this.h = h;	}}

//背景、地形、キャラ、イベント
let haid = [];let trrd = [];let chrd = [];let evtd = [];
	for( i=0; i<CK_MAX ; i++ ){	var retu=i % Schip_N;
					var gyou=Math.floor( i / Schip_N );
		if(i<Hai_N){		haid[i] = new Haid( retu * CSIZE, gyou * CSIZE, CSIZE, CSIZE );	}
		if(i<Trr_N){		trrd[i] = new Trrd( retu * CSIZE, gyou * CSIZE, CSIZE, CSIZE );	}
		if(i<Chr_N){		chrd[i] = new Chrd( retu * CSIZE, gyou * CSIZE, CSIZE, CSIZE );	}
		if(i<Evt_N){		evtd[i] = new Evtd( retu * CSIZE, gyou * CSIZE, CSIZE, CSIZE );	}	}

SHIFTキーで水平・垂直直線

マウスDownでタイル上にチップを置きますが、まっすぐ並べたい場合に苦労するのでSHIFTキー押すことでその座標のX軸・Y軸どちらか一致しないと置けないよう制限。

変数shiftkSHIFTキー+クリックが起きたら1、違うなら0マウスUpのタイミングで0クリア。
変数preXpreYSHIFTキーモード発動時の座標を記憶しておく変数。
変数koroマウスWHEELの増減量。次項で使用します。
let preX = 0, preY = 0, koro = 0, shiftk = 0;
function mouseDown(event) {
	var rect = event.target.getBoundingClientRect();
	tapX = event.clientX-rect.left;
	tapY = event.clientY-rect.top;
	tapC = 1;
		if (event.shiftKey == false){
			shiftk = 0;
		} else {
			shiftk = 1;
			preX = tapX; preY = tapY;
		}
	transformXY();
	setMap();	}

function mouseMove(event) {
	var rect = event.target.getBoundingClientRect();
	tapX = event.clientX-rect.left;
	tapY = event.clientY-rect.top;
		if (shiftk == 1){
			if (Math.abs(tapX-preX)>Math.abs(tapY-preY)){
			tapY = preY;
			} else {
			tapX = preX;
			}
		}
	transformXY();
	setMap();	}

function mouseUp(event){ shiftk = 0;tapC = 0; }

マウスWheelでワールドマップ移動

マウスのコロコロでワールドマップの上下移動を行えるように。SHIFTキー付きで左右移動。

‘wheel’のイベントリスナーを追加し、その増分を監視して時・時に表示座標をそれぞれ変化させます。

変数koroマウスWHEELの増減量。
let preX = 0, preY = 0, koro = 0, shiftk = 0;
can.addEventListener( 'wheel', function(event){
//	WheelEvent.deltaX 水平スクロール量
//	WheelEvent.deltaY 垂直方向のスクロール量
//	WheelEvent.deltaZ z軸のスクロール量
//	WheelEvent.deltaMode delta* 値のスクロール量の単位
//		指定可能な値は次
//		WheelEvent.DOM_DELTA_PIXEL delta* ピクセル
//		WheelEvent.DOM_DELTA_LINE  delta* 行単位(ブラウザ依存)
//		WheelEvent.DOM_DELTA_PAGE  delta* ページ単位

				koro = event.wheelDelta;
	if (event.shiftKey == false){
		if (koro > 0){
				if( eTop > 0 ) eTop --;//上へ
		} else {
				if( eTop < WMap_H-Echip_NY ) eTop ++;//下へ
		}
	} else {
		if (koro > 0){
				if( eLeft > 0 ) eLeft --;//左へ
		} else {
				if( eLeft < WMap_W-Echip_NX ) eLeft ++;//右へ
		}
	}
				koro = 0;	});

フレーム表示化に伴いキー判定の方式変更

ダイアログを出した際、直前のキー状態を保持し続けてしまうのでキー判定の方法を修正。aキーが“押された”でなく“離された”タイミングで機能を発動するように変更。

また、ワールドマップを広大にしたので「表示画面内全埋め」に変更し、「表示画面内の空タイルのみ全埋め」のbキーも追加。

変数req_dialog_aaキーが押されたら11の状態時にaキーが離されたら0に戻し、処理ルーチンへ。
変数req_dialog_bbキーが押されたら11の状態時にbキーが離されたら0に戻し、処理ルーチンへ。
変数kind:レイヤもどきの種。0:背景、1:地形、2:キャラ、3:イベント。
function KeySence(){
//ダイアログ出すとJSはその瞬間以降ずっと押されっぱなしと認識
//⇒「a」キーON→OFFのタイミングで出す req_dialog=1に記憶→条件成立時に0に戻す
var x;var y;
var f_fill = 0;
	//aキーで全埋め
	if( key[65] ) {
			req_dialog_a=1;
	} else {
		if( key[65]==false && req_dialog_a==1){
			req_dialog_a=0;
			if( confirm('【全埋め】現編集位置の全部を選択チップで埋めます。よろしいですか?') == true ){
			//仕様として画面内に表示された部分だけでないとマズイ
				for( y = 0; y < Echip_NY; y ++ ) {
				for( x = 0; x < Echip_NX; x ++ ) input_dt( kind, x, y, sel );	}
			} else {return;}
		}
					sleep( 0.5 );
	}
	//bキーで空いたとこ全埋め
	if( key[66] ) {
			req_dialog_b=1;
	} else {
		if( key[66]==false && req_dialog_b==1){
			req_dialog_b=0;
			if( confirm('【空欄埋め】現編集位置の空欄全部を選択チップで埋めます。よろしいですか?') == true ){
			//仕様として画面内に表示された部分だけでないとマズイ
				for( y = 0; y < Echip_NY; y ++ ){
				for( x = 0; x < Echip_NX; x ++ ){
				if( kind==0 ){
					if( hai[eTop+y][eLeft+x] == Hai_N - 1 ){f_fill=1;}
					}
				else{if(kind==1){
					if( trr[eTop+y][eLeft+x] == Trr_N - 1 ){f_fill=1;}
					}
				else{if(kind==2){
					if( chr[eTop+y][eLeft+x] == Chr_N - 1 ){f_fill=1;}
					}
				else{
					if( evt[eTop+y][eLeft+x] == Evt_N - 1 ){f_fill=1;}
				}}}
	//	if( confirm( gyo1[0] ) == true ){}

				if( f_fill == 1 ){
					input_dt( kind, x, y, sel );f_fill=0;	}
				}}
			} else {return;}
		}
					sleep( 0.5 );
	}
	//上下左右でWmapナビ移動
	var zouX = Math.floor(Echip_NX/4);
	var zouY = Math.floor(Echip_NY/4);
	var chk;
	if( key[38] ){ chk = eTop - zouY;{ if( chk < 0 ){ eTop = 0;}else{ eTop = chk; } } }
	if( key[40] ){ chk = eTop + zouY;{ if( chk > WMap_H-Echip_NY ){ eTop = WMap_H-Echip_NY;}else{ eTop = chk; } } }
	if( key[37] ){ chk = eLeft - zouX;{ if( chk < 0 ){ eLeft = 0;}else{ eLeft = chk; } } }
	if( key[39] ){ chk = eLeft + zouX;{ if( chk > WMap_W-Echip_NX ){ eLeft = WMap_W-Echip_NX;}else{ eLeft = chk; } } } }

ナビゲーションとしてタイル情報表示

マウスで指し示した場所のデータが何であるかを表示するよう変更。ワールドマップのナビゲーション表示ルーチンに入れ込みました。

変数offset:編集画面の原点のX座標。それより左はチップ選択部。
変数f_innload:マップデータ読み込み中なら1、違うなら0。次項で使用。
function MapNav(){
var x;var y;
var kw = SideW/WMap_W;
var kh = SideW*k/WMap_H;
var rangeW,rangeH ;
//	if( eLeft>-1 && eLeft+Echip_NX<WMap_W-1 && eTop>-1 && eTop+Echip_NY<WMap_H-1 ){
		if( Echip_NX*kw<5 ){
			//編集画面に対しWmapが広すぎる場合
				rangeW=5;rangeH=5;
		} else {
				rangeW=Echip_NX*kw;rangeH=Echip_NY*kh;
		}
			fRect( NavX, NavY, SideW, SideW*k, '#aaa' );
		for( y = 0; y < WMap_H; y ++ ){
		for( x = 0; x < WMap_W; x ++ ){
			if( hai[y][x] != Hai_N - 1 ){
			sRect( NavX+x*kw, NavY+y*kh, 1, 1, '#c4c' );	}
		}}
			sRect( NavX+eLeft*kw, NavY+eTop*kh, rangeW, rangeH, '#8ff' );
			fText( 'X='+eLeft+' Y='+eTop, SideX+70, NavY+25, 20, '#fff' );

//タイル情報表示
				transformXY()
	if( f_innload == 1 ){
		fText( 'ロードに時間掛かってもガマン。' , SideX+10, InfoY+ 40, 14, 'red' );
	} else {
	if( tapX-offset>0 && tapX<offset+Echip_NX*CSIZE && tapY>0 && tapY<Echip_NY*CSIZE){
				x = toInt((tapX-offset)/CSIZE);
				y = toInt(tapY/CSIZE);
		fText( 'X='+(eLeft+x)+' , Y='+(eTop+y) , SideX+20, InfoY+ 40, 14, '#222' );
		fText( '背:'+hai[eTop+y][eLeft+x], SideX+SideW-110,InfoY+ 40, 14, '#222' );
		fText( '地:'+trr[eTop+y][eLeft+x], SideX+SideW-50, InfoY+ 40, 14, '#222' );
		fText( 'キ:'+chr[eTop+y][eLeft+x], SideX+SideW-110,InfoY+ 60, 14, '#222' );
		fText( 'イ:'+evt[eTop+y][eLeft+x], SideX+SideW-50, InfoY+ 60, 14, '#222' );
	} else {
		fText( 'X='+' , Y=' , SideX+20, InfoY+ 40, 14, '#222' );
		fText( '背:', SideX+SideW-110,InfoY+ 40, 14, '#222' );
		fText( '地:', SideX+SideW-50, InfoY+ 40, 14, '#222' );
		fText( 'キ:', SideX+SideW-110,InfoY+ 60, 14, '#222' );
		fText( 'イ:', SideX+SideW-50, InfoY+ 60, 14, '#222' );
	}}
//LOAD、保存、仕上ボタン
	if( drawBtn( 'Load', 0, BtnY, 80, 36 ) == true ) {
				loadMapdt();tapC = 0;		}
	if( drawBtn( 'Save', SideW/2-40, BtnY, 80, 36 ) == true ) {
				saveMapdt();tapC = 0;		}
	if( drawBtn( '仕上', SideW-80, BtnY, 80, 36 ) == true ) {
				putMapdt();tapC = 0;		}
}

Mapデータの保存・クリップボードへコピー

保存できないと作る意味が無いのでその機能の実装。

関数saveMapdt():セーブダイアログを出し、保存ファイル名の指定。
関数putMapdt():背景が空でないタイルの座標を調べて、トリミングデータの保存&コピー。
関数makedata( lim_L, lim_U, lim_R, lim_B ):保存データを実際に作る関数。
function saveMapdt(){
			info = makedata( 0, 0, WMap_W-1, WMap_H-1);
	//ファイル出力
		const blob = new Blob([info],{type:'text/plain'});
		const link = document.createElement('a');
		link.href = URL.createObjectURL(blob);
		link.download = 'MapData.txt';
		link.click();
				sleep( 0.5 );	}

function putMapdt(){
//Wmapから上下左右何もない部分を切り出して出力
var x;var y;
var dat = [];var info = '';
var f_brk = 0;
var lim_L;var lim_U;var lim_R;var lim_B;
	//【要注意!】hai[y][x]←Y座標が先
	for( j = 0; j < WMap_H; j ++ ){
		if( f_brk == 1 ){f_brk = 0;break;
		}else{	for( i = 0; i < WMap_W; i ++ ){
			if( hai[j][i] != Hai_N - 1 ){ lim_U = j;f_brk = 1;break;}	}}}
	for( i = 0; i < WMap_W; i ++ ){
		if( f_brk == 1 ){f_brk = 0;break;
		}else{	for( j = 0; j < WMap_H; j ++ ){
			if( hai[j][i] != Hai_N - 1 ){ lim_L = i;f_brk = 1;break;}	}}}
	for( j = WMap_H - 1; j > -1; j -- ){
		if( f_brk == 1 ){f_brk = 0;break;
		}else{	for( i = 0; i < WMap_W; i ++ ) {
			if( hai[j][i] != Hai_N - 1 ){ lim_B = j;f_brk = 1;break;}	}}}
	for( i = WMap_W-1; i > -1; i -- ){
		if( f_brk == 1 ){f_brk = 0;break;
		}else{	for( j = 0; j < WMap_H; j ++ ) {
			if( hai[j][i] != Hai_N - 1 ){ lim_R = i;f_brk = 1;break;}	}}}

	//	if( confirm('L:'+lim_L+','+'U:'+lim_U+','+'R:'+lim_R+','+'B:'+lim_B) == true ){}
				info = makedata( lim_L, lim_U, lim_R, lim_B);
	//ファイル出力
		const blob = new Blob([info],{type:'text/plain'});
		const link = document.createElement('a');
		link.href = URL.createObjectURL(blob);
		link.download = 'MapData_trim.txt';
		link.click();
	//クリップボードへ出力
		if(navigator.clipboard){
				navigator.clipboard.writeText(info);	}
}

function makedata( lim_L, lim_U, lim_R, lim_B ){
var x;var y;
var dat0 = [];var dat1 = [];var dat2 = [];var dat3 = [];
var info1 = '';var info2 = '';var info3 = '';var info4 = '';
	for( y = lim_U; y < lim_B+1; y ++ ) {
		for( x = lim_L; x < lim_R+1; x ++ ){
			if( x == lim_L ){
				dat0[y] = hai[y][x];
				dat1[y] = trr[y][x];
				dat2[y] = chr[y][x];
				dat3[y] = evt[y][x];
			} else {
				dat0[y] = dat0[y] + ',' + hai[y][x];
				dat1[y] = dat1[y] + ',' + trr[y][x];
				dat2[y] = dat2[y] + ',' + chr[y][x];
				dat3[y] = dat3[y] + ',' + evt[y][x];
			}
		}
	}
		info1 = '水平:'+(lim_R-lim_L+1)+'タイル、垂直:'+(lim_B-lim_U+1)+'タイル\r\n';//改行:\r\n
		for( y = lim_U; y < lim_B+1; y ++ ){
			info1 = info1 + '背景['+y+']=\"'+dat0[y]+'\"\r\n'	}
		for( y = lim_U; y < lim_B+1; y ++ ){
			info2 = info2 + '地形['+y+']=\"'+dat1[y]+'\"\r\n'	}
		for( y = lim_U; y < lim_B+1; y ++ ){
			info3 = info3 + 'キャ['+y+']=\"'+dat2[y]+'\"\r\n'	}
		for( y = lim_U; y < lim_B+1; y ++ ){
			info4 = info4 + 'イベ['+y+']=\"'+dat3[y]+'\"\r\n';	}
			return info1+info2+info3+info4;	}

Mapデータの読み込み

保存したものを読み込めないと作業を休めないのでその機能の実装。Windows上で作ったので改行コードは\r\n使用。

Ubuntu・Macなら\nだし、OS9以前のMacなら\rですが、自分が書いて自分で読むだけなら問題無し?テキストエディタで開いた時に困るかな。

function loadMapdt(){
	//ファイル指定で読み込み
const showOpenFileDialog = () => {
	return new Promise(resolve => {
				const input = document.createElement('input');
				input.type = 'file';
				input.accept = '.txt, text/plain';
				input.onchange = event => { resolve(event.target.files[0]); };
				input.click();
	});	};
const readAsText = file => {
	return new Promise(resolve => {
				const reader = new FileReader();
				reader.readAsText(file);
				reader.onload = () => { resolve(reader.result); };
				f_innload = 1;
	});	};
(async () => {
				const file = await showOpenFileDialog();
				const content = await readAsText(file);
				mapdt = content;
	//ここで処理しないといけない
				resetdt();
				sleep( 0.5 );
				mapRead( mapdt )
				f_innload = 0;
	})();
}

function mapRead( dt ){
var x, y;
var gyo = [];var gyoN;var gyo1 = [];var gyo2 = [];var gyo3 = [];var gyo4 = [];
				gyoN = nthc( dt, '\r\n' );

	for( i = 2; i < gyoN; i ++ ) {
				gyo[i-2] = nthf( dt, '\r\n', i );
	}
	for( i = 0; i < gyo.length; i ++ ) {
			if(i<WMap_H){
				gyo1[i] = nthf( nthf( gyo[i], '\"', 2 ), '\"', 1 );		}
			else{if(i<WMap_H*2){
				gyo2[i-WMap_H] = nthf( nthf( gyo[i], '\"', 2 ), '\"', 1 );	}
			else{if(i<WMap_H*3){
				gyo3[i-WMap_H*2] = nthf( nthf( gyo[i], '\"', 2 ), '\"', 1 );	}
			else{
				gyo4[i-WMap_H*3] = nthf( nthf( gyo[i], '\"', 2 ), '\"', 1 );	}}}}

			for( y = 0; y < WMap_H; y ++ ){
			for( x = 0; x < WMap_W; x ++ ){
				hai[y][x] = nthf( gyo1[y], ',', x+1 );
				trr[y][x] = nthf( gyo2[y], ',', x+1 );
				chr[y][x] = nthf( gyo3[y], ',', x+1 );
				evt[y][x] = nthf( gyo4[y], ',', x+1 );
			}}
						f_innload = 0;
}

マップエディタの仕様を考える

ゲーム製作の経験が一切無い爺の構想なのでトンチンカンである可能性が無きにしも非ず。

RPG側のプログラムマップエディタは表裏一体。結局、RPG側のプログラムをどう作るかによって必要なマップデータが変わってきます。

今考えてる“必要な仕様”を整理。考え方としては、

●干渉物がマップ上にあるかどうかを判定し、キャラの移動と制限を実現。
●キャラの移動に従い、マップ座標からマップデータを監視→イベントを発見して処理。
マップデータにイベントIDを内包すれば、座標調べるだけでイベント内容も調べられる。イベント調査は別のイベントデータ列を準備しておけばOK。

RPG側のプログラムを見据えたチップの分類分け
名称 Pattern 個別ID Event 干渉 台詞 備考
2 条件 波打ち際、ステータス比較
条件 船判断
陸(草)
城・町・洞窟 縦横4 座標 表示をprog判断
毒沼 座標 ステータス、アニメ
林・山・橋
壁・険山・看板
条件
宝箱 条件 ステータス比較
立札・タンス 台詞 ステータス比較
4 台詞 移・向・動・ステータス比較
ev1 座標合致:ワールドマップへ戻る
ev2 座標合致:マップ座標からイベントID調べるのがよいか、マップデータそのものにID記入した方がよいか…。

やはり問題はイベント検出の仕方。マップ内にIDがあれば直接イベントデータ列をスキャンしに行けるが、座標データから探るとなるとその対応表を作らないといけないことに。

→マップ見ながらID入力できた方が当然楽だよな?

アイデアとして、「人」も「宝箱」と同じ扱いとし、動く場合はそのタイミング毎にマップデータを更新する手も→座標合致・アクション先合致の2種類の判定だけ行えばよいことに。

やっぱり、マップエディタ内でイベントIDを入力してしまう機構を作った方が賢明ね。

関係ないが、抜けてた要素1つ発見→各座標における敵難易度情報。これは追加するしかないな。

まさに仕様を考えるためのメモ投稿→何言ってんのかサッパリ分からない内容でした。

要するに、今何番の宝箱調べたかをRPG側のプログラムで検知しに行くより、マップエディタ上でこれが宝箱1、これが宝箱2、…て入力してしまえた方が楽だよね、という話。

それプラス、上の実例に足りないエンカウントする敵の難易度情報=マップ上のタイルごとに記録してないといけない値。

結局、RPG側のプログラムを作りながらマップエディタ側のプログラムもいじり続けないと仕様が固まらないだろうてのが結論。表裏一体なので。

RPG製作として真っ先に出てくるのはRPGツクールやフリーのWOLF RPGエディターウディタ

でも触ってみて幻滅、やりたいのはソレじゃない感。やりたいのは、

へんじがない ただの つけものいし のようだ

RPGを自分で作ってみたい」←この想いに語弊があり、正確には「堀井雄二の真似してみたい」。

T-ドラゴンクエストがまさにその想いでは。なので、それとは異なり、権利侵害せずに堀井雄二さんの世界を真似できるならそれは十分遊べるかと。

そんな訳で、暇を持て余したジジイが引き続き“正確なRPG製作”に勤しんで参りたいと思います。

ファミコン時代と異なり、好きなだけメモリ使える・何キャラでも表示できる環境なので何とかなるのでは。乞うご期待。

コメント

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