はじめに
今回はパックマンに登場する敵キャラクタを作成する。以下が完成動画。
作成した GameGhost.swift ファイルは、コードが 1000行程度になり全ては説明できないため、GitHub に公開しているものを参照してほしい。
テスト動作として、下記の仕様書にある「ノーイートタイム」を過ぎると、ゴースト(モンスター)が巣から出てきて攻撃中となるようにしている。そうでない時は休息中となる。またバッテン表示はデバッグ用としてゴーストの目標位置を示している。
仕様書
仕様書にはゴーストの出現タイミングや、それぞれ異なる4匹の性格仕様が記述されている。
整理していくと、以下のように全てのゴーストには共通の動作や状態があることがわかる。
- プレイフィールド中(巣から出た状態)のゴーストの移動は、攻撃中と休息中の2つの状態がある
- パックマンがパワーエサを食べた時、ゴーストはイジケ顔になってランダムに移動する
- パックマンがゴーストに噛みついた時、ゴーストは目玉だけとなって巣に逃げる
- ゴーストは特定のタイミングや条件によって巣から出てくる
これらは共通のクラスで実装し、攻撃中と休息中におけるゴーストの特徴の違いを派生クラスで実装するのが簡単そうだ。
キャラクタ・クラスの構成
下図のようなクラス継承構造で設計する。
各クラスの役割を補足すると、以下の通り。
- CbObject : イベントをハンドリングする
- CbContainer : CbObject(と派生クラス)を複数保持できるコンテナ
- CgActor : キャラクタ共通の位置や方向、スプライト処理を扱う
- CgGhost : ゴースト共通の処理を扱う
- CgGhostBlinky : アカ・ゴースト
- CgGhostPinky : ピンク・ゴースト
- CgGhostInky : シアン・ゴースト
- CgGhostClyde : オレンジ・ゴースト
- CgPlayer : パックマン
CgGhost クラスの設計
ゴースト共通の動作や状態をステート・チャートにまとめてみた。
- Standby は巣にいる状態
- GoOut は巣からプレイフィールドに出る状態
- Chase は攻撃中状態
- Scatter は休息中状態
- Frightened はイジケ状態
- Escape は目玉になって巣に戻る状態
- EscapeInNest は目玉の状態で巣に戻って、スタート位置まで戻る状態
Frightened は Standby, GoOut, Chase, Scatter をサブ状態とするスーパー状態となっている。
ソースコードは、フレーム毎に呼ばれる update メソッドをオーバーライドして、ステートマシンを実装していく。状態に入る時の entryアクション、状態に入っている時のアクティビティを一つ一つ作り込んで、ゴーストの動きを作成する。
class CgGhost : CgActor {
enum EnGhostAction {
case None, Walking, Spurting, Frightened, Warping, Standby, GoingOut, Escaping
}
enum EnMovementRestrictions {
case None, OnlyVertical
}
var target: CgPosition = CgPosition()
var state: CgGhostState!
private var movementRestriction: EnMovementRestrictions = .None
override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
super.init(binding: object, deligateActor: deligateActor)
state = CgGhostState(binding: self)
enabled = false
}
// ============================================================
// Core operation methods for actor
// - Sequence: reset()->start()->update() called->stop()
// ============================================================
/// Reset ghost states and draw at default position
override func reset() {
super.reset()
direction.reset()
state.reset()
movementRestriction = .None
}
/// Start
override func start() {
super.start()
draw()
}
/// Stop
override func stop() {
super.stop()
draw()
}
/// Update handler
/// - Parameter interval: Interval time(ms) to update
override func update(interval: Int) {
//
// Entry Action to state
//
if state.isChanging() {
switch state.getNext() {
case .Init : break
case .Standby: entryActionToStandby()
case .GoOut: entryActionToGoOut()
case .Scatter: entryActionToScatter()
case .Chase: entryActionToChase()
case .Escape: entryActionToEscape()
case .EscapeInNest: entryActionToEscapeInNest()
}
state.update()
}
//
// Do Action in state
//
switch state.get() {
case .Init : break
case .Standby: doActionInStandby()
case .GoOut: doActionInGoOut()
case .Scatter:
if state.isFrightened() {
doActionInFrightened()
} else {
doActionInScatter()
}
case .Chase:
if state.isFrightened() {
doActionInFrightened()
} else {
doActionInChase()
}
case .Escape: doActionInEscape()
case .EscapeInNest: doActionInEscapeInNest()
}
// Update direction and sprite animation for changes.
if state.isChanging() || direction.isChanging() || state.isDrawingUpdated() {
if direction.isChanging() {
position.roundDown()
direction.update()
}
draw()
state.clearDrawingUpdate()
}
// Update position.
sprite.setPosition(sprite_number, x: position.x, y: position.y)
}
// 以下、省略
}
Frightenedのスーパー状態は、別の CgGhostState クラスを作成して実現している。
state.isFrightened() が true ならイジケ状態で、scatter と chase状態において、doActionInFrightened() メソッド内でゴーストのランダム移動処理を行う。
CgGhostXxxx 敵キャラクタ・クラスの設計
CgGhost クラスを継承して、4匹のゴーストのスタート位置や攻撃動作を実装していく。
/// Ghost Blinky class
class CgGhostBlinky : CgGhost {
override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
super.init(binding: object, deligateActor: deligateActor)
actor = .Blinky
sprite_number = actor.getSpriteNumber()
}
// ============================================================
// Core operation methods for actor
// - Sequence: reset()->start()->update() called->stop()
// ============================================================
/// Reset ghost state and draw at default position
override func reset() {
super.reset()
position.set(column: 13, row: 21, dx: 4)
updateDirection(to: .Left)
state.set(to: .Scatter)
draw()
}
// ============================================================
// General methods in this class
// ============================================================
/// Chase player to enter chase state.
/// Always chase the Pacman during the chase mode.
/// - Parameter playerPosition: Player's position
func chase(playerPosition: CgPosition) {
super.setStateToChase(targetPosition: playerPosition)
}
/// Set the target position in scatter state.
/// Blinky moves around the upper right in the play field.
override func entryActionToScatter() {
target.set(column: 25, row: 35)
super.entryActionToScatter()
}
/// Set return destination in nest from escape state.
override func entryActionToEscapeInNest() {
target.set(column: 13, row: 18, dx: 4, dy: -4)
}
}
/// Ghost Pinky class
class CgGhostPinky : CgGhost {
override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
super.init(binding: object, deligateActor: deligateActor)
actor = .Pinky
sprite_number = actor.getSpriteNumber()
}
// ============================================================
// Core operation methods for actor
// - Sequence: reset()->start()->update() called->stop()
// ============================================================
/// Reset ghost state and draw at default position
override func reset() {
super.reset()
position.set(column: 13, row: 18, dx: 4)
updateDirection(to: .Down)
state.set(to: .Standby)
draw()
}
// ============================================================
// General methods in this class
// ============================================================
/// Chase player to enter chase state.
/// Aiming for Pacman's third destination
/// - Parameters:
/// - playerPosition: Player's position
/// - playerDirection: Player's direction
func chase(playerPosition: CgPosition, playerDirection: EnDirection) {
let dx = playerDirection.getHorizaontalDelta()*3
let dy = playerDirection.getVerticalDelta()*3
let newTargetPosition = CgPosition(column: playerPosition.column+dx, row: playerPosition.row+dy)
super.setStateToChase(targetPosition: newTargetPosition)
}
/// Set the direction in standby state.
override func entryActionToStandby() {
updateDirection(to: .Down)
}
/// Set the target position in scatter state.
/// Pinky moves around the upper left on the play field.
override func entryActionToScatter() {
target.set(column: 2, row: 35)
super.entryActionToScatter()
}
/// Set return destination in nest from escape state.
override func entryActionToEscapeInNest() {
target.set(column: 13, row: 18, dx: 4, dy: -4)
}
}
// 以下、省略
resetメソッドは、初期スタート位置を設定し、Init状態からの初期の状態遷移を行う。
- Blinky(アカ)の場合は、巣の上からスタートし Scatter状態へ遷移する。
- Pinky(ピンク)の場合は、巣の中からスタートし、Standby状態へ遷移する。
chaseメソッドは、目標位置に向けて移動する攻撃中の状態へと遷移する。
- Blinky(アカ)の場合は、パックマンの位置を目標位置とする。
- Pinky(ピンク)の場合は、パックマンの口先の3つ先のマスを目標位置とする。
entryActionToScatterメソッドは、休息中の状態へ遷移する。
- Blinky(アカ)の場合は、プレイフィールド上の右上を目標位置とする。
- Pinky(ピンク)の場合は、プレイフィールド上の左上を目標位置とする。
entryActionToEscapeInNestメソッドは、目玉の状態で巣に戻った時のスタート位置を目標位置として設定する。
といった具合に、CgGhostクラスの基底メソッドをオーバーライドして実装していく。
ゴーストのシーケンス起動
作成したゴーストのクラスをシーケンスに実装して動かしていく。
プレイヤーの時と同様に、CgSceneMaze迷路シーンクラスにゴーストのクラス CgGhostXxxx を関連付け(binding)して生成し、イベントが通知できるようにする。
シーケンス実装の handleSequenceメソッド内でゴーストを ghost.start()
(enabled=true)にすると、CgGhost内の updateメソッドがフレーム毎に呼ばれてステートマシンが実行される仕組みとなる。
class CgSceneMaze: CgSceneFrame, ActorDeligate {
var player : CgPlayer!
var blinky : CgGhostBlinky!
var pinky : CgGhostPinky!
var inky : CgGhostInky!
var clyde : CgGhostClyde!
var allGhosts : [CgGhost] = []
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)
allGhosts.append(blinky)
allGhosts.append(pinky)
allGhosts.append(inky)
allGhosts.append(clyde)
}
/// 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 {
switch sequence {
case 0:
drawBackground()
let _ = setAndDraw()
printPlayers()
player.reset()
for ghost in allGhosts { ghost.reset() }
goToNextSequence()
case 1:
printBlinking1Up()
drawPowerFeed(state: .Blinking)
player.start()
for ghost in allGhosts { ghost.start() }
goToNextSequence()
case 2:
// ===========
// Foever loop
// ===========
// Player checks to collide ghost.
for ghost in allGhosts {
if player.detectCollision(ghostPosition: ghost.position) {
if ghost.state.isFrightened() {
ghost.setStateToEscape()
}
}
}
// Set the chase state after after the time that the player doesn't eat feed.
if player.timer_playerNotToEat.isEventFired() {
blinky.chase(playerPosition: player.position)
pinky.chase(playerPosition: player.position, playerDirection: player.direction.get())
inky.chase(playerPosition: player.position, blinkyPosition: blinky.position)
clyde.chase(playerPosition: player.position)
for ghost in allGhosts { ghost.setStateToGoOut() }
} else {
for ghost in allGhosts { ghost.setStateToScatter() }
}
// For debug
for ghost in allGhosts { ghost.drawTargetPosition(show: true) }
break
default:
// Stop and exit running sequence.
return false
}
// Play BGM
if sequence >= 2 {
if player.timer_playerWithPower.isCounting() {
sound.playBGM(.BgmPower)
} else {
sound.playBGM(.BgmNormal)
}
}
// Continue running sequence.
return true
}
無限ループの case2 にテスト動作を実装している。
「ノーイートタイム(= player.timer_playerNotToEat.isEventFired())」を過ぎると、ゴースト(モンスター)が巣から出てきて攻撃中( = blinky.chase)となるようにしている。またゴーストが巣にいる時は外に出す( = ghost.setStateToGoOut())。
ノーイートタイムにならない間は、休息中( = ghost.setStateToScatter())としている。
まとめ
今回、パックマンの敵キャラクタの移動を作成した。ソースコードは全体で 4000行程度になっている。
細かい仕様が多く定義されていて、ゲームを面白くするための企画者のこだわりが感じられる。反面プログラマーは良く考えて設計していかないと、コードは大変な状況になりそうだ。Swiftでゲームアプリを開発する入門としては、意外と良い題材なのかもしれない。
次回は、スペシャルターゲットやパックマンがゴーストを噛み付いた時の得点表示、パックマンがゴーストに捕まった処理などを作成していく。
次の記事
[【入門】iOS アプリ開発 #8【スペシャルターゲット・得点表示】]
(https://qiita.com/KIKU_CHU/items/c32c2fe49915055041dd)