LoginSignup
5
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-24

今回のゴール

  • これからのことを考えてキャラクターをクラス化する
  • ついでにステータスを表示してみる
  • 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:

5
8
2

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
5
8