« ETPRO 3.1.13雑感 | メイン | 熱… »

May 02, 2005

■ W:ETソースコード朗読会 第七回

Reading the Source Code of W:ET #7

今回の目標

さてさて本日もソースコードを飽きずに見ます。ただ、あまりプレイそのものに直接関係ない部分を紹介しても誰も興味を持ってくれずに悲しいので、今回と次回はゲームプレイに大きく関与するところにしましょう。

今回はプレイヤーの射撃、特に散弾に関する処理を見ていきます。次回はプレイヤーの移動に関連する項目を取り上げる予定です。

散弾現象について

既にご存知の通り、ETではピストル・SMG・ライフルにて程度の差はあれど照準に対して着弾点がずれる散弾現象がおきます。もしこれがないとモバイルMGは事実上最強の武器となりゲーム性を著しく損なう結果となるでしょう。現実性とゲーム性の点から散弾現象は妥当であり、遊ぶ立場からするとこの特性を詳しく知っておくことは大きなアドバンテージになりえます。

ソースコード朗読会第二回目にて取り上げておりますが、武器には固有の散弾率が設定されています。この数値は弾丸が発射される際に参照され、数値を最大としてこの範囲内でランダムにずれを引き起こします。そのため武器単体での数値の比較ではステンが最小の散弾となります。

しかしETではこの散弾率はさらにプレイヤーのライトウェポン・クラススキル及び発射体勢、発射方法によってさらに補正を受けます。ただ補正を受けるといっても感覚的にしか経験できず、これを比較するのは困難でした。

そこで今回はソースコードの視点からこれを明示的に比較検討してゆきます。

PM_AdjustAimSpreadScale

さっそくソースコードを見ます。bg_pmove.c 3063行以降を見てください。

void PM_AdjustAimSpreadScale( void ) {
	int		i;
	float	increase, decrease;

	(省略)

	pm->ps->aimSpreadScale = (int)pm->ps->aimSpreadScaleFloat;
}

関数の名前をみると、PM_AdjustAimSpreadScaleとなっていますからいかにも散弾率を調整するような関数ですね。

	switch(pm->ps->weapon) {
	case WP_LUGER:
	case WP_SILENCER:
	case WP_AKIMBO_LUGER:
	case WP_AKIMBO_SILENCEDLUGER:

	(省略)

	case WP_K43_SCOPE:
	case WP_GARAND_SCOPE:
	case WP_FG42SCOPE:
		if( pm->skill[SK_MILITARY_INTELLIGENCE_AND_SCOPED_WEAPONS] >= 3 ) {
			wpnScale = 5.f;
		} else {
			wpnScale = 10.f;
		}
		break;

	(省略)

	case WP_STEN:
		wpnScale = 0.6f;
		break;
	case WP_KAR98:
	case WP_CARBINE:
		wpnScale = 0.5f;
		break;
	}

この関数で最初に目を引くのがこのcase文です。何をしているのかを見てみると、pm->ps->weaponが示す武器の種類に応じてwpnScaleを設定しています。これによるとピストルは0.4、エンジニアの持つライフルは0.5、サブマシンガン(STEN含め)が0.6、モバイルMGが0.9です。ちょっと注意が必要なのはCoの武器FG42とスコープ付ライフルです。Coのスキルが3以上の場合は5、それ以外は10となります。狙撃武器は他の武器に比べて極端に高い数値になっていることを留意しておいてください。

if (wpnScale) {
		if( pm->ps->eFlags & EF_CROUCHING || pm->ps->eFlags & EF_PRONE ) {
			wpnScale *= 0.5;
		}
		decrease = (cmdTime * AIMSPREAD_DECREASE_RATE) / wpnScale;

その次の部分に行きましょう。if文の条件がpm->ps->eFlags & EF_CROUCHING || pm->ps->eFlags & EF_PRONEを見ています。つまりプレイヤーがしゃがんでいる、もしくは伏せているかを見ており、もしそうならwpnScaleを半分にしています。これからwpnScaleは武器の散弾率と乗算して散弾を抑制する数値だろうと予想できます。それにしてもCoの武器は散らばりすぎの気がしますが…。

decreaseはなんでしょうかね。cmdTimeは前回の処理時間と今回の差を1000で割ったもの、つまり経過時間(秒)です。これとAIMSPREAD_DECREASE_RATE定数を乗算し、wpnScaleで割っていることから散弾率の時経緩和量でしょうかね。

		viewchange = 0;
		if(BG_IsScopedWeapon(pm->ps->weapon)) {
			for(i = 0; i < 2; i++) {
				viewchange += fabs(pm->ps->velocity[i]);
			}
		} else {
			for( i = 0; i < 2; i++ ) {
				viewchange += fabs( SHORT2ANGLE(pm->cmd.angles[i]) - SHORT2ANGLE(pm->oldcmd.angles[i]) );
			}
		}
		viewchange = (float)viewchange / cmdTime;
		viewchange -= AIMSPREAD_VIEWRATE_MIN / wpnScale;
		if (viewchange <= 0) {
			viewchange = 0;
		} else if( viewchange > (AIMSPREAD_VIEWRATE_RANGE / wpnScale) ) {
			viewchange = AIMSPREAD_VIEWRATE_RANGE / wpnScale;
		}
		viewchange = viewchange / (float)(AIMSPREAD_VIEWRATE_RANGE / wpnScale);
		increase = (int)(cmdTime * viewchange * AIMSPREAD_INCREASE_RATE);
	} else {
		increase = 0;
		decrease = AIMSPREAD_DECREASE_RATE;
	}

次のif節にいくと、プレイヤーが狙撃中かどうかを見ています。もしそうならプレイヤーの移動量を、そうでないなら視野の変化量をviewchangeに代入しています。そして時間差で割ることで単位時間当たりの量にしています。これからAIMSPREAD_VIEWRATE_MINをwpnScaleで割ったものを除算しています。次のif節でviewchangeが0〜AIMSPREAD_VIEWRATE_RANGE / wpnScaleになるように調整され、結局viewchangeは0〜1の値になります。最終的にincreaseがviewchangeと時間差によって決定されます。decreaseが散弾率の緩和でしたから、こちらは増加因子でしょうか。

	pm->ps->aimSpreadScaleFloat += (increase - decrease);
	if (pm->ps->aimSpreadScaleFloat < 0) pm->ps->aimSpreadScaleFloat = 0;
	if (pm->ps->aimSpreadScaleFloat > 255) pm->ps->aimSpreadScaleFloat = 255;
	pm->ps->aimSpreadScale = (int)pm->ps->aimSpreadScaleFloat;
}

計算によって得られたincreaseとdecreaseの差を取ってaimSpreadScaleFloatに代入しています。結局この関数は、照準から着弾点のぶれ補正を武器の種類と体勢によって増減させるもののようです。decreaseが大きいほど早く補正量が減少するので、しゃがみや伏せがずれ抑制に効果があることが確認できました。同様に狙撃時は移動しながら、それ以外では視野を動かしながらだとincreaseが余計に増加しますので、正確な射撃を求めるなら狙撃は動かず、それ以外はきょろきょろせずに射撃を行うとよいのでしょう(30度以内ならOK)。しかししゃがみも伏せも同じ補正とはちょっと不思議ですね。

PM_Weapon

pmove.cには武器以外にも移動やクライアントからの入力処理といったゲーム中核部分の処理が多く含まれています。3198行以降にはPM_WEAPONなる関数があるのですが、これは武器発射に関するもっとも大きな処理と判断を行うものです。実に1000行以上もあります。散弾率を知りたい私たちが用があるのは、実際に発射することが可能であると判断された以降の4188行以下です。

	aimSpreadScaleAdd = 0;

	switch( pm->ps->weapon ) {
	case WP_KNIFE:
	case WP_PANZERFAUST:
	case WP_DYNAMITE:

	(省略)

	case WP_SMOKE_MARKER:
		addTime = 1000;
		break;
	// -NERVE - SMF
	default:
		break;
	}

4188〜4325のswitch文は、武器ごとの散弾量(なぜかここで定数量が決定されている)と次の行動までの時間です。これによるとピストルは20、エンジニアのライフル50、Coのライフル200、FG42は100、SMGは15〜25、固定MG及びモバイルMG(セット)は20となっています。ここでもCoの武器は不遇です…。

	switch( pm->ps->weapon ) {
	case WP_GARAND_SCOPE:
	case WP_K43_SCOPE:
		pm->pmext->weapRecoilTime = pm->cmd.serverTime;
		pm->pmext->weapRecoilDuration = 300;
		pm->pmext->weapRecoilYaw = crandom() * .5f;

	(省略)

	default:
		pm->pmext->weapRecoilTime = 0;
		pm->pmext->weapRecoilYaw = 0.f;
		break;
	}

続いて4385行目までです。ここは武器の「反動」についての設定のようです。これによるとCoの狙撃銃は発射後0.3秒間、上方向に0.5ないし0.25(COスキル3以上)、水平方向に-0.5〜0.5の速度で補正を受けます。FG42では0.1秒間にまたモバイルMG(セットでない)は0.2秒間に垂直方向のみに0.01程(COスキル3以上で半減)の補正を受けます。またモバイルMGとピストルも補正を受けます。

	pm->ps->aimSpreadScaleFloat += 3.0*aimSpreadScaleAdd;
	if (pm->ps->aimSpreadScaleFloat > 255)
		pm->ps->aimSpreadScaleFloat = 255;
	if( pm->skill[SK_MILITARY_INTELLIGENCE_AND_SCOPED_WEAPONS] >= 3 && pm->ps->stats[STAT_PLAYER_CLASS] == PC_COVERTOPS ) {
		pm->ps->aimSpreadScaleFloat *= .5f;
	}
	pm->ps->aimSpreadScale = (int)(pm->ps->aimSpreadScaleFloat);

さらに進むとこんな部分があります。aimSpreadScaleFloatがここの関数によって計算された散弾量をさらに加算されています。どこまで散弾させるつもりなんでしょうか。注目して欲しいのは2つ目のifで、COのスキルが3以上になると(もちろんクラスがCOで)aimSpreadScaleFloatが半減されます。これは驚くべき効果です!…といいたいところですが、もともとCoの武器(ライフルとFG42)が無茶な散弾量設定なのでそれほど効果があるともいえません。しかししかし!ステンは他SMGと同じ散弾率なのにこの適応を受けますから、事実上散弾が半減したSMGといえます(威力とオーバーヒートがありますが)。

さて、ここでも散弾量の上がりましたが最終的にはどうなるのでしょうか。

FireWeapon

ところ変わってg_weapon.cです。3884行以降にFireWeaponといういかにもな名前の関数があります。見てみましょう。

	if (g_userAim.integer) {
		aimSpreadScale = ent->client->currentAimSpreadScale;
		aimSpreadScale+= 0.15f;
		if(aimSpreadScale > 1)
			aimSpreadScale = 1.0f;	// still cap at 1.0
	} else {
		aimSpreadScale = 1.0;
	}

currentAimSpreadScaleは、0〜255の値を持つaimSpreadScaleを255で割ったものですから0〜1となります。これに問答無用で0.15プラスされます。凄まじい処理ですが、狙った場所に必ず弾丸が当たるのも困るので良いことです。この最大は1となり、結局aimSpreadScaleは0.15〜1です。

	if( ent->client->ps.groundEntityNum == ENTITYNUM_NONE ) {
		aimSpreadScale = 2.0f;
	}

少し飛んでこの部分を見てみましょう。if条件はプレイヤーの足元の状態を見ており、何もない、つまりプレイヤーが空中の場合aimSpreadScaleが強制的に2に設定されます。後でわかりますが、これが2になると通常の散弾量の2倍となります。これは空中にいる限り必ず適応されますので、単発だろうが連射だろうが相当の散弾を覚悟しなければいけません。

さていよいよ個々の武器発射段階になります。

	switch( ent->s.weapon ) {
	case WP_KNIFE:
		Weapon_Knife( ent );
		break;

	(省略)

		Weapon_FlamethrowerFire( ent );
		break;
	case WP_MAPMORTAR:
		break;
	default:
		break;
	}

ここは武器ごとに弾丸を出したり、パックを出したり、炎を噴出したりする関数を呼び出しています。散弾率が影響される弾丸系の武器は次のような形になっています。

	case WP_LUGER:
		Bullet_Fire( ent, LUGER_SPREAD*aimSpreadScale, LUGER_DAMAGE, qtrue );
		break;

Bullet_Fireは朗読会三回目で取り上げました、弾丸の当たり判定を行うものです。これに引数としてent(プレイヤー)とLUGAR_SPREAD(第二回で紹介)とaimSpreadScaleをかけたもの、そしてダメージを渡しています。これは今までの流れからすんなりと受け入れられるかと思います。…しかし次のところを見てください。

case WP_CARBINE: aimSpreadScale = 1.0f; Bullet_Fire( ent, CARBINE_SPREAD*aimSpreadScale, CARBINE_DAMAGE, qfalse ); break;

ここではなんとあれほど苦労して計算してきた散弾率の計算をフイにして、ライフルの場合にはaimSpreadScaleを1に設定しています!つまりこれはライフルを撃つ場合、しゃがんでいようが視界をブン回そうが連射していようが空を飛んでいようがまったく同じ散弾率になってしまうのです。…これを作った人の考えてることがよくわからなくなります。ライフルを飛んだり走ったりする時の突入用武器としなさいということなのでしょうかね。

まとめ

最終的に当たり判定の関数に渡される散弾率のパラメータから、何が散弾率に影響しているのかがわかったかと思います。つまりピストルとSMG(含むステンとFG42)の場合武器固有の散弾量、武器固有の時間ごと散弾量減衰、体勢による散弾量減衰幅の違い、視野角変更による散弾量増加、弾丸発射による散弾量増加が要因です。一方でライフルはどんな状況でも一定の散弾量であり、これは利点にも欠点にもなりえるかと思います。

一般的な話だけだとわかりにくいので一つ例を挙げます。SMGは一秒間に6回射撃が行われるので、散弾量は一秒間で60*6=360増加します。もししゃがんでいて照準の動きが±30度以内で少なかった場合、散弾量は1秒間で600減少します。つまり射撃による散弾量の増加を完全に抑えることが可能になります。立っていた場合は300減少し、若干散弾に影響が出ます。数値的にいうと、約25%散弾量が増加します。さらに近接戦闘時のように走り回って(立っていて)照準を激しく動かすような場合、時間経過と共に散弾量が増加し、これに弾丸発射の追加があるとすぐに散弾上限に達してしまいます。

これをふまえてSMGで戦うもっとも実用的な戦術は、立って(走って)照準の動きを最低限にして一秒間に3〜4発発射すると攻撃力と散弾量をうまくコントロールできるのではないでしょうか。ただ、理論上のお話と実際出来るかどうかは別問題ですが…。そこをこなせる人が、いわゆる「すごい」人なのかもしれませんね。

蛇足ですがライトウェポンスキルが4でCoスキルが3以上のCoのステンは、空を飛んでいない限り走り回っても連射してもほとんど散弾しなくなります。

投稿者 ikanatto : May 2, 2005 04:49 PM

■ コメント