【プチコン4講座】ゲームオーバーシーンの実装

プチコン4

プチコン4 シューティングゲーム ゲームオーバー

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

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

第10回「【プチコン4講座】スコアシステムとライフシステムの実装」にてスコアとライフの実装と表示をしました。

ほぼゲームの仕組みは前回のスコアシステムとライフシステム出来上がりましたね。

しかしゲームとしては成立していません。なぜなら終わりが無いからです。

今回は終わり、つまりゲームオーバーの実装シーンの概念を勉強していきます。

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

  1. ゲームを作るにはシーンの概念を知る必要がある
  2. ゲームオーバーの処理を作ろう
  3. シューティングゲーム作り講座第11回まとめ

ゲームを作るにはシーンの概念を知る必要がある

だいぶゲームとしては完成してきたけどよ、このままだと終わりが無くて辛いぜ

そうだね。基本的にスコアは増え続けるからゲームオーバーが無いと記録の保持とか競い合うことも難しいね。

そういえば前回にライフを実装して数値がどんどんマイナスになってたわよね?0の時にゲームオーバーにすればいいんじゃない?

うん、今日はゲームオーバーの実装にする予定だったよ。その前にシーンという概念を理解しなくちゃいけないんだ。

シーン?

大体のゲームは大きく分けて以下のシーンで構成されるんだ。

  • タイトルシーン
  • ゲームプレイシーン
  • ポーズシーン
  • ゲームオーバーシーン

セーブロードもなくコンフィグもないゲームの場合はこんな感じになるよ。

あー、書かれるとよくわかるわ!ってことは今作ってるのはゲームシーンね?

そう、そこだけだからどこのシーンにも移れない状態になっちゃってるんだ。だから最初にポーズシーンを作ってシーンの概念を理解してもらうよ。

確かにプレイ中トイレに行きたくなって止められないと泣きたくなるな。

シーンの実装はフラグを作って条件分岐するだけよ。ここまで作ってきた人ならすぐにでも実装できるんだ。以下のコードを打ち込んでね。

G_SceneFlag = 1

' ループ開始
loop
  ' コントローラーはどのシーンでも動くようにする
  D_Controller

  ' フラグで処理を切り分ける
  case G_SceneFlag
    ' タイトルシーン
    when 0
    ' メインシーン
    when 1
      D_PlayerMove
      D_PlayerColCheck
      D_PlayerShotMove
      D_ObstacleMove
      D_ScoreUpdate
      D_PlayerLifeUpdate
    ' ポーズ中
    when2
    ' ゲームオーバー
    when3
    ' リザルト画面
    when4
  endcase

  ' 垂直同期
  VSYNC
endloop

ざっと必要そうなシーンでcaseを使って切り分けてみたよ。タイトルシーンを実装していないからとりあえず1をゲームプレイのシーンにして初期値を一旦1にしてるのに気を付けてね。

リザルト画面って何かしら?

結果画面だね。シューティングゲームならスコアのランキングだったり。RPGとかだと戦闘終了の経験値獲得だったり。

スコアランキングかぁ、やっぱランキング競い合うのが燃えるよな!

今回はゲーム作りの入門の入門だから保存機能はなくてゲームを終了しちゃうと消えちゃうけどね。データ保存に関しては別の項目を設けてやるよ。

ゲームオーバーの処理を作ろう

フラグも作ってcase文で切り分けたことだし早速ライフが0になったらゲームオーバーに移る処理を作るよ。プレイヤー衝突チェックを少し弄るだけでとっても簡単だよ。

' プレイヤー衝突チェック
'───────────────────────────────
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
      ' ゲームオーバーシーンへいく
      G_SceneFlag = 3
    endif

    SPVAR isHit, "IS_HIT", 1
  endif
end

ゲームオーバーにいくためにはこうするだけだね。ライフが0になったら強制的にシーンフラグを切り替えるんだ。

フラグって便利ね!

次はゲームオーバーの処理を作るよ。出来ればプレイヤーを爆発四散させたいところだけどアニメーションはちょっと時間がかかりそうだから省略するね。とりあえず画面に「GAME OVER」の文字を出してみよう。

あんまり見たくない画面だけどゲームとしては大切だもんな!

' ループ開始
loop
  ' コントローラーはどのシーンでも動くようにする
  D_Controller

  ' フラグで処理を切り分ける
  case G_SceneFlag
    ' タイトルシーン
    when 0
    ' メインシーン
    when 1
      D_PlayerMove
      D_PlayerColCheck
      D_PlayerShotMove
      D_ObstacleMove
      D_ScoreUpdate
      D_PlayerLifeUpdate
    ' ポーズ中
    when2
    ' ゲームオーバー
    when3
      D_GameOver

    ' リザルト画面
    when4
  endcase

  ' 垂直同期
  VSYNC
endloop

' ゲームオーバーシーン
'──────────────────────────
def D_GameOver
  GPUTCHR  72,100,"GAME OVER",16,RGB(255,255,255),1
end

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

え?これだけか?

とりあえずはね。中身は後から作るよ。とりあえずライフが0になったらGAMEOVERになるか試してみよう。

なんか寂しいけどちゃんとゲームオーバーになったわね。

この後は「L」ボタンを押したら最初から始まるようにしてみようか。ボタンが押されたらゲームを最初の状態に戻す処理が必要だよ。ハイスコアはそのままにしておこうね。

' コントローラー
'───────────────────────────────
def D_Controller

  ~~~ 省略 ~~~


  ' 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_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

ボタンは基本どのシーンでも押せるようにしておくよ。でもゲームリセットはゲームオーバーシーンの時に押されないと発動しないようにしとこうね。それじゃあ「D_GameReset」を作っていくよ。

' ゲームリセット
'───────────────────────────────
def D_GameReset
  GCLS
  D_SpriteInitialize
  D_PlayerLifeInitialize
  D_ScoreInitialize
  G_SceneFlag = 1
end

前に作っていた初期化命令はココでようやく使うんだ。スプライト初期化は横着しちゃったけどこのレベルのゲームなら最初に呼び出した初期化命令を呼べば一発リセットだよ。内部的にはわからないけどね。

何回かやってたらバグがでてくるってことか?

うーん現状バグることはないと思うけど、どちらにしろゲームは何度もテストプレイするのが前提だからね。あえてバグを残しておくのも面白いかもしれないよ。

バグなんてないほうがよくない?

何言ってんだ!バグはロマンだろ!バグは人生だ!

は?意味わかんないんだけど……

ボクもリキくんに賛成かな。ゲームが止まるようなフリーズバグは論外だけど笑えるようなバグがあると、ゲームも二度面白いし開発者も意図しない動きをされると意外と開発の刺激になるんだよ。

うー……私にはわかんないわそのロマン……

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

#B_L1

JoyConのLボタンを指す定数。ちなみにLトリガーは#B_L2となる

とてもシンプルですがシーンという概念を覚えてもらいました。

シーンフラグを設けておけば何かをトリガーにシーンを切り替えると色々なことが出来ます。

今回はあえてポーズ画面の作り方を書きませんでした。

やっていることはゲームオーバーシーンとほぼ同じだからです。

違うとすればもう一度ボタンを押したらゲームが再開するということです。

特に新しい命令は使わないので、復習として自力でポーズシーンを実装してみてください。

自分で作るとより理解度が高まりますよ。

次回はスコア上昇による難易度を高める仕組みを作ってみましょう。

それでは。

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

' スプライト管理番号まとめ変数
DIM G_SpvManageNo[] = [0,10,14,20,30]

' プレイヤー用状態変数
'   0=> HP, 1=>能力, 2=>現在スコア, 3=>ハイスコア
DIM G_PlayerStatus[4] = [2,0,0,0]


' ゲームの初期化
D_Initialize


' ループ開始
loop
  ' コントローラーはどのシーンでも動くようにする
  D_Controller

  ' フラグで処理を切り分ける
  case G_SceneFlag
    ' タイトルシーン
    when 0
    ' メインシーン
    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

  ' 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_RLEFTだったら
    when #B_RLEFT
      ' ボタンカウント配列の数が256未満かどうか調べる
      if G_BtnPressCount[2] < 256 then 
        ' 条件に一致したら+1する
        INC G_BtnPressCount[2]
      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
  for G_I=G_SpvManageNo[3], to G_SpvManageNo[4]
    ' 完全に画面左外でなければ
    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
      ' ゲームオーバーシーンへいく
      G_SceneFlag = 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
  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_PlayerLifeInitialize
  G_PlayerStatus[0] = 2
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_PlayerLifeInitialize
  D_ScoreInitialize
  G_SceneFlag = 1
end