Xamarin.iOS 中的 iOS 游戏 API

本文介绍 iOS 9 提供的新游戏增强功能,可用于改进 Xamarin.iOS 游戏的图形和音频功能

Apple 对 iOS 9 中的游戏 API 进行了多项技术改进,使 Xamarin.iOS 应用更容易实现游戏图形和音频。 这包括通过高级框架简化开发,以及利用 iOS 设备 GPU 的强大功能提高速度和图形能力。

运行群体行为的应用示例

其中包括 GameplayKit、ReplayKit、Model I/O、MetalKit 和 Metal Performance Shader,以及 Metal、SceneKit 和 SpriteKit 的新增强功能。

本文将介绍使用 iOS 9 的新游戏增强功能来改进 Xamarin.iOS 游戏的所有方法:

GameplayKit 简介

Apple 的新 GameplayKit 框架提供了一套技术,通过减少实现所需的重复性通用代码量,可以轻松为 iOS 设备创建游戏。 GameplayKit 提供了用于开发游戏机制的工具,然后可以轻松地将其与图形引擎(例如 SceneKit 或 SpriteKit)相结合,以快速交付完整的游戏。

GameplayKit 包括多种常见的游戏算法,例如:

  • 基于行为的代理模拟,允许定义 AI 将自动追求的操作和目标。
  • 用于回合游戏的 minmax 人工智能。
  • 数据驱动的游戏逻辑的规则系统,具有模糊推理以提供紧急行为。

此外,GameplayKit 通过使用提供以下功能的模块化体系结构,采用构建基块方法进行游戏开发:

  • 用于处理游戏中复杂的、基于程序代码的系统的状态机。
  • 用于提供随机游戏和不可预测性而不会导致调试问题的工具。
  • 可重用、组件化的基于实体的体系结构。

若要详细了解 GameplayKit 的详细信息,请参阅 Apple 的 Gameplaykit 编程指南GameplayKit 框架参考

GameplayKit 示例

让我们快速了解如何使用游戏工具包在 Xamarin.iOS 应用中实现一些简单的游戏机制。

寻路

寻路是游戏的 AI 元素在游戏板上找到路径的功能。 例如,2D 敌人在迷宫中寻找出路,或者 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 的示例应用:

Metal

在 iOS 9 中,Apple 对 Metal 进行了多项更改和添加,以提供对 GPU 的低开销访问。 使用 Metal,可以最大程度地发挥 iOS 应用的图形和计算潜力。

Metal 框架包括以下新功能:

  • OS X 的新专用和深度模具纹理。
  • 通过深度固定和单独的前后模具值提高了阴影质量。
  • 金属着色语言和金属标准库的改进。
  • 计算着色器支持更广泛的像素格式。

MetalKit 框架

MetalKit 框架提供了一组实用工具类和功能,可以减少在 iOS 应用中使用 Metal 所需的工作量。 MetalKit 在三个关键领域提供支持:

  1. 从各种源异步加载纹理,包括 PNG、JPEG、KTX 和 PVR 等常见格式。
  2. 轻松访问基于模型 I/O 的资产,以进行 Metal 特定的模型处理。 这些功能经过高度优化,可在 Model I/O 网格和 Metal 缓冲区之间提供高效的数据传输。
  3. 预定义的 Metal 视图和视图管理可大大减少在 iOS 应用中显示图形渲染所需的代码量。

若要详细了解 MetalKit 的详细信息,请参阅 Apple 的 MetalKit 框架参考Metal 编程指南Metal 框架参考Metal 着色语言指南

Metal Performance Shader 框架

Metal Performance Shader 框架提供了一组高度优化的图形和基于计算的着色器,可在基于 Metal 的 iOS 应用中使用。 Metal Performance Shader 框架中的每个着色器都经过专门优化,可在 Metal 支持的 iOS GPU 上提供高性能。

使用 Metal Performance Shader 类,可以在每个特定的 iOS GPU 上实现尽可能最高的性能,而无需定位和维护单独的代码库。 Metal Performance Shader 可与任何 Metal 资源(例如纹理和缓冲区)一起使用。

Metal Performance Shader 框架提供了一组常见的着色器,例如:

  • 高斯模糊 (MPSImageGaussianBlur)
  • 索贝尔边缘检测 (MPSImageSobel)
  • 图像直方图 (MPSImageHistogram)

有关详细信息,请参阅 Apple 的金属着色语言指南

模型 I/O 简介

Apple 的 Model I/O 框架提供了对 3D 资产(例如模型及其相关资源)的深入理解。 Model I/O 为 iOS 游戏提供基于物理的材料、模型和照明,可与 GameplayKit、Metal 和 SceneKit 一起使用。

使用 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 场景图 API,可简化 3D 图形的使用。 它首次在 OS X 10.8 中引入,现已引入 iOS 8。 使用 Scene Kit 创建沉浸式 3D 可视化和休闲 3D 游戏不需要 OpenGL 专业知识。 Scene Kit 以常见的场景图概念为基础,抽象化了 OpenGL 和 OpenGL ES 的复杂性,使得向应用添加 3D 内容变得非常容易。 但是,如果你是 OpenGL 专家,Scene Kit 也可以很好地支持直接与 OpenGL 结合。 它还包括许多补充 3D 图形的功能(例如物理),并与其他几个 Apple 框架(例如 Core Animation、Core Image 和 Sprite Kit)很好地集成。

有关详细信息,请参阅我们的 SceneKit 文档。

SceneKit 更改

Apple 在 iOS 9 的 SceneKit 中添加了以下新功能:

  • Xcode 现在提供了场景编辑器,允许直接在 Xcode 中编辑场景来快速生成游戏和交互式 3D 应用。
  • SCNViewSCNSceneRenderer 类可用于启用 Metal 渲染(在支持的 iOS 设备上)。
  • SCNAudioPlayerSCNNode 类可用于添加空间音频效果,自动跟踪 iOS 应用的玩家位置。

有关详细信息,请参阅我们的 SceneKit 文档和 Apple 的 SceneKit 框架参考以及 Fox:使用 Xcode 场景编辑器生成 SceneKit 游戏示例项目。

SpriteKit

Sprite Kit 是 Apple 的 2D 游戏框架,在 iOS 8 和 OS X Yosemite 中具有一些有趣的新功能。 其中包括与 Scene Kit 的集成、着色器支持、照明、阴影、约束、法线贴图生成和物理增强。 具体而言,新的物理功能使向游戏添加逼真效果变得非常容易。

有关详细信息,请参阅我们的 SpriteKit 文档。

SpriteKit 更改

Apple 在 iOS 9 的 SpriteKit 中添加了以下新功能:

  • 使用 SKAudioNode 类自动跟踪玩家位置的空间音频效果。
  • Xcode 现在具有场景编辑器和操作编辑器,可轻松创建 2D 游戏和应用。
  • 通过新的相机节点 (SKCameraNode) 对象轻松支持滚动游戏。
  • 在支持 Metal 的 iOS 设备上,SpriteKit 会自动使用它进行渲染,即使已经在使用自定义 OpenGL ES 着色器。

有关详细信息,请参阅我们的 SpriteKit 文档和 Apple 的 SpriteKit 框架参考及其 DemoBots:使用 SpriteKit 和 GameplayKit 生成跨平台游戏示例应用。

总结

本文介绍了 iOS 9 为 Xamarin.iOS 应用提供的新游戏功能。 其中介绍了 GameplayKit 和 Model I/O;Metal 的主要增强;以及 SceneKit 和 SpriteKit 的新功能。