ポク太郎です。自作RPG製作中です。
梅澤嗣佳さんのサンプルを参考に奮闘しておりますが、ちょっと困ったことが起きてしまいました。
正しいやり方なのか、本当に処理として速いのか分かりませんが、一応解決したのでそのスムーズスクロールの方法について書いておきます。
ただし、梅澤嗣佳さんのソースを前提とした話なので、そちらの解析・勉強した方以外にはチンプンカンプンな内容だと思います。
参考とするのは梅澤嗣佳さんのPRG
梅澤嗣佳さんのスムーズスクロール
話題とするのは梅澤嗣佳さんの1ドットスクロールの実現方法。
一般的なRPGでは、方向キーを1回押しただけで1タイル分動く・主人公は画面中央に固定され背景がニュッと1タイル分スクロール←これが1ドット単位で動くとスムーズスクロールとなります。
それを実現するのが上記part23.zip内のmain.js-196行目~。
// フィールド描画処理
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 ] );
}
}
(…ここはプレイヤ・ステータス表示…)
}
こちらは、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 );
}
やってることを日本語で言い表すと、
○1タイマーイベント毎に全画面分(16×15タイル)描き直す。
○タイルサイズ(今は8×8ドット)の分だけ、1ドットづつずらし繰り返す。
⇒つまり、上に1タイル動くのに全画面(16×15タイル)を1ドットずらしで8回描き直さないといけない。1タイル移動中の描き直し回数→16 x 15 x 8回 = 1920回。
使い回す際に出てきた問題
今自分が作る自作RPGでは、
●1画面タイル数を16×14に拡張スーファミ目指したら負荷減った
●マップデータを背景+地形+キャラクタのレイヤ構成に拡張
●波を表現するためのアニメ付き
⇒1タイル移動の間の描き直し回数が上記の4倍タイルサイズ8→32ドットなので=8000回で更にはxレイヤの数分。さすがに膨大で重すぎとなってしまいました。
スムーズスクロール改造案
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に転写する命令。
転写元の仮想画面の座標に変数scx、scyを足し算し、参照先だけを変化させます。そのscx、scyを演算するのが下の改造版DrawField()関数。
仮想画面への転写位置を表す変数追加
この関数の呼び出しを1タイマーイベントごと→1フレームごとに変更し、scx、scyを演算します。
演算方法は、プレイヤ座標をタイルの大きさで割った剰余。ただし、前回のプレイヤ座標と比較し、左や上に移動した場合は±を修正します。
剰余が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 );
}}
}
(…ここはプレイヤ・ステータス表示…)
}
前回フレーム時との比較判定できない瞬間は転写しない
上のソースでの問題は、進行方向が変化した際に前回座標prex、preyとの比較が上手く作用しないこと。
左→上・下→右とか操作した場合、12、13行目が意味をなさなくなり、一瞬チラッと1タイル戻った以前の座標が表示されます。
そこで行うのが変数f_1st。前回の方向を変数pre_dirに記憶しておき、進行方向が変化した最初の1フレームだけ1が代入されるようにします。下ソースの14行目。
それで「f_1st=1の時は仮想画面を転写しない上記タイマーイベント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回に減り、毎フレームごとの転写は座標変わるだけ。
まぁ、負荷は大幅に軽くなりました。
出来上がったスムーズスクロール実例
これが“最初の1フレーム表示しない戦法”の結果。特にガクガクは感じないし、方向転換した際の描画抜けも全く気にならない感じになりました。
※直進時にもこの“最初の1フレーム表示しない戦法”を使うと引っ掛かりが目立ちました。この“方向転換時だけ”ってのは苦肉の策。目立たなくてラッキーでした。
上手くいかん、上手くいかんと悩みながら作る自作RPG。
ドラクエ作るって大変なのね。
コメント