えぐぜりにゃ〜ソース解説3

えぐぜりにゃ〜ソース解説の続きです。
(※バージョンは「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) < -32.0 | enemy_x(cnt) > 640.0 + 32.0 | enemy_y(cnt) < -32.0 | enemy_y(cnt) > 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) < 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) < -32.0 | enemy_x(cnt) > 640.0 + 32.0 | enemy_y(cnt) < -32.0 | enemy_y(cnt) > 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〜 画面外に出て行く

 
kenmoの注目ポイントは「60」のところですね。
うまく画面外にでないような移動量を設定しています。
 
kenmoは昔、
「画面外に敵を叩き落す」という、
相撲のようなゲームを作ったことがあるのですが、
そのとき、「敵が画面外に移動しないようにする」アルゴリズムに苦労したことがあります。
(結局、いいアイデアが思いつかず、敵がいつも画面端に行ってしまうものになってしまいましたが…)
 
このようにうまく移動量を設定すればよかったのですね〜、、。
 
 
そして、「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
				}
			}
		}

 

当たり判定

最後は当たり判定ですが、これも円の当たり判定で、
特にこれといった処理をしていないので、省略です。
 
 
 
今日はここまでですー。
次回は敵の生成ロジックを解説する予定です。(たぶん