【プチコン4講座】SPVARを使って弾を複数撃てるようにしよう

プチコン4

プチコン4 シューティングゲーム 連射

こんにちは。継続の錬金術士なおキーヌです。

ブログ毎日更新は215日目になります。

第6回「【プチコン4講座】弾を発射できるようにしよう」にて弾を発射できるようになりました。

プレイヤーだけならいいのですが、弾にまでXとY座標をグローバル変数で持たせると、
大量の弾を管理するときとてつもないグローバル変数が生み出されてしまいます。

弾は味方だけとは限らないので敵も含めると凄まじい管理量になってしまうので非効率です。

クラス機能があれば各自インスタンスが変数を持つことが可能ですがプチコン4にはクラス機能はデフォルトではありません。

配列で一括管理をしてもいいのですが、メモっておかないとどれがどの変数だったのかも意味不明になります、

今回はSPVARというスプライトが持つ専用変数の使い方を覚えて効率化します。

それではプチコン4でSTG作りその第7回目始めましょう。

  1. スプライト専用変数SPVARを学ぼう
  2. プレイヤーと弾の座標をSPVARで管理しよう
  3. 弾を連射できるようにしてみよう
  4. シューティングゲーム作り講座第7回まとめ

スプライト専用変数SPVARを学ぼう

今回は先に進む前に覚えておいてほしいことがあるんだ。

またお勉強かよ!早くゲーム作りを進めたいぜ!

ダメよ。先生の言うことはちゃんと聞きなさい。

リキくん、弾を連射してみたくない?今回教えることを覚えてもらえれば弾の連射が可能になるんだ。

よし!トモカズ!早速教えてくれ!

現金なやつね

勉強するなら何か先に楽しみがないとね。それじゃあ下記のコードを打ち込んでみて。

' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ' スプライトを定義
  SPSET 0, 352 ' プレイヤー
  SPVAR 0, "X", 0
  SPVAR 0, "Y", 0
end

SPVAR?なんだこれ

VAR…もしかして変数宣言?

マユミちゃん正解。何もないところでVARをするとグローバル変数になるっていうのは前に教えたよね。

SPってことはスプライト変数ってことか!……って勢いで言ってみたけどあってんのか?

うん、大丈夫だよ。そのSPVARはスプライトだけが扱うことができる変数なんだ。

グローバル変数を使わなくなったけど、結局スプライト変数を宣言しないとだめなら一緒じゃないの?

確かに宣言するって点は同じなんだけど宣言するときにスプライト管理番号は数値で管理してるでしょ?その数値は今までプレイヤーなら0、弾なら10っ直接指定していたんだけどここを変数にしてしまえば中に入っている数値によって管理番号を切り替えられるんだ。

ってことはforとかループを使えば実質1回記述したらループ回数分の変数を宣言できるってこと?

そういうこと。変数の増減とかも管理番号を変数にして、変更したい管理番号を渡されたら該当のスプライトの変数が書き換わるって寸法さ。

それならスプライト毎に座標を持たせておけばメチャクチャ楽になりそうだな!

でも気を付けてね。SPSETをする前にはSPVARは使えないから注意して!

プレイヤーと弾の座標をSPVARで管理しよう

それじゃあ早速スプライト変数を作ろうか。下記のコードを打ちこんでね。あ、言い忘れていたけど今回からグローバル変数は「G_」を頭文字に付けるから気を付けてね。

' ループ用変数
var G_I

' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ' スプライトを定義
  SPSET 0, 352 ' プレイヤー
  SPVAR 0, "X", 0
  SPVAR 0, "Y", 0

  ' ループさせて弾を生成
  for G_I=10 to 14
    SPSET G_I, 1322,0 ' 弾は最初からHIDE状態
    SPOFS G_I, -16, -16
    SPVAR G_I, "X", 0
    SPVAR G_I, "Y", 0
    SPVAR G_I, "SHOT_FLAG", 0
  next
end

ループを使って弾を5つ生成したよ。ここでforの初期値を10にするのを忘れないでね。0にしちゃうとプレイヤーのものが上書きされちゃうんだ。

SPVARの後は管理番号を渡して、その次に文字列で変数名を決めるんだ。その後に入れたい値を指定する感じだよ。

後はボタンを押されたら1つずつ発射していくのかしら?

実は発射するタイミングの調整がちょっと難しいんだ。何もしないままだと弾が5つ重なって飛んでいくからね。

バルカンならちょっと間隔あけて連発しているけどピストルだと5弾同時発射は違和感しかねぇな!

これも正解は一つじゃないんだ。弾が発射されてから一定時間は弾が出ないようにしたり、ボタンを押した数だけしか弾を出せなかったり色々あるね。中でも時間で制御するのは時間を管理しないといけないから今回は見送りかな。バルカン打ちたい人はごめんね。

ピストルなら引鉄弾いて1発だからボタンを押した数だけでいいんじゃねぇか?押しっぱなしでいいだけだと、それこそ避けるだけのゲームになるしな。

僕もそっちの方が説明しやすくて助かるよ。バルカン風の連射はいずれ解説するね。

ねぇ、SPVARの設定方法はわかったんだけどどうやって使うの?普通の変数とはちょっと違うよね?

SPVARが命令だとしたら、SPVAR()は関数になるんだ。後者を使うと設定した変数名を指定すれば中に入っている値を取り出せるよ。使い方は簡単で設定した時とほぼ同じなんだ。使い方はまた後で説明するね。とりあえず弾の打つ数を増やしてみよう。

弾を連射できるようにしてみよう

プチコン4でのボタン命令は基本的にプログラム内部では押されていると超高速で連打されている状態になっているんだ。だからこのままだと弾が1フレーム単位で発射されて殆ど重なってみえるようになってしまうよ。

最初の1回だけ押されたって状態になればいいのよね?でも公式リファレンスにそんな設定あったかしら……リピートに関しては見た気がするんだけど……あ、「ボタンを押した瞬間(BREPEAT対応なし)」ってのがあったわね。これかしら?

うん、一応それなんだけどボタン状態を受け取る変数Bにその設定をしちゃうと十字キーも全部連打しないと動かせないんだ。流石にそれはしんどいでしょ?かといって何個も変数を作るのもナンセンスだよ。

じゃあどうするんだ?お手上げか?

これはボクなりのやり方なんだけど、専用のカウンター配列を作るんだ。

カウンター配列?

公式のやり方だとボタンを押されている秒数っていうのはカウントされてるかもしれないけどたぶん自由に取り出せないはずなんだ。もし何秒以上押してから離したらより弾が遠くに飛ぶみたいな処理を作ろうとするとカウント配列があった方が制御しやすいよ。いわゆるチャージショット的なものを作りたいときとかに便利なんだ。なぜ配列かというと、ボタン毎にカウントの管理をしたいからさ。

そうね、それなら1回押された時に変数が1かどうかを調べれば最初の1回だけ押されたかどうかの判断ができるわね!

それじゃあボタンカウント関数を作っていくよ。

ボタンカウントを作ろう

' ボタンカウント配列
DIM G_BtnPressCount[4]

' コントローラー
'───────────────────────────────
def D_Controller
  ' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
  VAR B = BUTTON(0)

  ' 押されているボタンの情報を表示
  if (B AND 1 << #B_LUP) != 0 then
    ' 上を押されたらY座標を減算
    SPVAR 0,"Y", SPVAR(0,"Y")-1
  elseif (B AND 1 << #B_LDOWN) != 0 then
    ' 下を押されたらY座標を加算
    SPVAR 0,"Y", SPVAR(0,"Y")+1
  endif
  if (B AND 1 << #B_LLEFT) != 0 then
    ' 左を押されたらX座標を減算
    SPVAR 0,"X", SPVAR(0,"X")-1
  elseif (B AND 1 << #B_LRIGHT) != 0 then
    ' 右を押されたらX座標を加算
    SPVAR 0,"X", SPVAR(0,"X")+1
  endif

  ' Yボタン処理
  if (B AND 1 << #B_LREFT) != 0 then
    ' ボタンカウント命令に#B_RLEFTを渡す
    D_BtnPressCount #B_RLEFT
    ' Yボタンのカウントが1であれば
    if G_BtnPressCount[2] == 1 then
      D_PlayerShot
    endif
  else
    ' ボタンが離されたらカウントが1以上の時
    if G_BtnPressCount[2] >= 1 then
      ' ボタンカウントを0にする
      G_BtnPressCount[2] = 0
    endif
  endif
end


' スプライト(プレイヤー)の移動
'───────────────────────────────
def D_PlayerMove
  ' プレイヤーの移動
  SPOPS 0, SPVAR(0,"X"), SPVAR(0,"Y")
end

グローバル変数のXとYを廃止するから十字操作もSPVARに置き換えておいてね。

ちょっと複雑になってきたけどやってることは単純だから1つずつ説明していくよ。まずはボタンカウント用の配列をグローバルで作るんだ。

次にコントローラー関数を改良するよ。ボタンの状態を受け取っていたBがグローバル変数だったけどここでしか使わないからローカル変数に切り替えているのに注意してね。グローバル変数の方のBは消してもいいよ。よくわからない場合は記事最終にのっけているコードを自分のものと照らし合わせて見てね。

Yボタン処理の中身を見ていくよ。まずYボタンが押されたら~っていうのは変わってないね。問題はその中。「D_BtnPressCount」っていう命令に「#B_RLEFT」を渡しているね。これは後ほど説明するよ。ここではボタンカウント命令を呼び出しているってだけ覚えておいて。

その次にボタンカウント2、つまり配列の0から数えて3つ目だね。プログラミングは基本0から数えるよ。覚えているかな?

ボタンカウント2が1であれば「PlayerShot」命令を呼び出しているよ。この命令はまだ作ってないから後で作るね。名前の通りプレイヤーのショット処理をする命令にするつもりだよ。

次に、ボタンが離されたらボタンカウントが1以上の場合はボタンカウントを0にするという処理を作っているから、ボタンを離したらカウンターがリセットされるという仕組みさ。

それじゃあ自作したボタンカウント関数の仕組みを見てみようか。大丈夫?ついてこれてる?

えぇ、続けて大丈夫よ。

だ、大丈夫だ……

リキくんは余り大丈夫そうじゃないね(笑) でももう少しだから頑張って!

ボタンカウント関数の仕組み

これがボタンカウント関数。自作命令・関数を作るときはDEF~ENDは前に教えた通りだよ。

' ボタンカウント関数
'───────────────────────────────
def D_BtnPressCount A_Button
  ' ボタンカウント分岐
  case A_Button
    ' 渡されたボタンが#B_RLEFTだったら
    when #B_RLEFT
      ' ボタンカウント配列の数が256未満かどうか調べる
      if G_BtnPressCount[2] < 256 then 
        ' 条件に一致したら+1する
        INC G_BtnPressCount[2]
      endif
  endcase
end

関数じゃなくても引数を取ることは出来るんだ(A_Buttonのこと)。中身を見てみると、「case」という新しい制御文が登場しているのに注目してね。

これはぶっちゃけifでも代用できるんだけどプチコン4で追加された命令だから使っていくよ。他のプログラミング言語でもcase文(swtich文)はあるから覚えておいて損はないね。

caseに変数(ここでは引数を渡している)を渡して、その後に「when」の後ろに書いた条件に一致する処理が動くようにできるんだ。ここだと「#B_RLEFT」になっているね。

もう一度この命令を使っている部分をみてみようか。

D_BtnPressCount #B_RLEFT

引数に「#B_RLEFT」が渡されているね。ってことはココで呼び出されているボタンカウント関数は条件に一致するってことになるよ。

条件が一致したらまずボタンカウント関数が256以上じゃないかを調べて、該当しなければボタンカウント関数に1を足していくんだ。

256にしているのはただの気分だからプログラムが許す限り好きな数字にしてもいいよ。1000回までカウントしたかったら256を1000とかに変えればOKさ。

それ以外の条件はないからこの命令はここで終了。あとは元の処理に戻って1ループが終わった手もまだボタンが押されてたら延々と繰り返す感じだね。

ちょっと長くなってしまったので頭がパンクしている人も出てくるかもしれません。

余裕だぜ!って人は続けて読んでくださって大丈夫です。

もしちょっとでもわからん!ってなった場合は一度コーヒーブレイクでも挟みましょう。

それでもわからない場合はとりあえずコードを打って動かしてみてどういう動きになるかを見たほうが理解しやすいかもしれませんね。

そんなときは「PRINT」文を使って表示される速度や回数をチェックしてみるとわかりやすくていいですよ。

それでは続きを始めましょう。

弾を複数打てるようにする。

ボタンカウントも出来るようになったし、最初の1回だけボタンを認識するようにも作れたからこれで複数弾を発射できる準備が整ったよ。

でもこのままだとどの弾が発射されているのかわからなくなりそうね。

そうだね。ボクもあまりシューティングを作ったことが無いから最適解は教えてあげられないんだけど、
弾を撃つ制限がある場合ボタンが押された時に毎ループ全弾の発射状態を確認する必要があるんだ。

毎回全部の状態を調べるってのか?なんか処理落ちしそうだな

1F単位処理且つ弾の数が膨大だった場合、性能次第では処理落ちするかもしれないね。
でもSwitchは性能は高いほうだし、たとえ人間レベルで最速で押せたとしても普通の人は60フレーム中16回も押せないだろうし全然問題ないよ。

ふーん、そういうもんなのか。

それじゃあ弾を複数発射する命令を作っていくよ。ちょっとSPVAR命令の中にSPVAR関数を使うという見辛いところもあるかもしれないけどふんばってね。

弾の移動処理常に監視しておきたいからショットと弾の動きは別の命令にしておくよ。

' ループ開始
loop
  D_Controller
  D_PlayerMove
  D_PlayerShotMove
  ' 垂直同期
  VSYNC
endloop


' プレイヤーショット処理
'───────────────────────────────
def D_PlayerShot
  for G_I=10 to 14
    ' 弾にショットフラグが立っていなければ
    if SPVAR(G_I,"SHOT_FLAG") == 0 then
      BEEP 10
      ' 表示フラグをONにする
      SPSHOW G_I
      ' 出現フラグをONにする
      SPVAR G_I,"SHOT_FLAG", 1
      ' 弾の座標をプレイヤーの前方に設定する
      SPVAR G_I, "X", SPVAR(0,"X") + 8
      SPVAR G_I, "Y", SPVAR(0,"Y") + 4
      ' ショット命令が1回動いたら無条件でループを抜ける
      break
  next
end

' プレイヤーの弾の移動処理
'───────────────────────────────
def D_PlayerShotMove
  for G_I=10 to 14
    ' 弾にショットフラグが立っていれば
    if SPVAR(G_I,"SHOT_FLAG") == 1 then
      ' 弾のX座標を加算
      SPVAR G_I, "X", SPVAR(G_I,"X") + 8
      ' 弾を移動させる
      SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
      ' 弾の座標が400を越えたら
      if G_I, SPVAR(G_I,"X") > 400 then
        ' 画面外に行くのでショットフラグを0にして非表示にする
        SPVAR G_I,"SHOT_FLAG",0
        SPHIDE G_I
      endif
    endif
  next
end

これで弾を連続して撃てるようになったはずだよ。

おぉ!撃てる!撃てるぞ!オラオラオラオラ!

プチコン4 シューティングゲーム 表示結果1

なんか連射できるだけで楽しくなるわね。敵がいないから虚しくなるけど(笑)

そうだな…俺はなんのために戦ってるんだ…

そこは追々やっていこうね。

この「break」って初めてよね。これってなにしてるの?名前からして破壊?

基本的にforやwhileのようなループを途中で抜けたいと思った時に使うものだと思ってくれればいいかな。処理を壊すっていうよりも処理から離脱するって感じかな。試しに「break」をコメントアウトするか消してみて動かしてみるとよくわかるよ。

あれ?1発しかでなくなったぞ?

1発分しかみえなくなったって言う方が正しいね。実際には5発ちゃんと発車されてるんだ。「break」がないとループ分全部の管理番号を処理しちゃうからループの中にG_IをPRINTしてみるとよくわかるよ。

連射の仕組みはまず最初のループで1つめの「SHOT_FLAG」が1(発射中)でなければ
条件を満たして処理が動いて、最後に「break」で次のループに行かないようにしています。

発射中であれば、条件に一致しないので次のループに移ります。

これがボタン押された度に呼び出されて発射可能なスプライトが発射されるといった感じです。

弾移動命令に関してはゲームループで常に呼ばれている感じにしないと弾が飛んで行きません。

発射フラグがONのスプライトだけ画面外に行くまで動き続ける感じです。

画面外に行くと発射フラグがOFFになるので、次にボタンを押した時に発射フラグがOFFのものが飛んでいくといった仕組みです。

プログラミングはごちゃごちゃしているようでやっていることは結構単純なので、
紐解いてみるとこんなもんかっていう重なりです。

シューティングゲーム作り講座第7回まとめ

SPVAR スプライト管理番号, 変数名, 代入する値

SPVARの設定です。値を代入したいときに使います。

スプライト変数はsmilebasic4の内部処理的に厳密には違いますが、扱い的にはグローバル変数と同じ感じで1度宣言すればどこでも使えます。
注意点としてはSPSETをしてから実行しなければいけないと言うことです。

SPVAR(スプライト管理番号, 変数名)

SPVAR変数の指定名の値を返してくれます。

関数なのでこれを使うと変数ではなく中身だけを取り出すのでこれを直接INCで+1することはできないようです。

case 変数 ~ when 条件 ~ endcase

JavaScriptだとSwitch文に該当する分岐用処理です。IF~ELSEIFよりシンプルに分岐できます。

caseの隣で指定した変数と合致するwhenの条件に合致すればそこの中の処理が実行されてcaseから抜けます。

他の言語だと処理の終わりにbreak命令を入れないと次のcaseも動いてしまうのですがプチコン4ではbreakなしでも大丈夫です。

break

処理を中断させる命令です。主にforやwhileなどのループ処理の中で使うことが多いです。

今回の弾連射のように何かが動いたらループを強制的に止めたいといった使い方に効果を発揮します。

今回でさらにシューティングゲームぽくなりましたね。

プレイヤーだけだとゲームにならないので、次回は当たったらミスになる障害物を作りましょう。

ネタバレしておくと障害物もスプライトで作って、ランダムでマップに配置して常に左に動き続けるようにすれば、
あたかもプレイヤーが前方に進んでいるかのように見えます。

背景は砂漠一食なのであまり進んでるようにみえないのでそのうち遠景を付けて延々と砂漠を走り続けてるイメージにしたいところです。

それでは。

' ゲーム初期化
ACLS
' ループ用変数
var G_I
' ボタンカウント配列
DIM G_BtnPressCount[4]

D_Initialize

' ループ開始
loop
  D_Controller
  D_PlayerMove
  D_PlayerShotMove
  ' 垂直同期
  VSYNC
endloop


' ゲーム初期化
'───────────────────────────────
def D_Initialize

  ' マップ描写
  D_MapDraw

  ' スプライト初期化
  D_SpriteInitialize
end

' コントローラー
'───────────────────────────────
def D_Controller
  ' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
  VAR B = BUTTON(0)

  ' 押されているボタンの情報を表示
  if (B AND 1 << #B_LUP) != 0 then
    ' 上を押されたらY座標を減算
    SPVAR 0,"Y", SPVAR(0,"Y")-1
  elseif (B AND 1 << #B_LDOWN) != 0 then
    ' 下を押されたらY座標を加算
    SPVAR 0,"Y", SPVAR(0,"Y")+1
  endif
  if (B AND 1 << #B_LLEFT) != 0 then
    ' 左を押されたらX座標を減算
    SPVAR 0,"X", SPVAR(0,"X")-1
  elseif (B AND 1 << #B_LRIGHT) != 0 then
    ' 右を押されたらX座標を加算
    SPVAR 0,"X", SPVAR(0,"X")+1
  endif

  ' Yボタン処理
  if (B AND 1 << #B_LREFT) != 0 then
    ' ボタンカウント命令に#B_RLEFTを渡す
    D_BtnPressCount #B_RLEFT
    ' Yボタンのカウントが1であれば
    if G_BtnPressCount[2] == 1 then
      D_PlayerShot
    endif
  else
    ' ボタンが離されたらカウントが1以上の時
    if G_BtnPressCount[2] >= 1 then
      ' ボタンカウントを0にする
      G_BtnPressCount[2] = 0
    endif
  endif
end

' ボタンカウント関数
'───────────────────────────────
def D_BtnPressCount A_Button
  ' ボタンカウント分岐
  case A_Button
    ' 渡されたボタンが#B_RLEFTだったら
    when #B_RLEFT
      ' ボタンカウント配列の数が256未満かどうか調べる
      if G_BtnPressCount[2] < 256 then 
        ' 条件に一致したら+1する
        INC G_BtnPressCount[2]
      endif
  endcase
end


' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ' スプライトを定義
  SPSET 0, 352 ' プレイヤー
  SPVAR 0, "X", 0
  SPVAR 0, "Y", 0

  ' ループさせて弾を生成
  for G_I=10 to 14
    SPSET G_I, 1322,0 ' 弾は最初からHIDE状態
    SPOFS G_I, -16, -16
    SPVAR G_I, "X", 0
    SPVAR G_I, "Y", 0
    SPVAR G_I, "SHOT_FLAG", 0
  next
end

' スプライト(プレイヤー)の移動
'───────────────────────────────
def D_PlayerMove
  ' プレイヤーの移動
  SPOPS 0, SPVAR(0,"X"), SPVAR(0,"Y")
end

' マップの描写
'───────────────────────────────
' マップの塗りつぶし描写
def D_MapDraw
  ' 背景の色味を変更
  TCOLOR 0, &Hdddddd00
  ' 背景の塗りつぶし
  TFILL 0,0,0,24,14,CHR$(&HE8D8)
end

' プレイヤーショット処理
'───────────────────────────────
def D_PlayerShot
  for G_I=10 to 14
    ' 弾にショットフラグが立っていなければ
    if SPVAR(G_I,"SHOT_FLAG") == 0 then
      BEEP 10
      ' 表示フラグをONにする
      SPSHOW G_I
      ' 出現フラグをONにする
      SPVAR G_I,"SHOT_FLAG", 1
      ' 弾の座標をプレイヤーの前方に設定する
      SPVAR G_I, "X", SPVAR(0,"X") + 8
      SPVAR G_I, "Y", SPVAR(0,"Y") + 4
      ' ショット命令が1回動いたら無条件でループを抜ける
      break
  next
end

' プレイヤーの弾の移動処理
'───────────────────────────────
def D_PlayerShotMove
  for G_I=10 to 14
    ' 弾にショットフラグが立っていれば
    if SPVAR(G_I,"SHOT_FLAG") == 1 then
      ' 弾のX座標を加算
      SPVAR G_I, "X", SPVAR(G_I,"X") + 8
      ' 弾を移動させる
      SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
      ' 弾の座標が400を越えたら
      if G_I, SPVAR(G_I,"X") > 400 then
        ' 画面外に行くのでショットフラグを0にして非表示にする
        SPVAR G_I,"SHOT_FLAG",0
        SPHIDE G_I
      endif
    endif
  next
end