【プチコン4講座】スコア一定数値で難易度調整

プチコン4

プチコン4 シューティングゲーム 難易度調整

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

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

第11回「【プチコン4講座】ゲームオーバーシーンの実装」にてシーンの概念を学びゲームオーバーとゲームリセットを実装しました。

基本的なゲームとしての機能はほぼ完成しましたが、今回はスコアに応じて難易度調整してゲームを面白くしてみましょう。

何かに応じて何かを変化させるのはプログラミングの得意分野ですし、特に新しいことはしないので気楽に作ってください。

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

  1. 難易度を上げるためにゲームレベル機能を作ろう
  2. レベルに応じて出現するスプライトの数を増やそう
  3. シューティングゲーム作り講座第12回まとめ

難易度を上げるためにゲームレベル機能を作ろう

このまま延々と続くのって結構退屈じゃない?やっぱり少しずつ刺激を増やしてメリハリを付けたいわよね。

そうだな。最終的には最初に出てきたぐらいの岩の数にすれば面白いんじゃね?

さ、流石にあのレベルはちょっと……

丁度いいね。スコアに応じてレベルアップする仕組みを作って出現する岩の数を増やしてみようよ。

でもどうやってレベルアップシステムを作ればいいの?

簡単さ。スコアっていう基準があるんだからそれを元に条件分岐させて一定値を越えたらレベルアップすればいいのさ。プレイヤー変数にレベルを追加しようか。ついでにレベルアップ基準値も作ろう。

' プレイヤー用状態変数
'   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 ]

うーん、10万はやりすぎたかな。プレイしてから調整してみようか。

そっか、配列って後から足せるから凄い便利だね。

あまり褒められたやり方じゃないけどね。最初にちゃんと設計しておかないとバグに繋がってくるんだ。今回は小さいゲームだからいいんだけどね。

わかったわ。

初期値はレベル0からスタートでプレイヤーには数値を見せないようにするよ。気付いたら岩が増えてきたって感じの方が緊張感でるでしょ。

確かにそっちの方が面白そうだな!予測できないゲームは好きだぜ!

理不尽にならないようにしないとね。それじゃあレベルアップの仕組みを作るよ。一応デバッグ中はわかりやすいようにレベルアップしたら音を鳴らしておこうか。

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

配列の添え字に配列を与える……なんか混乱してくるなこれ

現在のレベルで条件を見て突破したらレベルが上がって更にそのレベルを元に次の条件を監視……すごいわね。

これでレベルアップシステムは完成だよ。

レベルに応じて出現するスプライトの数を増やそう

後はレベルに応じて出現するスプライトを増やしゃいいんだよな?

でも初期化関数を呼び出したらプレイヤーも岩も初期化されちゃうわね。

そうだね。ここは増えるスプライトだけ処理したい所だからまず最初に全部の岩を初期化して、使う分だけ動かすようにしてみよう。

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

次に岩の移動処理をレベルに応じてループ回数を制御してみるよ。

' 岩の移動処理
'───────────────────────────────
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_PlayerStatusInitialize
  G_PlayerStatus[0] = 2
  G_PlayerStatus[1] = 0
  G_PlayerStatus[4] = 0
end 

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

プレイヤーライフの初期化だったところを能力とレベルの初期化も行うから関数名を変更しているのに注意してね。これでプレイしてみようか。

まずは俺からだ!やるぜ!

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

やるわね!次は私よ!

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

岩で見えてないけど4万いったー!

くっそ!マユミに負けちまった!

ふふん!伊達に避けゲーやってないわよ!

次はボクだね。実はSTG苦手なんだよね……

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

うー、3万いけなかったなぁ。思ったより左指が疲れるねこれ

それにしても10万は遠いな……でも狙えなくもない数字だ。連打力だけじゃいけそうにないぜ!

そうね。いかに当たらないかが勝負よ。

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

もうほぼゲームとして完成してきてますね。

講座を始めると前はクソゲーかと思いきや結構遊べるゲームになりました。

後はBGMを入れたり画面外にいけなくしたりチャージショットを撃って広範囲に攻撃したり様々な改良点を見出せます。

難易度を上げる手段としてチャージショットじゃないと潰せない岩を出現させたり、
絶対に壊せない岩を出現したりさせると面白いかもしれません。

入門記事なので作り込みはしませんが、講座の最終回に公開しようと思いますので好きなように弄ってください。

次回はゲームらしくBGMを設けてタイトルシーンも実装してみましょう。

それでは。

' ゲーム初期化
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 ]


' ゲームの初期化
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
    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
      ' ゲームオーバーシーンへいく
      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

  ' レベルアップ監視
  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
  G_SceneFlag = 1
end