【プチコン4講座】スコアシステムとライフシステムの実装

プチコン4

プチコン4 シューティングゲーム スコア

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

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

第9回「【プチコン4講座】ショットに衝突判定を付けて岩を壊してみよう!」にて弾と障害物に衝突判定を持たせて岩を壊すことが出来ました。

だいぶシューティングゲームとしては完成してきましたが、スコアもライフもない状態だとただ岩を壊して避けるだけのクソゲーなので、
今回はスコアの実装ライフシステムをどうするか決めていきましょう。

ライフシステムに関してはお好みだと思うので複数切り替えられるようにします。

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

  1. スコアの仕組みを作って描写
  2. ライフの仕組みを作って描写
  3. シューティングゲーム作り講座第10回まとめ

スコアの仕組みを作る

ココからはSTGというよりもゲーム作りに共通したシステム作りになってくるよ。STGに関しては前回で大体終わっちゃったんだ。あとは弾の動きを変化させたり弾を増やしたりするだけだから人によって作り方が変わっちゃう感じだね。そのうち応用編みたいな講座も作ろうと思うけどとりあえずゲームを完成させる方向でいくよ。

シューティングとしてはなんか物足りないんだよなぁ

確かに面白味に欠けるね。でも面白くするのって完成してからでもいいと思うんだ。とりあえず面白くなくてもゲームとして完成させるってのが大事だよ。

……そうね。プレイできないゲームよりもプレイできるゲームの方がいいもの。

それじゃあまずはスコアの仕組みを作るよ。こういうタイプのゲームは延々と続くからスコアを実装しなきゃ競いづらいからね。次のコードを打ちこんでね。

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


' 現在スコアの初期化
'───────────────────────────────
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
  GPUTCHR 300,16, STR$(G_PlayerStatus[2]),8,RGB(255,255,255),1
end

そんなに難しいことをしていないから一気に書いちゃったから1つずつ見ていくね。まずスコアやライフを保持するためにプレイヤー用の配列を作ったよ。

  • 0番目:プレイヤーのライフ
  • 1番目:プレイヤーの状態(攻撃アップシステム等用)
  • 2番目:現在のスコア
  • 3番目:ハイスコア

配列の内容はこんな感じだね。ライフは後で使うし、能力アップは講座が終わったら番外編として書いていくよ。

現在スコアの初期化

スコアの初期化はゲーム開始時だったり、ゲームオーバー後の再開に呼び出す用だよ。

スコアの加算

スコアの加算は常に行っていくよ。進んだ距離でもよかったんだけどスコアに集約したよ。ただ、このままだとスコアがものすごい勢いで加算されていくから後で調整するから一旦このままでいこう。
呼び出しはスコアの更新でやっているよ。

引数を受け取っているので岩を壊したときに専用の数値を渡せばその分加算されるようになるよ。

スコアの更新

ゲームループにいれておけばずっと動き続けるね。スコア加算命令を呼び出してから画面上のスコアの文字を更新していくよ。次に作った命令を既存のコードに埋め込もう。

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


' 弾衝突チェック
'───────────────────────────────
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

仕組みとしてはこれでほぼ完璧だよ。実行してみようか。

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

ん?なんか文字の色薄くね?

うん、なんか数字が加算されて動いてるようにはみえるけど……うーん?

実は文字の描写するレイヤーがデフォルトで奥の方に行ってしまっているんだ。正確には背景が手前に来ちゃってるって感じだけど。そして背景はちょっと透明度をあげて背景の黒となじませることで暗めにしてるから後ろにあるスコアが透けて見えるんだ。この順番を直すには「TLAYER」で背景の所属レイヤーを一番後ろ、というかテキストより後ろにしてあげる必要があるよ。

レイヤーってお絵かきソフトなんかにあるあれ?

そうだね。透明の下敷きだと思ってくれればいいかな。0~7枚あるんだけど数字が大きいほど奥に描写されるんだ。だからTLAYERで7にしてしまおう。

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

これで実行してみようか。

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

表にはきたけど……なんかぐちゃぐちゃの白いのが表示されたわね。

テキストを表示してそのまた上に描写してるから消えてないんだ。プログラミングでは再描写する時は基本的にそこにあるものを消してから描写するっていうのが基本なんだ。

' スコアの更新
'───────────────────────────────
def D_ScoreUpdate
  D_ScoreAdd 0.1
  GFILL 300,16,400,32,RGB(0,0,0,0)
  GPUTCHR 300,16, STR$(G_PlayerStatus[2]),8,RGB(255,255,255),1
end

どう?これで理想通りになったかな?

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

おぉ、ちゃんと数字が表示されてる!数字が出るとなんか一気にゲーム感でるな!

このGFILLはどういう意味なの?

グラフィックの塗りつぶしって意味で左上XY座標から右下XY座標をそれぞれ指定するとその範囲を指定した色で塗りつぶすんだ。今回やっているのは黒色の透明で塗りつぶしているんだよ。

透明でぬりつぶす?それって意味ないんじゃないの?

透明で塗りつぶすのはイメージ的には鉛筆だけを消すことが出来る消しゴムと思ってくれればいいかな。内部的な話をするとちょっと小難しい話になるからね。

わかったわ。今は透明で塗りつぶすと文字が消えるって思っておくわね。

スコアで小数点が出てるのってあんまり見ないよね。ボクにはパッと見スコアよりもゲーム内秒数みたいに感じるかな。

小数点を消すには加算を1にすればいいのか?

でもそれだと数字の増え方がすごく早くなって桁数がすぐ多くなっちゃうよ。だから内部的には小数点ありだけど表示は小数点を切り捨ててみようか。

' スコアの更新
'───────────────────────────────
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

お、小数点は消えたけどいい感じのスコアの増加だな!

FLOORは小数点切り捨て関数だよ。逆に切り上げるときはROUNDを使うんだ。これはエクセルとかでも同じ関数があって結構仕事場で使われてるんじゃないかな。

スコアの位置にスプライトが来ると見えなくなるわね。まぁ今のところ一瞬だからいいんだけど。

そうだね。本来はUIの部分は別に設けてそこにはプレイヤーや他のスプライトはいけないようにしたほうがいいね。昔のゲームを参考にレイアウトしてみるのも面白いよ。

ライフの仕組みを作る

次はライフを作るよ。これは好みの問題で1発アウトにするかダメージをいくつかうけたらアウトにするかの2つに分かれるね。

1発アウトの方が緊張感はあっていいな。私のやってる避けゲーは基本1発アウトだわ。

車が岩に激突したらそもそもやべぇよな……でも当たり所によっては車が凹むだけだし耐久度という名目でライフありでもいいんじゃないか?

そうだね。後々難易度を上げていくと無理ゲーになってくるだろうからライフがいくつかあったほうがいいかもだね。一定の点数になったら1つ増えたりとか。

見た目に変化が無かったらどこかにライフを記述しておかないと不親切よね。

それじゃあスコア描写の復習も兼ねてライフを数値で描写してみようか。

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

' プレイヤー衝突チェック
'───────────────────────────────
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]
    SPVAR isHit, "IS_HIT", 1
  endif
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

スコアとほとんど同じだね。ちょっと考えればスコアと関数をまとめられそうだけどそこまで複雑なプログラムじゃないし分けてても問題ないかな。出来そうならまとめてもいいよ。

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

ライフが表示されたぜ!これで後何回当たって良いかがわかるな。

LIFE:文字だけじゃ味気ないからこれを車アイコンxライフ数とかにするとグッとゲームぽくなるんじゃないかな?

私は数字じゃなくてハートを並べてみようと思うわ。

それもありだね。プチコンには確か全ハート・半ハート・空ハートのグラフィックがあったはずだよ。

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

GPUTCHR X,Y, 文字列 , 文字サイズ(8or16),RGB,表示フラグ

画面に文字を表示します。これ以外にも設定が色々あるので詳しい使い方は公式リファレンスを見てください。

GFILL 左上X,左上Y,右下X,右下Y,RGB

指定座標を矩形で塗りつぶす。RGBのアルファ値を0にすると透明で塗りつぶすことができ、Graphicで描写したものを範囲で消せる。

いちいちGCLSで全部消すまでもなく、そこだけ消したいって時によく使います。

TLAYER スクリーンID,レイヤーナンバー

指定のスクリーンIDの所属レイヤーを決める。レイヤー数は0~7を指定。数字が多いほど重なりは下になるので一番表面に表示したいものは0で後ろに表示したものは7

スプライトも同じような命令があるので重なりを変えたいときはSPLAYERで変更しよう。

スコアとライフを一気に実装しました。

文字を表示する勉強にうってつけの回でしたね。

これで好きな所に文字を描写することができるようになりましたが、STGではあまり文字を表示しないかもしれませんね。

他のゲームを作るときに応用してみてください。

次回は折角ライフを実装したのでライフが0になったらゲームオーバー処理を作りましょう。

それでは。

' ゲーム初期化
ACLS
' ループ用変数
var G_I
' ボタンカウント配列
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
  D_PlayerMove
  D_PlayerColCheck
  D_PlayerShotMove
  D_ObstacleMove
  D_ScoreUpdate
  D_PlayerLifeUpdate
  ' 垂直同期
  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
  ' スプライトを定義
  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]
    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