シューティング2(前半)

3章でもやりましたが、今回はもう少しシューティングゲームらしくしたいと思います。

 

まずどのようなものとなるのか説明から…。

DirectXを使わない標準命令を駆使(?)した縦スクロールで自機が上向きのゲーム。

初めに何機ものザコ敵が出現し、最後に長となるボス敵が待ち構えており倒すことでゲームクリア。

自機・敵機共に一度の衝突で潰れるのではなくパワーが減っていき、なくなると潰れるタイプ。

自機前方に発射・広範囲で威力が弱め、の2タイプの武器を初めから所持しておりスペースキーで発射。

自機や敵機、弾・背景を描いたりサウンドを作ることはしません。

音なしで、画像は文字で表わしたものを使用します。自機は▲、敵機は▼、弾は●のように。

 

さて概要説明はコレくらいで早速作成していきたいと思います。

まずは、背景作りから入りましょうね。

今回はエンドレス(終わりのない)ものとなりますので、継ぎ目が出来ないように工夫しましょう。

スロットで使った途切れないでいつまでも連続に表示できる方法をここでも使いましょう。

#define WX 640 ; 画面の幅
#define WY 480 ; 画面の高さ
#define STAR 1000 ; 星数
	dim count,1 ; ココでは画像スクロール情報
	randomize
	buffer 2,WX,WY*2
	color 1,1,1 : boxf : color 255,255,255
	repeat STAR
		rnd tmp,640
		rnd tmp.1,480
		pset tmp,tmp.1
	loop
	pos 0,480 : gcopy 2,0,0,640,480
	gsel 0
	gmode 2 ; 完全な黒を透過色にする

*main ; メインループ
	gosub *back
	await 10 ; この速度は任意
	count+
	goto *main

*back ; 背景描画
	redraw 0
	pos 0,0 : gcopy 2,0,-count\480+480,640,480
	redraw
	return

コレで宇宙遊泳しているような感じとなりましたね(^^;

スクロールする速度が遅いと感じる場合は、backラベル内の「-count\480+480」を

-count*2\480+480」とかにしてやれば速度が2倍になります。

変数countの増加値を増やすことでもスクロール速度を増やすことができるわけですが、

この後に別の処理でも使用するため上記位置の変更でお願いします。

またウェイトにawaitを使ってますが、パソコン処理速度の差をできるだけなくすために使用されています。

waitだとパソコンによる速度の差がモロに出てしまいます。

 

それでは前回のスクリプトに自機を入れて移動や弾を発射させられるようにしてみましょう。

#define WX 640
#define WY 480
#define SIZE 25 ; 自機(ザコ敵機)の大きさ
#define TSIZE 10 ; 弾の大きさ
#define TMAX 30 ; 自機弾最大数
#define STAR 1000
	dim count,1
	dim mx,1 ; 自機X方向位置情報
	dim my,1 ; 自機Y方向位置情報
	dim cartridge,TMAX ; 弾情報
	dim tx,TMAX ; 弾X方向の位置情報
	dim ty,TMAX ; 弾Y方向の位置情報
	mx=WX-SIZE/2 ; 初期X座標
	my=WY-50 ; 初期Y座標
	randomize
	buffer 2,WX,WY*2
	color 1,1,1 : boxf : color 255,255,255
	repeat STAR
		rnd tmp,WX
		rnd tmp.1,WY
		pset tmp,tmp.1
	loop
	pos 0,WY : gcopy 2,0,0,WX,WY
	buffer 3,TSIZE+SIZE/4+1*4,SIZE
	color : boxf
	color ,,255
	font "MS 明朝",TSIZE
	pos 0,0 : mes "●" ; 自機弾
	color 200
	font "MS 明朝",SIZE
	pos TSIZE,0 : mes "▲" ; 自機
; ---------- ココまでが準備 ---------- gsel 0 gmode 2 *main redraw 0 gosub *back gosub *body gosub *tama gosub *action redraw await 10 count+ goto *main *back pos 0,0 : gcopy 2,0,-count\WY+WY,WX,WY return *body ; 機体描画 pos mx,my : gcopy 3,TSIZE,0,SIZE,SIZE return *tama ; 弾描画 repeat TMAX if cartridge.cnt=1 { pos tx.cnt,ty.cnt : gcopy 3,0,0,TSIZE,TSIZE ty.cnt-=8 if -TSIZE>ty.cnt : cartridge.cnt=0 ; 画面外に出ると消滅させる } loop *action ; 自機の動作 stick key,31,1 ; 1(左) + 2(上) + 4(右) + 8(下) + 16(スペース) if key&1 : mx-=5 : if mx<0 : mx=0 if key&2 : my-=5 : if my<0 : my=0 if key&4 : mx+=5 : if WX-SIZE<mx : mx=WX-SIZE if key&8 : my+=5 : if WY-SIZE<my : my=WY-SIZE if key&16 : gosub *generation return *generation ; 弾の生成 repeat TMAX if cartridge.cnt=0 : cartridge.cnt=1 : tx.cnt=SIZE-TSIZE/2+mx : ty.cnt=my-TSIZE : break loop return

自機・弾を描画しているバッファのXサイズを妙なやり方で求めていますね?アレは何をしているのでしょうか?

HSPで高速コピーを行うためには横幅が4の倍数でないと斜めに崩れてしまうというバグがあるんですね。

自機と弾の合計Xサイズは35(10+25)ですのでコピーをするときちんと表示されず斜めに崩れてしまいます。

ソレを回避するために一旦指定倍数で割り、ソレに1を足した後(1足すのがミソ)指定倍数を掛けます。

そうすることで元々4の倍数の時は4ドット分余分に確保されてしまいますが、

4の倍数でない場合「(35÷4+1)×4=36」でも4の倍数になり、画像もきちんと入ります。

途中の1を足さなければ「35÷4×4=32」となり右端3ドット分が入りきらなくなってしまいます。

「(35+1)÷4×4=36」でもいけるんじゃないの、となりますがソレはダメです。

35だから良かったものの、もし33だったら?、34だったら?というわけです。

移動はシューティング1講座の移動できる方向に横が加わっただけですね(縦横同時押しで斜めもいけるが)。

弾の生成や描画も前回の講座でやりましたので説明を省略いたします。

キーを3つ以上同時押しするとビープ音が鳴ったりキーが反応しなかったりすることがありますが、

それはHSPが原因と言うわけではなくほかのアプリケーションにも当てはまるキーボードの制限です。

 (例)斜め(右上,右下)移動しながら弾を発射するときなど

 

自機・弾・背景の描画がそれぞれ別ラベルになっていますが、

この後に敵機が入ると見難くなるので同じ描画処理でも別処理と言うことで分けさせていただきました。

後、弾に付いてなのですが連続してだんごのように繋がっちゃいますよね?

アレを回避しなければなりませんが、どのようにすればよいでしょう。

stickの押し続けパラメータを切れば回避できますが、連続して撃つには連打しなければなりません。

シューティングで連打する姿を想像すると滑稽ですのでトラップを仕掛ける方法で行きましょう。

action」ラベルのショットキーの1行を下記のように書き換えてみるだけでOKでしょう。

	if key&16 : if shottime=0 : shottime=5 : gosub *generation : else : shottime-

 

続いて敵機を追加し自動移動もさせてみましょう。

完全なランダムな動きにすると縦横無尽に飛び回って面白そうですが、現実は甘くありません(謎

ほとんど同じ場所で小刻みに震えるだけで止まっているのとほとんど変わりません。

パターン化しておくと動きが同じで慣れると先が読めてしまいますが、

実際に試してみるとさほど気になるものではありませんのでパターン化することを強くお勧めします。

敵機の移動を複雑なものにしたければしていただいて構いません。ココでは単純な繰り返しで…。

敵機の出現時間はどうしますか?ランダムにするのは面白いかもしれませんが推奨できません。

一度にまとめて出現したり、予定を大幅に遅れたりすることがあるからです。

大抵のゲームでは決められた時間に決められた位置から出現し決められた行動を取ります。

ココではタイムテーブル(?)を使い、開始から指定時間後に出現させたいと思います。

まず準備として準備終了のコメントより前に下記スクリプトを追加してください。

#define TSPEED 8 ; 弾移動速度
#define MSPEED 5 ; 自機移動速度
#define ENMSPEED 10 ; 敵機基本移動速度
#define ENMMAX 5 ; 一度に出現出来る敵最大数
	dim enemy,ENMMAX ; 敵機情報
	dim enmx,ENMMAX ; 敵機X方向の位置情報
	dim enmy,ENMMAX ; 敵機Y方向の位置情報
	dim dir,ENMMAX ; 敵の進行方向
	dim tm1,30 ; タイムテーブル1
	dim tm2,30 ; タイムテーブル2
	tm1=1,1,1,1,1,2,2,2,2,2,1,1,2,1,2,2,1,1,3,3,1,3,1,3,3,1,2,2,3,4
	tm2.0  =  30,  70, 110, 130, 150, 200, 240, 275, 310, 340
	tm2.10 = 365, 415, 440, 460, 480, 500, 525, 540, 560, 590
	tm2.20 = 600, 620, 650, 675, 700, 730, 740, 750, 760, 850

また準備コメントの最後にあるウィンドウID3のバッファを以下のように書き換えてください。

	buffer 3,256,SIZE*2
	color : boxf
		color ,,255
	font "MS 明朝",10
	pos 0,0 : mes "●"
	color 230,150
	pos 0,TSIZE : mes "●" ; 敵弾
	color 200
	font "MS 明朝",SIZE
	pos TSIZE,0 : mes "▲"
	color ,200
	pos SIZE+TSIZE,0 : mes "▼" ; 敵機1
	color ,150,150
	pos SIZE*2+TSIZE,0 : mes "▼" ; 敵機2
	color 100,100
	pos SIZE*3+TSIZE,0 : mes "▼" ; 敵機3
	color 230,230
	font "MS 明朝",SIZE*2
	pos SIZE*4+TSIZE,-5 : mes "▼" ; 敵機4(ボス)
	repeat 256
		color 255-cnt,cnt
		line cnt,SIZE*2-10,cnt,SIZE*2 ; 後に使用するパワーゲージ用
	loop

コレでやっと準備の終了です。

次のスクリプトをmainラベル内に追加し、ラベルも入れてください。

*main
				:
	gosub *enmgene ; この行を追加する
	gosub *enmaction ; この行を追加する
				:

*enmgene ; 敵機生成
	if tm3>29 : return ; 配列要素のオーバーフローエラー回避分
	if count<tm2.tm3|(tm3.1>=ENMMAX) : return
	repeat ENMMAX
		if enemy.cnt=0 {
			enemy.cnt=tm1.tm3
			rnd enmx.cnt,WX/2 : enmx.cnt+=WX/4
			enmy.cnt=0
			rnd dir.cnt,2 : if enemy.cnt=4 : dir.cnt+
			break
		}
	loop
	if tm3<30 : tm3+ ; 現在何番目の敵か
	tm3.1+ ; 現在表示されている敵数
	return

*enmaction ; 敵機行動
	repeat ENMMAX
		if enemy.cnt=0 : continue
		if enemy.cnt=1 { ; 敵機1
			if dir.cnt=1 : enmx.cnt+=ENMSPEED : if WX-SIZE<enmx.cnt : dir.cnt=0
			if dir.cnt=0 : enmx.cnt-=ENMSPEED : if enmx.cnt<0 : dir.cnt=1
			enmy.cnt+=5
		}
		if enemy.cnt=2 { ; 敵機2
			if mx>enmx.cnt : enmx.cnt+=ENMSPEED/3 : else : enmx.cnt-=ENMSPEED/3 ; 自機に向かう
			enmy.cnt+=8
		}
		if enemy.cnt=3{ ; 敵機3
			if dir.cnt=1 : enmx.cnt+=ENMSPEED : if WX-SIZE<enmx.cnt : dir.cnt=0
			if dir.cnt=0 : enmx.cnt-=ENMSPEED : if enmx.cnt<0 : dir.cnt=1
			enmy.cnt+=4
		}
		if enemy.cnt=4{ ; 敵機4(ボス)
			if dir.cnt=1 : enmx.cnt-=ENMSPEED : if enmx.cnt<50 : dir.cnt=2
			if dir.cnt=2 : enmx.cnt+=ENMSPEED : if -SIZE*2+WX-50<enmx.cnt : dir.cnt=1
			if dir.cnt=4 : enmy.cnt+=ENMSPEED : if -SIZE*2+WY-20<enmy.cnt : dir.cnt=2
			if dir.cnt=8 : enmy.cnt-=ENMSPEED : if enmy.cnt<20 : dir.cnt=1
			if WX-20/2<enmx.cnt&(WX+20/2>enmx.cnt)&(dir.cnt&3) {
				rnd tmp,3
				if tmp=1 : if WY/2>enmy.cnt : dir.cnt=4 : else : dir.cnt=8
			}
		}
		if enmy.cnt>WY : enemy.cnt=0 : enmy.cnt=0 : tm3.1- ; 画面外に出て行くと消滅させる
	loop
	return

コレで敵の行動は完了です…が、敵機を表示していませんので確認できませんね(^^;

body」ラベル内に下記を追加すれば完璧(?)です。

	repeat ENMMAX
		if enemy.cnt=1 : pos enmx.cnt,enmy.cnt : gcopy 3,SIZE+TSIZE,0,SIZE,SIZE
		if enemy.cnt=2 : pos enmx.cnt,enmy.cnt : gcopy 3,SIZE*2+TSIZE,0,SIZE,SIZE
		if enemy.cnt=3 : pos enmx.cnt,enmy.cnt : gcopy 3,SIZE*3+TSIZE,0,SIZE,SIZE
		if enemy.cnt=4 : pos enmx.cnt,enmy.cnt : gcopy 3,SIZE*4+TSIZE,0,SIZE*2,SIZE*2-10
	loop

さて、それでは準備も含め解説していきましょう。

キャラクタを仮想メモリに描画しているところの敵機4(ボス)のY位置が-5となっていますが、

サイズが自機の2倍もあり無駄が大きいので5ドット分カットしているだけでそれ以外に特に理由はありません。

パワーゲージは当たり判定のところでしますのでココではまだ使用しません。

タイムテーブルを使い敵の出現時間を指定すると書きましたね。

tm●」という変数がソレに当たり、「tm1」は出現させる敵ID(種類)、「tm2」が出現時間テーブルです。

それでは敵機について紹介しましょう(笑

配列前半に多いID.1は左右端まで蛇行しながら進んでいくだけのタイプです。

ID.2は自機に向かって突っ込んでくるだけのタイプです。

ID.3はID.1のパワーアップ版で蛇行は同じですが弾を飛ばしてきます。

ID.4はボス敵です。縦横共に高速飛行し弾を8方向に飛ばしてきます。

…といってもまだ今回は敵弾の生成・描画がありませんので。

enmgene」ラベルの初めに「if count<tm2.tm3|(tm3.1>=ENMMAX)」という判断文がありますが、

コレは敵機の出現予定時間になっても現在出現している敵機数が予定より多くなってしまう場合、

出現させないで空くまで保留させるためのものです。

コレがないと確保量よりも大きいところにセットしようとしてエラーが出たり、

予定時間に出られないと出現させないとしてしまうとボスの場合などとんでもないことになりかねません。

ザコ敵の保留が一定時間を超えた場合、出現させないとした方がいいかもしれません。

コレくらいの数だと気になりませんけどね…。

ボス敵の動きが他のザコより少し複雑ですので解説しておくと、進行経路はカタカナの「エ」の形になります。

横移動が基本で大体X座標が画面中央に来ると3分1の確率(実際は違うが)で縦移動します。

dirの値が飛び飛びなのは横移動判定で「if dir=1 or dir=2」と2つ使うのではなく、

if dir&3」と1つだけでいけるようにするためのものです。

ビット(論理)演算の&3で引っかかるのは1又は2のどちらか1つ以上が当てはまる時です。

ココではAND演算についての解説はしませんがそういうものだということだけ覚えておいてください。

…で「if WX-20/2<enmx.cnt&(WX+20/2>enmx.cnt)&(dir.cnt&3)」の式なんですが、

「もし敵機(ボス)のX座標がほぼ真中で進行方向が3(1(左)or2(右))の時」に実行するということとなります。

 

敵の動きがずらずらと書かれてても問題はありませんが視認性を向上させることや、

別のシューティングを作成するときに動きの使い回しができる様にモジュールにしちゃいましょう。

下記スクリプトをmotion.asという名で保存し、一番初めにある#defineの次(前はダメ!)に結合してください。

#module "motion"
#deffunc motion int
	mref id,0 ; 敵番号
	if enemy@.id<1|(enemy@.id>4) : return
	switch enemy@.id
		case 1:
			if dir@.id=0 : enmx@.id-=ENMSPEED@ : if enmx@.id<0 : dir@.id=1
			if dir@.id=1 : enmx@.id+=ENMSPEED@ : if WX@-SIZE@<enmx@.id : dir@.id=0
			swbreak
		case 2:
			if mx@>enmx@.id : enmx@.id+=ENMSPEED@/3 : else : enmx@.id-=ENMSPEED@/3
			swbreak
		case 3:
			if dir@.id=1 : enmx@.id+=ENMSPEED@ : if WX@-SIZE@<enmx@.id : dir@.id=0
			if dir@.id=0 : enmx@.id-=ENMSPEED@ : if enmx@.id<0 : dir@.id=1
			swbreak
		case 4:
			if dir@.id=1 : enmx@.id-=ENMSPEED@ : if enmx@.id<50 : dir@.id=2
			if dir@.id=2 : enmx@.id+=ENMSPEED@ : if -SIZE@*2+WX@-50<enmx@.id : dir@.id=1
			if dir@.id=4 : enmy@.id+=ENMSPEED@ : if -SIZE@*2+WY@-20<enmy@.id : dir@.id=2
			if dir@.id=8 : enmy@.id-=ENMSPEED@ : if enmy@.id<20 : dir@.id=1
			if (dir@.id&3)&(WX@-10/2<enmx@.id)&(WX@+10/2>enmx@.id) {
				rnd tmp,3
				if tmp=1 : if WY@/2>enmy@.id : dir@.id=4 : else : dir@.id=8
			}
	swend
	tmp=0,5,8,4 ; 移動用には要素の1,2,3しか使用しない,それぞれの敵機IDに対応している
	if enemy@.id!=4 : tmp=enemy@.id : enmy@.id+=tmp.tmp ; ボス以外Y方向に進む
	if enmy@.id>WY@ : enemy@.id=0 : enmy@.id=0 : tm3@.1-
	return
#global

では上記スクリプトを解説していきます。

swichはHSP2.6から使えるようになったマクロ命令です。

ココでは解説しませんので入門講座をご覧ください。

それ以外については「enmaction」ラベルをそのまま持ってきた(移植)だけですので問題はありませんね。

…とよく見ると、変数名が微妙に違いますね。

潰れて見にくいと思いますけど「@(アット)」マークが変数の終端に付いています(全部ではないですが)。

 

入門講座では少しレベルが高そうだったので省略していたのですがココでは解説しておきたいと思います。

@の話の前にスコープについて話さなければダメっぽいと思うのでスコープから…。

「〜スコープ」という名前を一度くらいは聞いたことないでしょうか?オシロスコープとか。

まぁ何かを見たりするものであったり、範囲(scopeそのまま)であったりします。

変数のスコープ(変数の見える範囲、即ちその変数が使える範囲)と言うのも存在します(HSP以外の言語も)。

HSP以外の言語を触ったことある人はご存知と思いますが、

とある変数をその変数のスコープ外で使用しようとすると変数が使えなかったり、値が空だったりします。

HSPではどこかで使用していた変数を別の場所で使っても先ほどのデータが入ったままです。

しかし入門講座のモジュール編で「モジュールは別空間」というのが書かれていたと思います。

通常とは別空間になっていてモジュール外と同名変数を使用してもいける、というものでした。

#module "test"
#deffunc test
	mes testvar
	return
#global testvar="てすと" mes testvar test mes testvar stop

こんな感じで…。衝突(?)がないので便利と言えば便利ですね。

ただ逆の、とある変数のデータを使いたいとなった場合、

今までは新命令のパラメータ(引数)としてしか渡していませんでした。

数値パラメータ以外は3つ以上渡せませんでしたね(配列を使えば回避できるが)。

パラメータとして渡す以外に方法はないのか、となりますがきちんとあるんですね。

共有する(グローバルな)変数として使う場合、先ほどの様に変数名の最後に@を付けます

そーいえばFAQでもあったと思いますが変数だけでなくプラグインの拡張命令にも@を付けるのでした。

#module "test"
#deffunc test
	mes testvar@
	return
#global testvar="てすと" mes testvar test mes testvar stop

…で先ほどのスクリプトではこの方法を使用しているというわけですね。

移植作業をしたのでモジュール側が当然増えてくるわけですがコレで使い回しができると思えばOKでしょう^^;

当然元のスクリプトがあのままではモジュールを結合した意味がありません。

元スクリプトの「enmaction」ラベルの内容を下記のように書き換えてください。

*enmaction
	repeat ENMMAX
		if enemy.cnt=0 : continue
		if enemy.cnt : motion cnt ; パラメータとして何番目の敵機かを指定する
		loop
	return

新命令を使用する側の視認性・可読性は随分とよくなりました。

先ほどモジュールの結合は#defineの次と書いてましたね。

#defineより前に書くと定義しているENMSPEED等が使えなくなってしまうからです。

別にモジュール内で再定義してもいいのですが意味がありませんので…。

 

まだ少し長くなりそうなのでコレで一旦終了します。

この章のダウロード用スクリプトは敵の動きをモジュールにしたものだけです。コチラからどうぞ。

シューティング側のスクリプトは次章の初めに全部書かれています。

それでは。