ポク太郎です。自作RPG製作中です。
そう悟った爺。その根拠となるコトガラですが、“ハードコーティング”がめちゃめちゃ大変で、仕様が固まりません。
その言い訳的な話と、とりあえず動くようにした「ドラクエの一定時間ごとに1文字づつ進むセリフ表示」の例です。
ハードコーティングとは何ぞ
⇒もし、スイッチで切り替えられるようにすれば、1種類ですべてまかなえます。
同じく、パソコンのマザーボード。多種多様な機能実現のため多種多様なバージョンを作ると→マザーボードが多種類出来上がってしまいます。
⇒もし、BIOSの設定値で動きを制御すれば、1種類ですべてまかなえます。
コントローラ、マザーボード作るにはそれぞれ向けの設計・製造設備・何やかやが必要ですが、“操作で切り替えられる”ようにしておくことで、何にでも対応できるように。
これが“ハードコーティング”。
同じ話で、ゲームのプログラム。王様だけでなく、竜王との会話も追加したい場合にプログラムをいじると→王様との会話ルーチンと竜王との会話ルーチンの2種類が出来上がってしまいます。
⇒もし、データ部分(つまりは単なるテキストファイル編集)での“発言者”の設定枠に従うようにしておけば、1種類ですべてまかなえます。
このように、“より簡単に切り替える”ために、“変更に労力が掛かる方”をあらかじめ作りこんでおくことを“ハードコーティング”。
だから“ハード”って部分はハードウェアの意味でなく、作り替えに“厳しい”方て意味が正解なのかな。
「プリント基板作り替え・プログラムのソースいじくり」より、「スイッチで切り替え・BIOSで切り替え・データ(単なるテキストファイル編集)」で切り替える方が遥かに簡単。
そういった作り方をしておけば、「ゲームを作る」が「プログラム作る」と「ゲームのデータ作る」部分に完全に分離され、分業化もできるし何より混乱することが無くなります。
で、ジジイが「苦行だ」と言ってるのはココ。
ハードコーティングのために必要なのは取り決め。
どのようなフォーマットのデータを渡せば自分のやりたいこと全部実現可能か、がなかなか定まらず、どっちが鶏でどっちが卵か分からなくポイント。
例はドラクエの1文字づつ進むセリフデータ
ここでのハードコーティングの対象は、王様・竜王のデータ共通化でなく、改行・入力待ち・Yes/No質問等。セリフデータの形式を取り決め、これらを1行のデータとして表すことを図ります。
例1 | もょもと… おきなさい もょもと…★↓きょうは あなたの 16回目の誕生日。わたしはこれまで あなたを 立派な大人にするために 懸命に育ててきました。◆遂にきょうは 王様に呼ばれたのよ さぁ起きなさい。 |
例2 | 宿屋へようこそ。◆いっぱく pny} ですが お泊りですか?◆◇□それでは ごゆっくり どうぞ。□■また おこしくださいませ。 ご無理なさいませぬよう。■ |
例3 | この通りじゃ たのむ!●おねがいじゃ もょもと わしの願いを 叶えてくれ!◆◇□さすがは もょもと じゃな。□■※■ |
仕様 | ◆…改行。 ★…一時停止しMキー待ち。 ↓…画面クリア。 ◇と□□■■…Yes/No窓表示し返答待ち。Yes選択後は□□間、No選択後は■■間。 ●と□□■■と※…※でない方を選ばないと、永遠に●から繰り返し。 |
ドラクエの1文字づつ進むセリフJavaScript
1文字づつ進む台詞ソースコード
// グローバル変数 let isGameStarted = false; let gMsg; //入力されるメッセ let txtNow = ''; let txtfullNow = ''; let chrid = 0; let chrON = false; //表示する文字:true、非表示:false let waitInput = false; //Sメッセ表示停止中で入力待ちtrue let markON = true; //入力待ちの点滅カーソル表示:true、消:false let f_ynwin = false; //YN窓表示中:true、閉じてる:false let ynyns = 0; let YCmsg = ""; let NCmsg = ""; // 選択肢表示前のメッセージを別途管理するための変数 let preCmsg = ""; // YN窓が表示された後か追跡するフラグ let isAfterYN = false; let gFrame = 0; const FPS = 60; const gFmemo = new Array(2); const lf='\n'; let ctrlph=0; //Sメッセ窓表示中 let gMsgcomp=0; //Sメッセ窓の表示がすべて完了した状態 let f_stepmsg=0; //Sメッセ窓の表示中 const gMsgsp=40; //Sメッセ間隔[ms] 遅い75 速い35 let msgsp=gMsgsp; //Sメッセ間隔[ms]調整用 // 時間[ms]経過したらtrueを返す function Timing(mode, t = 0) { if (mode === 0) { gFmemo[1] = gFrame; return false; } else { if (gFrame > gFmemo[1] + FPS * t / 1000) { return true; } } } function chkLMptn(dt){ // 無限パターン●がないかチェックする // この通りじゃ たのむ!●おねがいじゃ nm1} わしの願いを 叶えてくれ!◇□さすがは nm1} じゃな。□■※■cond}したいんじゃ。 var mode;var target; if((kw(dt,'●') && !kw(dt,'※')) || !kw(dt,'●') && kw(dt,'※')){ return '【エラー】セリフのフォーマットが異常です。●※は対で使用されます。';} if(kw(dt,'●')){mode='mugen';}else{mode='normal';} if(mode=='mugen'){ target=nthf(nthf(dt,'◇',1),'●',2); return replaceall(dt,'※',target+'◇') }else{ return dt; } } function makeYNptn(dt,mode){ // YNパターンとして作り直す var tmp,tmp2,tmp3; if(kw(dt,'◎')){ return nthf(dt,'◎',1)+nthf(dt,'◎',2)+nthf(dt,'◎',3); }else{ if(mode=='□'){ tmp='■'+nthf(dt,'■',2)+'■'; tmp2=replaceall(dt,tmp,''); } else{ tmp='□'+nthf(dt,'□',2)+'□'; tmp2=replaceall(dt,tmp,''); } tmp3=replaceall(tmp2,mode,''); return tmp3; } } function EnableYN(){ // YN窓決定された f_ynwin = false;//YN窓閉じる条件 ctrlph=0;//////////////////// // ※があるのがどっちか調べる if(!kw(YCmsg,'※') && !kw(NCmsg,'※')){ if(ynyns==0){ showSmsg(YCmsg); } else { showSmsg(NCmsg); } }else{ const aftJmsg = nthf(gMsg,'●',2); if(kw(YCmsg,'※')){ if(ynyns==0){ showSmsg('●'+aftJmsg); } else { showSmsg(NCmsg); } } if(kw(NCmsg,'※')){ if(ynyns==0){ showSmsg(YCmsg); } else { showSmsg('●'+aftJmsg); } } } } function CHKpad( k ){ if (waitInput && (k === 'm' || k === 'M')) { waitInput = false; Timing(0); markON = true; } if (f_ynwin) { if (k === '8') { ynyns = 0; } else if (k === '2') { ynyns = 1; } else if (k === 'm' || k === 'M') { // txtNow += lf; EnableYN(); } } } function SetMsg( v1 ){ // Sメッセ開始時の準備 if(f_stepmsg==1){ return; }else{ f_stepmsg=1; } /////// // v1=chkLMptn(v1); const Ymsg = makeYNptn(v1,'□'); const Nmsg = makeYNptn(v1,'■'); // メッセージを分離 if(kw(Ymsg,'◇')){ preCmsg = nthf(Ymsg,'◇',1); //文章内にYN窓の◇が1つだけ→preCmsg終了時がYN窓出すタイミング YCmsg = nthf(Ymsg,'◇',2); NCmsg = nthf(Nmsg,'◇',2); } else { preCmsg = Ymsg; YCmsg = ''; NCmsg = ''; } // 画面をクリアしてメッセージ表示を開始 isAfterYN = false; // 新しいメッセージでフラグをリセット txtNow = ''; f_stepmsg=1; //////////////////////////// showSmsg(preCmsg); if (!isGameStarted) { gameLoop(); isGameStarted = true; } } const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const startButton = document.getElementById('startButton'); const MessageInput = document.getElementById('MessageInput'); startButton.addEventListener('click', () => { gMsg = MessageInput.value; f_stepmsg=0; //////////////////////////// SetMsg( gMsg ); }); // メッセージ表示関連 function showSmsg(msg) { txtfullNow = msg; chrid = 0; Timing(0); chrON = true; waitInput = false; markON = true; } function updateSmsg() { if (!chrON && !waitInput && !f_ynwin) { return; } if (waitInput) { if (Timing(1, 100)) { markON = !markON; Timing(0); } return; } if (f_ynwin) { return; } if (Timing(1, msgsp)) { //oto('ok1'); /////////////プツプツ音鳴らす if (chrid >= txtfullNow.length) { chrON = false; // Sメッセ内容すべて表示された gMsgcomp = 1; ////////////////////// ctrlph = 1; ////////////////////// // 最初のメッセージが完全に表示された場合にのみ選択肢を表示 // preCmsg終了、かつ、NCmsgが空白でない if (txtfullNow === preCmsg && (YCmsg !== "" || NCmsg !== "")) { f_ynwin = true;//これがYN窓開いている条件 ctrlph=-1;//////////////////////////// isAfterYN = true; // YN窓が表示された後にフラグを立てる } return; } const lines = txtNow.split(lf); const lastLine = lines[lines.length - 1]; const isAfterAutoNewline = lastLine.length >= 33; const nextChar = txtfullNow[chrid]; // 全角空白の自動スキップ const isBeginningOfLine = (txtNow.length === 0 || txtNow.endsWith(lf)); if ((isBeginningOfLine || isAfterAutoNewline) && nextChar === ' ') { chrid++; Timing(0); return; } switch (nextChar) { case '★': waitInput = true; chrid++; txtNow += lf; Timing(0); return; case '◆': txtNow += lf; chrid++; Timing(0); return; case '↓': txtNow = ''; chrid++; Timing(0); return; case '◇': chrON = false; f_ynwin = true; chrid++; // ◇をスキップ return; case '●': if (isAfterYN) { txtNow = ''; // YN窓の後なら画面クリア }else{ txtNow += lf; } chrid++; Timing(0); return; default: if (isAfterAutoNewline) { txtNow += lf; } txtNow += nextChar; chrid++; Timing(0); return; } } } function drawSmsg(ctx) { if (chrON || waitInput || f_ynwin || txtNow !== '') { const boxX = 50; const boxY = 150; const boxWidth = 500; const boxHeight = 200; ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(boxX, boxY, boxWidth, boxHeight); ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 2; ctx.strokeRect(boxX, boxY, boxWidth, boxHeight); ctx.fillStyle = '#FFFFFF'; ctx.font = '14px sans-serif'; const lines = txtNow.split(lf); let y = boxY + 30; for (const line of lines) { ctx.fillText(line, boxX + 20, y); y += 30; } if (waitInput && markON) { //ボタンを促す点滅表示 const markX = boxX + boxWidth/2; const markY = boxY + boxHeight - 20; ctx.fillStyle = '#FFFFFF'; ctx.font = '14px sans-serif'; ctx.fillText('▽', markX, markY); } if (f_ynwin) { //YN窓表示 const choiceBoxX = boxX + 300; const choiceBoxY = boxY - 100; const choiceWidth = 150; const choiceHeight = 80; //ynyns=0; /////////////////////////// //DrawYN; /////////////////////////// ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(choiceBoxX, choiceBoxY, choiceWidth, choiceHeight); ctx.strokeStyle = '#FFFFFF'; ctx.strokeRect(choiceBoxX, choiceBoxY, choiceWidth, choiceHeight); ctx.fillStyle = '#FFFFFF'; ctx.font = '14px sans-serif'; ctx.fillText('はい', choiceBoxX + 20, choiceBoxY + 30); ctx.fillText('いいえ', choiceBoxX + 20, choiceBoxY + 60); const cursorY = (ynyns === 0) ? choiceBoxY + 30 : choiceBoxY + 60; ctx.fillText('→', choiceBoxX, cursorY); } } } // メインのゲームループ function gameLoop() { gFrame++; updateSmsg(); ctx.clearRect(0, 0, canvas.width, canvas.height); drawSmsg(ctx); requestAnimationFrame(gameLoop); } // イベントリスナー document.addEventListener('keydown', (event) => { CHKpad( event.key ); });
中で使用する汎用の関数
function replaceall(dt,moji,moji2){ //指定の文字列に全置換 dt = dt.split(moji); return dt.join(moji2); } //指定のフィールドデータの総数を返す function nthc(dt,spr) { var tmp; var r=0; if (dt.indexOf(spr) == -1){ r=0; } else { tmp=dt.split(spr); r=tmp.length; } return r; } //指定のフィールドデータを取り出す function nthf(dt,spr,n) { var tmp; var rt=''; if (dt.indexOf(spr) == -1){ rt=dt; } else { tmp=dt.split(spr); if (n<1||n>tmp.length){ rt=''; } else { rt=tmp[n-1]; } } return rt; } //文字列の中に指定の文字あるかどうか判断 function kw(str, keyword){ if( str.indexOf(keyword) != -1 ){ return true; } else { return false; } }
ジジイの悟り「ゲーム製作とは苦行である」。ハードコーティングの仕様が固まらないってのも一大要因ですが、もう一つ。
通常何が通常なのかは分かりませんが、プログラムとは演算の対象が存在して、それに対して処理を考えるもの。例えば、測定器が吐き出すデータ列とか、こんなあんな項目を持つ伝票とか。
ゲームのプログラミングとは、その演算対象となるゲームのデータがまだ存在しない中で、自分の成すべきことを想定して作らないといけないもの。
でも、作るためには多少のサンプルデータ準備しながら、プログラム作りながら…。で、結局→「あっりゃ~!これじゃあ実現できんやん!」→全部作り直し。
つまり、ここでも「どっちが鶏でどっちが卵か分からない」状態で作業。それがゲームのプログラミング。厳しいわ。ドラクエという完璧なお手本が存在してもこのザマなんだ。
やっぱ、ゲーム系のソフトハウスって、その辺に相応なノウハウを持ってんでしょうな。それがまさにRPGツークルやウディタってことなのかな。結局はそれ作る仕事やん!
それがゲームプログラミングの道を歩み始めた爺の感想です。
コメント