Edited at

【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: