FlappySwift: パイプの生成位置と移動速度を制御する数学的アプローチ

Flappy Birdのような人気のゲームでは、画面上に現れる障害物(パイプ)の配置や動きは、一見ランダムに見えても、実は綿密な数学的計算に基づいています。本稿では、FlappySwiftプロジェクトを例にとり、パイプの生成位置、移動速度、および生成頻度がどのように数式によって制御されているかを解説します。これにより、ゲーム開発における要素の動的な制御方法について理解を深めることができるでしょう。

パイプの垂直方向の配置ロジック

FlappySwiftにおけるパイプの垂直位置は、シーンの高さに基づいた乱数生成と、固定されたパイプ間の間隔の組み合わせによって決定されます。この処理は主にspawnPipes()関数内で実装されています。

まず、ゲームシーンの現在の高さを取得し、パイプのギャップ(隙間)の中心位置が配置されうる範囲を定義します。これにより、パイプが画面の端に寄りすぎないように調整し、ゲームのプレイアビリティを確保します。

import SpriteKit

class GameScene: SKScene {

    // パイプテクスチャのプロパティ (例として、実際のプロジェクトではアセットからロード)
    let pipeTextureUp: SKTexture = SKTexture(imageNamed: "pipe_up_placeholder")
    let pipeTextureDown: SKTexture = SKTexture(imageNamed: "pipe_down_placeholder")
    
    // パイプ間の垂直方向の固定ギャップ(ポイント)
    let pipeVerticalGap: CGFloat = 150.0

    // パイプ生成をトリガーするためのダミーメソッド (didMove(to:)から呼び出すことを想定)
    func setupPipeGeneration() {
        // 例: 2秒ごとにパイプを生成するループを開始
        // self.run(SKAction.repeatForever(SKAction.sequence([
        //     SKAction.run(spawnPipes),
        //     SKAction.wait(forDuration: 2.0)
        // ])))
    }

    func spawnPipes() {
        let sceneHeight = self.frame.size.height

        // パイプギャップの中心Y座標の許容範囲を定義
        // 例: シーンの下端から30%の位置〜上端から30%の位置の範囲でランダム化
        let minGapCenterY = sceneHeight * 0.3
        let maxGapCenterY = sceneHeight * 0.7

        // ギャップの中心Y座標をランダムに決定
        let gapCenterY = CGFloat.random(in: minGapCenterY...maxGapCenterY)

        // 上部パイプと下部パイプのテクスチャの高さを取得
        let upperPipeHeight = pipeTextureUp.size().height
        let lowerPipeHeight = pipeTextureDown.size().height

        // 上部パイプの中心Y座標を計算
        // ギャップ中心Y + (ギャップの半分) + (上部パイプの高さの半分)
        let upperPipeY = gapCenterY + (pipeVerticalGap / 2.0) + (upperPipeHeight / 2.0)
        
        // 下部パイプの中心Y座標を計算
        // ギャップ中心Y - (ギャップの半分) - (下部パイプの高さの半分)
        let lowerPipeY = gapCenterY - (pipeVerticalGap / 2.0) - (lowerPipeHeight / 2.0)

        // パイプノードを作成し、初期位置を設定
        let pipeUpperNode = SKSpriteNode(texture: pipeTextureUp)
        pipeUpperNode.position = CGPoint(x: self.frame.size.width + pipeUpperNode.size.width / 2, y: upperPipeY) // 画面右端から半分はみ出た位置から開始
        self.addChild(pipeUpperNode)

        let pipeLowerNode = SKSpriteNode(texture: pipeTextureDown)
        pipeLowerNode.position = CGPoint(x: self.frame.size.width + pipeLowerNode.size.width / 2, y: lowerPipeY) // 画面右端から半分はみ出た位置から開始
        self.addChild(pipeLowerNode)

        // 後続の移動アクションを適用
        applyPipeMovement(to: pipeUpperNode)
        applyPipeMovement(to: pipeLowerNode)
    }

    // ... その他のゲームロジック ...
}

上記のコード例では、CGFloat.random(in:)を使用して現代的なSwiftの乱数生成方法を採用しています。パイプ間のpipeVerticalGapは150.0ポイントに設定されており、これによりプレイヤーが通過するのに十分なスペースが確保されます。

パイプの移動速度の数学的制御

ゲームの難易度に大きく影響するパイプの移動速度は、SpriteKitのSKActionを用いて制御されます。移動する総距離と所要時間から速度が決定されますが、ゲーム全体でパイプが一定の速度で移動するように設計されています。

まず、パイプが画面外に完全に移動するために必要な総距離を計算します。これは、シーンの幅とパイプ自体の幅を考慮したものです。

import SpriteKit

class GameScene: SKScene {
    // パイプテクスチャのプロパティ (例として)
    let pipeTextureUp: SKTexture = SKTexture(imageNamed: "pipe_up_placeholder")

    // パイプの水平方向の移動速度(ポイント/秒)
    let pipeHorizontalSpeed: CGFloat = 100.0 // 例: 1秒あたり100ポイント移動

    func applyPipeMovement(to pipeNode: SKSpriteNode) {
        let sceneWidth = self.frame.size.width
        let pipeWidth = pipeNode.size.width

        // パイプが画面の右端から左端の画面外に完全に移動するまでの総距離
        let totalTraversalDistance = sceneWidth + pipeWidth
        
        // 移動にかかる時間を計算: 距離 / 速度
        let moveDuration = TimeInterval(totalTraversalDistance / pipeHorizontalSpeed)

        // 左方向への移動アクションを作成
        let moveAction = SKAction.moveBy(x: -totalTraversalDistance, y: 0.0, duration: moveDuration)
        
        // 移動完了後にパイプをシーンから削除するアクション
        let removeAction = SKAction.removeFromParent()
        
        // 移動と削除を順次実行するシーケンスアクション
        let moveAndRemoveSequence = SKAction.sequence([moveAction, removeAction])
        
        pipeNode.run(moveAndRemoveSequence)
    }

    // ... その他のゲームロジック ...
}

このアプローチでは、移動距離が長くなっても、pipeHorizontalSpeedが一定であるため、それに応じてmoveDurationも長くなり、結果としてパイプの移動速度は常に一定に保たれます。これにより、ゲームの体験が一貫したものになります。

パイプの生成頻度の調整

パイプの生成頻度もゲームバランスを決定する重要な要素です。これはSKAction.wait(forDuration:)アクションを使用して制御されます。

import SpriteKit

class GameScene: SKScene {
    // パイプ生成の基本間隔(秒)
    let pipeSpawnInterval: TimeInterval = 2.0 // 例: 2秒ごとに新しいパイプを生成

    override func didMove(to view: SKView) {
        super.didMove(to: view)
        
        // パイプ生成処理をラップするアクション
        let generatePipeAction = SKAction.run { [weak self] in
            self?.spawnPipes()
        }

        // 生成後、一定時間待機するアクション
        let waitAction = SKAction.wait(forDuration: pipeSpawnInterval)
        
        // 生成と待機を繰り返すシーケンス
        let spawnThenWaitSequence = SKAction.sequence([generatePipeAction, waitAction])
        
        // このシーケンスを永遠に繰り返すアクション
        let spawnLoopAction = SKAction.repeatForever(spawnThenWaitSequence)
        
        // シーンでアクションを実行。キーを付与することで後でアクションを停止・参照可能に
        self.run(spawnLoopAction, withKey: "pipeGenerationLoop")
    }

    // ... spawnPipes() や applyPipeMovement() などのメソッド ...
}

上記の例では、pipeSpawnIntervalを2.0秒に設定しています。これにより、2秒ごとに新しいパイプのセットが生成されます。この生成間隔は、プレイヤーが反応するための時間を与えつつ、ゲームの緊張感を維持するように調整されています。

ゲームデザインにおける数学的パラメータの活用

FlappySwiftのパイプシステム設計は、ゲーム開発における数学的計算の重要性を示しています。厳密な数式やアルゴリズムを適用することで、開発者はゲーム要素の挙動を正確にコントロールし、挑戦的でありながらも楽しいゲーム体験を創出できます。

これらの数学的パラメータを調整することで、ゲームの難易度やテンポを容易に変更することが可能です。例えば、pipeVerticalGapの値を小さくすればパイプ間の隙間が狭くなり難易度が上昇し、pipeSpawnIntervalの値を短くすればパイプの生成頻度が高まり、ゲームのペースが速くなります。このように、数学はゲームのバランス調整に不可欠なツールとして機能します。

タグ: Swift SpriteKit Game Development SKAction mathematics

11月4日 09:55 投稿