【入門】iOS アプリ開発　#8【スペシャルターゲット・得点表示】

# はじめに

今回はスペシャルターゲット（くだもの類）、パックマンがそれを食べた時やゴーストを噛み付いた時の得点表示を作成していく。

完成図は下記の通り。スペシャルターゲット（サクランボ表示）と、それをパックマンが食べると得点が表示される。

![Image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/ccec0087-7ddb-a262-c4ae-662ad6389fae.png)


# 仕様書

まずはスペシャルターゲットの仕様の確認。

![Image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/996dbef8-f014-0308-a8bf-6c6f267cc4eb.png)

表示されている間にパックマンが食べると、得点が2秒だけ表示される（仕様にはないが実機確認）。

続いてパックマンがゴーストを噛み付いた時の得点表示の仕様は以下の通り。

![Image51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/7dde36db-e2b1-9b1b-791d-46e0bbeb856d.png)

こちらの得点表示は1秒間だけ（同じく仕様にはないが実機確認）。

# 設計方針

スペシャルターゲットは、パックマンがエサを70個、170個食べた時に、

```swift
specialTarget.start(kind: .Cherry)
```

とするとサクランボのスペシャルターゲットが表示され、10秒後に自動的に消えるように設計したい。

表示されている間にパックマンが食べると 100 というように得点表示する。表示の時間は２種類（２秒、１秒）あるため、得点の種類や位置に加えて時間指定も行えるようにする。

```swift
ptsManager.start(kind: .pts100, position: specialTarget.position, interval: 2000)  // 2000ms
```

といった具合にする。
得点表示は、スペシャルターゲットとゴースト４匹を連続で噛みつくとして、最大５つまで同時表示できるように設計しておきたい（実際はゴーストをかみついている間はパックマンの動きが停止するため、実質２つまでとなる）。

# クラス構造

スペシャルターゲットと得点表示は、CgActorクラスを継承して作成する。

![Image62.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/4d96e4ce-e605-c542-19d2-428b1d6b6f17.png)

得点表示は CgScorePtsクラスとして、これを最大５つ表示する管理クラス CgScorePtsManager を作成する。

# CgSpecialTarget クラス

スペシャルターゲット（くだもの類）を表示する。

```swift
class CgSpecialTarget : CgActor {

    enum EnSpecialTarget: Int {
        case Cherry
        case Strawberry
        case Orange
        case Apple
        case Melon
        case Galaxian
        case Bell
        case Key
        case None

        func getScorePts() -> CgScorePts.EnScorePts {
            switch self {
                case .Cherry:     return .pts100
                case .Strawberry: return .pts300
                case .Orange:     return .pts500
                case .Apple:      return .pts700
                case .Melon:      return .pts1000
                case .Galaxian:   return .pts2000
                case .Bell:       return .pts3000
                case .Key:        return .pts5000
                case .None:       return .pts0
            }
        }

        func getTexture() -> Int {
            switch self {
                case .Cherry:     return 16*3+2
                case .Strawberry: return 16*3+3
                case .Orange:     return 16*3+4
                case .Apple:      return 16*3+5
                case .Melon:      return 16*3+6
                case .Galaxian:   return 16*3+7
                case .Bell:       return 16*3+8
                case .Key:        return 16*3+9
                case .None:       return 16*3+10
            }
        }
    }

    private var kindOfSpecialTarget: EnSpecialTarget = .None
    private var timer_disappearSpecialTarget: CbTimer!

    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        timer_disappearSpecialTarget = CbTimer(binding: self)
        actor = .SpecialTarget
        sprite_number = actor.getSpriteNumber()
    }

    // ============================================================
    //   Core operation methods for actor
    //  - Sequence: reset()->start()->update() called->stop()
    // ============================================================

    /// Reset special target state.
    override func reset() {
        super.reset()
        timer_disappearSpecialTarget.set(interval: 10000) // 10s
        timer_disappearSpecialTarget.reset()
        position.set(column: 13, row: 15, dx: 4)
    }
    
    /// Start to draw special target at the specified position.
    override func start() {
        super.start()
        timer_disappearSpecialTarget.start()
        deligateActor.setTile(column: position.column, row: position.row, value: .Fruit)
        sprite.draw(sprite_number, x: position.x, y: position.y, texture: kindOfSpecialTarget.getTexture())
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        if timer_disappearSpecialTarget.isEventFired() {
            stop()
        }
    }
    
    /// Stop drawing special target.
    override func stop() {
        super.stop()
        timer_disappearSpecialTarget.stop()
        deligateActor.setTile(column: position.column, row: position.row, value: .Road)
        sprite.clear(sprite_number)
    }

    // ============================================================
    //  General methods in this class
    // ============================================================

    func start(kind: EnSpecialTarget) {
        kindOfSpecialTarget = kind
        self.start()
    }
}
```

コンストラクタで CbTimer を生成して、reset() メソッドで 10秒を設定しておく。
start()メソッドが呼ばれたら、スプライトを表示しタイマーを起動させる。
update()メソッドがフレーム毎に呼ばれる中でタイマーが 0 になったら、内部のstop()メソッドを呼びスプライトを消去する。

簡単なコードとなった。

# CgScorePts クラス

得点クラスはスペシャルターゲットとほぼ同様のコードとなる。1000点以上はスプライトを２つ使って表示する。

```swift
class CgScorePts : CgActor {

    enum EnScorePts: Int {
        case pts100 = 0
        case pts200
        case pts300
        case pts400
        case pts500
        case pts700
        case pts800
        case pts1000
        case pts1600
        case pts2000
        case pts3000
        case pts5000
        case pts0

        func getScore() -> Int {
            switch self {
                case .pts100  : return 100
                case .pts200  : return 200
                case .pts300  : return 300
                case .pts400  : return 400
                case .pts500  : return 500
                case .pts700  : return 700
                case .pts800  : return 800
                case .pts1000 : return 1000
                case .pts1600 : return 1600
                case .pts2000 : return 2000
                case .pts3000 : return 3000
                case .pts5000 : return 5000
                case .pts0    : return 0
            }
        }

        func get2times() -> EnScorePts {
            switch self {
                case .pts100  : return .pts200
                case .pts200  : return .pts400
                case .pts400  : return .pts800
                case .pts800  : return .pts1600
                default : return self
            }
        }
        
        func getTextures() -> (Int, Int) {
            switch self {
                case .pts100  : return (16*9   , 0)
                case .pts200  : return (16*8   , 0)
                case .pts300  : return (16*9+1 , 0)
                case .pts400  : return (16*8+1 , 0)
                case .pts500  : return (16*9+2 , 0)
                case .pts700  : return (16*9+3 , 0)
                case .pts800  : return (16*8+2 , 0)
                case .pts1000 : return (16*9+4 , 16*9+5)
                case .pts1600 : return (16*8+3 , 0)
                case .pts2000 : return (16*10+4, 16*10+5)
                case .pts3000 : return (16*11+4, 16*11+5)
                case .pts5000 : return (16*12+4, 16*12+5)
                case .pts0    : return (16*9   , 0)     //
            }
        }
    }

    private var ptsNumber: EnScorePts = .pts0
    private var timer_disappearPts: CbTimer!

    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        timer_disappearPts = CbTimer(binding: self)
        actor = .Pts
    }
        
    // ============================================================
    //   Core operation methods for actor
    //  - Sequence: reset()->start()->update() called->stop()
    // ============================================================
    
    /// Reset
    override func reset() {
        super.reset()
    }
    
    /// Start
    override func start() {
        super.start()
        timer_disappearPts.start()

        let textures: (Int,Int) = ptsNumber.getTextures()
        sprite.draw(sprite_number, x: position.x, y: position.y, texture: textures.0)
        if  textures.1 != 0 {
            sprite.draw(sprite_number+1, x: position.x+16, y: position.y, texture: textures.1)
        }
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        if timer_disappearPts.isEventFired() {
            stop()
        }
    }
    
    /// Stop
    override func stop() {
        super.stop()
        timer_disappearPts.reset()

        sprite.clear(sprite_number)
        if ptsNumber.getTextures().1 != 0 {
            sprite.clear(sprite_number+1)
        }
    }
    
    // ============================================================
    //  General methods in this class
    // ============================================================

    func start(kind: EnScorePts, position at: CgPosition, interval time: Int) {
        ptsNumber = kind
        timer_disappearPts.set(interval: time)
        self.position.set(at)
        start()
    }
    
}

```

# CgScorePtsManager クラス

CgScorePts のオブジェクトを５つ管理して表示の制御を行う。

```swift
class CgScorePtsManager: CbContainer {

    private let firstSpriteNumber: Int = CgActor.EnActor.Pts.getSpriteNumber()
    private let numberOfActors: Int = 5
    
    private var actors: [CgScorePts] = []
    private var doings: [CgScorePts] = []

    init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object)
        for i in 0 ..< numberOfActors {
            let actor: CgScorePts = CgScorePts(binding: object, deligateActor: deligateActor)
            actor.sprite_number = firstSpriteNumber+i*2
            actors.append(actor)
        }
    }
    
    /// Reset
    func reset() {
        for each in actors {
            each.reset()
        }
        self.enabled = false
    }
    
    /// Start to draw Pts
    /// - Parameters:
    ///   - kind: Kind of pts
    ///   - position: Position to draw
    ///   - time: Time to disappear
    func start(kind: CgScorePts.EnScorePts, position: CgPosition, interval time: Int) {
        let actor: CgScorePts

        if actors.count == 0 {
            actor = doings.remove(at: 0)
            actor.stop()
        } else {
            actor = actors.remove(at: 0)
        }
        actor.start(kind: kind, position: position, interval: time)
        doings.append(actor)
        self.enabled = true
    }
    
    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        for each in doings {
            if !each.enabled {
                actors.append(each)
                doings.remove(at: 0)
            }
        }

        if doings.count == 0 {
            enabled = false
        }
    }
    
    /// Stop
    func stop() {
        for each in doings {
            each.stop()
            actors.append(each)
            doings.remove(at: 0)
        }
        self.enabled = false
    }

}
```

コンストラクタで CgScorePtsオブジェクトを５つ生成して actors配列に格納しておく。
start()メソッドが呼ばれた時に、actors配列から CgScorePtsオブジェクトを取り出して start()し、doings配列へ移動しておく。enabled = true にすると、フレーム毎に update()が呼ばる。
update()メソッド内では、doings配列にある CgScorePtsオブジェクトが stop(enabled= false) していたら、doings 配列から actors へ戻しておく。

start()メソッドが呼ばれた時に５つ全部表示中で actors配列から取り出されなかった場合は、doings から１つ持ってきて使いまわす。

# まとめ

スペシャルターゲットと得点表示のクラスを作成した。作成したソースコードは合計300行程度。

必要な役者はそろったので、次回はこれら役者のシーケンス動作を組み合わせて、ゲームとしてプレイできるものを作成していく。

# 次の記事
[【入門】iOS アプリ開発　#9【ゲームの状態遷移とシーケンス動作】]
(https://qiita.com/KIKU_CHU/items/6b16a2b8970513b42afe)