0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[SwiftUI] 公式ドキュメントの FoundationModel を使った CoffeeShop ゲームを再現してみる - その4

Last updated at Posted at 2025-09-25

公式ドキュメントからダウンロードできる CoffeeShop ゲームのサンプルコードを読み解いて、自分でも実際に作ってどの程度正確に動作するかも確認してみたいと思います。最終的にはそれを使って類似するゲームをリリースするところまで実践していければと思います。

独学・初心者のため、コードの見解など間違っている箇所があれば勉強になりますのでコメントをいただけると幸いです。

前回までの内容↓

その都度ファイルごとに勉強し、まとめていきます。

今回は GameScene ファイルの前に、そこで表示させる プレイヤー と 静的AIキャラクター 表示と移動制御などについてのファイルを読んでいきます。


PlayerSprite.swift ファイル

class PlayerSprite: SKSpriteNode {
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        texture?.filteringMode = .nearest  // ピクセルアート用設定
    }
}

基本的な構造はこんな感じ。

texture?.filteringMode = .nearest

↑ この箇所は、今回は画像には用意したイラストを使っているので、.nearest を使ってアンチエイリアシングを無効にしてピクセルアート最適化させるための設定。

次に移動のアルゴリズムについて↓

func movePlayer(_ location: CGPoint) {
        // プレイヤーのスプライトをタッチポイントに向かって移動します。
        let step = 5.0 // 1フレームあたりの最大移動距離
        // X軸の移動計算とY軸の移動計算
        let xDist = CGFloat(min(abs(position.x - location.x), step))
        let yDist = CGFloat(min(abs(position.y - location.y), step))
        let xDir = position.x > location.x ? -1.0 : 1.0
        let yDir = position.y > location.y ? -1.0 : 1.0
        // 実際の移動適用
        position.x += CGFloat(xDist * xDir)
        position.y += CGFloat(yDist * yDir)
        // 深度ソート(Y座標に基づく表示順序)
        zPosition = position.y * -1.0
    }

段階移動の仕組みとして step = 5.0 を定数として用意。これを使って瞬間移動を防ぐ速度制限を設定。
min(距離, step) で目標まで5.0以下なら実距離、それ以上なら5.0。方向判定は目標が右なら +1、左なら -1 というように設定している。

zPosition = position.y * -1.0

Y座標が低い(下)ほどzPositionが高く → 手前に表示させて2Dゲームの奥行き感を演出するねらいみたい。

コード全体がこちら ↓

import SpriteKit
// CoffeeShopScene に表示されるプレイヤーキャラクターの移動制御
class PlayerSprite: SKSpriteNode {
    
    required init?(coder aDecoder: NSCoder) {
        super .init(coder: aDecoder)
        // 鮮明なピクセルアートを実現するためにアンチエイリアシングを行わない
        texture?.filteringMode = .nearest
    }
    
    func movePlayer(_ location: CGPoint) {
        // プレイヤーのスプライトをタッチポイントに向かって移動します。
        let step = 5.0 // 1フレームあたりの最大移動距離
        // X軸の移動計算とY軸の移動計算
        let xDist = CGFloat(min(abs(position.x - location.x), step))
        let yDist = CGFloat(min(abs(position.y - location.y), step))
        let xDir = position.x > location.x ? -1.0 : 1.0
        let yDir = position.y > location.y ? -1.0 : 1.0
        // 実際の移動適用
        position.x += CGFloat(xDist * xDir)
        position.y += CGFloat(yDist * yDir)
        // 深度ソート(Y座標に基づく表示順序)
        zPosition = position.y * -1.0
    }
}

CharacterSprite.swift ファイル

ここからは静的キャラクターのファイルについて↓

class CharacterSprite: SKSpriteNode {
    // SwiftUI会話UIへのコールバック
    internal var showDialog: ((any Character) -> Void)?
    
    // プレイヤーとの距離判定コールバック
    internal var checkProximity: ((CGPoint) -> Bool)?
    
    // このスプライトが表現するキャラクター
    private var character: (any Character)?

内容としては、

  • SpriteKit ↔ SwiftUI 連携。
  • showDialog : タップ時にGameViewの会話開始を呼び出し。
  • checkProximity : プレイヤーが近くにいるかチェック。

初期化とピクセルアートの設定↓

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    character = createCharacter()
    isUserInteractionEnabled = true
    texture?.filteringMode = .nearest  // ピクセルアート用設定
}

PlayerSprite と同様に、画像には用意したイラストを使うので .nearest を使ってアンチエイリアシングを無効にしてピクセルアート最適化を設定。

会話開始ロジックを用意↓

// 会話開始ロジック
    func startDialog() {
        if let showDialog, let character, let checkProximity {
            if checkProximity(position) { // 距離チェック
                showDialog(character)     // 会話開始
            }
        }
    }

会話のログ表示のため Player との距離をチェック。近ければ会話を開始する仕組み。ゲームでいうエンカウントってやつですね。
確かに Game 的にもめちゃくちゃ離れた距離のモブとの会話が始まったら不自然なので(なんなら怖い)関数を用意して必要な箇所で呼び出して使う感じ。

静的キャラクタースプライト ↓

// 静的キャラクタースプライト
class BaristaSprite: CharacterSprite {
    override func createCharacter() -> any Character {
        Barista() // 固定のBaristaキャラクター
    }
}

class CustomerLisaSprite: CharacterSprite {
    override func createCharacter() -> any Character {
        CustomerLisa() // 固定のLisaキャラクター
    }
}

AI生成のキャラクタースプライトも用意 ↓

// AI生成キャラクタースプライト
class GeneratedCustomerSprite: CharacterSprite {
    let character: any Character
    init(character: any Character) {
        self.character = character
        super.init(
            texture: SKTexture(imageNamed: "Customer2"),
            size: CGSize(width: 76.5, height: 180)
        )
        // 鮮明なピクセルアートを実現するためにアンチエイリアシングを行わない
        texture?.filteringMode = .nearest
    }
    
    @MainActor required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func createCharacter() -> any Character {
        character // 外部から渡されたAI生成キャラクター
    }
}

外部からGeneratedCustomerを受け取り、生成したAIキャラクターには固定テクスチャ "Customer2" を使用する。これは元のファイルに Image が入っていたので再現時もそのまま使わせてもらおう。

これらの設計により、静的キャラクターとAI生成キャラクターを統一的に扱いながら、SpriteKit の視覚表現と SwiftUI の会話システムを連携させています。

次にプラットフォーム対応操作 ↓

#if os(iOS)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    startDialog()
}
#elseif os(macOS)
override func mouseDown(with event: NSEvent) {
    startDialog()
}
#endif

クロスプラットフォームの対応として、iOS はタッチ操作、macOS はマウスクリックに設定。

コード全体はこんな感じ ↓

import SpriteKit
// CoffeeShopScene に表示されるキャラクタースプライト画像のビューモデル
class CharacterSprite: SKSpriteNode {
    // 実際のダイアログを画面に表示するSwiftUIコードへのコールバック
    internal var showDialog: ((any Character) -> Void)?
    // プレイヤーがこのキャラクターと対話できるほど近いかどうかを確認するコールバック
    internal var checkProximity: ((CGPoint) -> Bool)?
    // キャラクタープロファイルをこのビューモデルに接続します
    private var character: (any Character)?
    
    //  初期化とピクセルアート設定
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        character = createCharacter()
        isUserInteractionEnabled = true
        // 鮮明なピクセルアートを実現するためにアンチエイリアシングを行わない
        texture?.filteringMode = .nearest
        
    }
    
    init(texture: SKTexture?, size: CGSize) {
        super.init(texture: texture, color: .clear, size: size)
        character = createCharacter()
        isUserInteractionEnabled = true
    }
    
    func createCharacter() -> any Character {
        // バリス​​タはデフォルトのキャラクターです
        return Barista()
    }
    
#if os(iOS)
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        startDialog()
    }
#elseif os(macOS)
    override func mouseDown(with event: NSEvent) {
        startDialog()
    }
#endif
    
    // 会話開始ロジック
    func startDialog() {
        if let showDialog, let character, let checkProximity {
            if checkProximity(position) { // 距離チェック
                showDialog(character)     // 会話開始
            }
        }
    }
}

// 静的キャラクタースプライト
class BaristaSprite: CharacterSprite {
    override func createCharacter() -> any Character {
        Barista() // 固定のBaristaキャラクター
    }
}

class CustomerLisaSprite: CharacterSprite {
    override func createCharacter() -> any Character {
        CustomerLisa() // 固定のLisaキャラクター
    }
}

// AI生成キャラクタースプライト
class GeneratedCustomerSprite: CharacterSprite {
    let character: any Character
    init(character: any Character) {
        self.character = character
        super.init(
            texture: SKTexture(imageNamed: "Customer2"),
            size: CGSize(width: 76.5, height: 180)
        )
        // 鮮明なピクセルアートを実現するためにアンチエイリアシングを行わない
        texture?.filteringMode = .nearest
    }
    
    @MainActor required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func createCharacter() -> any Character {
        character // 外部から渡されたAI生成キャラクター
    }
}

とりあえず今回はここまで。
書き方・解釈に間違いがあれば勉強になりますのでぜひコメントお願いします!

引き続きAIApp開発に向けて勉強した内容をまとめつつ記録していきたいと思います。

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?