やっとでけた…RPGのパーティ追従動作アルゴリズム

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

ポク太郎です。自作RPG製作中です。

ナメてかかってたパーティが主人公の後ろを追従するあの動き。正式名称は分かりませんが、グラディウスのオプション動作というか、スネーク追従動作というかのアレ。

グラディウスのオプション動作なら、前回の座標を後ろのオプション座標に次々渡していけばよいですが、RPGの場合は主人公が常に画面中央。

相対位置で動かすだけだろとナメて掛かってましたが、スムーズスクロールの1ドット移動と絡まって酷い苦労をしました。

※酷い目に会った恐らくの原因はワールドマップ上の座標を元に主人公の表示位置を演算してないから、と思われますが混乱しまくりで気付きませんでした。

一応ソースを貼り付けてありますが、参考にしちゃダメな感じ。何故このやり方がダメなのかを気付くために見て下さい。


先頭が画面中央固定のスネーク追従と呼ぶのはコレ

下の実例では、赤パン→黄パン→緑ドレス→紫ドレス→青パンの順番で行進するかと。ここでスネーク追従と呼んでるのはコレのこと。

残る問題は、例えば池に突進した際に進行できないので全キャラの向きは固定しないといけませんが、何度か繰り返すと後ろのキャラも向きを変えること。

これは現状のルーチンでは回避できないので保留。対処を後回しに。

ここで作るRPGとは梅澤嗣佳(T.Umezawa)さんが説明するサンプルを元にした改造版。それを以下図のような描画構成に変更して奮闘しています。

JavaScriptで作るRPGの描画画面構成

スムーズスクロールのために仮想画面vgを実画面rgより上下左右1タイルづつ広げ、フレームごとの転写座標であるパラメータscxscyを1ドット刻みで動かすというもの。

プレイヤは実画面と同じ座標系の別仮想画面=プレイヤ画面vg-pの中央に描いています。←無条件に中央表示してるのがそもそもの間違いなのかも。

ややこしすぎたので論理を図解

規則性を掴むために図を書いて考えます。プレイヤが画面中央固定の場合のスネーク追従です。

「元」の配置から「上」「下」「左」「右」に動かした場合の各キャラの動きを上下左右に書いてあります。2パターン分で分析。

主人公が画面中央のRPGパーティ追従動作アルゴリズム 主人公が画面中央のRPGパーティ追従動作アルゴリズム

ニコちゃんで表すプレイヤが中央座標固定なので、後続キャラがどう動くべきかが描いてあります。上はyが減る方向なのでy-、右はxが増える方向なのでx+

順番に見ていくと規則性が。

A「ニコちゃんが動く値分だけ、全後続キャラを反対方向に動かさないといけない」
B「追従動作として、前回・前々回・前々々回の方向分動かさないといけない」
主人公が画面中央のRPGパーティ追従動作アルゴリズム

そのAB二つを足し算したものが後続キャラの座標になります。なので、一気に2タイル進む・斜めに動く・打ち消し合って動かないなどのパターンが出てきます。

上の実例で試してどうぞ。方向転換時は黄パンが赤パンの逆サイドまで一気に動いてます(2タイル移動)。また、背景スクロールに惑わされ分かりにくいですが曲がった時には斜めに動いてます。直進し続ける時は座標が一切変わってないのが分かるかと。
赤パンの位置は常に画面中央だと頭に入れて眺めてもらえば。

ドラクエの馬車の動きを思い出してもらえると。

ここでの先頭固定型のスネーク追従

最悪のスパゲティであることを先にお断り。タイル移動制限付きのスムーズスクロール中に別座標軸のスムーズ移動をやろうとして、しっちゃかめっちゃかに。

下がTickField()関数。この後DrawField()関数が呼ばれ背景タイルが並べられた後、出来上がった仮想画面が実画面に転写されます。(そこまでで1フレーム)

let prex=gPX,prey=gPY;
let scx,scy;				//スクロール座標(1タイル動く間保持する移動量rgコピー向け)
let pre_dir;				//前回の進行方向保持
let f_1st=0;	//座標変化量計算できない瞬間の表示禁止フラグ(方向変化した瞬間のみ1)
let f_turn=0;	//座標変化量計算できない瞬間のパーティ表示禁止フラグ(-方向へ変化して2フレーム程のみ1)
let Qx=[0,0,0,0,0];let Qy=[0,0,0,0,0];let Qa=[0,0,0,0,0];	//パーティメンバの座標・向き
let Vx=[0,0,0,0,0];let Vy=[0,0,0,0,0];				//パーティメンバの移動方向
let dsx=[];let dsy=[];						//パーティメンバの表示座標

//	フィールド進行処理
function TickField(){
	if( gPhase != 0 ){	return;		}

(…ここはコントローラの入力受信…)

				pre_dir=gAngle;

(…ここはイベント・敵エンカウント判定…)

	if( gMoveX != 0 || gMoveY != 0 || gMessage1 ){f_1st=0;	}		//移動中又はメッセ表示中の場合
	if( gKey[100]||gKey[65]||p_axes[14]==1 ){	gAngle=1;gMoveX=-TSIZE;f_tile=1;dx=-1;dy=0;}//左[37]
	else{ if( gKey[104]||gKey[87]||p_axes[12]==1 ){	gAngle=3;gMoveY=-TSIZE;f_tile=1;dx=0;dy=-1;}//上[38]
	else{ if( gKey[102]||gKey[68]||p_axes[15]==1 ){	gAngle=2;gMoveX= TSIZE;f_tile=1;dx=1;dy=0;}//右[39]
	else{ if( gKey[ 98]||gKey[88]||p_axes[13]==1 ){	gAngle=0;gMoveY= TSIZE;f_tile=1;dx=0;dy=1;}//下[40]
	}}}//入力が無い時はそのまま抜けてくる→キー離しでgAngle更新されてしまう
			//マップ遷移1歩目と方向転換したフレームは表示禁止
			if( pre_dir!=gAngle||step_1st==0 ){f_1st=1;}
			//-軸方向へ転換したフレームはキャラの表示禁止
			if( pre_dir!=gAngle&&(gAngle==1||gAngle==3) ){f_turn=1;}

	//	移動後のタイル座標判定
	let	mx = Math.floor( ( gPX + gMoveX ) / TSIZE );		//	移動後のタイル座標X
	let	my = Math.floor( ( gPY + gMoveY ) / TSIZE );		//	移動後のタイル座標Y

	mx += Mapw;							//	マップループ処理X
	mx %= Mapw;							//	マップループ処理X
	my += Maph;							//	マップループ処理Y
	my %= Maph;							//	マップループ処理Y

	if( Kansho(Nowmap,mx,my)==1 ){						//	侵入不可の地形の場合
		gMoveX = 0;							//	移動禁止X
		gMoveY = 0;							//	移動禁止Y
	}else{
		if(f_tile==1){
			//Qx[0]:1人目、Qx[1]:2人目、Qx[2]:3人目、Qx[3]:4人目、Qx[4]:5人目
					Qx[0] = 0;Qy[0] = 0;
			for( i = 4; i > 0; i-- ){
				Qx[i] = dsx[i];//現状の座標コピー
				Qy[i] = dsy[i];
			}

			for( i = 4; i > 0; i-- ){
				Vx[i] = Qx[i] - Qx[i-1];
				Vy[i] = Qy[i] - Qy[i-1];
				Qa[i] = Qa[i-1];

				Vx[i] = TUG.Sign(Vx[i]);
				Vy[i] = TUG.Sign(Vy[i]);
			}
				//ループした時にトチ狂う
				Qa[0]=gAngle;
				f_tile=0;
				f_tileinn=1;
	}	}
}			//ここまではキッチリタイルに乗ってる部分

	if( gMoveX == 0 && gMoveY == 0 ){	f_tileinn=0;	}//これで移動終了

	if(f_tileinn==1){
				var tmpx,tmpy;
		if(Math.abs(scx)>TSIZE-SCROLL-1&&Math.abs(scx)<TSIZE-SCROLL+1){tmpx=TSIZE;}else{tmpx=Math.abs(scx);}
		if(Math.abs(scy)>TSIZE-SCROLL-1&&Math.abs(scy)<TSIZE-SCROLL+1){tmpy=TSIZE;}else{tmpy=Math.abs(scy);}
			for( i = 4; i > 0; i-- ){
				dsx[i] = Qx[i]-Vx[i]*Math.max(tmpx,tmpy)+dx*tmpx;
				dsy[i] = Qy[i]-Vy[i]*Math.max(tmpx,tmpy)+dy*tmpy;
					//		近+		一律
			}
	}

	if(f_turn>2){
			vg_p.clearRect( plx-6*CHRW, ply-6*CHRH, CHRW*13, CHRH*13 );
		for( i = 4; i > 0; i-- ){
			vg_p.drawImage( gImgPlayer,
				(i*2+( gFrame >> 4 & 1 )) * CHRW, Qa[i] * CHRH, CHRW, CHRH,
				plx-dsx[i], ply-dsy[i], CHRW, CHRH );
		}
	}
		f_turn ++;

	gPX += TUG.Sign( gMoveX ) * SCROLL;		//	プレイヤー座標移動X
	gPY += TUG.Sign( gMoveY ) * SCROLL;		//	プレイヤー座標移動Y
	gMoveX -= TUG.Sign( gMoveX ) * SCROLL;			//	移動量消費X
	gMoveY -= TUG.Sign( gMoveY ) * SCROLL;			//	移動量消費Y

	//	マップループ処理
	gPX += ( Mapw * TSIZE );
	gPX %= ( Mapw * TSIZE );
	gPY += ( Maph * TSIZE );
	gPY %= ( Maph * TSIZE );
}

45行目~64行目がパーティメンバ座標・向き・進行方向を保持する部分。変数f_tileで制限してるのは1タイルピッタリの場所だけで行うため。

67行目~78行目がスムーズ移動のための演算。このルーチンは1フレームごとに通るので、1タイルピッタリでない時だけ通るよう制御してます。

また、1タイルピッタリでない時は表示のための座標dsx[]dsy[]のみ更新し、それを座標として記憶するのは4750行目の1タイルピッタリタイミング。

80行目~88行目が実際に仮想画面へ書き込む部分。最初の1、2フレームは演算できないため、非表示にして逃げています。(チラッと変なのが表示されるので)

その逃げるためのフラグが変数f_turn29行目の判定で1にして、描き終わったらインクリメント→それを条件に表示命令を有効化するかどうかを決めています。

まごうこと無きひっどいスパゲティです。

まぁやり方として間違ってんだろうな、てのが結論です。

やはりポリゴンゲームのようにパソコンの頭の中に空間(もしくは2次元平面)を描かせておき、カメラでどこ映すかをプログラムするのがスジがいいようです。

それであれば何も考えることなくグラディウスのオプションと化すので。

別の座標軸で、しかも1タイル移動制限付きスムーズスクロールと辻褄合わそうとすると訳分かんなくなるもの。

これぞ、下手くそプログラム。

コメント

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