スマホアプリで一人回しがしたい(ついでにSwiftUIお勉強)その1

スマホで一人回しがしたい

どうも、湊です。
私はTCGでよく一人回しというのをします。*1

ただこの一人回し、自宅などカードを広げることが許されるスペースでしかできません(当然)。

どこでもやりたくない...?
スマホでやりたくない...?

と思ったのでアプリを作ることにしました。
趣味のツール作成と仕事の勉強(SwiftUI)ができて一石二鳥です。

題材

今一番練習したいのはLyceeなのでLyceeで作ってみることにします。
(盤面として表現する必要のあるものが少ないので作るのが楽そうという事情もあります。)

要件(ざっくり)

  • 盤面の情報表示
    • AF, DFに配置しているカード情報
    • 山札、ゴミ箱、○○置き場などの情報
    • 手札
  • 盤面の操作
    • ドロー, シャッフル, デッキを見る, デッキからカードを抜き出し手札に加える etc...
    • カードを盤面に配置する, 盤面のカードを移動する, キャラクターをタップする(攻撃など) etc...

まずは情報表示の部分をやっていこうと思います。

表示内容洗い出し

プログラムを作るにはゲームの要素をコードで表現する必要があります。
対象を洗い出していきましょう。

カード

f:id:minato_lotus:20201113231232p:plain
カード参考(かわいい)

リセのカードはこんな感じ。
今回の要件ですと、とりあえず盤面にカードを表示できればよさそうなので、一旦テキストは無視して画像だけ呼び出せるようにしてみます。
オブジェクトとしてはIDを持っていてそれに対応した画像を呼び出せれば十分そうなので以下のようにしました。

struct Card {
    let id: CardID
    private let imageName: String
    
    init(id: CardID) {
        self.id = id
        self.imageName = id.prefix + String(id.number) + (id.pallarelSuffix ?? "")
    }
}

struct CardID {
    let prefix = "LO-"
    let number: Int
    let pallarelSuffix: String?
    
    init?(number: Int, pallarel: String? = nil) {
        guard number > 0 else { return nil }
        self.number = number
        if let pallarel = pallarel {
            self.pallarelSuffix = "-\(pallarel)"
        } else {
            self.pallarelSuffix = nil
        }
    }
    
    var string: String {
        return prefix + String(format: "%04d", number) + (pallarelSuffix ?? "")
    }
}

盤面

Lyceeの盤面はこんな感じ

f:id:minato_lotus:20201113162627p:plain
盤面
キャラクターなどのカードを置く枠が6つ、デッキやゴミ箱を置く枠があります。
この画像には現れていませんが、その他「横」と呼ばれる領域や「〇〇置き場」という領域が利用できます。
(実際の対戦では左の部分に置いている方が多いと思います)

キャラクターなどのカードを配置する枠 (6枠)

紹介していませんが、リセのカード種類には4種類(キャラクター、イベント、アイテム、エリア)があります。
そしてこの6枠にはキャラクター、アイテム、エリアがそれぞれ一枚ずつしか配置できません。*2

というわけでこのように表現してみました。

struct Field {
    var character: Card?
    var characterState: CharacterState = .stand
    var item: Card?
    var area: Card?
    
    enum CharacterState {
        case stand, rest
    }
}

紹介していませんでしたが、キャラクターは縦向き、横向きの状態で配置されることがあり、CharacterStateはそれを表現しています。

盤面全体

上記キャラクターなどを配置する6枠に加え、デッキ、ゴミ箱、「横」、「〇〇置き場」などをプレイヤーの管理領域として表現するならこのような形でしょうか

struct PlayerDomain {
    var fields = Fields()
    var deck = Deck()
    var trashBox = TrashBox()
    var somePlaces = [Place]()
    var hand = Hand()
}

struct Fields {
    var leftAF = Field()
    var centerAF = Field()
    var rightAF = Field()
    var leftDF = Field()
    var centerDF = Field()
    var rightDF = Field()
}

struct Deck {
    var cards = [Card]()
}

struct TrashBox {
    var cards = [Card]()
}

struct Hand {
    var cards = [Card]()
}

struct Place {
    let name: String
    var cards = [Card]()
}

粗はありそうですが、これでカードや盤面の情報を表現できました。
これをSwiftUIを用いて画面に表示していきます。

表示の実装

キャラクターを配置する枠

まずはキャラクターを枠に配置できるようにしましょう。
また、1枠にはキャラクターやアイテム、エリアが配置されうるため、複数のものを配置している場合はすべてが視認できるようにずらして配置してあげるのが良さそうですね。
こんな感じにしてみました。

struct FieldView: View {
    var field: Field
    
    var body: some View {
        GeometryReader { geometry in
            let fieldWidth = min(geometry.size.width, geometry.size.height)
            let areaOffset = CGSize(width: fieldWidth / 16, height: fieldWidth / 10)
            let itemOffset = CGSize(width: fieldWidth / 8, height: fieldWidth / 6)
            
            ZStack {
                if let area = field.area {
                    Image(area.imageName)
                        .resizable()
                        .scaledToFit()
                        .offset(field.character != nil ? areaOffset : .zero)
                }
                if let character = field.character {
                    Image(character.imageName)
                        .resizable()
                        .rotationEffect(.init(degrees: field.characterState == .rest ? 270 : 0))
                        .scaledToFit()
                    if let item = field.item {
                        Image(item.imageName)
                            .resizable()
                            .offset(itemOffset)
                            .scaledToFit()
                    }
                }
            }
            .frame(width: fieldWidth, height: fieldWidth)
        }
    }
}

プレビューするとこんな感じです。

f:id:minato_lotus:20201114004508p:plain
Preview

あとはこれを複数個並べたらキャラクターなどを配置する枠は出来上がりそうです。

次回

長くなってきたので今回はここまでにします。
②以降で他のViewの構築や操作周りの実装をしていきます。
SwiftUIは不慣れでまだ探り探りですので、ご指摘・ご質問等歓迎です。


各種画像はLycee公式サイトから拝借しました。

LYCEE OVERTURE TRADING CARD GAME || リセ オーバーチュア トレーディングカードゲーム

*1:一人回し:一人で2つのデッキを使って練習すること

*2:正確にはアイテムはキャラクターに装備させるものなので、キャラクターがいる状態でしか配置できません。なのでプログラムとしてもキャラクターがアイテムを持つことを表現できるとよさそうです。ただ実現したいことに対して費用対効果が高いと判断し今回はかんたんにしています。