はじめに
今回はパックマンのキャラクタ操作を作成する。操作はスワイプで行い、以下が完成動画。ソースコードは GitHub に公開しているので参照してほしい。
仕様書
注目すべきところはスピードレベルの仕様。単に1ドットずつ動かすなら簡単だが、微妙なスピード調整の仕様があり、ゲームの難易度を調整している。
エサを食べる、食べない、パワーエサを食べた状態などで、移動スピードが異なる。仕様段階でここまで定義していたとは奥深い。
「スピードの数字は、1フレームに1ドット移動するスピードを”16”として、2ドット移動を”32”として、他はそれに準じて分割。」と書いてあるので、1ドット進むスピードを 16 として、作成していく。
また仕様書には記載がないが、パックマンが迷路を曲がる時はモンスターより早く内側に曲り、曲りながら移動することでモンスターから引き離せるチューニングがされているようだ。本当に奥深い。
スワイプ操作の作成
スワイプ操作をパックマン・オブジェクトに伝えるのは、
sendEvent(message: .Swipe, parameter: [direction])
というようにしたい。
イベントは、迷路シーンの中で生成したパックマン・オブジェクトへ通知する。
大もととなる GameScene クラスの touchesBegan, touchesEnded メソッドにスワイプ動作のコードを実装する。gameMain オブジェクトの sendEvent を呼ぶ。
class GameScene: SKScene {
/// Main object with main game sequence
private var gameMain: CgGameMain!
/// Points for Swipe operation
private var startPoint: CGPoint = CGPoint.init()
private var endPoint: CGPoint = CGPoint.init()
override func didMove(to view: SKView) {
// Create and start game sequence.
gameMain = CgGameMain(skscene: self)
gameMain.startSequence()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Get start touchpoint for swipe.
if let t = touches.first {
let location = t.location(in: self)
startPoint = CGPoint(x: location.x, y: location.y)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// Get end touchpoint for swipe.
if let t = touches.first {
let location = t.location(in: self)
endPoint = CGPoint(x: location.x, y: location.y)
let x_diff = endPoint.x - startPoint.x
let y_diff = endPoint.y - startPoint.y
// Send swipe message to GameMain.
if abs(x_diff) > abs(y_diff) {
gameMain.sendEvent(message: .Swipe, parameter: [Int(x_diff > 0 ? EnDirection.Right.rawValue : EnDirection.Left.rawValue)])
} else {
gameMain.sendEvent(message: .Swipe, parameter: [Int(y_diff > 0 ? EnDirection.Up.rawValue : EnDirection.Down.rawValue)])
}
}
}
CgGameMain class
gameMain: CgGameMain (オブジェクト:クラス)は、前回のアーキテクチャでいう、root のオブジェクトとして作成した。
gameMain の sendEvent メソッドは、gameMain が持つアクティブ(enabled=true)なオブジェクトにメッセージを送信する。
以下のコードにより、scene_maze(迷路を描画するシーン) が startSequence() により enabled=true となり該当する。
/// Main sequence of game scene.
class CgGameMain : CgSceneFrame {
private var scene_attractMode: CgSceneAttractMode!
private var scene_maze: CgSceneMaze!
private var scene_intermission1: CgSceneIntermission1!
private var scene_intermission2: CgSceneIntermission2!
private var scene_intermission3: CgSceneIntermission3!
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_maze = CgSceneMaze(object: self)
scene_intermission1 = CgSceneIntermission1(object: self)
scene_intermission2 = CgSceneIntermission2(object: self)
scene_intermission3 = CgSceneIntermission3(object: self)
}
/// 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:
// Start maze sequence.
scene_maze.startSequence()
goToNextSequence()
default:
// Forever loop
break
}
// Continue running sequence.
return true
}
}
迷路シーンクラスでは、初期化時に新規に作成したパックマンのクラス CgPlayer を CgSceneMaze に関連付け(binding)て生成し、イベントが通知できるようにする。
そのため、CgPlayerクラスの基底クラスは CbObject となっている。
シーケンス実装の handleSequenceメソッド内でパックマンを player.start()
(enabled=true)にすると、CgPlayer内の updateメソッドがフレーム毎に呼ばれて移動処理を更新する仕組みとなる。
class CgSceneMaze: CgSceneFrame, ActorDeligate {
var player : CgPlayer!
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)
}
/// 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()
printBlinking1Up()
drawPowerFeed(state: .Blinking)
player.reset()
player.start()
goToNextSequence()
case 1:
// Foever loop
break
//
// Round clear animation(Maze flashes)
//
case 10:
blinkingTimer = 104 // 104*16ms = 1664ms
goToNextSequence()
case 11:
if blinkingTimer == 0 {
goToNextSequence()
} else {
let remain = blinkingTimer % 26
if remain == 0 {
drawMazeWall(color: .White)
} else if remain == 13 { // 13*16ms = 208ms
drawMazeWall(color: .Blue)
}
blinkingTimer -= 1
}
case 12:
// Stop and exit running sequence.
return false
default:
// Stop and exit running sequence.
return false
}
// Play BGM
if player.timer_playerWithPower.isCounting() {
sound.playBGM(.BgmPower)
} else {
sound.playBGM(.BgmNormal)
}
// Continue running sequence.
return true
}
CgPlayer class
パックマンの CgPlayerクラスは、CbContainer & CbObjectクラスを継承した CgActorクラスを継承している。handleEventメソッドをオーバーライドすれば、Swipe イベントを取得できる。これがやりたいためにクラス構成を設計してきた。
/// Player(Pacman) class derived from CgAcotr
class CgPlayer : CgActor {
enum EnPlayerAction: Int {
case None, Stopping, Walking, Turning, EatingDot, EatingPower, EatingFruit
}
var targetDirecition: EnDirection = .Stop
var actionState: EnPlayerAction = .None
var timer_playerWithPower: CbTimer!
var timer_playerNotToEat: CbTimer!
override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
super.init(binding: object, deligateActor: deligateActor)
timer_playerWithPower = CbTimer(binding: self)
timer_playerNotToEat = CbTimer(binding: self)
actor = .Pacman
sprite_number = actor.getSpriteNumber()
enabled = false
}
// ============================================================
// Event Handler
// ============================================================
/// Event handler
/// - Parameters:
/// - sender: Message sender
/// - id: Message ID
/// - values: Parameters of message
override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
switch message {
case .Swipe:
if let direction = EnDirection(rawValue: values[0]) {
targetDirecition = direction
}
default:
break
}
}
/// Update handler
/// - Parameter interval: Interval time(ms) to update
override func update(interval: Int) {
if actionState == .Turning {
turn()
} else {
if canMove(to: targetDirecition) {
direction.set(to: targetDirecition)
} else {
direction.update()
if canTurn() {
actionState = .Turning
direction.set(to: targetDirecition)
return
}
}
move()
}
}
handleEventメソッドでパックマンの移動方向を設定し、updateメソッドで設定された移動方向の処理を行う。
まとめ
今回、パックマンのキャラクタ操作を実装するにあたり、以下のファイル(クラス)を新規で作成した。
GameMain.swift(CgGameMain)
GamePlayer.swift(CgPlayer)
GameActor.swift(CgActor,CgPosition,CgDirection)
GameContext.swift(CbContext)
ソースコードは全体で 3000行程度。
操作のパフォーマンスは問題ない状況。
次はモンスターを作成していく。
次の記事
[【入門】iOS アプリ開発 #7【敵キャラクタの移動】]
(https://qiita.com/KIKU_CHU/items/b5f8a4e147cab22b14b8)