はじめに
今回はラウンドによって変化する難易度やスピードレベルの詳細を作り込み、#5【シーケンスの設計】で作成した各画面モードを結合して、ほぼ完成の状態に持っていく(以下が完成動画)。ソースコードは GitHub に公開しているので参照してほしい。
仕様書
ラウンドによって変化する難易度やスピードレベルの仕様は以下の通り。
今回、難易度表の外国用バージョンと、スパート②設定は作成しない。
難易度表とスピード表の実装
タプルの配列として、難易度表とスピード表を実装する。
以下、各画面モードの結合を除き、全て CgContextクラスに含める。
enum EnLevel: Int {
case Level_A = 0, Level_B, Level_C, Level_D
}
let table_difficultySettings: [(round: Int, levelOfSpeed: EnLevel, timeWithPower: Int, numberOfFeedsRemaingToSpurt: Int, levelOfAppearance: EnLevel, kindOfSpecialTarget: CgSpecialTarget.EnSpecialTarget, timeNotToEat: Int, intermission: Int)] = [
//round, speedLevel, PowerTime[ms], Spurtfeeds, GhostAppear, SpecialTarget, NoEatTime[ms], Intermission
( 1, .Level_A, 6000, 20, .Level_A, .Cherry, 4000, 0 ),
( 2, .Level_B, 5000, 30, .Level_B, .Strawberry, 4000, 1 ),
( 3, .Level_B, 4000, 40, .Level_C, .Orange, 3000, 0 ),
( 4, .Level_B, 3000, 40, .Level_C, .Orange, 3000, 0 ),
( 5, .Level_C, 2000, 40, .Level_C, .Apple, 3000, 2 ),
( 6, .Level_C, 5000, 50, .Level_C, .Apple, 3000, 0 ),
( 7, .Level_C, 2000, 50, .Level_C, .Melon, 3000, 0 ),
( 8, .Level_C, 2000, 50, .Level_C, .Melon, 3000, 0 ),
( 9, .Level_C, 1000, 60, .Level_C, .Galaxian, 3000, 3 ),
( 10, .Level_C, 5000, 60, .Level_C, .Galaxian, 3000, 0 ),
( 11, .Level_C, 2000, 60, .Level_C, .Bell, 3000, 0 ),
( 12, .Level_C, 1000, 80, .Level_C, .Bell, 3000, 0 ),
( 13, .Level_C, 1000, 80, .Level_C, .Key, 3000, 3 ),
( 14, .Level_C, 3000, 80, .Level_C, .Key, 3000, 0 ),
( 15, .Level_C, 1000, 100, .Level_C, .Key, 3000, 0 ),
( 16, .Level_C, 1000, 100, .Level_C, .Key, 3000, 0 ),
( 17, .Level_C, 0, 100, .Level_C, .Key, 3000, 3 ),
( 18, .Level_C, 1000, 100, .Level_C, .Key, 3000, 0 ),
( 19, .Level_C, 0, 100, .Level_C, .Key, 3000, 0 ),
( 20, .Level_C, 0, 100, .Level_C, .Key, 3000, 0 ),
( 21, .Level_C, 0, 100, .Level_C, .Key, 3000, 0 ),
( 22, .Level_D, 0, 100, .Level_C, .Key, 3000, 0 )
]
let table_speedSettings: [ (eatNone: Int, eatFeed: Int, eatPow: Int, eatNoneInPow: Int, eatFeedInPow: Int, eatPowInPow: Int,
ghost: Int, ghostInSpurt: Int, ghostInPow: Int, ghostInWarp: Int) ]
= [
// Level A
( eatNone: 16, eatFeed: 15, eatPow: 13, eatNoneInPow: 18, eatFeedInPow: 17, eatPowInPow: 15,
ghost: 15, ghostInSpurt: 16, ghostInPow: 10, ghostInWarp: 8 ),
// Level B
( eatNone: 18, eatFeed: 17, eatPow: 15, eatNoneInPow: 19, eatFeedInPow: 18, eatPowInPow: 16,
ghost: 17, ghostInSpurt: 18, ghostInPow: 11, ghostInWarp: 9 ),
// Level C
( eatNone: 20, eatFeed: 19, eatPow: 17, eatNoneInPow: 20, eatFeedInPow: 19, eatPowInPow: 17,
ghost: 19, ghostInSpurt: 20, ghostInPow: 12, ghostInWarp: 10 ),
// Level D
( eatNone: 18, eatFeed: 17, eatPow: 15, eatNoneInPow: 18, eatFeedInPow: 17, eatPowInPow: 15,
ghost: 19, ghostInSpurt: 20, ghostInPow: 10, ghostInWarp: 9 )
]
このタプルの配列からラウンドに合わせたデータを取り出して、CgContext クラスのメンバにそれぞれ設定する。
/// Set difficulty of the round
func setDifficulty() {
let index = demo ? 0 : round-1
let count = table_difficultySettings.count
let table = (index < count) ? table_difficultySettings[index] : table_difficultySettings[count-1]
levelOfSpeed = table.levelOfSpeed
timeWithPower = table.timeWithPower
numberOfFeedsRemaingToSpurt = table.numberOfFeedsRemaingToSpurt
levelOfAppearance = table.levelOfAppearance
kindOfSpecialTarget = table.kindOfSpecialTarget
timeNotToEat = table.timeNotToEat
intermission = table.intermission
}
プレイヤー(パックマン)のスピードを取得するメソッドについては以下の通り。パワーエサを食べて逆転している時と、そうでない時で取得する値を変える。
func getPlayerSpeed(action: CgPlayer.EnPlayerAction, with power: Bool ) -> Int {
let index = levelOfSpeed.rawValue
let count = table_speedSettings.count
let table = (index < count) ? table_speedSettings[index] : table_speedSettings[count-1]
switch action {
case .Walking where !power : return table.eatNone
case .Walking where power : return table.eatNoneInPow
case .EatingFeed where !power : return table.eatFeed
case .EatingFeed where power : return table.eatFeedInPow
case .EatingPower where !power : return table.eatPow
case .EatingPower where power : return table.eatPowInPow
case .EatingFruit where !power : return table.eatNone
case .EatingFruit where power : return table.eatNoneInPow
default: return 16
}
}
モンスター(ゴースト)出現タイミングの実装
パックマンがプレイ開始からエサを食べた数 numberOfFeedsEated によって、レベル毎に出現するゴーストの数を返す。
func getNumberOfGhostsForAppearace() -> Int {
let numberOfGhosts: Int
// Miss Bypass Sequence
if playerMiss {
if numberOfFeedsEatedByMiss < 7 {
numberOfGhosts = 1
} else if numberOfFeedsEatedByMiss < 17 {
numberOfGhosts = 2
} else if numberOfFeedsEatedByMiss < 32 {
numberOfGhosts = 3
} else {
playerMiss = false
numberOfGhosts = getNumberOfGhostsForAppearace()
}
} else {
switch levelOfAppearance {
case .Level_A:
if numberOfFeedsEated < 30 {
numberOfGhosts = 2
} else if numberOfFeedsEated < 90 {
numberOfGhosts = 3
} else {
numberOfGhosts = 4
}
case .Level_B:
if numberOfFeedsEated < 50 {
numberOfGhosts = 3
} else {
numberOfGhosts = 4
}
case .Level_C: fallthrough
default:
numberOfGhosts = 4
}
}
return numberOfGhosts
}
波状攻撃の実装
スタート時からカウントしている時間によって、ChaseMode と ScatterMode を切り替える。レベル毎に ChaseMode の時間を判定する。
func judgeGhostsWavyChase(time: Int) -> Bool {
var chaseMode: Bool = false
switch levelOfSpeed {
case .Level_A:
chaseMode = (time >= 7000 && time < 27000) || (time >= 34000 && time < 54000)
|| (time >= 59000 && time < 79000) || (time >= 84000)
case .Level_B:
chaseMode = (time >= 7000 && time < 27000) || (time >= 34000 && time < 54000)
|| (time >= 59000)
case .Level_C: fallthrough
case .Level_D:
chaseMode = (time >= 5000 && time < 25000) || (time >= 30000 && time < 50000)
|| (time >= 55000)
}
return chaseMode
}
各画面モードの結合
最後に CgGameMainクラスに、今まで作成した各モードを結合していく。
- アトラクトモード:CgSceneAttractMode
- クレジットモード:CgSceneCreditMode ※今回作成 GameSequences.swift に追加
- スタートモード:CgSceneMaze
- プレイモード:CgSceneMaze実行中
class CgGameMain : CgSceneFrame {
enum EnMainMode: Int {
case AttractMode = 0, CreditMode, WaitForStartButton, StartMode, PlayMode
}
enum EnSubMode: Int {
case Character = 0, StartDemo, PlayDemo
}
private var scene_attractMode: CgSceneAttractMode!
private var scene_creditMode: CgSceneCreditMode!
private var scene_maze: CgSceneMaze!
private var subMode: EnSubMode = .Character
init(skscene: SKScene) {
super.init()
// Create SpriteKit managers.
self.sprite = CgSpriteManager(view: skscene, imageNamed: "pacman16_16.png", width: 16, height: 16, maxNumber: 64)
self.background = CgCustomBackgroundManager(view: skscene, imageNamed: "pacman8_8.png", width: 8, height: 8, maxNumber: 2)
self.sound = CgSoundManager(binding: self, view: skscene)
self.context = CgContext()
scene_attractMode = CgSceneAttractMode(object: self)
scene_creditMode = CgSceneCreditMode(object: self)
scene_maze = CgSceneMaze(object: self)
}
/// Event handler
/// - Parameters:
/// - sender: Message sender
/// - id: Message ID
/// - values: Parameters of message
override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
if message == .Touch {
if let mode: EnMainMode = EnMainMode(rawValue: getSequence()) {
if mode == .AttractMode || mode == .WaitForStartButton {
goToNextSequence()
}
}
}
}
/// 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 mode: EnMainMode = EnMainMode(rawValue: sequence) else { return false }
switch mode {
case .AttractMode: attarctMode()
case .CreditMode: creditMode()
case .WaitForStartButton: break // Forever loop
case .StartMode: startMode()
case .PlayMode: playMode()
}
// Continue running sequence.
return true
}
// ============================================================
// Execute each mode.
// ============================================================
func attarctMode() {
switch subMode {
case .Character:
scene_attractMode.resetSequence()
scene_attractMode.startSequence()
subMode = .StartDemo
case .StartDemo:
if !scene_attractMode.enabled {
context.demo = true
sound.enableOutput(false)
scene_maze.resetSequence()
scene_maze.startSequence()
subMode = .PlayDemo
}
case .PlayDemo:
if !scene_maze.enabled {
subMode = .Character
}
}
}
func creditMode() {
context.demo = false
if scene_attractMode.enabled {
scene_attractMode.stopSequence()
scene_attractMode.clear()
}
if scene_maze.enabled {
scene_maze.stopSequence()
scene_maze.clear()
}
context.credit += 1
scene_creditMode.resetSequence()
scene_creditMode.startSequence()
sound.enableOutput(true)
sound.playSE(.Credit)
goToNextSequence()
}
func startMode() {
context.credit -= 1
scene_creditMode.stopSequence()
scene_maze.resetSequence()
scene_maze.startSequence()
goToNextSequence()
}
func playMode() {
if !scene_maze.enabled {
subMode = .Character
goToNextSequence(EnMainMode.AttractMode.rawValue)
}
}
}
またアトラクトモードにはキャラクター紹介に加えてプレイのデモがある。
こちらは、demo のフラグで、スワイプ操作と予め用意した操作テーブルを切り替えることで簡単に実装できた。
操作テーブルと取得メソッドは CgContextクラスに実装。
スタートからのフレーム数によって方向を取り出す。
let table_operationInDemo: [ (frameCount: Int, direction: EnDirection) ] = [
(9, .Left), (36, .Down), (61, .Right), (82, .Down), (109, .Right), (133, .Up), (162, .Right),
(189, .Up), (215, .Right), (238, .Down), (261, .Right), (308, .Down), (335, .Left), (523, .Up),
(555, .Right), (569, .Up), (609, .Left), (632, .Up), (648, .Right), (684, .Up), (732, .Left),
(831, .Down), (864, .Left), (931, .Up), (948, .Left), (970, .Up), (1063, .Right), (1113, .Down),
(1157, .Right), (1218, .Down)
]
func getOperationForDemo() -> EnDirection {
guard(demoSequence < table_operationInDemo.count) else { return .None }
let table = table_operationInDemo[demoSequence]
var direction: EnDirection = .None
if counterByFrame >= table.frameCount {
direction = table.direction
demoSequence += 1
}
return direction
}
getOperationForDemo メソッドで操作方向を取得してプレイヤーに設定する。CgSceneMazeクラスの sequenceUpdating シーケンスに追加実装。
func sequenceUpdating() {
// Operate player in demonstration automatically.
if context.demo {
let direction = context.getOperationForDemo()
if direction != .None {
player.targetDirecition = direction
}
}
// 以下、省略
まとめ
ようやく、ほぼ完成の状態になった。
ゲームの動作スピードも問題ない。
ソースコードも 5000行程度のままで、結構簡単にできるものだ。
次回は、せっかく自作しているので色々とアレンジを行って完成としたい。