ポク太郎です。自作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ツークルやウディタってことなのかな。結局はそれ作る仕事やん!
それがゲームプログラミングの道を歩み始めた爺の感想です。
コメント