yumeの日記: C#学習 11 Console Adventure#2 3
コメントありがとうございます。
Console Adventure
前回より:
・メソッドを分割
・コマンド入力(数値入力で、入力したページへ)
・コマンドに入力された数値から、選ばれたテキストをリソースから読み出し。
・テキストを描画
まで。
基本的な形(選択肢があって、選択して進む)はこれで完成したかもしれない。
現状はゲームブック風の形になっているから、いつでも任意のページに飛べてしまうが。
もし選択肢を制限するとしたら、テキスト末尾の[command]に飛び先のリストや選択肢の数を書いておいて、テキストを読み込むたびにその命令も読み出す。などすればよいか。
コンソール上で上下カーソルなどで、表示される文字列をフォーカスする、という仕組みが必要になりそう。
ストーリーテキスト、初めは多少整形すればいけるだろう、と思ったが:
・もともとがTRPG用のテキストなので、描写以外の文がかなり多い。
>描写以外の文を描写文として統一する
・版権的には大丈夫か?
>元がクトゥルフ神話モノなので、大丈夫だろうが、一応固有の要素を置き換えていく。
・分岐処理は新たに書く。また分岐によって新しいテキストが必要
などありやはり地道に書いていく必要はありそうだ。
さしあたって、分岐処理とEND処理に必要最低限なだけ入れた。この文は後程修正される可能性が高い。
元にしたシナリオは、TRPGらしい要素をほとんど使わないイロモノであったので、それでも作業としてはそれほど大きくはない。
このシナリオは「選択肢のみ」のノベルゲームとして作って、余力があればさらに別の、フラグや能力値のような要素があるアドベンチャー風味のシナリオもできるとよいが、それはまた後の話か。
以下は今日書いた分。
namespace Console_Adventure
{
class SelectAdventure
{
static string txt = Resources.yume_1;
static string[] bodyText = txt.Split("\r\n");
static void Main()
{
const string command = "[command]";
const string jumpStart = "[jump start]";
const string gameEnd = "[game end]";
int result = 0;
for (int i = 0; i < bodyText.Length; i++)
{
result = bodyText[i] switch
{
command => Command(),
jumpStart => TextJump(ref i),
gameEnd => GameEnd(),
_ => TextRead(i),
};
if (result == 1) break;
}
if (result == 1) Main();
else Console.ReadKey();
}
static void LoadText(int number)
{
/*
* テキストを読み込む。
*/
System.Resources.ResourceManager resource = Properties.Resources.ResourceManager;
txt = resource.GetString("yume_" + number);
if(txt == null)
{
Console.WriteLine("ページ無し");
Command();
}
bodyText = txt.Split("\r\n");
Console.WriteLine("");
Console.WriteLine("――――――――――――");
Console.WriteLine("");
}
static int TextRead(int i)
{
/*
* テキストを一行読み込む
* キー入力で一行送る
*/
Console.WriteLine(bodyText[i]);
Console.ReadKey();
return 0;
}
static int TextJump(ref int i)
{
const string jumpEnd = "[jump end]";
i++;
while (bodyText[i] != jumpEnd)
{
Console.WriteLine(bodyText[i]);
i++;
}
Console.ReadKey();
return 0;
}
static int Command()
{
/*
* コマンド(数値)を入力し、数値に応じたページへ飛ぶ。
*/
Console.Write("コマンド:");
string inputCommand = Console.ReadLine();
try
{
int inputNum = int.Parse(inputCommand);
LoadText(inputNum);
}
catch
{
if (inputCommand == "quit") GameEnd();
else Command();
}
return 1;
}
static int GameEnd()
{
//ゲーム終了処理。とりあえず終わるだけ。
Environment.Exit(0);
return 0;
}
}
}
一つバグがある (スコア:0)
今のソースコードは、ゲームを進めていくと突然ゲームが落ちるバグがあります。
原因は Main() の内部で Main() を呼び出しているからです。
ある関数Xの内部から関数X自身を呼び出す方法を「再帰呼び出し」といいますが
再帰呼び出しが出来る回数には上限があります
たとえば
static int count = 0;
static void Main()
{
count++;
Console.WriteLine("count: {0}", count);
Main();
}
これを実行するとプログラムがクラッシュします
多分デフォルトのC#環境なら count の値が10000ぐらいになると落ちると思います
(手元にC#環境が無いので具体的な数値はわかりません)
そしてプログラムが複雑になるとより早くクラッシュするようになります。
手元にC#環境が無いので実際にどうなるかは試していませんが多分
static int count = 0;
static void Main()
{
string txt = Resources.yume_1;
string[] bodyText = txt.Split("\r\n");
count++;
Console.WriteLine("count: {0} {1}", count, bodyText[0]);
Main();
}
を実行したら、より早くプログラムが落ちるはずです。
大規模なプログラムなら Mainを数回再帰呼び出しするだけでクラッシュすることもありえます。
そのため、一般に Main()を再帰呼び出しすることは禁じ手と考えたほうが良いです。
Re: (スコア:0)
末尾呼び出しや末尾再帰は、処理系によっては最適化されて、ただのジャンプやループに変換されるから、必ずしもスタックオーバーフローが起きるわけではない。C#の場合はx64でリリースビルドなら末尾最適化があるらしい。gcc(C++)だったら、末尾呼び出しの最適化があったので、それを利用したプログラムを作ったことはある。
まあ、使える条件がかなり限定的なので、普通は使わないほうが良いと思うが、使える場合はコードがシンプルになり、実行速度も速い。
Re: (スコア:0)
再帰はまあ使えばいいと思う。メイン関数を再帰呼び出しするのはやめたほうがいいと思う。
単純に紛らわしいしいわゆる初期化処理を何度も実行するはめになる。