【プチコン4講座】各シーンのBGMとタイトル画面の実装
こんにちは。継続の錬金術士なおキーヌです。
ブログ毎日更新は220日目になります。
第12回「【プチコン4講座】スコア一定数値で難易度調整」にてスコアに応じて難易度を上昇させるようにしました。
本講座も残りもあと少しです。
現状のままでは少し寂しいので各シーンでBGMを流すようにしてみましょう。
ついでにタイトル画面の実装も行いよりゲームらしく完成の方向にもっていきます。
それではプチコン4でSTG作りその第13回目始めましょう。
BGMを鳴らしてみよう
ねぇトモくん。ゲームに音楽って流せないの?
そういえば作るのに夢中でBGMの存在を忘れていたぜ。
作ってる時に流すとプログラムを止めても流れたままで自分でBGMSTOPを打ち込まなきゃ止まらないから今まで使ってなかったんだ。折角だし何かBGMを付けてみようか。
BGMを選ぶ
キーボードの「F9」かJoyConのマイナスボタンを押して「Ctrl」を押してスマイルツール1を起動してね。
効果音の一覧が出てきたぜ。
Rを押すと次の項目へ行けるんだ。1回押したらBGMのリストになるよ。プログラミングには影響が無いからBGMは自分の好きなものを選んでいいよ。ボクは以下のようにしてみたよ。
- タイトル画面:13
- Lv0~1:2
- Lv2~3:31
- LvMaX:41
- ゲームオーバー:4
それじゃあまずはゲーム中のBGMを付けてみようか。BGMの再生は1回だけでいいよ。ループの中で再生しちゃうとループごとに曲の最初になって何も聞こえないから注意してね。
こんなのはどうかしら?フラグを1つ用意して、ONの時は処理を実行せず、シーンが切り替わるときにそのフラグをOFFにしてシーンに応じてBGMを再生するの。そしてフラグをONにすればループの中に入れておいても大丈夫じゃない?
いいね。それ採用。マユミちゃん段々とプログラマーらしくなってきたね!
でしょでしょ!
シーンがどれかはシーンフラグで管理してるから分岐はそれを使えば完璧だね。早速コードを書いてみようか。
G_BgmFlag = #FALSE
' ループ開始
loop
' コントローラーはどのシーンでも動くようにする
D_Controller
' BGM監視
D_BgmSelect
~~~省略~~~
endloop
' BGMセレクタ
'───────────────────────────────
def D_BgmSelect
if G_BgmFlag == #FALSE then
case G_SceneFlag
when 0
BGMPLAY 13
when 1
if G_PlayerStatus[4] >= 0 && G_PlayerStatus[4] <= 1 then
BGMPLAY 2
elseif G_PlayerStatus[4] >= 2 && if G_PlayerStatus[4] <= 3 then
BGMPLAY 31
else
BGMPLAY 41
endif
when 2
BGMSTOP
when 3
BGMPLAY 4
endcase
G_BgmFlag = #TRUE
endif
end
#FALSEと#TRUEってなんだこれ?
Bool値っていうんだけど、日本語なら真偽値。機械って0と1しか認識できないから0がOFFで1がON……っていうのは二進数の勉強の時にやったでしょ?そして#が付いているのは定数で中に数字や文字列が入っているんだ。FALSEは0、TRUEは1が入っているよ。
1と0じゃだめなのか?
他のプログラミング言語だとTRUEとFALSEって普通に使えるんだけどプチコンの場合は用意されていないみたいでどうやら定数を使って実装しているみたい。中身は結局「有るか無いか」だから1を有る。0を無いとみ立てているんだ。それに0とか1にしちゃうと必ずしも0がOFF出ない可能性があるんだ。
シーンのフラグなんかがそうよね?0はタイトルシーンであって別に無しってわけじゃないし。
た、たしかに……#FALSEだと0より無しっていう風にとらえやすいかもしれねぇ……
確かに0か1かの方が1文字で済むね。でも他の人が見た時何かわからないから出来る限りパッと見で意味の分かるものが最適なんだ。
後はシーン切り替えの時にBGMのフラグをOFFに出来れば完璧そうね。
毎回BGMSTOPとシーン切り替えを書くのが面倒だから引数ありの1つの命令にしてしまおうか。
' プレイヤー衝突チェック
'───────────────────────────────
def D_PlayerColCheck
var isHit = SPHITSP(G_SpvManageNo[0], G_SpvManageNo[3], G_SpvManageNo[4])
if isHit != -1 && SPVAR(isHit,"IS_HIT") == 0 then
BEEP 91, -300
DEC G_PlayerStatus[0]
' ライフが0になったら
if G_PlayerStatus[0] <= 0 then
' ゲームオーバーシーンへいく
D_SceneChange 3
endif
SPVAR isHit, "IS_HIT", 1
endif
end
' スコアの更新
'───────────────────────────────
def D_ScoreUpdate
D_ScoreAdd 0.1
' レベルアップ監視
if G_PlayerStatus[2] > G_GameLvUp[G_PlayerStatus[4]] then
INC G_PlayerStatus[4]
D_SceneChange 1
endif
GFILL 300,16,400,32,RGB(0,0,0,0)
GPUTCHR 300,16, STR$(FLOOR(G_PlayerStatus[2])),8,RGB(255,255,255),1
end
' ゲームリセット
'───────────────────────────────
def D_GameReset
GCLS
D_SpriteInitialize
D_PlayerStatusInitialize
D_ScoreInitialize
D_SceneChange 1
end
' シーン切り替え
'───────────────────────────────
def D_SceneChange A_Scene
BGMSTOP
G_BgmFlag = #FALSE
G_SceneFlag = A_Scene
endif
今のところ「ゲームオーバー」「Lvアップ」「リスタート」ぐらいしかシーンの切り替えが無いからそこのシーンフラグチェンジを置き換えたよ。
なるほど、レベルアップした時も呼び出して同じシーンだけどレベルによってBGMを変更しているのか!
タイトル用のシーンを実装しよう
本来はタイトル画面を用意したい所なんだけど絵を用意するのが面倒だったしプログラミングにはあまり関係なくなっちゃうから、プログラムをスタートしてから何かボタンが押されるまではゲームを開始しないようにするシーンを作るよ。
トモカズのそういう正直な所、好きだぜ。
まぁ、即席で絵を描くのは難しいわよね。
多重スクロールとか使って車がかっこよく走っているシーンをタイトル画面にしたかったんだけどまた今度だね。それじゃあタイトルシーンを作るよ。開始シーンを0にしておいてね。
' シーン用変数
G_SceneFlag = 0
' タイトル点滅用フラグ
G_FlashFlag = #FALSE
' ループ開始
loop
' コントローラーはどのシーンでも動くようにする
D_Controller
' BGM監視
D_BgmSelect
' フラグで処理を切り分ける
case G_SceneFlag
' タイトルシーン
when 0
D_GameTitle
~~~省略~~~
endcase
' 垂直同期
VSYNC
endloop
' タイトルシーン
'───────────────────────────────
def D_GameTitle
var frame = MAINCNT()
' プチコン4を起動してからの経過時間を元に30で割り切れたらフラグを切り替え
if (frame MOD 30) == 0 then
G_FlashFlag = !G_FlashFlag
endif
' 文字をフラグの状態により点滅させる
if G_FlashFlag == #FALSE
GPUTCHR 40,100,"PLEASE PRESS BUTTON",16,RGB(255,255,255),1
GPUTCHR 40+(16*13+8),100,"A",16,RGB(255,0,0),1
else
GFILL 40,100,400,116, RGB(0,0,0,0,)
endif
end
これで起動するとタイトルシーンで文字が点滅するよ。
#petitcom #プチコン4 #NintendoSwitch
やっつけタイトルシーン pic.twitter.com/JAaMrq7p8v
— なおキーヌ@ゲームクリエイターLv3 (@naokeyzmt) August 7, 2019
なんかBGMも入れるとゲームぽさがめちゃくちゃ増したぜ!
後はAボタンを押してゲーム開始の処理を入れたらいいわね。
最後にAボタン処理を作ろうか。
' コントローラー
'───────────────────────────────
def D_Controller
' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
VAR B = BUTTON(0)
~~~ 省略 ~~~
' Aボタン処理
if (B AND 1 << #B_LRIGHT) != 0 then
' ボタンカウント命令に#B_RLEFTを渡す
D_BtnPressCount #B_LRIGHT
' Aボタンのカウントが1であれば
if G_BtnPressCount[0] == 1 then
endif
else
' ボタンが離されたらカウントが1以上の時
if G_BtnPressCount[0] >= 1 then
' ボタンカウントを0にする
G_BtnPressCount[0] = 0
endif
endif
~~~ 省略 ~~~
end
' ボタンカウント関数
'───────────────────────────────
def D_BtnPressCount A_Button
' ボタンカウント分岐
case A_Button
' 渡されたボタンが#B_RRIGHTだったら
when #B_RRIGHT
' ボタンカウント配列の数が256未満かどうか調べる
if G_BtnPressCount[0] < 256 then
' 条件に一致したら+1する
INC G_BtnPressCount[0]
endif
~~~ 省略 ~~~
endcase
end
' タイトルシーン
'───────────────────────────────
def D_GameTitle
var frame = MAINCNT()
' プチコン4を起動してからの経過時間を元に30で割り切れたらフラグを切り替え
if (frame MOD 30) == 0 then
G_FlashFlag = !G_FlashFlag
endif
' 文字をフラグの状態により点滅させる
if G_FlashFlag == #FALSE
GPUTCHR 40,100,"PLEASE PRESS BUTTON",16,RGB(255,255,255),1
GPUTCHR 40+(16*13+8),100,"A",16,RGB(255,0,0),1
else
GFILL 40,100,400,116, RGB(0,0,0,0,)
endif
if G_BtnPressCount[0] == 1 then
GFILL 40,100,400,116, RGB(0,0,0,0,)
D_SceneChange 1
endif
end
これでゲームとしてはほぼ完璧だね!……ただタイトル画面で十ボタンを押すとプレイヤーの初期値が移動してしまってるバグがあるんだ。Aボタンを押したときにプレイヤーの座標を修正するか、ボタンの制御を行ってみると良いよ。改良点として自分で直せるか試してみてよ!
トモカズは意外と面倒くさがり屋だからな。
そういえば、面倒くさがりはプログラマに向いてるって聞いたことがあるわ。
そうだね……プログラミングを覚えたきっかけの1つでもあるかな。っと、最後にデバッグとしてレベルアップ点数を下げて確認してみようね。
#petitcom #プチコン4 #NintendoSwitch
お の れ は い れ つ ! pic.twitter.com/931HKiXfiT
— なおキーヌ@ゲームクリエイターLv3 (@naokeyzmt) August 7, 2019
あちゃー……配列の要素数越えちゃったね。まぁ次直そうか。
シューティングゲーム作り講座第13回まとめ
微妙にバグが潜んでいますが、自力で見つけてみて修正してみてください。
私も完全に忘れてた部分もあるのでデバッグしてみてココ直した方がいいんじゃないか?
って点を見つけたら自分で修正してみるとプログラミング力がアップしますよ。
それでは。
' ゲーム初期化
ACLS
' ループ用変数
var G_I
' シーン用変数
G_SceneFlag = 1
' ボタンカウント配列
DIM G_BtnPressCount[4]
' スプライト管理番号まとめ変数
DIM G_SpvManageNo[] = [0,10,14,20,30]
' プレイヤー用状態変数
' 0=> HP, 1=>能力, 2=>現在スコア, 3=>ハイスコア, 4=>ゲームLv
DIM G_PlayerStatus[5] = [2,0,0,0,0]
' レベルアップ基準値
DIM G_GameLvUp[5] = [5000, 15000, 30000, 50000, 100000 ]
' タイトル点滅用フラグ
G_FlashFlag = #FALSE
' ゲームの初期化
D_Initialize
' ループ開始
loop
' コントローラーはどのシーンでも動くようにする
D_Controller
' BGM監視
D_BgmSelect
' フラグで処理を切り分ける
case G_SceneFlag
' タイトルシーン
when 0
D_GameTitle
' メインシーン
when 1
D_PlayerMove
D_PlayerColCheck
D_PlayerShotMove
D_ObstacleMove
D_ScoreUpdate
D_PlayerLifeUpdate
' ポーズ中
when2
' ゲームオーバー
when3
D_GameOver
' リザルト画面
when4
endcase
' 垂直同期
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
' Aボタン処理
if (B AND 1 << #B_LRIGHT) != 0 then
' ボタンカウント命令に#B_RLEFTを渡す
D_BtnPressCount #B_LRIGHT
' Aボタンのカウントが1であれば
if G_BtnPressCount[0] == 1 then
endif
else
' ボタンが離されたらカウントが1以上の時
if G_BtnPressCount[0] >= 1 then
' ボタンカウントを0にする
G_BtnPressCount[0] = 0
endif
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
' L1ボタン処理
if (B AND 1 << #B_L1) != 0 then
' ボタンカウント命令に#B_L1を渡す
D_BtnPressCount #B_L1
' L1ボタンのカウントが1であれば
if G_BtnPressCount[3] == 1 then
if G_SceneFlag == 3 then
D_GameReset
endif
endif
else
' ボタンが離されたらカウントが1以上の時
if G_BtnPressCount[3] >= 1 then
' ボタンカウントを0にする
G_BtnPressCount[3] = 0
endif
endif
end
' ボタンカウント関数
'───────────────────────────────
def D_BtnPressCount A_Button
' ボタンカウント分岐
case A_Button
' 渡されたボタンが#B_RRIGHTだったら
when #B_RRIGHT
' ボタンカウント配列の数が256未満かどうか調べる
if G_BtnPressCount[0] < 256 then
' 条件に一致したら+1する
INC G_BtnPressCount[0]
endif
' 渡されたボタンが#B_RLEFTだったら
when #B_RLEFT
' ボタンカウント配列の数が256未満かどうか調べる
if G_BtnPressCount[2] < 256 then
' 条件に一致したら+1する
INC G_BtnPressCount[2]
endif
when #B_L1
' ボタンカウント配列の数が256未満かどうか調べる
if G_BtnPressCount[3] < 256 then
' 条件に一致したら+1する
INC G_BtnPressCount[3]
endif
endcase
end
' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
' スプライトを定義
var PID = G_SpvManageNo[0]
SPSET PID, 352 ' プレイヤー
SPVAR PID, "X", 0
SPVAR PID, "Y", 0
SPCOL PID
' ループさせて弾を生成
for G_I = G_SpvManageNo[1] to G_SpvManageNo[2]
SPSET G_I, 1322,0 ' 弾は最初からHIDE状態
D_ShotPosSettings G_I
SPCOL G_I
next
' ループさせて障害物を生成
for G_I = G_SpvManageNo[3] to G_SpvManageNo[4]
SPSET G_I, 262 ' 岩
D_ObstacleRndPosSettings G_I
SPCOL G_I, 1
next
end
' スプライト(プレイヤー)の移動
'───────────────────────────────
def D_PlayerMove
' プレイヤーの移動
SPOPS 0, SPVAR(0,"X"), SPVAR(0,"Y")
end
' マップの描写
'───────────────────────────────
' マップの塗りつぶし描写
def D_MapDraw
' 所属レイヤー変更
TLAYER 0,7
' 背景の色味を変更
TCOLOR 0, &Hdddddd00
' 背景の塗りつぶし
TFILL 0,0,0,24,14,CHR$(&HE8D8)
end
' プレイヤーショット処理
'───────────────────────────────
def D_PlayerShot
for G_I=G_SpvManageNo[1], to G_SpvManageNo[2]
' 弾にショットフラグが立っていなければ
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=G_SpvManageNo[1], to G_SpvManageNo[2]
' 弾にショットフラグが立っていれば
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")
' 移動した後に衝突判定
if D_ShotColCheck(G_I) == -1 then
' 弾の座標が400を越えたら
if G_I, SPVAR(G_I,"X") > 400 then
' 画面外に行くのでショットフラグを0にして非表示にする
SPVAR G_I,"SHOT_FLAG",0
SPHIDE G_I
endif
endif
endif
next
end
' 岩の移動処理
'───────────────────────────────
def D_ObstacleMove
var lpNum = G_SpvManageNo[4]
' ループ最大値調整
case G_PlayerStatus[4]
when 0
DEC lpNum, 69
when 1
DEC lpNum, 59
when 2
DEC lpNum, 49
when 3
DEC lpNum, 29
endcase
for G_I=G_SpvManageNo[3], to lpNum
' 完全に画面左外でなければ
if SPVAR(G_I,"X") > -16 then
' 岩のX座標を加算
SPVAR G_I, "X", SPVAR(G_I,"X") - 5
' 岩を移動させる
SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
else
' 画面左外に行ったら右画面外にランダム再配置する
D_ObstacleRndPosSettings G_I
endif
next
end
' 障害物再配置
'───────────────────────────────
def D_ObstacleRndPosSettings A_I
VAR X = 400 + RND(400)
VAR Y = RND(225)
SPVAR A_I, "X", X
SPVAR A_I, "Y", Y
SPVAR A_I, "IS_HIT", 0
SPOFS A_I, SPVAR(A_I,"X"), SPVAR(A_I,"Y")
SPSHOW A_I
end
' プレイヤー衝突チェック
'───────────────────────────────
def D_PlayerColCheck
var isHit = SPHITSP(G_SpvManageNo[0], G_SpvManageNo[3], G_SpvManageNo[4])
if isHit != -1 && SPVAR(isHit,"IS_HIT") == 0 then
BEEP 91, -300
DEC G_PlayerStatus[0]
' ライフが0になったら
if G_PlayerStatus[0] <= 0 then
' ゲームオーバーシーンへいく
D_SceneChange 3
endif
SPVAR isHit, "IS_HIT", 1
endif
end
' 弾衝突チェック
'───────────────────────────────
def D_ShotColCheck(A_I)
var isHit = SPHITSP(A_I, G_SpvManageNo[3], G_SpvManageNo[4])
if isHit != -1 && SPVAR(isHit,"IS_HIT") == 0 then
BEEP 120
' ショットスコアの加算
D_ScoreAdd 100
SPVAR isHit, "IS_HIT", 1
D_ShotPosSettings A_I
SPHIDE isHit
return 0
else
return -1
endif
end
' 弾リセット処理
'───────────────────────────────
def D_ShotPosSettings A_I
SPOFS A_I, -16, -16
SPVAR A_I, "X", 0
SPVAR A_I, "Y", 0
SPVAR A_I, "SHOT_FLAG", 0
end
' 現在スコアの初期化
'───────────────────────────────
def D_ScoreInitialize
G_PlayerStatus[2] = 0
end
' スコアの加算
'───────────────────────────────
def D_ScoreAdd A_AddScore
' スコア加算
INC G_PlayerStatus[2], A_AddScore
' ハイスコア更新チェック
if G_PlayerStatus[2] > G_PlayerStatus[3] then
G_PlayerStatus[3] = G_PlayerStatus[2]
endif
end
' スコアの更新
'───────────────────────────────
def D_ScoreUpdate
D_ScoreAdd 0.1
' レベルアップ監視
if G_PlayerStatus[2] > G_GameLvUp[G_PlayerStatus[4]] then
BEEP 45
INC G_PlayerStatus[4]
endif
GFILL 300,16,400,32,RGB(0,0,0,0)
GPUTCHR 300,16, STR$(FLOOR(G_PlayerStatus[2])),8,RGB(255,255,255),1
end
' プレイヤーステータスを初期化
'───────────────────────────────
def D_PlayerStatusInitialize
G_PlayerStatus[0] = 2
G_PlayerStatus[1] = 0
G_PlayerStatus[4] = 0
end
' プレイヤーライフの加算
'───────────────────────────────
def D_PlayerLifeAdd
' ライフ加算
INC G_PlayerStatus[0]
end
' プレイヤーライフの更新
'───────────────────────────────
def D_PlayerLifeUpdate
GFILL 16,16,100,32,RGB(0,0,0,0)
GPUTCHR 16,16, "LIFE:"+STR$(G_PlayerStatus[2]),8,RGB(255,255,125),1
end
' ゲームオーバーシーン
'──────────────────────────
def D_GameOver
GPUTCHR 72,100,"GAME OVER",16,RGB(255,255,255),1
end
' ゲームリセット
'───────────────────────────────
def D_GameReset
GCLS
D_SpriteInitialize
D_PlayerStatusInitialize
D_ScoreInitialize
D_SceneChange 1
end
' BGMセレクタ
'───────────────────────────────
def D_BgmSelect
if G_BgmFlag == #FALSE then
case G_SceneFlag
when 0
BGMPLAY 13
when 1
if G_PlayerStatus[4] >= 0 && G_PlayerStatus[4] <= 1 then
BGMPLAY 2
elseif G_PlayerStatus[4] >= 2 && if G_PlayerStatus[4] <= 3 then
BGMPLAY 31
else
BGMPLAY 41
endif
when 2
BGMSTOP
when 3
BGMPLAY 4
endcase
G_BgmFlag = #TRUE
endif
end
' シーン切り替え
'───────────────────────────────
def D_SceneChange A_Scene
BGMSTOP
G_BgmFlag = #FALSE
G_SceneFlag = A_Scene
endif
' タイトルシーン
'───────────────────────────────
def D_GameTitle
var frame = MAINCNT()
' プチコン4を起動してからの経過時間を元に30で割り切れたらフラグを切り替え
if (frame MOD 30) == 0 then
G_FlashFlag = !G_FlashFlag
endif
' 文字をフラグの状態により点滅させる
if G_FlashFlag == #FALSE
GPUTCHR 40,100,"PLEASE PRESS BUTTON",16,RGB(255,255,255),1
GPUTCHR 40+(16*13+8),100,"A",16,RGB(255,0,0),1
else
GFILL 40,100,400,116, RGB(0,0,0,0,)
endif
if G_BtnPressCount[0] == 1 then
GFILL 40,100,400,116, RGB(0,0,0,0,)
D_SceneChange 1
endif
end