今回はコンソールアプリ、つまりテキスト(printf)だけで出来ている戦闘シミュレータなのでプロジェクト自体も別に作りました。 モンスター名や特性もまだ決めてませんので(実はグラフィックがまだ未完成)仮のものとなっています。
Level10ぐらいでボスと対決するぐらいのつもりで仮パラメータは入れましたが実ゲームでバランス調整しないと、本当のところは良く分かりません。あくまで仮の値って事でですね。
まぁ、それが簡単なシミュレータの限界でもあります。
それでもパラメータの初期値決めやレベルアップ・システムの検証、ダメージ計算式の妥当性など検証にはすごく役立ちます。
あと、シミュレータで作った関数はRPGの本編へ組み込みで使えるので完全には損はしませんが、連続処理といちいちメッセージループに戻る処理では組み方が違うので完全にはそのまま使えません。
ソースコードは1つのファイルのみで関数も18個と多めですが一気に解説します。
[main.cpp]
●先頭~ヘッダのインクルードまで
ヘッダですが、まぁいっぱい標準関数とか非標準関数を使ってしまいました。
そのためLinuxなどでは動きません。
//----------------------------------------------------------------------
// 簡単RPG(RPG講座初級編) 戦闘シミュレータ(コンソール)
//----------------------------------------------------------------------
// 作った人:ソフト屋 巣 http://softyasu.net/
//----------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
#include
//----------------------------------------------------------------------
// 定数
//----------------------------------------------------------------------
DEBUG_PRINTは、デバックのためのスイッチです。0でOFF,1でON
INI_???は、初期パラメータです。
LEVEL_MAXは、想定最大レベルですが、その後のレベルアップも可能にはなっています。
TRUE/FALSEは、真偽の値を定義しています。
MAGIC_???は、魔法の種別です。
ENEMY_???は、敵の種別です。
// デバッグSW 0でOFF
#define DEBUG_PRINT (0)
// 初期パラメータ
#define INI_HP_MAX (10)
#define INI_MP_MAX (5)
#define INI_ATTACK (9)
#define INI_DEFENSE (11)
// 最大値
#define LEVEL_MAX (30)
// 戻り値の定義。
#define FALSE 0
#define TRUE 1
// まほうの定義
enum {
MAGIC_FIRE, //ほのおの攻撃魔法
MAGIC_HEAL, //回復魔法
MAGIC_MAX_NUMS //まほう最大数
};
// 敵の種類
enum {
ENEMY_FIELD_ZAKO1, //フィールド雑魚1
ENEMY_FIELD_ZAKO2, //フィールド雑魚2
ENEMY_DUNGEON_ZAKO1, //ダンジョン雑魚1
ENEMY_DUNGEON_ZAKO2, //ダンジョン雑魚2
ENEMY_BOSS, //ボス
ENEMY_MAX,
};
定数の続きです。
ENEMY_ATK_TYPE_???は、敵の攻撃タイプです。
STATE_???は、戦闘処理の状態です。
CMD_???は、戦闘コマンドの種類です。
LIVE_???は、生存チェックのbitフラグです。
// 攻撃タイプ
enum {
ENEMY_ATK_TYPE_NORMAL, //攻撃は打撃のみ。
ENEMY_ATK_TYPE_FIRE, //「ほのお」の攻撃も含めるタイプ。
ENEMY_ATK_TYPE_BIGWIND, //「きょうれつなかぜ」の攻撃も含めるタイプ。
ENEMY_ATK_TYPE_BOSS, //ボスの特殊攻撃
};
// 状態
enum {
STATE_LEVEL, //レベル決定
STATE_ENEMY, //敵選択
STATE_BATTLE, //バトル
};
// 戦闘コマンド
enum {
CMD_ATTACK, //攻撃
CMD_ESCAPE, //にげる
CMD_FIRE, //ほのう
CMD_HEAL, //かいふく
};
// 生存チェックのbitフラグ
#define LIVE_PLAYER (1level = 1; //レベル
playerParam->expp = 0; //経験値(Experience point)
playerParam->hp = INI_HP_MAX; //HP
playerParam->hp_max = INI_HP_MAX; //HPMAX
playerParam->mp = INI_MP_MAX; //MP
playerParam->mp_max = INI_MP_MAX; //MPMAX
playerParam->attack = INI_ATTACK; //攻撃力
playerParam->def = INI_DEFENSE; //防御力
}
与えられた表示文字列を表示して指定範囲の数値の文字を入力して、その数値を持ち帰ります。
指定範囲外の数値や文字は受け付けません。
_getch()でエコーバックのない入力を行っています。
//----------------------------------------------------------------------
// 範囲の数値入力
//----------------------------------------------------------------------
static int inputNum(char *str,int under,int upper)
{
int key = -1;
// メッセージを表示
printf( "%s", str );
// 正しく入力されるまで繰り返す。
while( 1 ) {
// キー入力して数値化する。
key = _getch() - '0';
// 範囲内?
if( (underlevel + 1;
while( playerParam->expp >= getLevelupExpp(nextlevel) ) {
// レベルアップした
bLevelup = TRUE;
// レベルアップ値の計算
int hp_up = LevelScaler(nextlevel,2,12);
int mp_up = LevelScaler(nextlevel,1,15);
int atk_up = LevelScaler(nextlevel,4,5);
int def_up = LevelScaler(nextlevel,5,6);
// 表示
if( bView ) {
// レベルアップの情報を表示
printf( "レベルが%dにあがった 最大hpが+%d、最大mpが+%d、攻撃力が+%d、防御力が+%dあがった\n", nextlevel,hp_up,mp_up,atk_up,def_up );
// まほうを覚えた!
for( int i=0; ilevel = nextlevel; //レベル
playerParam->hp_max += hp_up; //HPMAX
playerParam->mp_max += mp_up; //MP
playerParam->attack += atk_up; //攻撃力
playerParam->def += def_up; //防御力
// 次のレベル
nextlevel = playerParam->level + 1;
}
// レベルアップの有無を持ち帰る。
return bLevelup;
}
レベルアップに必要な経験値を計算で求めます。
レベルアップに必要な経験値が指数関数的な理由は弱い雑魚いじめで簡単にレベルアップされるのを防ぐためです。
なお、実際にはテーブル化して微調整できるようにするとバランス調整で困ったときに助かりますので検討しましょう。
まぁ、敵の経験値でも調整できるので必須ではないですが、ここらへんのさじ加減を間違うとレベル差があるとレベルアップできない経験値稼ぎが辛いゲームと言われるようになります。
//----------------------------------------------------------------------
// そのレベルになるのに必要な経験値を得る。
// 実際のゲームだと微調整のために、この値をテーブル化する事が多いです。
//----------------------------------------------------------------------
static int getLevelupExpp(int level)
{
// レベル1は経験値不要
if( level == 1 ) {
return 0;
} else {
// 経験値はマジックナンバーとlevelのn乗(適度になるように調整)
#define LEVEL_MAGIC_NO 6
level -= 1; //補正
int expp = (int)pow((double)level,2.8) * LEVEL_MAGIC_NO;
return expp;
}
}
主人公のパラメータを表示します。
//----------------------------------------------------------------------
// 主人公パラメータの表示
//--------- -------------------------------------------------------------
static void ViewPlayerParam(PlayerParam_t *playerParam)
{
printf( "level=%d 経験値=%d hp=%d/%d mp=%d/%d 攻撃力=%d 防御力=%d\n"
,playerParam->level
,playerParam->expp
,playerParam->hp
,playerParam->hp_max
,playerParam->mp
,playerParam->mp_max
,playerParam->attack
,playerParam->def
);
}
level1とlevel30の時のパラメータ増分値を指定してもらって、指定レベル時に増分値が幾つになるか求める関数です。
つまり、最大と最小の差分を線型処理して指定レベルの値を求めています。
最後に値に乱数で90%から110%のゆらぎを与えています。
//----------------------------------------------------------------------
// レベルに合わせてパラメータを調整
//----------------------------------------------------------------------
static int LevelScaler(int level,int min,int max)
{
// 最大レベルならmax値を返す。
if( LEVEL_MAX name
,enemyParam->hp
,enemyParam->attack
,enemyParam->def
,enemyParam->atk_type
,enemyParam->expp
);
}
攻撃のコマンドを表示して戦闘コマンドの番号を入力していもらいます。
まほうはレベルによって変わるので、レベルを見て表示する戦闘コマンドとコマンドの数値範囲を決めています。
//----------------------------------------------------------------------
// 攻撃の種類を選択「こうげき」「にげる」「かいふく」「ほのう」←レベルで変化
//----------------------------------------------------------------------
static int inputCmd(PlayerParam_t *playerParam)
{
int cmd = 0;
int max = 0;
char msg[124];
// レベル合わせてコマンドを表示する。
strcpy( msg, "\n*** どうする?\n" );
strcat( msg, "0 - こうげき\n" );
strcat( msg, "1 - にげる\n" );
max = 1;
for( int i=0; ilevel ) {
// つかえる魔法コマンドの表示。
char work[1024];
max++;
sprintf( work, "%d - %s の まほう mp=%d\n", max, s_magicDatas[i].name, s_magicDatas[i].mp );
strcat( msg, work );
}
}
strcat( msg, "? " );
// コマンドを入力してもらって持ち帰る。
cmd = inputNum(msg,0,max);
return cmd;
}
バトル処理です。
50%の確率で主人公が先か敵が先か決まります。
パラメータに素早さなどがある場合は、それを元に行動順番を変えるべきでしょう。
逃げるのに成功したときは、戻り値でTRUEを返します。それ以外はFALSEです。
//----------------------------------------------------------------------
// バトル処理
//----------------------------------------------------------------------
static int battleCmd(int cmd,PlayerParam_t *playerParam,EnemyParam_t *enemyParam)
{
// 素早さパラメータが無いので、どちらが先に攻撃するかは50%の確率でランダムに決めます。
if( rand() > (RAND_MAX/2) ) {
// 主人公の行動
if( battlePlayer(cmd,playerParam,enemyParam) ) {
// にげだした。
return TRUE;
}
// 敵の行動
battleEnemy(enemyParam,playerParam);
} else {
// 敵の行動
battleEnemy(enemyParam,playerParam);
// 主人公の行動
if( battlePlayer(cmd,playerParam,enemyParam) ) {
// にげだした。
return TRUE;
}
}
// 逃げなかった
return FALSE;
}
どちらも生きている場合は、選択された戦闘コマンドに従って主人公が行動します。
戦闘コマンド別に説明します。
●CMD_ATTACK: //攻撃
ダメージを敵の防御力と主人公の攻撃力からdamageCalc()で求めます。
そのダメージをdamageEnemy()でHPから際し引いたり状況表示します。
●CMD_ESCAPE: //にげる
50%の確率で逃げるのに成功し、逃げ損なったら何もしません。逃げるのに成功した場合だけTRUEを返します。
●CMD_FIRE: //ほのう
mpが足りるなら、mpを消費して魔法攻撃をします。mpが足りなければメッセージを表示して何もしません。
魔法ダメージは防御力ほとんど無視します。敵の防御力/4と魔法攻撃力からdamageCalc()で求めます。
そのダメージをdamageEnemy()でHPから際し引いたり状況表示します。
●CMD_HEAL: //かいふく
mpが足りるなら、mpを消費して回復をします。mpが足りなければメッセージを表示して何もしません。
hpに魔法の回復値を足して、この時最大値をオーバーしないようにガードしてます。
回復値を表示して終わります。
//----------------------------------------------------------------------
// 主人公の行動
//----------------------------------------------------------------------
static int battlePlayer(int cmd,PlayerParam_t *playerParam,EnemyParam_t *enemyParam)
{
int damage=0;
// どちらも生きているか?
if( liveCheck(LIVE_ALL,playerParam,enemyParam) ) {
// 戦闘コマンド別の処理
switch( cmd ) {
case CMD_ATTACK: //攻撃
// ダメージ処理。
damage = damageCalc(enemyParam->def,playerParam->attack);
damageEnemy(damage,NULL,playerParam,enemyParam);
break;
case CMD_ESCAPE: //にげる
// 逃げる確率は50%
waitMsg("…");
if( rand() > (RAND_MAX/2) ) {
waitMsg("にげだした!");
return TRUE;
} else {
waitMsg("にげられなかった!");
}
break;
case CMD_FIRE: //ほのうのまほう
// 魔法が使えるだけのmpがあるか?
if( playerParam->mp >= s_magicDatas[MAGIC_FIRE].mp ) {
// ダメージ処理。まほうは防御力をほとんど無視してダメージ
damage = damageCalc(enemyParam->def/4,s_magicDatas[MAGIC_FIRE].power);
damageEnemy(damage,s_magicDatas[MAGIC_FIRE].name,playerParam,enemyParam);
// mpを消費する。
playerParam->mp -= s_magicDatas[MAGIC_FIRE].mp;
} else {
waitMsg("あなたの まほう こうげき!");
waitMsg("mpが足らなかった!");
}
break;
case CMD_HEAL: //かいふく
// 魔法が使えるだけのmpがあるか?
if( playerParam->mp >= s_magicDatas[MAGIC_HEAL].mp ) {
// 回復処理
playerParam->hp += s_magicDatas[MAGIC_HEAL].power;
if( playerParam->hp > playerParam->hp_max ) {//オーバーガード
playerParam->hp = playerParam->hp_max;
}
waitMsg("あなたは %s の まほう つかった!",s_magicDatas[MAGIC_HEAL].name);
waitMsg("HPが%dにかいふくした。",playerParam->hp);
// mpを消費する。
playerParam->mp -= s_magicDatas[MAGIC_HEAL].mp;
} else {
waitMsg("あなたの %s の まほう",s_magicDatas[MAGIC_HEAL].name);
waitMsg("mpが足らなかった!");
}
break;
}
}
// 逃げなかった
return FALSE;
}
敵への攻撃処理。敵にダメージを与えます。
技名と言うか魔法名があれば表示、ダメージ値の表示、HPからダメージ値を引いて敵が死んだ場合は、メッセージを表示して倒された処理と経験値の入手を行います(ここで本当はお金も手に入れます)。
//----------------------------------------------------------------------
// 主人公からのダメージ処理
//----------------------------------------------------------------------
static void damageEnemy(int damage,const char *wazaname,PlayerParam_t *playerParam,EnemyParam_t *enemyParam)
{
// 攻撃メッセージ
if( wazaname!=NULL ) {
waitMsg("あなたの %s の まほう こうげき!",wazaname);
} else {
waitMsg("あなたのこうげき!");
}
waitMsg("あなたは %sに %dのダメージをあたえた!",enemyParam->name,damage);
// 敵を倒した?
enemyParam->hp -= damage;
if( enemyParam->hp hp = 0;
waitMsg("%sをたおした!",enemyParam->name);
// 経験値
waitMsg("%dの経験値をえた!",enemyParam->expp);
playerParam->expp += enemyParam->expp;
}
}
敵の行動処理です。基本は打撃で、乱数で1/3の確率で特殊攻撃を行います。
攻撃タイプ別に説明します。
●ENEMY_ATK_TYPE_NORMAL: //攻撃は打撃のみ。
基本的な打撃攻撃です。
ダメージを主人公の防御力と敵の攻撃力からdamageCalc()で求めます。つまり、主人公の処理の逆ですね。
そのダメージをdamagePlayer()でHPから際し引いたり状況表示します。
●ENEMY_ATK_TYPE_FIRE: //「ほのお」の攻撃も含めるタイプ。
ダメージを主人公の防御力/2とほのおの攻撃力からdamageCalc()で求めます。防御力の影響が低めです。
そのダメージをdamagePlayer()でHPから際し引いたり状況表示します。
●ENEMY_ATK_TYPE_BIGWIND: //「きょうれつなかぜ」の攻撃も含めるタイプ。
ダメージを主人公の防御力/2と強烈な風の攻撃力からdamageCalc()で求めます。防御力の影響が低めです。
そのダメージをdamagePlayer()でHPから際し引いたり状況表示します。
●ENEMY_ATK_TYPE_BOSS: //ボスの特殊攻撃
ダメージを主人公の防御力/2と特殊攻撃力からdamageCalc()で求めます。防御力の影響が低めです。
そのダメージをdamagePlayer()でHPから際し引いたり状況表示します。
//----------------------------------------------------------------------
// 敵の行動
//----------------------------------------------------------------------
static void battleEnemy(EnemyParam_t *enemyParam,PlayerParam_t *playerParam)
{
int atk = enemyParam->atk_type;
int damage = 0;
// どちらも生きているか?
if( liveCheck(LIVE_ALL,playerParam,enemyParam) ) {
// 敵の思考ルーチン。特殊攻撃か、普通の攻撃か?
// まぁ、思考とは言えないけど。
if( rand() > (RAND_MAX/3) ) {
// 普通の攻撃。大きめの確率
atk = ENEMY_ATK_TYPE_NORMAL;
}
// 攻撃タイプ別の処理
switch( atk ) {
case ENEMY_ATK_TYPE_NORMAL: //攻撃は打撃のみ。
damage = damageCalc(playerParam->def,enemyParam->attack);
damagePlayer(damage,NULL,enemyParam,playerParam);
break;
case ENEMY_ATK_TYPE_FIRE: //「ほのお」の攻撃
damage = damageCalc(playerParam->def/2,15);
damagePlayer(damage,"ほのう",enemyParam,playerParam);
break;
case ENEMY_ATK_TYPE_BIGWIND: //「きょうれつなかぜ」の攻撃
damage = damageCalc(playerParam->def/2,27);
damagePlayer(damage,"きょうれつなかぜ",enemyParam,playerParam);
break;
case ENEMY_ATK_TYPE_BOSS: //ボスの特殊攻撃
damage = damageCalc(playerParam->def/2,36);
damagePlayer(damage,"やきつくす火炎",enemyParam,playerParam);
break;
}
}
}
主人公への攻撃処理。主人公にダメージを与えます。
技名があれば表示、ダメージ値の表示、HPからダメージ値を引いて主人公が死んだ場合は、メッセージを表示します。
//----------------------------------------------------------------------
// 敵からのダメージ処理
//----------------------------------------------------------------------
static void damagePlayer(int damage,const char *wazaname,EnemyParam_t *enemyParam,PlayerParam_t *playerParam)
{
// 攻撃メッセージ
if( wazaname!=NULL ) {
waitMsg("%sの %s こうげき!",enemyParam->name, wazaname);
} else {
waitMsg("%sのこうげき!",enemyParam->name);
}
waitMsg("あなたは %dのダメージをうけた!",damage);
// 敵を倒した?
playerParam->hp -= damage;
if( playerParam->hp hp = 0;
waitMsg("あなたは しんでしまった!");
}
}
ダメージ計算処理です。
ダメージは、攻撃力から防御力/2を引いたものですね。
最低1ダメージは得られるように補正します。
//----------------------------------------------------------------------
// ダメージ計算
//----------------------------------------------------------------------
static int damageCalc(int def,int atk)
{
// 攻撃力をランダム補正する。90%~110%
atk = ( atk * (90 + (20 * rand() / RAND_MAX)) ) / 100;
// ダメージの計算。攻撃力 - 防御力/2
int damage = atk - (def/2);
// 1以下なら補正。最低限1ダメージ。
if( damagehp > 0 ) {
liveCode |= LIVE_PLAYER; //主人公の生存フラグをON;
}
// 敵の生存?
if( enemyParam->hp > 0 ) {
liveCode |= LIVE_ENEMY; //敵生存フラグをON;
}
// フラグと等しいか判定する。
if( (liveCode&flag) == flag ) {
// 生存
return TRUE;
} else {
// 生存していない
return FALSE;
}
}
で、今回のプロジェクトです。 これだけでも遊べるといえば遊べるので色々自分のほしい機能を追加して遊んでみてくださいね。
次回は、簡単RPGの戦闘システムの画面周りから組み立てていきます。