1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【入門】iOS アプリ開発 #6【キャラクタの操作】

Last updated at Posted at 2020-08-22

はじめに

今回はパックマンのキャラクタ操作を作成する。操作はスワイプで行い、以下が完成動画。ソースコードは GitHub に公開しているので参照してほしい。

※YouTube動画
IMAGE ALT TEXT HERE

仕様書

注目すべきところはスピードレベルの仕様。単に1ドットずつ動かすなら簡単だが、微妙なスピード調整の仕様があり、ゲームの難易度を調整している。

Spec1.png

エサを食べる、食べない、パワーエサを食べた状態などで、移動スピードが異なる。仕様段階でここまで定義していたとは奥深い。

Spec3.png

「スピードの数字は、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)

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?