「えぐぜりにゃ~」ソース解説3

投稿者: | 2012年5月22日

えぐぜりにゃ~ソース解説の続きです。(※バージョンは「exelinya_0214.zip」です)

今回は敵の移動関数の解説になります。

移動関数概要

ソースとしては、801~1029行目が、敵の移動関数になります。

803行目は、存在チェックですね。

if(enemy_type(cnt) == ENEMY_TYPE_NULL) : continue

敵が存在していなければ、何もしません。

807行目は、画面外に出てしまった敵を削除しています。

if(enemy_x(cnt) < -128.0 | enemy_y(cnt) < -128.0 | enemy_y(cnt) > 480.0 + 128.0) : enemy_type(cnt) == ENEMY_TYPE_NULL

809~811行目では、距離・ラジアンを求めています。

// 敵からプレイヤーまでの距離を求める
vx = mychar_x - enemy_x(cnt)
vy = mychar_y - enemy_y(cnt)
// ラジアンを取得
rr = atan(vy , vx)

準備は終わりました! ここから、ようやく敵の移動です。

まずは弾です。まあ、、単純な直線移動です。

if((enemy_type(cnt) == ENEMY_TYPE_SHOT1) | (enemy_type(cnt) == ENEMY_TYPE_SHOT2)){
	// 直線移動
	enemy_x(cnt) += enemy_sx(cnt)
	enemy_y(cnt) += enemy_sy(cnt)
	// 画面外に出たら消える
	if(enemy_x(cnt) &lt; -32.0 | enemy_x(cnt) &gt; 640.0 + 32.0 | enemy_y(cnt) &lt; -32.0 | enemy_y(cnt) &gt; 480.0f + 32.0) : enemy_type(cnt) == ENEMY_TYPE_NULL
		}

ポッキー&大根

つづいて、ポッキー&大根です。(820~837行目)

if((enemy_type(cnt) == ENEMY_TYPE_SHOT3) | (enemy_type(cnt) == ENEMY_TYPE_SHOT4)){
	enemy_timer(cnt)++ // タイマー増加
	if(enemy_timer(cnt) &lt; 40){
		// タイマーが0~39の場合:回転するだけ
		enemy_rotate(cnt) += 0.3
	}else{
		// タイマーが40~:
		if(enemy_timer(cnt) == 40){
			// タイマーが40のとき、プレイヤーの方向を見る
			enemy_rotate(cnt) = rr
		}
		// レベルに応じたスピードを算出
		sp = double(level) * 0.01 + 0.2
		// 向いている方向に移動量を足しこむ
		enemy_sx(cnt) += sp * cos(enemy_rotate(cnt))
		enemy_sy(cnt) += sp * sin(enemy_rotate(cnt))
	}
	// 速度減衰
	enemy_sx(cnt) *= 0.95
	enemy_sy(cnt) *= 0.95
	// 座標更新
	enemy_x(cnt) += enemy_sx(cnt)
	enemy_y(cnt) += enemy_sy(cnt)
	// 画面外に出たら消える
	if(enemy_x(cnt) &lt; -32.0 | enemy_x(cnt) &gt; 640.0 + 32.0 | enemy_y(cnt) &lt; -32.0 | enemy_y(cnt) &gt; 480.0f + 32.0) : enemy_type(cnt) == ENEMY_TYPE_NULL
}

大体このような動きをします。

この一連の動作を切り替えるために、ポッキー&大根では、タイマーを使っています。「enemy_timer」がタイマー変数です。

「タイマー」と「行動」を表にするとこんな感じです。

タイマ 行動
0~39 回転→直線運動
40 プレイヤーの方向を向く→直線運動
41~ 向いている方向に進む→直線運動
<0~39>

後で説明しますが、ポッキー&大根は、プリンが「適当な方向」に発射する特殊な弾です。この段階では、その「適当な方向」に回転しながら進むだけです。

<40>

この瞬間だけ、プレイヤーの方向を向きます。

<41~>

そして、向いている方向にひたすら進みます。そのため、「プレイヤーをひたすら追いかける」という極端にイヤラシイ動きをしないようになっています。

それについては「readme_j.txt」にこのような書き方をしています。

 上記のように、「掴み」状態にならないと攻撃が出来ないため、自機狙い弾=「通常」モードへ切り替え強制 である。このため、自機狙い弾はレベルデザイン上、使いどころが難しい。

えぐぜりにゃ~では、自機狙い弾は回避手段を限定されるため、このように、「ゆるい」自機狙い弾になっているかと思われます。

なす

839~889行目はなすです。少し凝った動きをしております。

if(enemy_type(cnt) == ENEMY_TYPE_ZAKO1){
	enemy_timer(cnt)++
	if(enemy_timer(cnt) < 60){
		// 0~59:減速しながら直線移動
		enemy_x(cnt) += enemy_sx(cnt)
		enemy_y(cnt) += enemy_sy(cnt)
		enemy_sx(cnt) *= 0.95
		enemy_sy(cnt) *= 0.95
	}
	if(enemy_timer(cnt) == 60){
		// 60:
		if(enemy_y(cnt) < 120.0 | enemy_y(cnt) > 360.0){
			// 上下の端っこ
			if(enemy_x(cnt) < 320.0){
				// 左寄りなので右に動く
				enemy_sx(cnt) = 1.0
				enemy_sy(cnt) = 0.0
			}else{
				// 右寄りなので左に動く
				enemy_sx(cnt) = -1.0
				enemy_sy(cnt) = 0.0
			}
		}else{
			// 中寄り
			if(enemy_y(cnt) < 240.0){
				// 上寄りなので下に移動
				enemy_sx(cnt) = 0.0
				enemy_sy(cnt) = 1.0
			}else{
				// 下寄りなので上移動
				enemy_sx(cnt) = 0.0
				enemy_sy(cnt) = -1.0
			}
		}
	}
	if(enemy_timer(cnt) > 60 & enemy_timer(cnt) < 310){
		// 61~309:
		// 直線移動
		enemy_x(cnt) += enemy_sx(cnt)
		enemy_y(cnt) += enemy_sy(cnt)
		// 左回りをする
		enemy_x(cnt) += sin(double(enemy_timer(cnt)) * 0.1)
		enemy_y(cnt) += cos(double(enemy_timer(cnt)) * 0.1)

		if(enemy_timer(cnt) > 310 - level * 5){
			// レベルが高いと、
			enemy_rotate(cnt) += 0.1
			if(enemy_timer(cnt)  10 == 0){
				// 16方向のいずれかに、
				rr = double(rnd(16)) / 8.0 * MATH_PI
				// 弾発射
				addenemy enemy_x(cnt) , enemy_y(cnt) ,  4.0 * cos(rr) , 4.0 * sin(rr) , rr ,ENEMY_TYPE_SHOT1
			}
		}
	}
	if(enemy_timer(cnt) > 309){
		// 310~:加速しながら画面外に出る
		// 画面中央までのベクトルを求める
		vx = 320.0 - enemy_x(cnt)
		vy = 240.0 - enemy_y(cnt)
		rr = sqrt(vx * vx + vy * vy)
		// 反転して足しこむ
		enemy_sx(cnt) = -vx / rr * double(enemy_timer(cnt) - 310) * 0.1
		enemy_sy(cnt) = -vy / rr * double(enemy_timer(cnt) - 310) * 0.1
		enemy_x(cnt) += enemy_sx(cnt)
		enemy_y(cnt) += enemy_sy(cnt)
	}
}

大体このような動きをします。

「タイマー」と「行動」です

タイマ 行動
0~59 減速しながら画面内に入ってくる
60 画面外にでないような移動量を設定する
61~309 左回転をする
310~ 画面外に出て行く

注目ポイントは「60」のところですね。うまく画面外にでないような移動量を設定しています。

昔、「画面外に敵を叩き落す」という、相撲のようなゲームを作ったことがあるのですが、そのとき、「敵が画面外に移動しないようにする」アルゴリズムに苦労したことがあります。(結局、いいアイデアが思いつかず、敵がいつも画面端に行ってしまうものになってしまいましたが…)

このようにうまく移動量を設定すればよかったのですね、、。

そして、「61~309」で、直線移動+回転移動という2つの動きを組み合わせて、なんとも不思議な動きをするのも、非常に面白いです。

たこ焼き

891~907行目がたこ焼きです。

if(enemy_type(cnt) == ENEMY_TYPE_ZAKO2){
	enemy_timer(cnt)++
	if(enemy_timer(cnt)  120 == 0){
		// 120での剰余が0:自機を狙う移動量を設定
		vx = mychar_x - enemy_x(cnt)
		vy = mychar_y - enemy_y(cnt)
		rr = sqrt(vx * vx + vy * vy)
		enemy_sx(cnt) = vx / rr * double(level + 48) / 8.0
		enemy_sy(cnt) = vy / rr * double(level + 48) / 8.0
	}else{
		// 20での剰余が0でない:ぐるぐる回るながら減速
		rr = double(enemy_timer(cnt)  120) / 120.0
		enemy_x(cnt) += enemy_sx(cnt)
		enemy_y(cnt) += enemy_sy(cnt)
		enemy_sx(cnt) *= 0.95
		enemy_sy(cnt) *= 0.95
		enemy_rotate(cnt) += (rr * rr)
	}
}

動きとしては、このようになります。

1フレームだけ自機を狙う動きをします。自機を狙う直前には、回転速度がかなり速くなっており、いかにも、「いまから狙うぞ!」という兆候が感じられます。

簡潔ながらも、プログラム的にもゲームデザイン的にも優れたアルゴリズムですね。

5箱とX箱

単なる直線移動なので、省略します…。

牛乳

914~961行目は牛乳です。

if(enemy_type(cnt) == ENEMY_TYPE_MID1){
	enemy_timer(cnt)++
	if(enemy_timer(cnt) < 60){
		// ~59:減速直線移動・ショットの種類決め
		enemy_x(cnt) += enemy_sx(cnt)
		enemy_y(cnt) += enemy_sy(cnt)
		enemy_sx(cnt) *= 0.95
		enemy_sy(cnt) *= 0.95
		enemy_flag(cnt) = rnd(8) // ※①「0」が4way「4」がにんじんなので、1秒(30FPS)の間に25%の確率で発射
	}
	if(enemy_timer(cnt) > 59){
		// 60~:左回り・画面外に出ないように移動量を設定
		enemy_sx(cnt) = 0.0
		enemy_sy(cnt) = 0.0
		enemy_x(cnt) += sin(double(enemy_timer(cnt)) * 0.05) * 0.25
		enemy_y(cnt) += cos(double(enemy_timer(cnt)) * 0.05) * 0.25
		if(enemy_x(cnt) < 32.0			) : enemy_x(cnt) += 0.5
		if(enemy_x(cnt) > 640.0 - 32.0	) : enemy_x(cnt) -= 0.5
		if(enemy_y(cnt) < 32.0			) : enemy_y(cnt) += 0.5
		if(enemy_y(cnt) > 480.0 - 32.0	) : enemy_y(cnt) -= 0.5
	}
			
	if(enemy_timer(cnt)  30 == 0){
		// 30での剰余が0:※②弾のロジック
		enemy_flag(cnt)++
		if(enemy_flag(cnt)  8 == 0){ // 4way
			// flagを8で剰余したら0:4way弾発射
			up_cnt = cnt
			repeat 1 + level / 5
				// レベルに応じた回数発射
				v = double(cnt) * 0.7 + 0.8 // ※④回数が多いほど速い弾
				if(level < 20) : v = double(cnt) * 0.7 + 2.8 - 0.1 * double(level)
				// ※③
				addenemy enemy_x(up_cnt) , enemy_y(up_cnt) ,  v * cos(rr + MATH_PI * 0.40) , v * sin(rr + MATH_PI * 0.40) , 0.0 ,ENEMY_TYPE_SHOT1
				addenemy enemy_x(up_cnt) , enemy_y(up_cnt) ,  v * cos(rr + MATH_PI * 0.15) , v * sin(rr + MATH_PI * 0.15) , 0.0 ,ENEMY_TYPE_SHOT1
				addenemy enemy_x(up_cnt) , enemy_y(up_cnt) ,  v * cos(rr - MATH_PI * 0.15) , v * sin(rr - MATH_PI * 0.15) , 0.0 ,ENEMY_TYPE_SHOT1
				addenemy enemy_x(up_cnt) , enemy_y(up_cnt) ,  v * cos(rr - MATH_PI * 0.40) , v * sin(rr - MATH_PI * 0.40) , 0.0 ,ENEMY_TYPE_SHOT1
			loop
			ds_play SND_ENEMYSHOT
		}
		if(enemy_flag(cnt)  8 == 4){	// にんじんショット
			// flagを8で剰余したら4:にんじんショット
			sp = 6.0 + double(level) * 0.1
			// 自機に向けて発射
			addenemy enemy_x(cnt) , enemy_y(cnt) , sp * cos(rr) , sp * sin(rr) , rr ,ENEMY_TYPE_SHOT2
			up_cnt = cnt
			repeat level / 7
				// ※⑤「5°」の開きを持つ2way弾。回数が多いほど幅が広がる
				r = MATH_PI / 36.0 * double(cnt + 1)
				sp *= 0.8
				addenemy enemy_x(up_cnt) , enemy_y(up_cnt) ,  sp * cos(rr + r) , sp * sin(rr + r) , 0.0 ,ENEMY_TYPE_SHOT1
				addenemy enemy_x(up_cnt) , enemy_y(up_cnt) ,  sp * cos(rr - r) , sp * sin(rr - r) , 0.0 ,ENEMY_TYPE_SHOT1
			loop
			ds_play SND_ENEMYSHOT
		}
	}
}

牛乳自体の動きは、なすとほぼ同じなので解説を省略します。雰囲気としては、なすよりも少しキレがある感じですね。

ただ、※①のところはレベルデザイン的に重要な部分です。

enemy_flag(cnt) = rnd(8) // 「0」が4way「4」がにんじんなので、1秒(30FPS)の間に25%の確率で発射

※②にあるように、牛乳はtimerが「30」での剰余が0の場合に弾を発射します。

if(enemy_timer(cnt)  30 == 0){

そして、えぐぜりにゃ~のフレームレートは「30FPS」で、ちょうど1秒間になりますね。また、乱数は0~7の値を取り、「0」または「4」のとき弾を撃つので、2÷8=0.25となります。つまり1秒間に25%の確率で弾を発射するわけです。

ではでは、弾の発射部分を見ていきます。まずは4way弾です。

※③で「addenemy」関数を呼び出し、弾を生成しているわけですが、3つ目と4つ目の引数に注目です。

v * cos(rr + MATH_PI * 0.40) , v * sin(rr + MATH_PI * 0.40)
v * cos(rr + MATH_PI * 0.15) , v * sin(rr + MATH_PI * 0.15)
v * cos(rr - MATH_PI * 0.15) , v * sin(rr - MATH_PI * 0.15)
v * cos(rr - MATH_PI * 0.40) , v * sin(rr - MATH_PI * 0.40)

これは何をしているのかというと、、、。「rr」は自機がいる方向です。それを軸に、

「MATH_PI * 0.40」「MATH_PI * 0.15」「-MATH_PI * 0.15」「-MATH_PI * 0.40」

それぞれの方向に弾を発射しています。MATH_PIは「3.14」なので、180°です。

つまり、

MATH_PI * 0.40=180°*0.40=72°
MATH_PI * 0.15=180°*0.15=27°
 -MATH_PI * 0.15=180°*0.15=-27°
 -MATH_PI * 0.40=180°*0.40=-72°

という方向に発射しているわけです。

イメージとしてはこんな感じです。

あと、レベルが上がるほど、発射回数を増やしています。(※④)

そして、多くなるほど速度が速いです。

これにより、時間がたつにつれ、それぞれの差が「ぐいーん」と広がる弾幕を発射しているのが、このロジックになります。(弾幕シューでは定番ともいえるロジックなので、覚えておいてソンはないと思います)

そして、にんじんショットの部分です。にんじんそのものは問題ないと思います。その後の2way弾ですが、(※⑤)

MATH_PI / 36.0=180°÷36.0=5°

ということで、5°の開きの2way弾を発射していることになります。

プリン

プリンは、、、牛乳と似た部分が多く、牛乳が理解できていれば、そんなに難しくないので省略です。一応、コメントを追加しておきましたので、参考にしてもらえるとありがたいです。

if(enemy_type(cnt) == ENEMY_TYPE_MID2){
	enemy_timer(cnt)++
	if(enemy_timer(cnt) == 1) : enemy_flag(cnt) = rnd(8)
	if(enemy_timer(cnt)  120 == 0){
		// 120での剰余が0:テキトーに16方向への移動
		sp = 4.0 + double(level) * 0.2
		rr = double(rnd(16)) / 8.0 * MATH_PI
		enemy_sx(cnt) = sp * cos(rr)
		enemy_sy(cnt) = sp * sin(rr)
	}else{
		// 画面外にでないように移動量を反転・直線移動
		if(enemy_x(cnt) < 32.0			& enemy_sx(cnt) < 0) : enemy_sx(cnt) *= -1.0
		if(enemy_x(cnt) > 640.0 - 32.0	& enemy_sx(cnt) > 0) : enemy_sx(cnt) *= -1.0
		if(enemy_y(cnt) < 32.0			& enemy_sy(cnt) < 0) : enemy_sy(cnt) *= -1.0
		if(enemy_y(cnt) > 480.0 - 32.0	& enemy_sy(cnt) > 0) : enemy_sy(cnt) *= -1.0
		enemy_x(cnt) += enemy_sx(cnt)
		enemy_y(cnt) += enemy_sy(cnt)
		enemy_sx(cnt) *= 0.95
		enemy_sy(cnt) *= 0.95
	}
			
	if(enemy_timer(cnt)  30 == 0){
		enemy_flag(cnt)++
		if(enemy_flag(cnt)  8 == 0){ // 8ばら撒き
			vx = enemy_x(cnt)
			vy = enemy_y(cnt)
			repeat 1 + level / 5
				// レベルが高いほどたくさん撃つ
				sp = double(cnt) * 0.6 + 0.8 // たくさん撃つほど速い弾
				if(level < 20) : sp = double(cnt) * 0.6 + 2.8 - 0.1 * double(level)
				ty = ENEMY_TYPE_SHOT1
				if(cnt == level / 5) : ty = ENEMY_TYPE_SHOT2
				repeat 8
					// 全方向8way
					rr = MATH_PI / 4.0 * double(cnt)
					addenemy vx , vy ,  sp * cos(rr) , sp * sin(rr) , rr , ty
				loop
			loop
			ds_play SND_ENEMYSHOT
		}
	}
	if(enemy_timer(cnt)  3 == 0 & enemy_timer(cnt)  60 < level / 3 + 1){
		if(enemy_flag(cnt)  8 == 4){	// 大根みそー
			sp = 8.0 + double(level) * 0.1
			// 32方向のいずれかにランダム発射
			rr = double(rnd(32)) / 16.0 * MATH_PI
			addenemy enemy_x(cnt) , enemy_y(cnt) , sp * cos(rr) , sp * sin(rr) , rr ,ENEMY_TYPE_SHOT4
			ds_play SND_ENEMYSHOT
		}
	}
}

当たり判定

最後は当たり判定ですが、これも円の当たり判定で、特にこれといった処理をしていないので、省略です。