次の方法で共有


Xamarin.iOS の iOS ゲーム API

この記事では、Xamarin.iOS ゲームのグラフィックスとオーディオ機能を向上させるために使用できる、iOS 9 によって提供される新しいゲーム拡張機能について説明します。

Apple は、Xamarin.iOS アプリでゲーム グラフィックスとオーディオを簡単に実装できるように、iOS 9 のゲーム API に技術的な改善をいくつか加えました。 たとえば、高レベルのフレームワークによる開発の簡易化、iOS デバイスの GPU のパワーを利用した速度とグラフィック機能の向上などです。

フロックを実行しているアプリの例

これには、GameplayKit、ReplayKit、Model I/O、MetalKit、Metal Performance Shaders に加えて、Metal、SceneKit、SpriteKit の新しい強化された機能が含まれます。

この記事では、iOS 9 の新しいゲーム拡張機能を使用して Xamarin.iOS ゲームを改善するためのすべての方法について説明します。

GameplayKit の概要

Apple の新しい GameplayKit フレームワークは、実装に必要な反復的な一般的なコードの量を減らすことで、iOS デバイス用のゲームを簡単に作成できるようにする一連のテクノロジを提供します。 GameplayKit には、完成したゲームをすばやく配信するためにグラフィック エンジン (SceneKit や SpriteKit など) と簡単に組み合わせることができるゲームのしくみを開発するためのツールが用意されています。

GameplayKit には、次のようないくつかの一般的なゲームプレイ アルゴリズムが含まれています。

  • AI が自動的に追尾する動きと目標を定義できる、動作ベースのエージェント シミュレーション。
  • ターンベースのゲームプレイのための minmax 人工知能。
  • ファジー推論を使用して、出現動作を提供するデータ駆動型ゲームのロジックのルール システム。

さらに、GameplayKit は、ゲーム開発に構成要素アプローチを取っており、そのため次の機能を提供するモジュールアーキテクチャを使用します。

  • ゲーム プレイにおける複雑な手続き型コード ベースのシステムを処理するための状態マシン。
  • デバッグの問題を引き起こさずにランダム化されたゲームプレイと予測不能性を提供するためのツール。
  • 再利用可能な、コンポーネント化されたエンティティ ベースのアーキテクチャ。

GameplayKit の詳細については、Apple の「Gameplaykit プログラミング ガイド」と「GameplayKit Framework リファレンス」を参照してください。

GameplayKit の例

ゲーム プレイ キットを使用して Xamarin.iOS アプリで簡単なゲーム プレイのしくみを実装する方法を簡単に見てみましょう。

パス検索

パス検索は、ゲームの AI 要素がゲーム ボード上を移動する経路を見つける機能です。 たとえば、2D の敵は、一人称シューティング ゲーム (FPS) の世界の地形上で経路を見つけながら迷路や 3D キャラクターを通過します。

次のアプリを考えてみましょう。

パス検索マップの例

この C# コードのパス検索を使用すると、マップを通過する経路を見つけることができます。

var a = GKGraphNode2D.FromPoint (new Vector2 (0, 5));
var b = GKGraphNode2D.FromPoint (new Vector2 (3, 0));
var c = GKGraphNode2D.FromPoint (new Vector2 (2, 6));
var d = GKGraphNode2D.FromPoint (new Vector2 (4, 6));
var e = GKGraphNode2D.FromPoint (new Vector2 (6, 5));
var f = GKGraphNode2D.FromPoint (new Vector2 (6, 0));

a.AddConnections (new [] { b, c }, false);
b.AddConnections (new [] { e, f }, false);
c.AddConnections (new [] { d }, false);
d.AddConnections (new [] { e, f }, false);

var graph = GKGraph.FromNodes(new [] { a, b, c, d, e, f });

var a2e = graph.FindPath (a, e); // [ a, c, d, e ]
var a2f = graph.FindPath (a, f); // [ a, b, f ]

Console.WriteLine(String.Join ("->", (object[]) a2e));
Console.WriteLine(String.Join ("->", (object[]) a2f));

従来のエキスパート システム

C# コードの次のスニペットは、従来のエキスパート システムを実装するために GameplayKit を使用する方法を示しています。

string output = "";
bool reset = false;
int input = 15;

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();

    /*
    If reset is true, clear the output and set reset to false
    */
    var clearRule = GKRule.FromPredicate ((rules) => reset, rules => {
        output = "";
        reset = false;
    });
    clearRule.Salience = 1;

    var fizzRule = GKRule.FromPredicate (mod (3), rules => {
        output += "fizz";
    });
    fizzRule.Salience = 2;

    var buzzRule = GKRule.FromPredicate (mod (5), rules => {
        output += "buzz";
    });
    buzzRule.Salience = 2;

    /*
    This *always* evaluates to true, but is higher Salience, so evaluates after lower-salience items
    (which is counter-intuitive). Print the output, and reset (thus triggering "ResetRule" next time)
    */
    var outputRule = GKRule.FromPredicate (rules => true, rules => {
        System.Console.WriteLine(output == "" ? input.ToString() : output);
        reset = true;
    });
    outputRule.Salience = 3;

    var rs = new GKRuleSystem ();
    rs.AddRules (new [] {
        clearRule,
        fizzRule,
        buzzRule,
        outputRule
    });

    for (input = 1; input < 16; input++) {
        rs.Evaluate ();
        rs.Reset ();
    }
}

protected Func<GKRuleSystem, bool> mod(int m)
{
    Func<GKRuleSystem,bool> partiallyApplied = (rs) => input % m == 0;
    return partiallyApplied;
}

特定の一連のルール (GKRule) と既知の入力セットに基づいて、エキスパート システム (GKRuleSystem) によって予測可能な出力が作成されます (上の例では fizzbuzz)。

フロック

フロックを使用すると、AI で制御されたゲーム エンティティのグループが群れとして動作し、飛行中の鳥の群れや泳いでいる魚の群れのように、リード エンティティの動きやアクションに応答します。

次の C# コードのスニペットは、グラフィックス表示に GameplayKit と SpriteKit を使用してフロック動作を実装します。

using System;
using SpriteKit;
using CoreGraphics;
using UIKit;
using GameplayKit;
using Foundation;
using System.Collections.Generic;
using System.Linq;
using OpenTK;

namespace FieldBehaviorExplorer
{
    public static class FlockRandom
    {
        private static GKARC4RandomSource rand = new GKARC4RandomSource ();

        static FlockRandom ()
        {
            rand.DropValues (769);
        }

        public static float NextUniform ()
        {
            return rand.GetNextUniform ();
        }
    }

    public class FlockingScene : SKScene
    {
        List<Boid> boids = new List<Boid> ();
        GKComponentSystem componentSystem;
        GKAgent2D trackingAgent; //Tracks finger on screen
        double lastUpdateTime = Double.NaN;
        //Hold on to behavior so it doesn't get GC'ed
        static GKBehavior flockingBehavior;
        static GKGoal seekGoal;

        public FlockingScene (CGSize size) : base (size)
        {
            AddRandomBoids (20);

            var scale = 0.4f;
            //Flocking system
            componentSystem = new GKComponentSystem (typeof(GKAgent2D));
            var behavior = DefineFlockingBehavior (boids.Select (boid => boid.Agent).ToArray<GKAgent2D>(), scale);
            boids.ForEach (boid => {
                boid.Agent.Behavior = behavior;
                componentSystem.AddComponent(boid.Agent);
            });

            trackingAgent = new GKAgent2D ();
            trackingAgent.Position = new Vector2 ((float) size.Width / 2.0f, (float) size.Height / 2.0f);
            seekGoal = GKGoal.GetGoalToSeekAgent (trackingAgent);
        }

        public override void TouchesBegan (NSSet touches, UIEvent evt)
        {
            boids.ForEach(boid => boid.Agent.Behavior.SetWeight(1.0f, seekGoal));
        }

        public override void TouchesEnded (NSSet touches, UIEvent evt)
        {
            boids.ForEach (boid => boid.Agent.Behavior.SetWeight (0.0f, seekGoal));
        }

        public override void TouchesMoved (NSSet touches, UIEvent evt)
        {
            var touch = (UITouch) touches.First();
            var loc = touch.LocationInNode (this);
            trackingAgent.Position = new Vector2((float) loc.X, (float) loc.Y);
        }

        private void AddRandomBoids (int count)
        {
            var scale = 0.4f;
            for (var i = 0; i < count; i++) {
                var b = new Boid (UIColor.Red, this.Size, scale);
                boids.Add (b);
                this.AddChild (b);
            }
        }

        internal static GKBehavior DefineFlockingBehavior(GKAgent2D[] boidBrains, float scale)
        {
            if (flockingBehavior == null) {
                var flockingGoals = new GKGoal[3];
                flockingGoals [0] = GKGoal.GetGoalToSeparate (boidBrains, 100.0f * scale, (float)Math.PI * 8.0f);
                flockingGoals [1] = GKGoal.GetGoalToAlign (boidBrains, 40.0f * scale, (float)Math.PI * 8.0f);
                flockingGoals [2] = GKGoal.GetGoalToCohere (boidBrains, 40.0f * scale, (float)Math.PI * 8.0f);

                flockingBehavior = new GKBehavior ();
                flockingBehavior.SetWeight (25.0f, flockingGoals [0]);
                flockingBehavior.SetWeight (10.0f, flockingGoals [1]);
                flockingBehavior.SetWeight (10.0f, flockingGoals [2]);
            }
            return flockingBehavior;
        }

        public override void Update (double currentTime)
        {
            base.Update (currentTime);
            if (Double.IsNaN(lastUpdateTime)) {
                lastUpdateTime = currentTime;
            }
            var delta = currentTime - lastUpdateTime;
            componentSystem.Update (delta);
        }
    }

    public class Boid : SKNode, IGKAgentDelegate
    {
        public GKAgent2D Agent { get { return brains; } }
        public SKShapeNode Sprite { get { return sprite; } }

        class BoidSprite : SKShapeNode
        {
            public BoidSprite (UIColor color, float scale)
            {
                var rot = CGAffineTransform.MakeRotation((float) (Math.PI / 2.0f));
                var path = new CGPath ();
                path.MoveToPoint (rot, new CGPoint (10.0, 0.0));
                path.AddLineToPoint (rot, new CGPoint (0.0, 30.0));
                path.AddLineToPoint (rot, new CGPoint (10.0, 20.0));
                path.AddLineToPoint (rot, new CGPoint (20.0, 30.0));
                path.AddLineToPoint (rot, new CGPoint (10.0, 0.0));
                path.CloseSubpath ();

                this.SetScale (scale);
                this.Path = path;
                this.FillColor = color;
                this.StrokeColor = UIColor.White;

            }
        }

        private GKAgent2D brains;
        private BoidSprite sprite;
        private static int boidId = 0;

        public Boid (UIColor color, CGSize size, float scale)
        {
            brains = BoidBrains (size, scale);
            sprite = new BoidSprite (color, scale);
            sprite.Position = new CGPoint(brains.Position.X, brains.Position.Y);
            sprite.ZRotation = brains.Rotation;
            sprite.Name = boidId++.ToString ();

            brains.Delegate = this;

            this.AddChild (sprite);
        }

        private GKAgent2D BoidBrains(CGSize size, float scale)
        {
            var brains = new GKAgent2D ();
            var x = (float) (FlockRandom.NextUniform () * size.Width);
            var y = (float) (FlockRandom.NextUniform () * size.Height);
            brains.Position = new Vector2 (x, y);

            brains.Rotation = (float)(FlockRandom.NextUniform () * Math.PI * 2.0);
            brains.Radius = 30.0f * scale;
            brains.MaxSpeed = 0.5f;
            return brains;
        }

        [Export ("agentDidUpdate:")]
        public void AgentDidUpdate (GameplayKit.GKAgent agent)
        {
        }

        [Export ("agentWillUpdate:")]
        public void AgentWillUpdate (GameplayKit.GKAgent agent)
        {
            var brainsIn = (GKAgent2D) agent;
            sprite.Position = new CGPoint(brainsIn.Position.X, brainsIn.Position.Y);
            sprite.ZRotation = brainsIn.Rotation;
            Console.WriteLine ($"{sprite.Name} -> [{sprite.Position}], {sprite.ZRotation}");
        }
    }
}

次に、このシーンをビュー コントローラーに実装します。

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();
        // Perform any additional setup after loading the view, typically from a nib.
        this.View = new SKView {
        ShowsFPS = true,
        ShowsNodeCount = true,
        ShowsDrawCount = true
    };
}

public override void ViewWillLayoutSubviews ()
{
    base.ViewWillLayoutSubviews ();

    var v = (SKView)View;
    if (v.Scene == null) {
        var scene = new FlockingScene (View.Bounds.Size);
        scene.ScaleMode = SKSceneScaleMode.AspectFill;
        v.PresentScene (scene);
    }
}

実行すると、アニメーション化された小さな "Boid" が指でタップするとその周りに群がります。

指でタップすると、アニメーション化された小さな Boid がその周りに群がります

Apple でのその他の例

上記のサンプルに加えて、Apple は C# と Xamarin.iOS にコード変換できる次のサンプル アプリを提供しています。

メタル

iOS 9 では、Apple は GPU への低オーバーヘッドアクセスを提供するために、Metal にいくつかの変更と追加を加えました。 Metal を使用すると、iOS アプリのグラフィックスとコンピューティングの可能性を最大限に高めることができます。

Metal フレームワークには、次の新機能が含まれています。

  • OS X 用の新しいプライベートおよび深度ステンシル テクスチャ。
  • 深度クランプと、フロントス テンシルとバック ステンシル値の分離により、シャドウ品質が向上しました。
  • Metal シェーディング言語と Metal 標準ライブラリの機能強化。
  • 計算シェーダーでは、より広い範囲のピクセル形式がサポートされています。

MetalKit フレームワーク

MetalKit フレームワークには、iOS アプリで Metal を使用するために必要な作業量を減らす一連のユーティリティ クラスと機能が用意されています。 MetalKit は、次の 3 つの重要な領域でサポートを提供します。

  1. PNG、JPEG、KTX、PVR などの一般的な形式を含むさまざまなソースからの非同期テクスチャ読み込み。
  2. Metal 固有のモデル処理のための Model I/O ベースの資産に簡単にアクセスできます。 これらの機能は、Model I/O メッシュと Metal バッファー間の効率的なデータ転送を提供するために高度に最適化されています。
  3. iOS アプリ内でグラフィック レンダリングを表示するために必要なコードの量を大幅に削減する、定義済みの Metal ビューとビュー管理。

MetalKit の詳細については、Apple の「MetalKit Framework リファレンス」、「Metal プログラミング ガイド 、「Metal フレームワーク リファレンス」、および「Metal シェーディング言語ガイド」を参照してください。

Metal パフォーマンス シェーダー フレームワーク

Metal パフォーマンス シェーダー フレームワークは、Metal ベースの iOS アプリで使用するために、グラフィックスと計算ベースのシェーダーの高度に最適化されたセットを提供します。 Metal パフォーマンス シェーダー フレームワークの各シェーダーは、Metal でサポートされている iOS GPU で高いパフォーマンスを提供するように特別に調整されています。

Metal パフォーマンス シェーダー クラスを使用すると、個々のコード ベースをターゲットにして維持することなく、特定の iOS GPU ごとに可能な限り最高のパフォーマンスを実現できます。 Metal パフォーマンス シェーダーは、テクスチャやバッファーなどの任意の Metal リソースで使用できます。

Metal パフォーマンス シェーダー フレームワークには、次のような一連の一般的なシェーダーが用意されています。

  • ガウスぼかし (MPSImageGaussianBlur)
  • Sobel 境界検出 (MPSImageSobel)
  • 画像ヒストグラム (MPSImageHistogram)

詳細については、Apple の「Metal シェーディング言語ガイド」を参照してください。

Model I/O の概要

Apple の Model I/O フレームワークでは、3D アセット (モデルとその関連リソースなど) を深く理解できます。 Model I/O では、GameplayKit、Metal、SceneKit で使用できる物理ベースの素材、モデル、ライティングを iOS ゲームに提供します。

Model I/O では、次の種類のタスクをサポートできます。

  • ライティング、マテリアル、メッシュ データ、カメラ設定、その他のシーンベースの情報を、さまざまな一般的なソフトウェアおよびゲーム エンジン形式からインポートします。
  • 手続き型テクスチャのスカイ ドームの作成や、メッシュへのライティングのベイクなど、シーンベースの情報を処理または生成します。
  • MetalKit、SceneKit、GLKit と連携して、レンダリング用の GPU バッファーにゲーム アセットを効率的に読み込みます。
  • シーンベースの情報を、さまざまな一般的なソフトウェアおよびゲーム エンジン形式にエクスポートします。

Model I/O の詳細については、Apple の「Model I/O フレームワーク リファレンス」を参照してください

ReplayKit の概要

Apple の新しい ReplayKit フレームワークを使用すると、ゲーム プレイの記録を iOS ゲームに簡単に追加でき、ユーザーはアプリ内からこのビデオをすばやく簡単に編集して共有できます。

詳細については、「Apple の ReplayKit と Game Center ビデオを使用したソーシャル化」と「DemoBots : SpriteKit と GameplayKit を使用したクロス プラットフォーム ゲームの構築」のサンプル アプリを参照してください。

SceneKit

Scene Kit は、3D グラフィックスの操作を簡略化する 3D シーン グラフ API です。 OS X 10.8 で初めて導入され、現在は iOS 8 にも導入されています。 Scene Kit を使用すると、イマーシブな 3D 視覚化やカジュアルな 3D ゲームを作成するために OpenGL の専門知識は必要ありません。 Scene Kit は、一般的なシーン グラフの概念に基づいて構築されており、OpenGL と OpenGL ES の複雑さを取り除き、3D コンテンツをアプリケーションに非常に簡単に追加できます。 その一方で、OpenGL 専門家にとって、Scene Kit は OpenGL と直接連携するための優れたサポートを提供する API でもあります。 また、物理学などの 3D グラフィックスを補完し、Core Animation、Core Image、Sprite Kit など、他の複数の Apple フレームワークと非常にうまく統合できる多数の機能も含まれています。

詳細については、Microsoft の SceneKit のドキュメントを参照してください。

SceneKit の変更

Apple は、iOS 9 用 SceneKit に次の新機能を追加しました。

  • Xcode では、Xcode 内から直接シーンを編集してゲームや対話型の 3D アプリをすばやく構築できるシーン エディターが提供されるようになりました。
  • SCNView クラスと SCNSceneRenderer クラスを使用して、(サポートされている iOS デバイスで) Metal レンダリングを有効にすることができます。
  • SCNAudioPlayer クラスと SCNNode クラスを使用して、プレーヤーの位置を自動的に追跡する空間オーディオ効果を iOS アプリに追加できます。

詳細については、Microsoft の SceneKit のドキュメントと Apple の SceneKit Framework リファレンス、および「Fox: Xcode シーン エディター を使用した SceneKit ゲームの構築」のサンプル プロジェクトを参照 してください。

SpriteKit

Apple の 2D ゲーム フレームワークである Sprite Kit には、iOS 8 と OS X Yosemite でのいくつかの興味深い新機能があります。 これらの機能としては、Scene Kit との統合、シェーダーのサポート、照明、シャドウ、制約、法線マップの生成、物理機能の強化などがあります。 特に、新しい物理機能を使用すると、ゲームに現実的な効果を非常に簡単に追加できます。

詳細については、Microsoft の SpriteKit のドキュメントを参照してください。

SpriteKit の変更

Apple は、SpriteKit for iOS 9 に次の新機能を追加しました。

  • SKAudioNode クラスを使用してプレイヤーの位置を自動的に追跡する空間オーディオ効果。
  • Xcode では、2D ゲームとアプリを簡単に作成するためのシーン エディターとアクション エディターが追加されました。
  • 新しいカメラ ノード (SKCameraNode) オブジェクトを使用してゲームを簡単にスクロールできます。
  • Metal をサポートする iOS デバイスでは、カスタム OpenGL ES シェーダーを既に使用していた場合でも、SpriteKit はレンダリングに自動的に使用します。

詳細については、Microsoft の SpriteKit のドキュメント、Apple の SpriteKit Framework リファレンスおよび「DemoBots: SpriteKit と GameplayKit を使用したクロス プラットフォーム ゲームの構築」のサンプル アプリを参照してください。

まとめ

この記事では、Xamarin.iOS アプリ用に iOS 9 で提供される新しいゲーム機能について説明しました。 GameplayKit と Model I/O が導入されました。Metal の主な機能強化。および SceneKit と SpriteKit の新機能が追加されました。