« W:ETソースコード朗読会 第三回 | メイン | W:ETソースコード朗読会 第五回 »

April 15, 2005

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

前回のおさらいと今回のねらい

第三回では具体的に、ゲーム中での弾丸当たり判定についての処理を見ました。これはサーバーサイドのコードでしたね。そのため実際に弾を発射した時に銃口に煙を出したり、弾丸の軌跡、着弾点のエフェクトなど、ビジュアル的な処理はここで行われていません。

今回は再び武器の一つとして、ナイフの処理を見ていこうかと思います。ナイフは素敵な武器です。背後から忍び寄ることが出来れば、弾薬の消費無しに相手を倒せます。何より、相手も倒されるとなぜか楽しい武器です。ベクトル演算が結構多いので、数学の参考書があるといいかもしれませんね。

ナイフで刺す

早速コードを見ていきましょう。g_weapon.cの85行以降あたりがナイフに相当します。

#define KNIFE_DIST 48

さて、冒頭に定数定義がされています。字面から、ナイフの射程距離を表すものと予想できます。48インチ、つまり1メートルくらいが有効範囲ということになります。ちなみにこれを10000くらいに変更すると、かなり離れた相手にも見えている限り確実・即座にヒットする強烈な武器になります。もちろん、背後に当たれば即死です。

続いて関数を見ていきましょう。

void Weapon_Knife( gentity_t *ent ) {

	trace_t		tr;
	gentity_t	*traceEnt, *tent;
	int			damage, mod;
	vec3_t		pforward, eforward;
	vec3_t		end;
	mod = MOD_KNIFE;

他の関数と同じですね。変数宣言と初期設定をここでしています。

	AngleVectors (ent->client->ps.viewangles, forward, right, up);
	CalcMuzzlePoint ( ent, ent->s.weapon, forward, right, up, muzzleTrace );
	VectorMA (muzzleTrace, KNIFE_DIST, forward, end);
	trap_Trace (&tr, muzzleTrace, NULL, NULL, end, ent->s.number, MASK_SHOT);

最初のAngleVectorsから見ていきましょう。この関数の定義部分は、q_math.cにあります。q_math.cは他にも色々な演算関数がと定義が入っていますので、一通り見ておくと今後の理解がスムーズかと思います。

void AngleVectors( const vec3_t angles, vec3_t forward, vec3_t right, vec3_t up) {
	float		angle;
	static float		sr, sp, sy, cr, cp, cy;

	angle = angles[YAW] * (M_PI*2 / 360);
	sy = sin(angle);
	cy = cos(angle);

(省略)

	if (forward)
	{
		forward[0] = cp*cy;
		forward[1] = cp*sy;
		forward[2] = -sp;
	}
(省略)
}

まさに計算だけをする関数です。結果的にプレイヤーが見ている方向に応じた座標系軸ベクトルを返します。以下の解説は読みたい人だけ呼んでください。引数として、角度と結果を格納するベクトルが与えられます。この関数は、与えられた角度系(ヨー・ピッチ・ロールの三成分)から、直交座標系(xyz)の直行基底ベクトルを計算し、それぞれの基底ベクトルをforward、right、upとして返します。ピッチ、ヨー、ロール全てが0の場合、fowardは(1,0,0)、rightは(0,1,0)、upは(0,0,1)となって、標準の直交座標系になります。詳しい角度と基底ベクトルの変換については参考書などをご覧ください。

話を本筋に戻します。結局この行:

	AngleVectors (ent->client->ps.viewangles, forward, right, up);

についてですが、角度系としてent->client->ps.viewanglesを渡しています。つまり、ナイフで攻撃しようとしたプレイヤーが見ている方向角によって、それぞれforward、right、upに回転した直交座標系の軸ベクトルを代入します。

続いてCalcMuzzlePointですが、これはプレイヤーの位置と向き(先ほど得た座標系)と武器の種類に応じ、攻撃判定が発生する開始点座標を計算します。この結果がmuzzleTraceに代入されます。

VectorMA関数が後に続きますが、MAはMutiply & Addの省略です。つまりあるベクトルを定数倍したものを他のベクトルに加える計算をします。これ以外にもベクトルの基本演算がq_math.cにて定義されていますので列挙しておきます。

#define DotProduct(x,y)			((x)[0]*(y)[0]+(x)[1]*(y)[1]+(x)[2]*(y)[2])
#define VectorSubtract(a,b,c)	((c)[0]=(a)[0]-(b)[0],(c)[1]=(a)[1]-(b)[1],(c)[2]=(a)[2]-(b)[2])
#define VectorAdd(a,b,c)		((c)[0]=(a)[0]+(b)[0],(c)[1]=(a)[1]+(b)[1],(c)[2]=(a)[2]+(b)[2])
#define VectorCopy(a,b)			((b)[0]=(a)[0],(b)[1]=(a)[1],(b)[2]=(a)[2])
#define	VectorScale(v, s, o)	((o)[0]=(v)[0]*(s),(o)[1]=(v)[1]*(s),(o)[2]=(v)[2]*(s))
#define	VectorMA(v, s, b, o)	((o)[0]=(v)[0]+(b)[0]*(s),(o)[1]=(v)[1]+(b)[1]*(s),(o)[2]=(v)[2]+(b)[2]*(s))

それぞれの意味は名前と定義を見るとわかると思います。DotProductは内積、VectorSubstractは引き算、VectorAddは足し算、VectorCopyは単に代入、VectorScaleは定数倍です。

この計算によって、end = muzzleTrace + KNIFE_DIST * forwardが代入されました。つまり、endは攻撃判定の終端座標になります。

そして四行目にて実際に当たり判定をしています。当たった場合、trにその座標が代入されます。

次に進みましょう。

	if ( tr.surfaceFlags & SURF_NOIMPACT )
		return;

	// no contact
	if(tr.fraction == 1.0f)
		return;

この部分は当たり判定の結果を見て、何にも当たっていなければここで終了する処理をします。

	if(tr.entityNum >= MAX_CLIENTS) {	// world brush or non-player entity (no blood)
		tent = G_TempEntity( tr.endpos, EV_MISSILE_MISS );
	} else {							// other player
		tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT );
	}

ここはナイフの当たったものがプレイヤーなのかそれ以外のものかを判断しています。

	tent->s.otherEntityNum = tr.entityNum;
	tent->s.eventParm = DirToByte( tr.plane.normal );
	tent->s.weapon = ent->s.weapon;
	tent->s.clientNum = ent->r.ownerNum;

	if(tr.entityNum == ENTITYNUM_WORLD)	// don't worry about doing any damage
		return;

この部分はtentに対しての代入操作が中心です。2行目にDirToByteという変な関数がありますね。これは、通信データ量削減のためのもので、162のあらかじめ決定されている代表的なベクトルにもっとも近いものを探し、何番目が最適かを戻り値とします。このため、3つのfloatからなるベクトルを1つのcharにすることが出来ます(精密さは欠きますが)。

最後のifは、当たったものがWORLD、つまりマップオブジェクトであるから何にもダメージ与えられなかったよ、ということでここで処理を終わります。実際クライアント側ではプレイヤーでなくとも壁などに当たればシャキシャキ音がしますが、サーバー側ではこのような処理は一切行われていないことがここでわかりますね。

	traceEnt = &g_entities[ tr.entityNum ];

	if(!(traceEnt->takedamage))
		return;

ここでは当たったものがダメージを受けうるものかどうかを判定しています。そうでないならここで処理を終わります。

	damage = G_GetWeaponDamage(ent->s.weapon); // JPW		// default knife damage for frontal attacks

	if( ent->client->sess.playerType == PC_COVERTOPS )
		damage *= 2;	// Watch it - you could hurt someone with that thing!

無事ナイフがダメージを受けうるものに当たったと判定されると、いよいよダメージ量の計算になります。最初の行にG_GetWeaponDamageがあります。この関数は前々回にて取り上げて、武器の種類に応じてダメージ値を返す関数であることを確認しました。ですからdamageにはナイフの標準ダメージである10が代入されます。

次のifの条件が、攻撃者がCVOPSであるかを確認しています。もしそうなら、ダメージが二倍にする処理をしています。つまりCVOPSのナイフは20ダメージです。

	if(traceEnt->client) 
	{
		AngleVectors (ent->client->ps.viewangles,		pforward, NULL, NULL);
		AngleVectors (traceEnt->client->ps.viewangles,	eforward, NULL, NULL);

		if( DotProduct( eforward, pforward ) > 0.6f )		// from behind(-ish)
		{
			damage = 100;	// enough to drop a 'normal' (100 health) human with one jab
			mod = MOD_KNIFE;

			if ( ent->client->sess.skill[SK_MILITARY_INTELLIGENCE_AND_SCOPED_WEAPONS] >= 4 )
				damage = traceEnt->health;

		}
	}

	G_Damage( traceEnt, ent, ent, vec3_origin, tr.endpos, (damage + rand()%5), 0, mod);
}

最後の部分を見ていきましょう。ここが一番面白いところです。まず始めのifで、当たった相手がプレイヤー(人)であるかを確認しています。もしそうなら、自分と相手の視線方向角をベクトル化し、内積を見ます。ベクトル内積の値は、ベクトルの向きが近いほど大きな正、また直角の時は0、反対向きでは負になります。今内積を取った2ベクトルは正規化ベクトルですから、大きさは1なのでこの内積の値域は-1〜1となります。つまり、内積が1に近いほど向きが揃っている、つまりは攻撃者が相手の背後から攻撃しているという意味になります。

内積が0.6以上(角度にして約±30度以内)の場合、以下の処理をしています。まずダメージが先ほどの10ないし20から、100に設定し直されます。ほぼ即死です。さらにプレイヤーのCVOPSスキルが4以上の時、ダメージが相手のヘルスそのものになります。そうです、どんな相手でも即死です。

最後にentityへダメージを与える関数を呼び出してナイフ攻撃処理は終わります。

まとめ

今回はナイフの攻撃処理を見ました。そしてベクトルの基本的な計算と扱い方、また内積の性質なども確認しました。みなさん中学・高校などで数学を学び、「一体こんなのどこで使うんだ」と疑問に思われている方もいらっしゃったかもしれません。しかし、数学とはこれ単体では意味のないものです。ドライバーやかなづちといったものと同じで、これを作用させて便利なものをつくる道具に過ぎません。

ということで、これからMOD作りを考えている方は、是非数学(高校生あたりの三角関数やベクトルを中心に)の復習をしてみてはいかがでしょうか?

投稿者 ikanatto : April 15, 2005 06:28 PM

■ コメント

コメントしてください




保存しますか?