【プチコン4講座】イベントシステム構築:イベント配置とアニメ編

プチコン4

プチコン4 イベントアニメーション

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

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

前回「【プチコン4講座】RPGを作るためのグリッド衝突判定」でグリッド移動の衝突判定を実装しました。

ついにイベントシステムの実装になります。

イベントというのはモンスターや宝箱、何もないタルやツボなど様々な設置物です。

これが無いとだだっ広いフィールドを歩くだけのクソゲーになってしまうのでRPGとしては必須なシステムになります。

面倒くさいですが、じっくりと作っていきましょう。

グリッド移動衝突判定はプレイヤー以外にも使えますので、汎用性がある作り方にしています。

プチコン4では宝箱を守っているモンスター以外は移動させてみようと思うので、
のちにグリッド移動と衝突判定関数を改善します。

イベントの作り方の考え方についてはプチコンBIGの時でも解説しているので

【プチコン講座】RPGのイベントシステムの作り方

を参照してみてください。

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

  1. キャラクターアニメーションの作成
  2. アニメーション専用配列の作成
  3. スプライト関数をイベントも使えるように書き換える
  4. イベント表示用の配列の作成
  5. イベントを用意して画面に表示してみよう

キャラクターアニメーションの作成

プレイヤーは現在直立状態になっていますね。
ドラクエのように動いていなくても足踏みをさせてイキイキさせてみたいと思います。

プチコンBIGではドラクエ1のように下方向しか向いていませんでしたが、
プチコン4では折角プレイヤーの向きを保持する変数を作ったのですから
それを使って上下左右に向くように作っていきます。

それではまずは手始めに下向きのアニメーションを実装していきましょう。

SPSETを使った後、つまりスプライト初期化後に行わないとエラーが起るので注意しましょう。

' スプライト初期化
def D_SpInitialize
  ' プレイヤーキャラクターの描写と初期化
  SPSET 0, 500
  SPVAR 0, "X", 0
  SPVAR 0, "Y", 0
  SPVAR 0, "Direct", 0
  SPVAR 0, "MoveFlag", 0
  SPANIM 0, "I", 20,500, 20,501, 20,502, 20,503, 0
  SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
end

これで実行してみるとアニメーションが開始されます。
今は直接指定していますが、ここを配列にしておけば向きによってアニメを変更することが出来ます。

アニメーション専用配列の作成

このままだとアニメーションが固定なので配列化しておいて後で変更できるようにしましょう。

プチコン4から配列の代入がしやすくなって助かります。

BIGだとラベルにDATAを設定して~と面倒だったのでこれが出来ると一気にコードが見やすくなりますね。

' アニメーション開始スプライト定義No配列
DIM G_SpAnimNo[] = [500,1040,920,1000,980,1080,1020]

' スプライト初期化
def D_SpInitialize
  ' プレイヤーキャラクターの描写と初期化【追加部分】
  SPSET 0, 500
  SPVAR 0, "X", 0
  SPVAR 0, "Y", 0
  SPVAR 0, "Direct", 0
  SPVAR 0, "MoveFlag", 0
  SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
  SPANIM 0, "I", 20,G_SpAnimNo[0], 20,G_SpAnimNo[0]+1, 20,G_SpAnimNo[0]+2, 20,G_SpAnimNo[0]+3, 0
end

作った配列は表示するイベントのアニメーションです。

ネタバレをしておくと0番目はプレイヤーでその後はモンスターです。

BIGではモンスターが少なかく感じたので今回は1匹増やしてみました。

配列に最初のアニメーションさえ記憶しておけば、後はプラスいくつかをすればアニメの画像を参照できます。

上下左右を表現したいときは、マップ配列参照と同じような感じで+アニメ数分みたいな感じで指定してやれば、
向きによって変更することが可能になります。

文字だけだとわかり辛いので、上下左右のアニメーションを実装する時にもう一度説明します。

スプライト関数をイベントも使えるように書き換える

現状、スプライト関数はプレイヤーのSPナンバーである0を直接していしていますね。

折角汎用性のある関数を作っても中身がリテラルであれば意味がありません。

関数に引数を持たせて渡したSPナンバーによって動かすものを変化させましょう。

' スプライト初期化
def D_SpInitialize A_Id
  SPSET 1, G_SpAnimNo[A_Id]
  SPVAR A_Id, "X", 0
  SPVAR A_Id, "Y", 0
  SPVAR A_Id, "Direct", 0
  SPVAR A_Id, "MoveFlag", 0
  SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
  SPANIM A_Id, "I"20,G_SpAnimNo[A_Id],¥
   20,G_SpAnimNo[A_Id]+120,G_SpAnimNo[A_Id]+220,G_SpAnimNo[A_Id]+3,¥
   G_SpAnimNo[A_Id]+4
end

' スプライトの移動
def D_SpriteGridMove A_Id, A_Direct

  if SPVAR(A_Id,"MoveFlag") == 1 then 
    case SPVAR(A_Id,"Direct")
    when #B_RUP
      SPVAR A_Id, "Y", SPVAR(A_Id, "Y") - 1
    when B_RDOWN
      SPVAR A_Id, "Y", SPVAR(A_Id, "Y") + 1
    when B_RLEFT
      SPVAR A_Id, "X", SPVAR(A_Id, "X") - 1
    when B_RRIGHT
      SPVAR A_Id, "X", SPVAR(A_Id, "X") + 1
    endcase

    ' キャラクターの移動
    SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")

    ' 割った余りが0になったら動きを止める
    if SPVAR(A_Id, "X") MOD #CSZ == 0 && SPVAR(A_Id, "Y") MOD #CSZ == 0 then
      SPVAR A_Id, "MoveFlag", 0
    endif
  else
    if A_Direct > -1 then
      var pos = D_GetMapPosition(SPVAR(A_Id, "X"), SPVAR(A_Id, "Y"), 0)
      SPVAR A_Id, "Direct", A_Direct
      if D_CheckCollision(POS, SPVAR(A_Id, "Direct")) == #TRUE then
        SPVAR A_Id, "MoveFlag", 1
      endif
    endif
  endif
end

これで初期化時にイベントIDを渡してやればそれぞれのスプライトが初期化され、
個別に動くことが出来るようになりました。

IDは被らないように最初に決めておくといいでしょう。

0は問答無用でプレイヤーにしておくと色々と楽になります。

例えばプレイヤー以外を画面に表示したい場合はforループで開始位置を1からにすれば可能です。

とりあえず今はプレイヤーしかいないので関数には0を渡しておきましょう。

イベントを作ったらループで回すように変更します。

イベント表示用の配列の作成

イベントを表示するためにはイベント用のデータが必要になります。

能力やら様々な数値がありますが、まずはとりあえず画面に表示することだけを考えましょう。

表示するだけで絞れば

  • ID
  • X座標
  • Y座標
  • アニメーションの有無

があればいですね。

これらを含んだ配列を作成します。

var G_SpEvStep = 4
DIM G_EvId[] = [0,1,0,01,4,2,02,5,2,03,6,2,04,7,2,05,8,2,06,9,2,0]

0~3番目はプレイヤーの値となります。

それぞれ4つのデータを持たせているのでイベント1つ目は4から開始するということになりますね。

スプライトは初期化時にforで回して全部表示するのでプレイヤーもここに含めておきましょう。

もし開始時に透明で何かしらの演出をしたいときはプレイヤーを画面外に置いておくという手もありです。

イベントを用意して画面に表示してみよう

イベント配列も用意してスプライト初期化関数も改造したことですし画面に表示させてみましょう。

'━━━━━━━━━━━━━━━━━━━━━━━━━━
' ┠─ ▼ 初期化関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
DEF D_GameInitialize
  ' マップデータのロード
  D_MapInitialize "MAP001.DAT"
  ' マップデータの描写
  'D_MapDraw
  ' スプライトの初期化
  for G_I=0 to LEN(G_EvId)-1 step G_SpEvStep
    D_SpInitialize G_EvId[G_I]
  next
END

' スプライト初期化
def D_SpInitialize A_Id
  SPSET 1, G_SpAnimNo[A_Id]
  SPVAR A_Id, "X", G_EvId[G_SpEvStep * A_Id + 1] * #CSZ
  SPVAR A_Id, "Y", G_EvId[G_SpEvStep * A_Id + 2] * #CSZ
  SPVAR A_Id, "Direct", 0
  SPVAR A_Id, "MoveFlag", 0
  SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
  SPANIM A_Id, "I"20,G_SpAnimNo[A_Id],¥
   20,G_SpAnimNo[A_Id]+120,G_SpAnimNo[A_Id]+220,G_SpAnimNo[A_Id]+3,¥
   G_SpAnimNo[A_Id]+4
end

プチコン4表示結果その1

スプライト初期化の座標がまだ0のままだったのでここも先ほど定義したイベント表示用配列を使います。

これで実行するとモンスターが整列して足踏みしているのが分かると思います。

しかしこのままではイベントにぶつかってみてもすり抜けてしまいます。

まだイベントに対する衝突判定は作成していませんのであたりまえですね。

逆に現状はプレイヤーの位置にモンスターが来ることも出来ます。

これらを解決するにはマップ配列のイベントレイヤーにイベントが居る位置を書き替えてやらなければいけません。

そのイベントレイヤーに番号を書き込んでおくとイベントレイヤーを調べた時にどのIDがいるのかわかりますね。

しかしプレイヤーは0なので判り辛いです。

なので0を書き込むときは-1と書き込んでもいいかもしれません。

マイナスの値の時はプレイヤーがいるという風にすれば0は何もないという状態を維持できます。

この辺は他のプログラムに影響のない数値にすれば問題ありません。

99とかにしてもよかったのですが、もしかすると99個以上イベントがあった場合不都合になるからです。

-1にしておけば基本的にありえない数値なので妨害されることはありません。

──プチコンBIGの時のソースコードと比べると結構変化点が多いですね。

やはり配列宣言時に値を他のプログラミング言語のように代入出来るのが大きいです。

後は私が同じような講座を3回ほどやっているため効率的な書き方をできるようになったのでしょう。

講座は別始点で3回ほどやってみると完全に体に叩き込めるので勉強には最適ですね。

最後に今回の完成版ソースコードを置いておきます。

それでは

'──────────────────────────
' ▼ 動作モード設定:変数宣言は必須
'──────────────────────────
OPTION STRICT
'──────────────────────────
' ▼ 画面のクリア
'──────────────────────────
ACLS
'──────────────────────────
' ▼ 定数・構造体の定義
'──────────────────────────
CONST #CSZ = 16
'──────────────────────────
' ▼ 変数・配列の定義
'──────────────────────────
' ┠─ 汎用変数
'━━━━━━━━━━━━━━━━━━━━━━━━━━
var G_I = 0, G_J = 0, ¥
G_BGW = 0, G_BGH = 0
' ゲームループを制御するフラグ
var G_GameLoopFlag = #TRUE
'──────────────────────────
' ┠─ イベント・スプライト用配列
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' アニメーション開始スプライト定義No配列
DIM G_SpAnimNo[] = [500,1040,920,1000,980,1080,1020]
var G_SpEvStep = 4
DIM G_EvId[] = [0,1,0,01,4,2,02,5,2,03,6,2,04,7,2,05,8,2,06,9,2,0]
'──────────────────────────
' ┠─ マップシーン用配列
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' 空配列宣言
DIM G_MapData[]

'──────────────────────────
' ▼ プログラム初期化処理
'──────────────────────────
D_GAME_INITIALIZE
'──────────────────────────
' ゲームループ開始
'──────────────────────────
loop
  ' コントローラー関数呼び出し
  D_Controller

  ' ゲームループフラグ監視
  if !G_GameLoopFlag then BREAK
  ' フレームレート安定
  VSYNC 1
endloop

'──────────────────────────
' ▼ 関数の定義
'──────────────────────────
' ┠─ ▼ 初期化関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
DEF D_GameInitialize
  ' マップデータのロード
  D_MapInitialize "MAP001.DAT"
  ' マップデータの描写
  'D_MapDraw
  ' スプライトの初期化
  for G_I=0 to LEN(G_EvId)-1 step G_SpEvStep
    D_SpInitialize G_EvId[G_I]
  next
END

'──────────────────────────
' ┗━┓ ▼ マップ関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
DEF D_MAP_INITIALIZE A_FILENAME$
  ' マップデータファイル読み込み
  G_MAPDATA = LOADV(A_FILENAME$)

  ' マップデータ配列の先頭2つを取り出す
  G_BGW = SHIFT(G_MapData)
  G_BGH = SHIFT(G_MapData)
END
'──────────────────────────
'   ┗━┳━ ▼ マップデータ描写
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' マップデータの描写関数
DEF D_MapDraw
  ' マップチップ配列を描写
  for G_J = 0 to G_BGH-1
    for G_I = 0 to G_BGW-1
      ' ローカル変数CPに計算した値を入れる 
      var cp = G_I + ( G_J * G_BGW )
      ' CASE文でMAPDATA[CP]の内容に応じて処理を切り替える
      case G_MapDat[cp]
        ' 0のとき
        when 0:
          ' 地面を表示
          TPUT 0, G_I, G_J, CHR(&HE8C9)
        ' 1のとき
        when 1:
          ' 木を表示
          TPUT 0, G_I, G_J, CHR(&HE8CA)
      endcase
    next
  next
end

'──────────────────────────
' ┠─ コントローラー関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' コントローラー関数
def D_Controller
  var B = BUTTON(0)

  # #B_RRIGHTは右コントローラーの右ボタンつまりAボタンの意
  var BTN_UP = 1 << #B_LUP
  var BTN_DOWN = 1 << #B_LDOWN
  var BTN_LEFT = 1 << #B_LLEFT
  var BTN_RIGHT = 1 << #B_LRIGHT

  var direct = -1

  if (B and BTN_UP) > 0 then
    direct = #B_RUP
  endif
  if  (B AND BTN_DOWN) > 0 then
    direct = #B_RDOWN
  endif
  if  (B AND BTN_LEFT) > 0 then
    direct = #B_RLEFT
  endif
  if  (B AND BTN_RIGHT) > 0 then
    direct = #B_RRIGHT
  endif

  D_SpriteGridMove direct

end

'──────────────────────────
' ┠─ ▼ スプライト関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' スプライト初期化
def D_SpInitialize A_Id
  SPSET 1, G_SpAnimNo[A_Id]
  SPVAR A_Id, "X", G_EvId[G_SpEvStep * A_Id + 1] * #CSZ
  SPVAR A_Id, "Y", G_EvId[G_SpEvStep * A_Id + 2] * #CSZ
  SPVAR A_Id, "Direct", -1
  SPVAR A_Id, "MoveFlag", 0
  SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
  SPANIM A_Id, "I"20,G_SpAnimNo[A_Id],¥
   20,G_SpAnimNo[A_Id]+120,G_SpAnimNo[A_Id]+220,G_SpAnimNo[A_Id]+3,¥
   G_EvId[G_SpEvStep * A_Id + 3]
end

' スプライトの移動
def D_SpriteGridMove A_Id, A_Direct

  if SPVAR(A_Id,"MoveFlag") == 1 then 
    case SPVAR(A_Id,"Direct")
    when #B_RUP
      SPVAR A_Id, "Y", SPVAR(A_Id, "Y") - 1
    when B_RDOWN
      SPVAR A_Id, "Y", SPVAR(A_Id, "Y") + 1
    when B_RLEFT
      SPVAR A_Id, "X", SPVAR(A_Id, "X") - 1
    when B_RRIGHT
      SPVAR A_Id, "X", SPVAR(A_Id, "X") + 1
    endcase

    ' キャラクターの移動
    SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")

    ' 割った余りが0になったら動きを止める
    if SPVAR(A_Id, "X") MOD #CSZ == 0 && SPVAR(A_Id, "Y") MOD #CSZ == 0 then
      SPVAR A_Id, "MoveFlag", 0
    endif
  else
    if A_Direct > -1 then
      var pos = D_GetMapPosition(SPVAR(A_Id, "X"), SPVAR(A_Id, "Y"), 0)
      SPVAR A_Id, "Direct", A_Direct
      if D_CheckCollision(POS, SPVAR(A_Id, "Direct")) == #TRUE then
        SPVAR A_Id, "MoveFlag", 1
      endif
    endif
  endif
end

'──────────────────────────
' ┠─ ▼ 衝突判定関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' ドット座標をグリッド座標に変換
def D_GetGridXY(A_XY)
  return A_XY / #CSZ
end

' グリッド座標とレイヤーを与えてマップ配列の値を取得する
def D_GetMapPosition(A_X, A_Y, A_Layer)
  var gridX = D_GetGridXY(A_X)
  var gridY = D_GetGridXY(A_Y)

  var layerNo = (G_BGW * G_BGH) * A_Layer

  if A_Y <= 0 then
    return gridX + layerNo
  else
    return gridX +  (gridY * G_BGW) + layerNo
  endif
end

' 進む先のグリッドが移動可能か同化を調べる
def D_CheckCollision(A_Pos, A_Direct)
  var direct = 0
  var sumPos = 0

  case A_Direct
    when #B_RUP
    direct = -G_BGW
    when #B_RDOWN
    direct = G_BGW
    when #B_RLEFT
    direct = -1
    when #B_RRIGHT
    direct = 1
  endcase

  sumPos = A_Pos + direct

  if G_MapData[sumPos] == 0 then
    return #TRUE
  endif
  return #FALSE
end