ポク太郎です。自作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。
ドラクエ作るって大変なのね。
コメント