自作RPG~ブロック制限付のスムーズスクロール方法を改造してみる

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

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

梅澤嗣佳さんのサンプルを参考に奮闘しておりますが、ちょっと困ったことが起きてしまいました。

ブロック制限付きのスムーズスクロールが自分の条件だと重すぎ

正しいやり方なのか、本当に処理として速いのか分かりませんが、一応解決したのでそのスムーズスクロールの方法について書いておきます。

ただし、梅澤嗣佳さんのソースを前提とした話なので、そちらの解析・勉強した方以外にはチンプンカンプンな内容だと思います。


参考とするのは梅澤嗣佳さんのPRG

上記動画梅澤嗣佳(T.Umezawa)さんのページがこちら。“ソース一式”内のpart23.zipをダウンロードし、解凍後のindex.htmlをブラウザにD&Dするとゲームが走ります。

梅澤嗣佳さんのスムーズスクロール

話題とするのは梅澤嗣佳さんの1ドットスクロールの実現方法。

一般的なRPGでは、方向キーを1回押しただけで1タイル分動く・主人公は画面中央に固定され背景がニュッと1タイル分スクロール←これが1ドット単位で動くとスムーズスクロールとなります。

それを実現するのが上記part23.zip内のmain.js196行目~。

//	フィールド描画処理
function DrawField( g )
{

	let		mx = Math.floor( gPlayerX / TILESIZE );			//	プレイヤーのタイル座標X
	let		my = Math.floor( gPlayerY / TILESIZE );			//	プレイヤーのタイル座標Y

	for( let dy = -SCR_HEIGHT; dy <= SCR_HEIGHT; dy++ ){
		let		ty = my + dy;								//	タイル座標Y
		let		py = ( ty + MAP_HEIGHT ) % MAP_HEIGHT;		//	ループ後タイル座標Y
		for( let dx = -SCR_WIDTH; dx <= SCR_WIDTH; dx++ ){
			let		tx = mx + dx;							//	タイル座標X
			let		px = ( tx + MAP_WIDTH  ) % MAP_WIDTH;	//	ループ後タイル座標X
			DrawTile( g,
			          tx * TILESIZE + WIDTH  / 2 - gPlayerX,
			          ty * TILESIZE + HEIGHT / 2 - gPlayerY,
			          gMap[ py * MAP_WIDTH + px ] );
		}
	}
(…ここはプレイヤ・ステータス表示…)
}

引用:梅澤嗣佳(T.Umezawa)さんソース

こちらは、1タイルごとに動かすための制限部分330行目~。

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

	if( gMoveX != 0 || gMoveY != 0 || gMessage1 ){}				//	移動中又はメッセージ表示中の場合
	else if( gKey[ 37 ] ){	gAngle = 1;	gMoveX = -TILESIZE;	}	//	左
	else if( gKey[ 38 ] ){	gAngle = 3;	gMoveY = -TILESIZE;	}	//	上
	else if( gKey[ 39 ] ){	gAngle = 2;	gMoveX =  TILESIZE;	}	//	右
	else if( gKey[ 40 ] ){	gAngle = 0;	gMoveY =  TILESIZE;	}	//	下

	//	移動後のタイル座標判定
	let		mx = Math.floor( ( gPlayerX + gMoveX ) / TILESIZE );	//	移動後のタイル座標X
	let		my = Math.floor( ( gPlayerY + gMoveY ) / TILESIZE );	//	移動後のタイル座標Y
	mx += MAP_WIDTH;								//	マップループ処理X
	mx %= MAP_WIDTH;								//	マップループ処理X
	my += MAP_HEIGHT;								//	マップループ処理Y
	my %= MAP_HEIGHT;								//	マップループ処理Y
	let		m = gMap[ my * MAP_WIDTH + mx ];		//	タイル番号
	if( m < 3 ){									//	侵入不可の地形の場合
		gMoveX = 0;									//	移動禁止X
		gMoveY = 0;									//	移動禁止Y
	}

	if( Math.abs( gMoveX ) + Math.abs( gMoveY ) == SCROLL ){	//	マス目移動が終わる直前
(…ここは各種イベントの判断・処理…)
	}

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

	//	マップループ処理
	gPlayerX += ( MAP_WIDTH  * TILESIZE );
	gPlayerX %= ( MAP_WIDTH  * TILESIZE );
	gPlayerY += ( MAP_HEIGHT * TILESIZE );
	gPlayerY %= ( MAP_HEIGHT * TILESIZE );
}

引用:梅澤嗣佳(T.Umezawa)さんソース

やってることを日本語で言い表すと、

○この関数DrawField()はタイマーイベントごとに呼び出される。
○1タイマーイベント毎に全画面分(16×15タイル)描き直す。
○タイルサイズ(今は8×8ドット)の分だけ、1ドットづつずらし繰り返す。

⇒つまり、上に1タイル動くのに全画面(16×15タイル)を1ドットずらしで8回描き直さないといけない。1タイル移動中の描き直し回数→16 x 15 x 8回 = 1920回。

使い回す際に出てきた問題

今自分が作る自作RPGでは、

●タイルサイズを32×32ドットに拡張
●1画面タイル数を16×14に拡張スーファミ目指したら負荷減った
●マップデータを背景+地形+キャラクタのレイヤ構成に拡張
●波を表現するためのアニメ付き

⇒1タイル移動の間の描き直し回数が上記の4倍タイルサイズ8→32ドットなので=8000回で更にはxレイヤの数分。さすがに膨大で重すぎとなってしまいました。

スムーズスクロール改造案

そこで考えたのは仮想画面を、実際に表示するcanvas画面より1タイル分大きくし、1フレームごとに仮想画面からの転写位置をずらすというもの。

const	OFS		= TSIZE;				//仮想画面と実画面の座標ズレ
let prex=gPX,prey=gPY;
let scx,scy;				//スクロール座標(1タイル動く間保持する移動量rgコピー向け)
let pre_dir;				//前回の進行方向保持
let f_1st=0;				//座標変化量計算できない瞬間の表示禁止フラグ(方向変化した瞬間のみ1)

//(…)

//タイマーイベント発生時の処理
TUG.onTimer = function( d ){
	//	if( !gMessage1 ){
		while( d-- ){
			gFrame++;					//内部カウンタ加算
		}
	//	}
			TickField();					//フィールド進行処理
			DrawAll();

//仮想画面を実画面へ転送(仮想画面は実画面より、上下左右にOFSだけ大きい)
	if(f_1st==0){
	rg.drawImage( TUG.GR.mCanvas, OFS+scx, OFS+scy,
		TUG.GR.mCanvas.width-2*OFS, TUG.GR.mCanvas.height-2*OFS, 0, 0, gWidth, gHeight*ASP );
	}
	rg.drawImage( vpl, OFS, OFS, vpl.width-2*OFS, vpl.height-2*OFS, 0, 0, gWidth, gHeight*ASP );
	rg.drawImage( vms, 0, 0, vms.width, vms.hegiht, 0, 0, gWidth, gHeight*ASP );
}
改造作業を施し済なので梅澤嗣佳さんのソースから変数名や関数の引数等変化してます。

21行目が仮想画面を実canvasに転写する命令。

転写元の仮想画面の座標に変数scxscyを足し算し、参照先だけを変化させます。そのscxscyを演算するのが下の改造版DrawField()関数。

仮想画面への転写位置を表す変数追加

この関数の呼び出しを1タイマーイベントごと→1フレームごとに変更し、scxscyを演算します。

演算方法は、プレイヤ座標をタイルの大きさで割った剰余。ただし、前回のプレイヤ座標と比較し、左や上に移動した場合は±を修正します。

剰余が0になったタイミングで1タイル前の背景に戻ってしまうのでは?と不安になりますが、一応大丈夫。

処理の順番が「先にDrawField()関数実行→その後仮想画面の転写を行う」ので転写のタイミング時は既に1タイル先に進んだ仮想画面が出来上がっています。

ただし、上の改良では対処できない“方向転換時”に対応するための仕掛けがif文中にある変数f_1st。これに関しては次項で。

//	フィールド描画処理
function DrawField(){
var x,y;
var pmx=0,pmy=0;
var ptn=0;
				scx=(gPX-TSIZE/2) % TSIZE;
				scy=(gPY-TSIZE/2) % TSIZE;

		if(scx==0&&scy==0){
			f_1st=0;
		}else{
			if(gPX-prex<0){scx=-1*(TSIZE-scx);}
			if(gPY-prey<0){scy=-1*(TSIZE-scy);}
			if(prex!=gPX){prex=gPX;}
			if(prey!=gPY){prey=gPY;}
		}

	if(scx==0&&scy==0){
		let mx=Math.floor( (gPX-SCR_W/2)/TSIZE );	//プタイル座標X(仮想画面座標)
		let my=Math.floor( (gPY-SCR_H/2)/TSIZE );	//プタイル座標Y(仮想画面座標)
		if(gFrame >> 6 & 1){	ptn=0;	}else{	ptn=1;}

		for( y = -1; y < SCR_H/TSIZE+1; y++ ){
			let	ty = my + y;
			let	py = ( ty + MAP_HEIGHT ) % MAP_HEIGHT;
		for( x = -1; x < SCR_W/TSIZE+1; x++ ){
			let	tx = mx + x;
			let	px = ( tx + MAP_WIDTH  ) % MAP_WIDTH;
			DrawTile( -TSIZE/2+x*TSIZE, -TSIZE/2+y*TSIZE, px, py, ptn );
		}}
	}
(…ここはプレイヤ・ステータス表示…)
}
改造作業を施し済なので梅澤嗣佳さんのソースから変数名や関数の引数等変化してます。

前回フレーム時との比較判定できない瞬間は転写しない

上のソースでの問題は、進行方向が変化した際に前回座標prexpreyとの比較が上手く作用しないこと。

左→上・下→右とか操作した場合、1213行目が意味をなさなくなり、一瞬チラッと1タイル戻った以前の座標が表示されます。

そこで行うのが変数f_1st。前回の方向を変数pre_dirに記憶しておき、進行方向が変化した最初の1フレームだけ1が代入されるようにします。下ソースの14行目。

それで「f_1st1の時は仮想画面を転写しない上記タイマーイベント20行目」とすれば、特に違和感なくスムーズスクロールが実現しました。名付けて“最初の1フレーム表示しない戦法”。

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

				pre_dir=gAngle;
	if( gMoveX != 0 || gMoveY != 0 || gMessage1 ){f_1st=0;}		//移動中又はメッセ表示中の場合
	else if( gKey[ 37 ] ){	gAngle = 1;	gMoveX = -TSIZE;	}	//	左
	else if( gKey[ 38 ] ){	gAngle = 3;	gMoveY = -TSIZE;	}	//	上
	else if( gKey[ 39 ] ){	gAngle = 2;	gMoveX =  TSIZE;	}	//	右
	else if( gKey[ 40 ] ){	gAngle = 0;	gMoveY =  TSIZE;	}	//	下

			if( pre_dir!=gAngle ){f_1st=1;}	//変化したフレームは表示禁止

	//	移動後のタイル座標判定
	let		mx = Math.floor( ( gPX + gMoveX ) / TSIZE );	//	移動後のタイル座標X
	let		my = Math.floor( ( gPY + gMoveY ) / TSIZE );	//	移動後のタイル座標Y
	mx += MAP_WIDTH;							//	マップループ処理X
	mx %= MAP_WIDTH;							//	マップループ処理X
	my += MAP_HEIGHT;							//	マップループ処理Y
	my %= MAP_HEIGHT;							//	マップループ処理Y
	let		m = gMap[ my * MAP_WIDTH + mx ];			//	タイル番号

	if( Kansho(mx,my)==1 ){							//	侵入不可の地形の場合
		gMoveX = 0;							//	移動禁止X
		gMoveY = 0;							//	移動禁止Y
	}

	if( Math.abs( gMoveX ) + Math.abs( gMoveY ) == SCROLL ){	//	マス目移動が終わる直前
(…ここは各種イベントの判断・処理…)
	}

	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 += ( MAP_WIDTH  * TSIZE );
	gPX %= ( MAP_WIDTH  * TSIZE );
	gPY += ( MAP_HEIGHT * TSIZE );
	gPY %= ( MAP_HEIGHT * TSIZE );
}
改造作業を施し済なので梅澤嗣佳さんのソースから変数名や関数の引数等変化してます。

大幅に減った描き直し回数

結局、1タイルのスムーズスクロールに対し、仮想画面の描き直し1回で済む方法に変化しました。タイル区切りピッタリの位置で1回だけ。

仮想画面から実画面への転写は元々毎フレーム行ってた処理。同じ座標を転写してたものを1ドットずらしで転写するように変えただけ。

その座標ずらしが処理速度に影響するのか分かりませんが、何にしてもタイル移動に数千回描き直してたものが1回に減り、毎フレームごとの転写は座標変わるだけ。

まぁ、負荷は大幅に軽くなりました。

出来上がったスムーズスクロール実例

これが出来上がったスムーズスクロールの状況。変なソースをアップしてたので動かない場合はスーパーリロード(Ctrl+F5)してみて下さい。

これが“最初の1フレーム表示しない戦法”の結果。特にガクガクは感じないし、方向転換した際の描画抜けも全く気にならない感じになりました。

※直進時にもこの“最初の1フレーム表示しない戦法”を使うと引っ掛かりが目立ちました。この“方向転換時だけ”ってのは苦肉の策。目立たなくてラッキーでした。

上手くいかん、上手くいかんと悩みながら作る自作RPG。

ドラクエ作るって大変なのね。

コメント

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