top of page
検索

iOS Swift におけるオンデバイス AI と NLP を活用したゲームの没入感向上

  • Matt
  • 7 日前
  • 読了時間: 6分

ChatGPT などのコード作成を支援するツールについて、多くの議論が交わされています。これらのツールは非常に便利ですが、本記事では AI を活用してアプリやゲームの没入感を向上させる方法を紹介します。


また、これを完全にオンデバイスで実現できることも示したいと思います。OpenAI のようなサードパーティのサービスは高度な AI 機能を提供していますが、最新の OS に搭載された AI 機能もますます強力になっています。現在、多くのユースケースがオンデバイスで処理可能です。本記事では、iOS と SwiftUI を用いた具体例を紹介します。コードをすぐに見たい方は、こちらをクリックしてください


AI を活用したテキストベースのアドベンチャーゲームの強化


今回の例では、テキストベースのアドベンチャーゲームを取り上げます。従来のポイント&クリック型アドベンチャーゲームと同様に、プレイヤーには複数の選択肢が提示され、その中から 1 つを選ぶ形式です。このシステムは何十年にもわたって機能し、プレイヤーに選択の自由を提供し、ゲームの進行を左右する要素として機能してきました。選択の自由があることで、没入感とエンゲージメントが向上します。


しかし、従来の UI には制限があります。画面サイズの制約により、事前に定義された選択肢しか提示できず、通常は 3 ~ 4 つの選択肢に制限されてしまいます。





従来のアプローチ


探偵ゲームをプレイしていて、駅の犯罪現場を捜査していると想像してください。従来のゲームでは、以下のような固定の選択肢が提示されるでしょう。

  1. 線路を調べる

  2. 列車の車両を調べる

  3. チケット売り場へ行く


このシステムは機能しますが、プレイヤーの自由度を制限してしまいます。


より没入感のある AI 駆動型アプローチ


AI を活用することで、よりダイナミックな体験を実現できます。事前に決められた選択肢ではなく、プレイヤーが自由に行動を入力できるようになります。ゲーム内部では、プレイヤーの入力を事前に用意された多数の選択肢と照合します。例えば、以下のような追加アクションが可能になるかもしれません。

  1. 目撃者に話を聞く

  2. 駅周辺を歩き回る

  3. 駅前の店を訪れる

  4. 怪しい人物を追う


自由入力が可能になることで、プレイヤーは限られた選択肢に縛られることなく、よりオープンで没入感のあるゲーム体験を得られます。


自然言語処理(NLP)の実装


NLP を活用すると、ユーザーの入力を解析し、事前に用意されたアクションとマッチングできます。AI は入力の意図を評価し、最も適切な選択肢を選びます。もし、「駅を飛び越えて宇宙へ行く」といった実行不可能な入力がされた場合は、「ここではそれはできません」といったレスポンスを返すことができます。


しかし、新しい NLP の実装により、たとえば「線路に飛び降りて手がかりを探す」と入力した場合、それは「線路を調べる」というアクションにマッチさせることができます。


コード実装


この例では、可能なアクションを格納するクラスを定義します。次に、ゲームのストーリーを表示し、ユーザー入力を受け付けるビューを作成します。最後に、NLP ロジックが入力を処理し、最も適切なアクションを見つけ出します。





GameTypes.swift - サンプルコードの基本型

struct GameScene {
	let description: String
    let validActions: [String]
}

struct GameWorld {
    let scenes: [String: GameScene]
}

GameEngine.swift - ゲーム世界とシーンの読み込み、アクションの処理. このファイルには processAction 関数が含まれており、ユーザーの入力を受け取り、適切なアクションとマッチングします。

class GameEngine: ObservableObject {
	@Published var gameWorld: GameWorld?
	@Published var currentScene: GameScene?
	
	init() {
		loadGameWorld()
	}
	
	func loadGameWorld() {
		let scenes: [String: GameScene] = [
			"train_station": GameScene(
				description: "A crime has just been committed at the city train station! You have been tasked to solve it. You go to the station immediately and are welcomed by the police",						
				validActions: ["Inspect the tracks", "Examine the train car", "Go to the ticket office", "Question the witnesses", "Walk around the station", "Visit the street-level shops", "Follow a suspicious character", ...]
			),
			...
		]
		
		gameWorld = GameWorld(scenes: scenes)
		currentScene = gameWorld?.scenes["train_station"]
		print("Game world initialised successfully!")
	}
	
	func processAction(_ input: String) -> String {
        guard let scene = currentScene else { return "You are lost in the void." }
        if let matchedAction = TextMatcher.bestMatch(for: input, in: scene.validActions) {
            return "You chose: \(matchedAction)"
        } else {
            return "You can’t do that here."
        }
    }
}

TextMatcher.swift:

import NaturalLanguage

class TextMatcher {
    static let embedding = NLEmbedding.wordEmbedding(for: .english)

    static func averageVector(for phrase: String) -> [Double]? {
        guard let embedding = embedding else { return nil }
        let tokens = phrase.lowercased().split(separator: " ").map(String.init)

        let vectors = tokens.compactMap { embedding.vector(for: $0) }
        guard !vectors.isEmpty else { return nil }

        let vectorLength = vectors[0].count
        var average = [Double](repeating: 0, count: vectorLength)

        for vector in vectors {
            for i in 0..<vectorLength {
                average[i] += vector[i]
            }
        }

        for i in 0..<vectorLength {
            average[i] /= Double(vectors.count)
        }

        return average
    }

    static func bestMatch(for input: String, in options: [String], threshold: Double = 0.7) -> String? {
        guard let inputVector = averageVector(for: input) else { return nil }

        var bestScore: Double = 0.0
        var bestMatch: String?

        for option in options {
            if let optionVector = averageVector(for: option) {
                let score = cosineSimilarity(inputVector, optionVector)
                if score > bestScore {
                    bestScore = score
                    bestMatch = option
                }
            }
        }

        return bestScore >= threshold ? bestMatch : nil
    }

    static func cosineSimilarity(_ v1: [Double], _ v2: [Double]) -> Double {
        let dotProduct = zip(v1, v2).map(*).reduce(0, +)
        let magnitude1 = sqrt(v1.map { $0 * $0 }.reduce(0, +))
        let magnitude2 = sqrt(v2.map { $0 * $0 }.reduce(0, +))
        return dotProduct / (magnitude1 * magnitude2)
    }
}

このコードでは、Apple の NaturalLanguage (NL) フレームワーク を使用する TextMatcher クラスを定義し、単語の埋め込みベクトルを基にテキストの類似度を比較します。以下に、その仕組みを説明します。


主な機能:
  1. Uses Pre-trained Word Embeddings

    NLEmbedding.wordEmbedding(for: .english) を使用し、英語の単語埋め込みモデルをロードします。単語を数値ベクトルとして表現します。

  2. Computes the Average Vector for a Phrase (averageVector(for:))

    • 入力フレーズを単語に分割します。

    • 各単語を埋め込みモデルで数値ベクトルに変換します。

    • すべての単語ベクトルの平均を計算し、フレーズ全体を表す単一のベクトルを生成します。

  3. Finds the Best Matching Phrase (bestMatch(for:in:threshold:))

    • 入力フレーズと候補アクションをすべてベクトル化します。

    コサイン類似度(cosine similarity) を使用して、どのアクションが最も類似しているかを評価します。

    • 類似度がしきい値(デフォルト 0.7)を超えた場合、そのアクションを返します。

  4. Computes Cosine Similarity (cosineSimilarity(_:_:))

    • 2 つのベクトルの角度を計算し、それらがどれくらい類似しているかを測定します。

    • 1 に近いほど、より高い類似度を示します。


サポートのお願い


このコンテンツは常に無料で提供されます。価値を感じたら、ぜひ他の方と共有してください。また、アプリのダウンロードや書籍の購入、レビューの投稿を通じてサポートしていただけると嬉しいです。皆様のご意見やフィードバックをお待ちしております!


Apple App Store で「Falling Sky」をダウンロード: https://apps.apple.com/app/id6446787964



X でフォロー: https://x.com/_kingdomarcade

 
 
 

Comments


bottom of page