はじめに
公開されている仕様書をもとに作成したパックマンに、アレンジを加えてみたい。パックマンををスワイプだけでなくタッチや加速度センサーで操作できるようにし、オリジナルの迷路も追加してみる。ソースコードは GitHub に公開しているので参照してほしい。
コンフィグレーション・メニュー
操作や迷路を切り替えるためのコンフィグレーション・メニューを追加する。
追加する迷路は開発途中の下記、仕様書を参考にした。
ソースコード
CgSceneCreditMode にメニューを追加する。クレジットを入れて暫くすると case 1 でメニューの入り口が表示される。そこをタッチするとメニューに入れるようにした。
CgSceneFrameクラスの構造は、以前の【入門】iOS アプリ開発 #5【シーケンスの設計】を参照してほしい。
/// Credit Mode
class CgSceneCreditMode : CgSceneFrame {
enum EnEvent: Int {
case EnterConfig = 3
case Operation = 5
case ExtraMode = 6
case DebugMode = 7
case Language = 8
case ResetSetting = 9
case ExitConfig = 10
case None
}
private let table_enterConfiguration: [(Int,Int,Int,Int,EnEvent)] = [
( 26, 34, 28, 36, .EnterConfig)
]
private let table_setConfiguration: [(Int,Int,Int,Int,EnEvent)] = [
( 26, 34, 28, 36, .ExitConfig),
( 4, 25, 28, 27, .Operation),
( 4, 20, 28, 22, .ExtraMode),
( 4, 15, 28, 17, .DebugMode),
( 4, 10, 28, 12, .Language),
( 4, 5, 28, 7, .ResetSetting)
]
private var table_search: [(column0:Int,row0:Int,column1:Int,row1:Int,event:EnEvent)] = []
private var configMode: Bool = false
/// 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 {
let position = CgPosition.init(x: CGFloat(values[0]), y: CGFloat(values[1]))
let event = search(column: position.column, row: position.row)
if event == .None {
if !configMode {
stopSequence()
}
} else {
goToNextSequence(event.rawValue)
}
}
}
/// 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:
configMode = false
table_search = []
clear()
printFrame()
printPlayerScore()
printHighScore()
printCredit()
printRounds()
background.print(0, color: .Orange, column: 6, row: 19, string: "PUSH START BUTTON")
background.print(0, color: .Cyan, column: 8, row: 15, string: "1 PLAYER ONLY")
if context.language == .English {
background.print(0, color: .Pink, column: 1, row: 11, string: "BONUS PAC-MAN FOR 20000 ]^_")
background.print(0, color: .Purple, column:4, row: 4, string: "@ 2020 HIWAY.KIKUTADA")
} else {
background.print(0, color: .Pink, column: 1, row: 11, string: "BONUS PAC-MAN FOR 10000 ]^_")
background.print(0, color: .Purple, column: 7, row: 7, string: "@ #$%&'()* 2020") // NAMACO
}
goToNextSequence(after: 60*16*2)
case 1:
table_search = table_enterConfiguration
background.print(0, color: .White, column: 27, row: 35, string: "$")
goToNextSequence()
case 2:
// wait for event
break;
case 3:
configMode = true
table_search = table_setConfiguration
background.fill(0, texture: 0)
printFrame()
printPlayerScore()
printHighScore()
printCredit()
background.print(0, color: .White, column: 27, row: 35, string: "#")
background.print(0, color: .Red, column: 8, row: 31, string: "CONFIGURATION")
background.print(0, color: .White, column: 4, row: 26, string: "OPERATION")
background.print(0, color: .White, column: 4, row: 21, string: "EXTRA MODE")
background.print(0, color: .White, column: 4, row: 16, string: "DEBUG MODE")
background.print(0, color: .White, column: 4, row: 11, string: "LANGUAGE")
background.print(0, color: .White, column: 4, row: 6, string: "SETTING")
print_operation(color: .Pink)
print_extraMode(color: .Pink)
print_debugMode(color: .Pink)
print_language(color: .Pink)
print_resetSetting(color: .Pink)
goToNextSequence()
case 4:
// wait for event
break;
case 5: // operation
context.operationMode = context.operationMode.getNext()
print_operation(color: .Yellow)
goToNextSequence(4)
case 6: // ExtraMode/
context.extraMode = context.extraMode.getNext()
print_extraMode(color: .Yellow)
goToNextSequence(4)
case 7: // DebugMode
context.debugMode = context.debugMode.getNext()
print_debugMode(color: .Yellow)
goToNextSequence(4)
case 8: // Language
context.language = context.language.getNext()
print_language(color: .Yellow)
goToNextSequence(4)
case 9: // ResetSetting
context.resetSetting = context.resetSetting.getNext()
print_resetSetting(color: .Yellow)
goToNextSequence(4)
case 10:
context.saveConfiguration()
goToNextSequence(0)
default:
clear()
// Stop and exit running sequence.
return false
}
return true
}
func clear() {
background.fill(0, texture: 0)
}
func print_operation(color: CgCustomBackgroundManager.EnBgColor) {
let str = context.operationMode.getString()
background.print(0, color: color, column: 16, row: 26, string: str)
}
func print_extraMode(color: CgCustomBackgroundManager.EnBgColor) {
let str = context.extraMode.getString()
background.print(0, color: color, column: 16, row: 21, string: str)
}
func print_debugMode(color: CgCustomBackgroundManager.EnBgColor) {
let str = context.debugMode.getString()
background.print(0, color: color, column: 16, row: 16, string: str)
}
func print_language(color: CgCustomBackgroundManager.EnBgColor) {
let str = context.language.getString()
background.print(0, color: color, column: 16, row: 11, string: str)
}
func print_resetSetting(color: CgCustomBackgroundManager.EnBgColor) {
let str = context.resetSetting.getString()
background.print(0, color: color, column: 16, row: 6, string: str)
}
func search(column: Int, row: Int) -> EnEvent {
var event: EnEvent = .None
for i in 0 ..< table_search.count {
let t = table_search[i]
if (t.column0 <= column && t.row0 <= row) && (t.column1 >= column && t.row1 >= row) {
event = t.event
break
}
}
return event
}
}
handelEventメソッドで Touch のイベントを受信したら、search関数で押された範囲をチェックして該当していれば、そのシーケンスの case を実行する。
設定値は CgContextクラスで定義しておく。
class CgContext {
enum EnOperationMode: Int {
case Swipe = 0, Touch, Accel
func getString() -> String {
switch self {
case .Swipe: return "(SWIPE)"
case .Touch: return "(TOUCH)"
case .Accel: return "(ACCEL)"
}
}
func getNext() -> EnOperationMode {
switch self {
case .Swipe: return .Touch
case .Touch: return .Accel
case .Accel: return .Swipe
}
}
}
enum EnLanguage: Int {
case English = 0, Japanese
func getString() -> String {
switch self {
case .English: return "(ENGLISH) "
case .Japanese: return "(JAPANESE)"
}
}
func getNext() -> EnLanguage {
switch self {
case .English: return .Japanese
case .Japanese: return .English
}
}
}
enum EnOnOff: Int {
case Off = 0, On
func getString() -> String {
switch self {
case .On: return "(ON) "
case .Off: return "(OFF)"
}
}
func getNext() -> EnOnOff {
switch self {
case .On: return .Off
case .Off: return .On
}
}
}
enum EnSetting: Int {
case Clear = 0, Keep
func getString() -> String {
switch self {
case .Clear: return "(CLEAR)"
case .Keep: return "(KEEP) "
}
}
func getNext() -> EnSetting {
switch self {
case .Clear: return .Keep
case .Keep: return .Clear
}
}
}
var operationMode: EnOperationMode = .Swipe
var extraMode: EnOnOff = .Off
var debugMode: EnOnOff = .Off
var resetSetting: EnSetting = .Clear
var language: EnLanguage = .Japanese
// 以下、省略
加速度センサーによる操作
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()
// MotionManager for accel
private var motionManager: CMMotionManager!
override func didMove(to view: SKView) {
// Create and start game sequence.
gameMain = CgGameMain(skscene: self)
gameMain.startSequence()
// Create motion manager
motionManager = CMMotionManager()
motionManager.accelerometerUpdateInterval = 0.05
motionManager.startAccelerometerUpdates(
to: OperationQueue.current!, withHandler: {
(accelData: CMAccelerometerData?, errorOC: Error?) in self.sendAccelEvent(acceleration: accelData!.acceleration)
}
)
}
func sendAccelEvent(acceleration: CMAcceleration){
let x_diff: Int = Int(acceleration.x * 100)
let y_diff: Int = Int(acceleration.y * 100)
if abs(x_diff) > abs(y_diff) {
gameMain.sendEvent(message: .Accel, parameter: [Int(x_diff > 0 ? EnDirection.Right.rawValue : EnDirection.Left.rawValue)])
} else {
gameMain.sendEvent(message: .Accel, parameter: [Int(y_diff > 0 ? EnDirection.Up.rawValue : EnDirection.Down.rawValue)])
}
}
GameSceneクラスで CMMotionManager を生成し、値を取得する周期を accelerometerUpdateIntervalメンバに設定、
コールバック・メソッドに sendAccelEvent を設定しておく。
sendAccelEvent メソッドは 0.05s周期で呼ばれ、この中で加速度センサーの傾きから方向を算出し、スワイプ操作と同様に gameMain.sendEvent でオブジェクトにイベント送信する。
class CgPlayer : CgActor {
/// Event handler
/// - Parameters:
/// - sender: Message sender
/// - id: Message ID
/// - values: Parameters of message
override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
guard !deligateActor.isDemoMode() else { return }
switch message {
case .Accel where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Accel): fallthrough
case .Swipe where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Swipe):
if let direction = EnDirection(rawValue: values[0]) {
targetDirecition = direction
}
case .Touch where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Touch):
setTargetPosition(x: values[0], y: values[1])
targetDirecition = decideDirectionByTarget(forcedDirectionChange: true)
position.amountMoved = 0
default:
break
}
}
CgPlayerクラスの handleEvent の中で、コンフィグレーション・メニューで設定した操作が有効ならば(deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Accel))、加速度センサーのイベントを受け付けるようにする。
こちらは以前の、【入門】iOS アプリ開発 #6【キャラクタの操作】を参照してほしい。
オリジナル迷路の追加
CgSceneMazeクラスの getMazeSourceメソッドを、メニューの値で迷路データを切り替えるようにする。この迷路データの作りは古典的だが意外と簡単。
func getMazeSource() -> [String] {
let mazeSource: [String] = [
"aggggggggggggjiggggggggggggb",
"e111111111111EF111111111111f",
"e1AGGB1AGGGB1EF1AGGGB1AGGB1f",
"e3E F1E F1EF1E F1E F3f",
"e1CHHD1CHHHD1CD1CHHHD1CHHD1f",
"e11111111111111111111111111f",
"e1AGGB1AB1AGGGGGGB1AB1AGGB1f",
"e1CHHD1EF1CHHJIHHD1EF1CHHD1f",
"e111111EF1111EF1111EF111111f",
"chhhhB1EKGGB1EF1AGGLF1Ahhhhd",
" e1EIHHD2CD2CHHJF1f ",
" e1EF EF1f ",
" e1EF QhUWWVhR EF1f ",
"gggggD1CD f e CD1Cggggg",
"____ 1 f e 1 ____" ,
"hhhhhB1AB f e AB1Ahhhhh",
" e1EF SggggggT EF1f ",
" e1EF EF1f ",
" e1EF AGGGGGGB EF1f ",
"aggggD1CD1CHHJIHHD1CD1Cggggb",
"e111111111111EF111111111111f",
"e1AGGB1AGGGB1EF1AGGGB1AGGB1f",
"e1CHJF1CHHHD2CD2CHHHD1EIHD1f",
"e311EF1111111 1111111EF113f",
"kGB1EF1AB1AGGGGGGB1AB1EF1AGl",
"YHD1CD1EF1CHHJIHHD1EF1CD1CHZ",
"e111111EF1111EF1111EF111111f",
"e1AGGGGLKGGB1EF1AGGLKGGGGB1f",
"e1CHHHHHHHHD1CD1CHHHHHHHHD1f",
"e11111111111111111111111111f",
"chhhhhhhhhhhhhhhhhhhhhhhhhhd"
]
let mazeSourceExtra1: [String] = [
"aggggjiggggggjiggggggjiggggb",
"e1111EF111111EF111111EF1111f",
"e1AB1EF1AGGB1CD1AGGB1EF1AB1f",
"e3EF1EF1E F1111E F1EF1EF3f",
"e1CD1CD1CHHD1AB1CHHD1CD1CD1f",
"e111111111111EF111111111111f",
"kGGB1AGGB1AGGLKGGB1AGGB1AGGl",
"YHJF1EIHD1CHHJIHHD1CHJF1EIHZ",
"e1EF1EF111111EF111111EF1EF1f",
"e1EF1EKGGGGB1EF1AGGGGLF1EF1f",
"e1CD1CHHHHHD2CD2CHHHHHD1CD1f",
"e11111111 11111111f",
"e1AB1AGGB QhUWWVhR AGGB1AB1f",
"e1EF1CHHD f e CHHD1EF1f",
"e1EF11111 f e 11111EF1f",
"kGLF1AGGB f e AGGB1EKGl",
"YHHD1EIHD SggggggT CHJF1CHHZ",
"e1111EF11 11EF1111f",
"e1AGGLF1AGGGGGGGGGGB1EKGGB1f",
"e1CHHJF1CHHHHJIHHHHD1EIHHD1f",
"e1111EF111111EF111111EF1111f",
"kGGB1EKGGGGB1EF1AGGGGLF1AGGl",
"YHHD1CHHHHHD2CD2CHHHHHD1CHHZ",
"e111111111111 111111111111f",
"e1AGGGB1AGGGGGGGGGGB1AGGGB1f",
"e1CHHJF1CHHHHJIHHHHD1EIHHD1f",
"e3111EF111111EF111111EF1113f",
"kGGB1EF1AB1AGLKGB1AB1EF1AGGl",
"YHHD1CD1EF1CHHHHD1EF1CD1CHHZ",
"e1111111EF11111111EF1111111f",
"chhhhhhhnmhhhhhhhhnmhhhhhhhd"
]
return context.extraMode == CgContext.EnOnOff.Off ? mazeSource : mazeSourceExtra1
}
まとめ
今回パックマン・ゲームのアレンジとして以下を追加した。
- コンフィグレーション・メニュー
- 加速度センサー、タッチ操作
- オリジナル迷路
最後に
パックマンのゲームを題材にして、SwiftによるiOSプログラミングを勉強してきた。
ソースコードはコメント含む 5000行程度で、アーケードゲームに相当するクオリティとアレンジを簡単に実現することができた。
コロナ禍のどこに行けない夏休みから始まったが、また一つプログラミングの面白さを味わう良い機会ともなった。
全11回のiOSアプリ開発入門は、これにて終了。
<読んでいただいた方、ありがとうございました〜>