はじめに
今回はゲームとしてプレイできるように、ゲームの開始からゲームオーバーなどの状態遷移や各シーケンスの動作を作成する。以下が完成イメージ。ソースコードは GitHub に公開しているので参照してほしい。
状態遷移に関する仕様書
スタートモードとして、ゲームを開始する時のシーケンスが詳細に定義されている。
プレイモードはゲームプレイ中の状態で、プレイヤーがミスするとそのままのエサの状態からスタートし、残りのパックマンがいなくなるとゲーム・オーバーとなる。
またプレイフィールドのエサを全て食べるとラウンド・クリアとなる。
状態遷移とシーケンス動作の設計
ゲームプレイ中での必要なシーケンスを考慮して、下記の状態遷移図を作成した。
スタートモードは Start, Ready, Go の3つの状態に分けてシーケンスを作成する。
プレイモードは、主に Updating, ReturnToUpdating の2つの状態からなり、
プレイヤーがミスした場合は、PlayerMiss→PlayerDisappeared→PlayerRestartの状態/シーケンスを実行し、残りのパックマンがいれば Ready状態へ、なくなれば GameOver状態となる。
またプレイフィールドのエサを全て食べると、RoundClear→PrepareFlashMaze→FlashMazeの状態/シーケンスを実行しラウンドクリアとなり、Ready状態へ移って次のラウンドが開始される。
状態遷移のソースコード
CgSceneMaze の handleSequenceメソッドに、状態遷移とシーケンス処理を実装していく。
/// Maze scene class for play mode
/// This class has some methods to draw a maze and starting messages.
class CgSceneMaze: CgSceneFrame, ActorDeligate {
var player: CgPlayer!
var blinky: CgGhostBlinky!
var pinky: CgGhostPinky!
var inky: CgGhostInky!
var clyde: CgGhostClyde!
var ptsManager: CgScorePtsManager!
var specialTarget: CgSpecialTarget!
var ghosts = CgGhostManager()
var counter_judgeGhostsWavyChase: Int = 0
convenience init(object: CgSceneFrame) {
self.init(binding: object, context: object.context, sprite: object.sprite, background: object.background, sound: object.sound)
player = CgPlayer(binding: self, deligateActor: self)
blinky = CgGhostBlinky(binding: self, deligateActor: self)
pinky = CgGhostPinky(binding: self, deligateActor: self)
inky = CgGhostInky(binding: self, deligateActor: self)
clyde = CgGhostClyde(binding: self, deligateActor: self)
ptsManager = CgScorePtsManager(binding: self, deligateActor: self)
specialTarget = CgSpecialTarget(binding: self, deligateActor: self)
ghosts.append(blinky)
ghosts.append(pinky)
ghosts.append(inky)
ghosts.append(clyde)
}
/// States of game model
enum EnGameModelState: Int {
case Init = 0
case Start, Ready, Go, Updating, ReturnToUpdating, RoundClear, PrepareFlashMaze, FlashMaze,
PlayerMiss, PlayerDisappeared, PlayerRestart, GameOver
}
/// Handle sequence
/// To override in a derived class.
/// - Parameter sequence: Sequence number
/// - Returns: If true, continue the sequence, if not, end the sequence.
override func handleSequence(sequence: Int) -> Bool {
guard let state: EnGameModelState = EnGameModelState(rawValue: sequence) else { return false }
switch state {
case .Init: sequenceInit()
case .Start: sequenceStart()
case .Ready: sequenceReady()
case .Go: sequenceGo()
case .Updating: sequenceUpdating()
case .ReturnToUpdating: sequenceReturnToUpdating()
case .RoundClear: sequenceRoundClear()
case .PrepareFlashMaze: sequencePrepareFlashMaze()
case .FlashMaze: sequenceFlashMaze()
case .PlayerMiss: sequencePlayerMiss()
case .PlayerDisappeared: seauencePlayerDisappeared()
case .PlayerRestart: sequencePlayerRestart()
default:
// Stop and exit running sequence.
return false
}
// Continue running sequence.
return true
}
// ============================================================
// Execute sequence in each state.
// ============================================================
func sequenceInit() {
drawBackground()
goToNextSequence()
}
func sequenceStart() {
context.resetGame()
context.resetRound()
context.numberOfFeeds = drawMazeWithSettingValuesAndAttributes()
printBlinking1Up()
printPlayers(appearance: false)
printStateMessage(.PlayerOneReady)
sound.enableOutput(true)
sound.playSE(.Beginning)
goToNextSequence(.Ready, after: 2240)
}
func sequenceReady() {
printStateMessage(.ClearPlayerOne)
printPlayers(appearance: true)
player.reset()
ghosts.reset()
specialTarget.reset()
ptsManager.reset()
goToNextSequence(.Go, after: 1880)
}
func sequenceGo() {
printStateMessage(.ClearReady)
drawPowerFeed(state: .Blinking)
player.start()
ghosts.start()
// Reset counter for wavy attack of ghosts
counter_judgeGhostsWavyChase = 0
goToNextSequence()
}
// 〜 以下、省略 〜
}
Start, Ready, Go それぞれの状態に対応するシーケンスを sequenceStart(), sequenceReady(), sequenceGo()メソッドで実装する。
仕様書とメソッドを対応させると以下の通り。
- ”1UP”表示が点滅 → printBlinking1Up()
- 巣の上に”PLAYER ONE”表示、巣の下に”READY”表示 → printStateMessage(.PlayerOneReady)
- 設定パックマンの数だけ”●”がプレイフィールド外の左下に表示される → printPlayers(appearance: false)
- スタートミュージック → sound.playSE(.Beginning)
- ”PLAYER ONE”表示が赤モンスターに代わり → printStateMessage(.ClearPlayerOne), ghosts.reset()
- 設定パックマン(プレイフィールド外の左下)が1つ減る → printPlayers(appearance: true)
- パックマンがスタート位置に表示される → player.reset()
- ”READY!”表示が消えて、プレイモードに移る → printStateMessage(.ClearReady)
プレイモードの仕様書
プレイモードにおいては、モンスターの出現タイミングや波状攻撃、アカモンスターのスパートといった細かい仕様が定義されている。これらによってモンスターは単調な追いかけ動作にならず、またプレイヤーのゲーム進行に合わせて難易度が調整される仕組みになっている。
ラウンドに対して更にモンスター出現タイミングのレベル、波状攻撃のスピードレベル、スパートする残りエサ数が定義されているが、今回は固定のレベル「A」、スパートは「①イ」のみ実装していく。
プレイモードのソースコード
プレイモードの仕様は sequenceUpdating() に動作を実装していく。
func sequenceUpdating() {
// Player checks to collide ghost.
let collisionResult = ghosts.detectCollision(playerPosition: player.position)
switch collisionResult {
case .None:
// When it's no eat time, ghost goes out one by one.
if player.timer_playerNotToEat.isEventFired() {
player.timer_playerNotToEat.restart()
ghosts.setStateToGoOut(numberOfGhosts: 4, forcedOneGhost: true)
}
// Appearance Timing of Ghosts
ghosts.setStateToGoOut(numberOfGhosts: context.getNumberOfGhostsForAppearace(), forcedOneGhost: false)
// Wavy Attack of ghosts
// - Do not count timer when Pac-Man has power.
if !player.timer_playerWithPower.isCounting() {
counter_judgeGhostsWavyChase += SYSTEM_FRAME_TIME
}
// Select either Scatter or Chase mode.
let chaseMode = context.judgeGhostsWavyChase(time: counter_judgeGhostsWavyChase)
if chaseMode {
pinky.chase(playerPosition: player.position, playerDirection: player.direction.get())
inky.chase(playerPosition: player.position, blinkyPosition: blinky.position)
clyde.chase(playerPosition: player.position)
} else {
pinky.setStateToScatter()
inky.setStateToScatter()
clyde.setStateToScatter()
}
// If Blinky becomes spurt or not.
let blinkySpurt: Bool = context.judgeBlinkySpurt() && !ghosts.isGhostInNest()
blinky.state.setSpurt(blinkySpurt)
// Blinky doesn't become scatter mode when he spurts.
if blinkySpurt || chaseMode {
blinky.chase(playerPosition: player.position)
} else {
blinky.setStateToScatter()
}
// For debug
ghosts.drawTargetPosition(show: true)
case .PlayerEatsGhost:
let pts = context.ghostPts
ptsManager.start(kind: pts, position: ghosts.collisionPosition, interval: 1000) //ms
context.ghostPts = pts.get2times()
addScore(pts: pts.getScore())
player.stop()
player.clear()
specialTarget.enabled = false
ghosts.stopWithoutEscaping()
sound.playSE(.EatGhost)
sound.stopBGM() // REMARKS: To change playBGM(.BgmEscaping) immediately.
goToNextSequence(.ReturnToUpdating, after: 1000)
case .PlayerMiss:
goToNextSequence(.PlayerMiss)
}
playBGM()
}
はじめにプレイヤーとモンスターの衝突判定を ghosts.detectCollision(playerPosition: player.position) で行う。
衝突がなければ(.None)、ノーイートタイムによって、ゴーストが巣から出ている処理を行う。4匹の中から必ず1匹は外に出して、ノーイートタイムをリスタートさせる。
次は、食べたエサの数でゴーストを巣から出していく。すでに指定の数が出ていたら何もしない。ミスして新たにスタートするときは、ミスバイパスシーケンスを通す。
CgContextクラスの getNumberOfGhostsForAppearace()メソッドは以下の通り。
func getNumberOfGhostsForAppearace() -> Int {
let numberOfGhosts: Int
// Miss Bypass Sequence
if playerMiss {
// Level A
if numberOfFeedsEatedByMiss < 7 {
numberOfGhosts = 1
} else if numberOfFeedsEatedByMiss < 17 {
numberOfGhosts = 2
} else if numberOfFeedsEatedByMiss < 32 {
numberOfGhosts = 3
} else {
playerMiss = false
numberOfGhosts = getNumberOfGhostsForAppearace()
}
} else {
// Level A
if numberOfFeedsEated < 30 {
numberOfGhosts = 2
} else if numberOfFeedsEated < 90 {
numberOfGhosts = 3
} else {
numberOfGhosts = 4
}
}
return numberOfGhosts
}
sequenceUpdating() メソッドの処理フローに戻る。
次の波状攻撃は、パターンスタートからカウントしている時間 counter_judgeGhostsWavyChase によって Scatter と Chase を切り替える。
ただしプレイヤーが逆転している時は、カウントをストップする。
CgContextクラスの judgeGhostsWavyChase()メソッドは以下の通り。
func judgeGhostsWavyChase(time: Int) -> Bool {
let mode: Bool
// Level A
if time < 7000 || (time >= 27000 && time < 34000) ||
(time >= 54000 && time < 59000) || (time >= 79000 && time < 84000) {
mode = false
} else {
mode = true
}
return mode
}
sequenceUpdating() メソッドの最後の処理フローでは、アカモンスター(Blinky)のスパート処理を行う。残りエサ数に達した時かつ全モンスターが巣から出ている時(!ghosts.isGhostInNest())にスパートする。
残りは衝突判定で、パックマンがモンスターを噛み付いた時(.PlayerEatsGhost)
、逆に捕まった時(.PlayerMiss)の処理をそれぞれ実装する。
まとめ
ようやくゲームとしてプレイができるようになってきた。現在のソースコードは約5000行。
次はラウンドによって変化する難易度レベルやスピードレベルの詳細を作り込んでいく。
次の記事
[【入門】iOS アプリ開発 #10【ゲームの各種設定(難易度やスピードレベル)】]
(https://qiita.com/KIKU_CHU/items/d342b9a878fd66639d1b)