Xcode
iOS
ゲーム制作
Swift

【Swift】iOSで放置型育成ゲームを作るよ(3) ~キャラクターをクラス化してViewControllerから分離する~

More than 1 year has passed since last update.

今回のゴール

  • これからのことを考えてキャラクターをクラス化する
  • ついでにステータスを表示してみる
  • ViewControllerから生成の処理を剥がす

status.gif

今回のキーワード

  • クラス設計
  • プロパティ
  • initialize
  • ステータス表現

やったこと

なんかちょっとリファクタしたくなった

  • ViewControllerでやってる処理を整理したよ
    • せっかくだしオブジェクト指向でプログラミングしよう
    • キャラ用の画像を生成してUIImageViewに当て込んでTimerで定期的に動かしているけど、キャラ画像の生成とImageに当て込むのはキャラクターのクラス作ってそいつにもたせた方がよくね?
    • ついでにViewControllerにキャラをプロパティとしてもたせた方がよくね?
ViewController.swift
        // まずは素材画像からUIImageを宣言する
        let image = UIImage(named: "sozai.png")

        // 素材のimageからgifのURLが生成できたかをnilチェックする
        if let url = image?.createGifURL() {
            // urlが生成されていたらimageViewのimageに代入
            imageView.image = UIImage.gif(url: url.absoluteString)
        }

これを

        // これをこんな感じにした方がよくね?

        // キャラクタークラスからインスタンスを生成
        let character = Character(init処理)

        // ViewControllerのプロパティに設定する
        self.character = character

        // キャラのimageをViewControllerのimageViewにセットする
        self.imageView.image = self.character.image

  • ということでキャラクター生成用のクラスを作るよ

クラス設計とかしてみる

  • まずはキャラクターがもつべき要素を考えるよ
    • 色々考えちゃうと複雑になってしまうのでとりあえず最低限だけ
    • キャラクターとは
      • 名前(name)をもっている
      • gif画像の情報を持っている
      • 右向き・左向きのフラグを持っている
      • 攻撃力、防御力などのステータスをもっている

って感じに考えてたけど、gif画像をどこにもたせてやればいいかすごく悩んだよ(というか今も悩んでるよ)

imageの情報はどこでもつべきなんだ・・??

  1. CharacterクラスにUIImageViewをもたせる
    1. ViewControllerのViewDidLoad時にそのimageViewをaddSubViewする
  2. ViewControllerにUIImageViewをもたせる
    1. ViewDidLoadでImageViewのimageにキャラクターのimageをセットする ちょうど上のコードで書いているような感じ

この二つのやり方があるかなと思ったよ
1のやり方だとmoveとstartTimerもCharacterクラスに移動させてしまえば、キャラのインスタンスが「動く」メソッドを自動的に実行してくれる形にできるかなと思ったけど、左向き・右向きの判定のためにCharacterクラスにwindowのframeを教えてあげなきゃいけなくてめんどくさそうだったので2のやり方を採用するよ(多分このへん将来的に壁にぶち当たる気がするけど、今は動くことを優先するよ)

クラス設計を基にクラスを作成していくよ

  • File → New Fileを選択して「Character.swift」というファイルを作成するよ
  • こんな感じで書いていくよ
Character.swift
import Foundation
import UIKit

final class Character {
    private let name: String   // 生成したら基本変更しない方針なのでletで宣言
    private let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    var isLeft: Bool = true {
        willSet(newValue) {
           imageView?.transform =  newValue ? CGAffineTransform(scaleX: 1, y: 1) : CGAffineTransform(scaleX: -1, y: 1)
        }
    }

    // 後々APIを追加した時に、レスポンスがnilだったりすることを考慮してFailable Initializerにする
    init?(name: String?, charaImage: UIImage?) {
        guard let name = name, let charaImage = charaImage, let gifURL = charaImage.createGifURL()?.absoluteString, let image = UIImage.gif(url: gifURL) else {
            print("Error: Faild to Initialize Character")
            return nil
        }

        self.name = name
        self.image = image
    }
}

isLeftいる・・・??

  • ここまで書いてCharacterにisLeftのプロパティいらねんじゃねぇ・・?という疑問にぶちあたったよ
  • そもそもCharacterがimageViewをもたないのでimageView.transformのあたりで当たり前にコンパイルエラーが出るよ
  • てことはViewControllerで管理すればいいのかな・・?かな?
    • ViewのControllerだしとりあえずそれでいっか!の精神で先に進むよ
    • CharacterクラスのisLeftはとりあえず消すよ

こうなった

  • せっかく名前も設定したので表示用のUILabelをstoryboardにおいてIBOutletでつないで表示できるようにしたよ
ViewController.swift
class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var name: UILabel!
    private var timer = Timer()

    private var valkyrie: Character?

    private var isLeft: Bool = true {
        willSet(newValue) {
            // 新しい値がtrueだったら正しい方向に、falseだったら反転させる
            if newValue {
                imageView.transform = CGAffineTransform(scaleX: 1 , y: 1)
            } else {
                imageView.transform = CGAffineTransform(scaleX: -1 , y: 1)
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // キャラクターのインスタンスを生成してViewControllerのプロパティに設定
        valkyrie = Character(name: "【戦乙女】ヴァルキリー", charaImage: UIImage(named: "sozai.png"))

        // imageViewのimageにキャラのimageを設定
        imageView.image = valkyrie?.image
        name.text = valkyrie?.image

    }
~~~省略~~~

出面は全然変化ないけど、とりあえずでけた!
class.gif

なんか寂しいのでステータスのパラメーター決めて表示しようとしたけど沼にハマる

  • 名前だけだとなんか寂しいので仮でパラメーターを設定して表示しようと試みたよ

パラメータ表現どうしましょ問題

  • とりあえず攻撃力(Str)、防御力(Def)、運(Luc)を設定しよう
Character.swift
final class Character {
    let name: String   // 生成したら基本変更しない方針なのでletで宣言
    let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    let str: Int       // 攻撃力
    let def: Int       // 防御力
    let luc: Int       // 運
~~~省略~~~

こんな感じで設定してもよかったんだけど、なんかもっとSwiftyに書きたくてenumとか使おうとしたよ

Character.swift
final class Character {
    let name: String   // 生成したら基本変更しない方針なのでletで宣言
    let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    let status: Status

    // ステータスを表すenum
    enum Status {
        case Base(str: Int, def: Int, luc: Int)

        func all() -> (str: Int, def: Int, luc: Int) {
            switch self {
            case .Base(let str, let def, let luc):
                return (str, def, luc)
            }
        }
    }

    // 後々APIを追加した時に、レスポンスがnilだったりすることを考慮してFailable Initializerにする
    init?(name: String?, charaImage: UIImage?) {
        guard let name = name, let charaImage = charaImage, let gifURL = charaImage.createGifURL()?.absoluteString, let image = UIImage.gif(url: gifURL) else {
            print("Error: Faild to Initialize Character")
            return nil
        }

        self.name = name
        self.image = image
        self.status = Status.Base(str: 100, def: 100, luc: 100)
    }
}

うーん・・・ このステータス呼び出す側が使いにくいなあ・・・ これってどうなんだ・・??
let str = character.status.all().str
そもそもステータス表現にenumは合ってない気がするぞ・・・

タプルにするか

こうするとどうだろう

Character.swift
final class Character {
    let name: String   // 生成したら基本変更しない方針なのでletで宣言
    let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    let status: (str: Int, def: Int, luc: Int)


    // 後々APIを追加した時に、レスポンスがnilだったりすることを考慮してFailable Initializerにする
    init?(name: String?, charaImage: UIImage?) {
        guard let name = name, let charaImage = charaImage, let gifURL = charaImage.createGifURL()?.absoluteString, let image = UIImage.gif(url: gifURL) else {
            print("Error: Faild to Initialize Character")
            return nil
        }

        self.name = name
        self.image = image
        self.status = (str: 100, def: 100, luc: 100)
    }
}

こうすれば呼び出し側は分かりやすい気がするよ
let str = character.status.str

でも後々ステータスの数だけTableViewのセル作りたいとかなったら困るだろうなあ・・・
タプルの要素数だけ返すことってできるんかなあ・・・
なおかつ育成ゲームだしステータス上がることを考えればgetter, setterあった方がいいんだろうなあ・・・

・・・あああああああああああ

まあいっか動くし 将来の自分が綺麗に解決してくれることを祈ります:pray:
とりあえず表示用のラベルを3つ用意してそれぞれのtextに設定するよ

ViewController.swift
~~省略~~~
    @IBOutlet weak var strLabel: UILabel!
    @IBOutlet weak var defLabel: UILabel!    
    @IBOutlet weak var lucLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()

        // キャラクターのインスタンスを生成してViewControllerのプロパティに設定
        valkyrie = Character(name: "【戦乙女】ヴァルキリー", charaImage: UIImage(named: "sozai.png"))

        // imageViewのimageにキャラのimageを設定
        imageView.image = valkyrie?.image
        name.text = valkyrie?.name
        name.adjustsFontSizeToFitWidth = true

        strLabel.text = "攻撃力: \(String(describing: valkyrie!.status.str))"
        defLabel.text = "防御力: \(String(describing: valkyrie!.status.def))"
        lucLabel.text = "天運: \(String(describing: valkyrie!.status.luc))"
    }
~~~省略~~~

それっぽくなってきたな・・・?:thinking:
status.gif
(Forced unwrapしてるけど表示のためだけということでご愛嬌・・・)

結果

  • CharacterのinitをFailable Initializerにする意味あるんかな・・・??
    • エラーの時どうするか考えていかなければ・・・
  • ステータス表示すると途端にそれっぽくなるな:thinking:
  • さあ本格的にブログ案件になってきました
  • 今日の作業ブランチはこちら

次回

BGMと選択音を鳴らすよ

最後に

ご指摘・リファクタ大歓迎です:pray: