澳门新浦京娱乐场网站-www.146.net-新浦京娱乐场官网
做最好的网站

澳门新浦京娱乐场网站:的iOS游戏开发实例,基

GameplayKit是一个面向对象的框架,为构建游戏提供基础工具和技术。 GameplayKit包含用于设计具有功能性,可重用架构的游戏的工具,以及用于构建和增强诸如角色移动和对手行为的游戏玩法特征的技术。

版本记录

版本号 时间
V1.0 2018.02.06

苹果去年发布了GameplayKit的代码框架,为游戏开发者提供了很多超级实用的API及工具,来提升游戏开发者的效率,使得制作游戏能更加聚焦在游戏本身-也就是游戏性的策划和创意上了。

翻译自:https://www.raywenderlich.com/155780/gameplaykit-tutorial-entity-component-system-agents-goals-behaviors-2
原文作者:Ryan Ackermann
(注:以下内容中代码部分本人将进行OC转换,并将常用头文件添加进PCH,所以代码中并没有添加头文件的步骤,如要看Swift代码,请移步原文)
正文:

原文地址
这是一个系列文章,查看更多请移步目录页

 

前言

GameplayKit框架,构建和组织你的游戏逻辑。 整合常见的游戏行为,如随机数生成,人工智能,寻路和代理行为。接下来几篇我们就一起看一下这个框架。

由于业余时间会用Spritekit做些小游戏demo,GameplayKit出来后感觉给了极大的方便,我就借由苹果提供的Maze的Sample代码,来跟大家介绍下GameplayKit提供的新功能,希望大家能够喜欢!

澳门新浦京娱乐场网站 1

在上一篇我们学习了利用 GameplayKit的 pathfinding API 来计算位于场景中的两点之间的路径,并避开指定的障碍物的算法。

澳门新浦京娱乐场网站 2

Overview

下面先看一下该框架的基本信息。

澳门新浦京娱乐场网站 3

GameplayKit是一个面向对象的框架,为构建游戏提供基础工具和技术。 GameplayKit包含用于设计具有功能性,可重用架构的游戏的工具,以及用于构建和增强诸如角色移动和对手行为的游戏玩法特征的技术。

下面看一下该框架的基本结构。

澳门新浦京娱乐场网站 4

澳门新浦京娱乐场网站 5


之前小武并不是做游戏开发出身的,所以并没有学习很多游戏开发里面已经成熟的算法或者编程模式来提升开发效率。仅仅凭借爱好和随性的编写,完成游戏代码。

截图1

在这一篇中,让我们来实现一种不同的在场景中移动的效果。GameplayKit 介绍了 Behaviours(行为) 和 Goals(目标) 的概念.他们提供了一种方式,让你能够依赖约束和目标把节点的放置在场景中某个特定位置。让我们先看一下视频,然后再来详细看一下。

GamePlayKit

Getting Started with GameplayKit - GameplayKit开始

GameplayKit涵盖游戏设计和开发的许多方面。 有关游戏设计模式的更深入讨论,您可以使用GameplayKit,以及说明使用GameplayKit功能构建游戏的教程,请参阅GameplayKit Programming Guide。


1,需求描述

比如小武以前曾经制作过一款类似“合金装备”的逃脱类游戏:

战士需要躲避敌人的监视,逃到制定的出口。

敌人巡逻有规定路线,敌人的能力也有些变化。

战士能够利用各种道具,来完成逃离。

GameplayKit在iOS9后引入的一个令人惊叹的框架,它可以非常容易地实现在游戏中进行模块化任务执行。
GameplayKit是个实用工具集合,其中包括像寻路,随机算法,状态机,规则系统等等。
在这个教程中,你将关注GameplayKit中两个部分:实体组件系统、代理,目标和行为。
GameplayKit的这些功能可以让你在游戏变得更大更复杂时,让你的代码在不同的方法下变得更加清晰且复用。
OK,让我们赶紧开始吧!

上面的例子中(我们马上要创建它),你可以看到一个黄色的盒子代表一个用户。黄色的盒子随着用户点击场景中的任意一处来移动。特别基本的东西,对吧。有趣的是导弹部分,它能够寻找到player,并且总是试图通过player节点的中心。

我们这里主要讲GKEntity和GKComponent这二个类;

Related Sample Code - 相关示例代码

要实验GameplayKit,请参阅以下示例代码项目:

  • Boxes: GameplayKit Entity-Component Basics

  • Dispenser: GameplayKit State Machine Basics

  • Pathfinder: GameplayKit Pathfinding Basics

  • AgentsCatalog: Using the Agents System in GameplayKit

  • FourInARow: Using the GameplayKit Minmax Strategist for Opponent AI

  • DemoBots: Building a Cross Platform Game with SpriteKit and GameplayKit


2,朴素的编程

朴素的编程设计(其实就是不过脑子,嘿嘿!):

对“战士”,和“敌人”做了一个公共的类“人”作为祖先,然后“战士”和“敌人”类分别继承自“人”,根据各自需要扩展私有部分。

其它操作、移动、AI等都在各自私有类中完成。

后续需求增加了,我想做更多类型的“敌人”,不同“敌人”的能力不尽相同。一开始,将相同部分的能力,放到一个“敌人”的公共类中,所有“敌人”都继承自这个公共类。后来,随着“敌人”类型的不同,可以有很多种能力搭配。“敌人”公共类的代码就越来越多,类也越来越大了。

有时候,同样的能力也想为“战士”提供,要在“战士”和“敌人”类里重复Copy很多代码。不管怎么样,随着游戏越来越复杂,里面重复的代码也有可能越来越多。

开始准备

在这个GameplayKit教程中,你将开发一个名为MonsterWars的简单小游戏。下载开始项目,在Xcode中打开,然后build运行一下,你将看到以下界面:

澳门新浦京娱乐场网站 6

截图2

现在,这个游戏仅仅是一个UI外壳,并没有游戏机制。以下是这个游戏的机制:

  • 你可以看到左上角,你会不停地获取金钱,然后点击屏幕下方按钮,使用金钱购买单位。
  • 游戏中有三种单位类型:Quirk(速度快且便宜),Zap(远程攻击)和Munch(缓慢但拥有范围啃咬的能力)。
  • 如果你的单位将敌方城堡击溃,则胜利。

看一下现有的代码,当你有一些眉目的时候,看看这个GameplayKit教程的目录结构:这就是典型的实体组件系统。

这不需要任何的物理或者自定义代码来完成,这完全有一个行为可寻址目标来控制。

GKEntity类(实体): 可以容纳很多组件的容器,根据自己的需求来加入相应的Component组件。

GKComponent类(组件):代表一种功能和行为,不同的组件代表不同的功能。

Topics

3,具体分析

一开始“战士”和“敌人”继承自“人”,“人”提供基本的移动能力和精灵;“战士”和“敌人”分别有隐藏,跑动和侦查,射击等功能。

如下图所示:

澳门新浦京娱乐场网站 7最初的类继承图

现在,“敌人”的种类加了3种,分别具有狙击,透视,喷火这些能力的组合。而且,战士升级了,需要有射击能力了。此时,只能将公共的狙击,透视和喷火能力,提升到“敌人”的公共类;而射击能力,要提到“人”这个公共类。

如下图所示:

澳门新浦京娱乐场网站 8需求变化后的类继承图

因为不是专职做游戏,仅仅是业余时间玩一下,所以写个小Demo后,也就没有继续开发,继续深究下去(其实是实在写不下去了,扩展性和可维护性太差了!)。

小武有一颗积极向好的心,但是懒癌重度,放着吧~~!

直到苹果推出了GameplayKit框架,我又持续对游戏开发有点兴致。开始学习后,发现游戏开发里面,早就有很多事实上的开发模式和方法了。其中“系统-实体-组件”是比较好的解决以上问题的方法。(因为小武水平有限,且比较懒,恶意吐槽伤害作者玻璃心的。。。。。也懒得理你们!)

上面小武所做的那个游戏Demo,实际上是一种面向对象的继承(Inheritance)体系,按照这个体系组织游戏代码会造成公共祖先类巨大无比,子孙类也默默继承了很多无用的代码,代码明显有“坏味道”。

实际上,有很多能力是可以抽象出来的,同时给“战士”和不同类型的“敌人”使用,比如渲染,移动,AI,特殊技能等。这些功能通过不同组件的组合,就构成了战士和不同类型敌人。这是一种组合(Composite)体系。也就是下面需要介绍的ECS模式了。

到底什么是实体组件系统?

十分简单,实体组件系统是一种架构方法,在你的游戏不断变大,不断变得复杂的时候,代码间的相互依赖却不会因此增长。
这就需要两个部分来完成这样的事情:

  • 实体:实体在游戏中是一个对象,玩家、敌人或者城堡都是实体。由于实体就是一个对象,你可以通过应用组件来使这个对象实现很多有趣的动作行为。
  • 组件:在实体中,一个组件包含特定的逻辑来执行特定的工作,例如改变外形或者发射一枚火箭。你可以为你的实体制作各种不同动作行为的组件,比方说你需要一个移动组件,一个生命组件,一个近战攻击的组件等等。
    这个系统最棒的优点就是你可以复用组件,将其组装到你任何需要该组件的实体上,这样你的代码看上去就更加干净整洁。

注意:如果你想对理论方面进行深究,查看原文作者几年前写的老教程,他从实体组件系统的开始进行讲解,并将其与其他模式进行比较。

现在,让我们通过 Demo了解一下 behaviours 和 goals 是怎么工作的。

实用功能
(1)方便通过聚合多种组件,构建复杂的新实体Entity。
(2)不同的实体GKEntity,通过聚合不同种类的组件GKComponent来实现。
(3)不必重复写组件,组件可以复用,也易于扩展。
(4)实体可以实现动态添加组件,以动态获得或者删除某些功能。

1. Entities and Components

用于设计可组合的,可重复使用的游戏逻辑的通用架构。

  • GKEntity

    • 与游戏相关的对象,其功能完全由一组组件对象提供。
  • GKComponent

    • 抽象超类,用于创建将特定游戏玩法功能添加到实体的对象。
  • GKComponentSystem

    • 为指定类的所有组件对象管理定期更新消息。

1,“实体-组件-系统”:ECS(Entity,Component,System)

ECS(Entity,Component,System)即“实体-组件-系统”。实体,是可以容纳很多组件的容器。组件代表一种功能和行为,不同的组件代表不同的功能。实体可以根据自己的需求来加入相应的组件。

比如上面的那个游戏,我们将游戏角色的移动,精灵,隐藏,跑动,侦查,设计,狙击,透视,跑步等都作为组件类,每一个组件都描述了一个实体某些属性特征。我们的实体根据自己的实际需要,来组合这些组件,让对应的实体获得组件所代表的功能。

现在实体“战士” “敌人1”“敌人2”“敌人3”“敌人4”实体与功能组件之间的对应关系如下图所示:

澳门新浦京娱乐场网站 9ECS模式下游戏对象管理方式

从继承(Inheritance)体系到组合(Composite)体系好处显而易见:

方便通过聚合多种组件,构建复杂的新实体。

不同的实体,通过聚合不同种类的组件来实现。

不必重复写组件,组件可以复用,也易于扩展。

实体可以实现动态添加组件,以动态获得或者删除某些功能。

第一个组件

现在开始,我们首先创建一个组件让其在场景中代表一个精灵。在GameplayKit中,我们创建组件都是继承于GKComponent的子类。
首先,我们选择项目中的MonsterWars_OC组,右键,选择New Group,然后创建一个Components的新组。
然后右键Components组,选择New File...,选择iOS/Source/Cocoa Touch Class选项,创建GKComponent的子类名为SpriteComponent
做到这,你的项目应该像这样:

澳门新浦京娱乐场网站 10

截图3

打开SpriteComponent.h文件,加入以下代码:

//1  
@property (strong, nonatomic) SKSpriteNode *node;  
//2  
- (instancetype)initWithTexture:(SKTexture*) texture;  

打开SpriteComponent.m文件,加入以下代码:

- (instancetype)initWithTexture:(SKTexture *)texture {  
    if (self = [super init]) {  
        self.node = [[SKSpriteNode alloc]initWithTexture:texture color:[SKColor whiteColor] size:texture.size];  
    }  

    return self;  
} 

代码说明:
1.声明了一个公开的node,方便以后获取。
2.声明了一个公开的init方法,用来创建component的同时初始化node,在这个方法中我们将参数纹理添加给node,并将它的颜色默认为白色,大小为纹理对应的大小。

Creating a Behavior and Goal Example

使用默认的 SpriteKit 模版创建项目,打开 GameScene.swift

setup1.png

首先,我们定义一个实例

let player:Player = Player()
var missile:Missile?

GKEntity 是一个通用的实体,可以给它添加组件和方法。在我们的例子中,我们有两个实例,一个代表 player,一个代表导弹。我们马上来看一下它的细节实现。

我们还需要创建一个组件系统的数组。这个组件系统是指符合同样类型的组件的一个集合。我们可以在需要时候的时候,再定义它(lazy var),因为我们仅需初始化它一次。我们有一个组件作为靶子(可以用来追踪player的位置,并添加冒烟的效果),另一个作为导弹。我们定义的顺序,会成为一会儿运动的顺序。所以我们先返回targeting 然后是 rendering. 因为我们希望根据目标的变化,来追踪显示他们的。

lazy var componentSystems:[GKComponentSystem] = {
  let targetingSystem = GKComponentSystem(componentClass: TargetingComponent.self)
  let renderSystem = GKComponentSystem(componentClass: RenderComponent.self)
  return [targetingSystem, renderSystem]
 }()

但什么才是一个 GameKit 组件呢?我们已经讨论了在场景中的实体的效果,但没讲具体做了什么。一个 GKComponent 在特定部分,囊括了数据和逻辑。组件和实体联系,一个实体可能对应多个组件。它们为组件提供可重用的行为。它们通过组件模型,来帮助解决大型游戏中可能出现的复杂而大型的继承树问题。

在这个场景中,两个实体都有渲染组件,导弹实体还有靶子组件。

直接通过代码来理解这二个类:

2. State Machines

用于定义状态相关游戏逻辑的模块化系统。

  • GKState

    • 抽象超类,用于将状态特定的逻辑定义为状态机的一部分。
  • GKStateMachine

    • 一个有限状态机 - 一个状态对象的集合,每个状态对象定义了游戏玩法的特定状态的逻辑和状态之间转换的规则。

2,游戏运行的发动机

我们需要将“战士”“敌人”这些实体放入到游戏中运行,游戏需要驱动这些实体的组件发挥功能。我们如何驱动游戏运行呢?我来跟大家来八一八。

游戏引擎(如SpriteKit和SceneKit,也包括客户定制的引擎及直接使用Metal或者OpenGL)在一个称为“更新/渲染”循环里,执行游戏有关的代码逻辑。这个循环贯穿游戏的生命周期,按字面意思分为更新阶段和渲染阶段。在更新阶段,与游戏逻辑相关所有的状态更新,数值计算,操作指令,执行序列计算,AI处理,动画调度等都在该阶段完成。在渲染阶段,游戏引擎部分进行自动处理,主要处理动画渲染和物理引擎的执行,基于当前状态渲染画出所有游戏的场景。通常,游戏引擎的设计,是让游戏开发者完成更新阶段的所有逻辑,然后引擎负责游戏渲染阶段的所有工作。苹果的SpriteKit和SceneKit就是这样设计的,从苹果提供的SpriteKit游戏引擎的运行Loop图中可以略窥一二。

具体用SpriteKit引擎来验证下上面的描述。游戏时看到的每一帧画面,展示在我们面前时,游戏引擎都会经历下面这个Loop图所示的处理:

澳门新浦京娱乐场网站 11SpriteKit的生命周期

每一帧在SKScene里会调用update:来进行更新操作,游戏开发者在这里放入所有的游戏逻辑层面的代码。

后面的Action,Physics,Render阶段分别负责Action执行,物理引擎检测处理,和游戏图像的绘制渲染处理,这些工作都是交由SpriteKit来进行。

驱动游戏运行的动力:

更新阶段,开发者在update里面计算出所有游戏逻辑和游戏状态;

渲染阶段,物理检测,图像渲染由游戏引擎完成。

第一个实体

在GameplayKit中,GKEntity代表了实体。你可以通过添加组件来让实体进行工作,就像我们之前创建组件一样简单。
在你的游戏中,为你的每一个类型对象添加实体通常是非常有用的,在这个游戏示例中,这里有五种不同类型的对象:城堡,Quirk单位,Zap单位,Munch单位和激光。
你的GKEntity子类要尽可能的简单,通常情况下,它仅仅作为一个初始化器,可以添加你想要的组件即可。
我们来为城堡添加实体,右键项目中的MonsterWars_OC组,创建一个Entities的新组。
我们在Entities新组下创建一个GKEntity的子类,名为Castle
完成这些,你的项目应该像这样:

澳门新浦京娱乐场网站 12

截图4

打开Castle.h,加入以下代码:

//1  
- (instancetype)initWithImageName:(NSString*) imageName;  

打开Castle.m,加入以下代码:

- (instancetype)initWithImageName:(NSString *)imageName {  
    if (self = [super init]) {  
    //2  
    SpriteComponent *spriteComponent = [[SpriteComponent alloc]initWithTexture:[SKTexture textureWithImageNamed:imageName]];
    [self addComponent:spriteComponent]; 
} 

    return self;
}  

这里有两个事情需要提醒:
1.如前所述,为游戏中每个类型对象创建GKEntity子类是很方便的。另一种方法,创建一个基础的GKEntity,并动态去添加你想要类型的组件,但经常你想只为特定的对象创建相应实体,那就用上面所述的方法。
2.在目前的情况下,你只需要为此实体添加一个组件-即你之前创建的组件。
现在,咱们已经有了一个实体和一个组件,我想你已经等不及将它加入到游戏中了。

设置实体

 

3. Spatial Partitioning

数据结构,组织游戏世界中的对象,以快速搜索位置或邻近度。

  • GKQuadtree

    • 用于根据二维空间中的位置来组织对象的数据结构。
  • GKQuadtreeNode

    • 用于管理在四叉树中组织的对象的辅助类。
  • GKOctree

    • 用于根据三维空间中的位置来组织对象的数据结构。
  • GKOctreeNode

    • 用于管理在八叉树中组织的对象的辅助类。
  • GKRTree

    • 一种数据结构,根据二维空间中的位置自适应组织对象。

3,GameplayKit里的ECS运行方式

因此,驱动实体和组件在游戏里面运行可以放在更新阶段,在SpriteKit引擎里的update方法里。GameplayKit提供了

GKEntity类GKComponent类及GKComponetSystem类

来实现ECS模式。GKEntity提供接口添加GKComponent组件,并管理这些组件。GKComponentSystem也能对GKComponent提供同样的管理操作。

游戏执行时,每一次调用update的更新操作时(spriteKit的update:操作和senceKit的renderer:updateAtTime:方法),调用游戏实体的更新方法,实体会将更新消息分发给组件(GKComponent),组件执行updateWithDeltaTime来驱动组件运行。

GameplayKit提供了两种分发策略:

实体更新:如果一个游戏仅有很少的游戏实体,遍历所有活动的实体,执行实体的updateWithDeltaTime方法。实体对象会将updateWithDeltaTime消息转发给该实体的所有组件(GKComponent),驱动组件执行updateWithDeltaTime方法。

组件更新:在一个更加复杂的游戏里面,不需要跟踪所有实体和组件对应关系,同类型组件能按顺序独立更新更加有用。为了满足此种情况,可以创建GKComponetSystem对像,GKComponetSystem管理一具体类型的组件,当每次游戏更新的时候,调用GKComponetSystem的updateWithDeltaTime的方法,系统(GKComponetSystem)对象会将updateWithDeltaTime消息转发给该系统的所有组件(GKComponent),驱动组件执行updateWithDeltaTime。

在方式里面,GKComponetSystem系统出现了,它可以被认为是管理同一类组件的容器,并驱动它更新所有该类型的更新操作。

综合以上说明,就是ECS模式。

Ok,我们回到正题,苹果为我们更好的进入苹果开发这个大坑,提供各种便利。代码的Sample是个非常好的手段。Maze游戏是一个类似吃豆人的简化版,只是这里没有吃豆豆,就是一个菱形与四个方形敌人的捕猎和反捕猎的循环。

Maze游戏代码下载地址

实体管理

在这一节中,你将创建一个工具类来管理你将要添加到游戏中的实体。这个工具将会保存所有游戏中的实体到一个列表中,并提供一些非常实用的方法,如添加和删除实体。
右键Entities组,在其下创建一个NSObject子类,名为EntityManager
打开EntityManager.h文件,添加以下代码:

//1
@property (strong, nonatomic) NSMutableSet <GKEntity*>*entities;
@property (strong, nonatomic) SKScene *scene;

//2
- (instancetype)initWithScene:(SKScene*) scene;

//3
- (void)addEntity:(GKEntity*) entity;//4- (void)removeEntity:(GKEntity*) entity;

打开EntityManager.m文件,添加以下代码:

- (NSMutableSet<GKEntity *> *)entities {
    if (!_entities) {
        _entities = [NSMutableSet set];
    }

    return _entities;
}

- (instancetype)initWithScene:(SKScene *)scene {
    if (self = [super init]) {
        self.scene = scene;
    }

    return self;
}

- (void)addEntity:(GKEntity *)entity {
    [self.entities addObject:entity];

    SKSpriteNode *spriteNode = [(SpriteComponent*)[entity componentForClass:[SpriteComponent class]] node];
    if (spriteNode) {
        [self.scene addChild:spriteNode];
    }
}

- (void)removeEntity:(GKEntity *)entity {
    SKSpriteNode *spriteNode = [(SpriteComponent*)[entity componentForClass:[SpriteComponent class]] node];
    if (spriteNode) {
        [spriteNode removeFromParent];
    }

    [self.entities removeObject:entity];
}

让我们看看这章节代码的含义:
1.这里声明了一个实体集合,并包含了场景。
2.这是个简单的初始化方法,并代入一个场景来初始化scene属性。
3.这个工具方法实现了添加实体到游戏中的功能,它将实体添加到集合中,然后检查这个实体是否包含一个SpriteComponent组件,如果有,则将它的节点加入到场景中。
4.当你要从游戏中删除实体时,调用这个工具方法,与addEntity:方法相反,如果实体包含了SpriteComponent组件,将其节点从场景中删除,并从集合中移除该实体。
在以后,你会添加更多的方法到这个工具类中,现在不正是一个好的开始吗?我们来显示些东西到场景中吧!

The Player Entity

下面代码是 player 类,它是一个简单的几成字 NodeEntity的类,拥有唯一一个组件。注意还有一个 GKAgent2D 的属性.

GKAgent2D 是 GKAgent的一个子类, 呈现为一个根据速度定位的本地坐标系统。

class Player: NodeEntity, GKAgentDelegate {
    let agent:GKAgent2D = GKAgent2D()

在本例中,代理其实是无言的。如果不是用户手动干预,它不会做任何事情,也不会对位置进行任何变化。我们需要一个代理,因为靶子组件必须有一个代理。

override init() {
    super.init()

在初始化中,我们添加一个 RenderComponent 和一个PlayerNode. 我们不详细讲 PlayerNode 了,因为非常枯燥。这里我们仅简单画一个黄色的方盒。

let renderComponent = RenderComponent(entity: self)
    renderComponent.node.addChild(PlayerNode())
    addComponent(renderComponent)

我们把代理设为自己,通过把代理添加到实体上去。

    agent.delegate = self
    addComponent(agent)
}

我们还需要去生命 GKAgentDelegate 的代理方法。这样,当代理更新后,Node 的位置会自动更新,同时,当用户手动更新了位置后,代理也会通过计算更新位置。

func agentDidUpdate(agent: GKAgent) {
    if let agent2d = agent as? GKAgent2D {
        node.position = CGPoint(x: CGFloat(agent2d.position.x), y: CGFloat(agent2d.position.y))
    }
}

func agentWillUpdate(agent: GKAgent) {
    if let agent2d = agent as? GKAgent2D {
        agent2d.position = float2(Float(node.position.x), Float(node.position.y))
    }
}
}

澳门新浦京娱乐场网站 13

4. Strategists

用于计划的AI形式在回合制游戏中移动。 通过创建采用游戏模型协议的类来描述您的游戏玩法,然后使用这些类与策略对象来创建AI玩家或建议移动。

  • GKStrategist

    • 提供人工智能的对象的一般接口,用于回合(和类似)游戏。
  • GKMinmaxStrategist

    • 使用确定性策略选择回合制游戏的人工智能。
  • GKMonteCarloStrategist

    • 使用概率策略选择回合制游戏的人工智能
  • GKGameModel

    • 实施这个协议来描述你的游戏模型,这样一个战略对象可以计划游戏的移动
  • GKGameModelPlayer

    • 执行这个协议来描述你的回合制游戏中的一个玩家,这样一个策略对象可以计划游戏的移动。
  • GKGameModelUpdate

    • 实施这个协议来描述在你的回合制游戏中的一个移动,以便一个策略对象可以计划游戏的移动。

1, Maze游戏的需求分析

Maze游戏的实体很少,就是player和enemies。分析下它们的需求:

player和enemies都需要在游戏中显示,很显然,它们需要显示渲染的组件。

player需要受到控制,在Maze迷宫里面运动。它需要一个控制组件。

enemies的控制需要计算机来给,因此要一个组件来给出enemies的状态。

enemies需要一个系统对象来管理状态的更新。

所以,我们现在至少需要:

2个实体,player和enemies。

3个组件,负责渲染的组件SpriteComponent,负责player控制的。PlayerControlComponent,负责enemies的AI的IntelligenceComponent。

1个系统,负责AI即IntelligenceComponent的控制。

如下图所示意:

澳门新浦京娱乐场网站 14Maze游戏ECS设计架构

添加城堡

打开GameScene.m文件,在@interface下方添加一个新的属性:

@property (strong, nonatomic) EntityManager *entityManager;

这里用来存放你刚刚创建的工具类实例。
接下来咱们先将entityManager懒加载,以方便之后调用:

- (EntityManager *)entityManager {
    if (!_entityManager) {
        _entityManager = [[EntityManager alloc]initWithScene:self];
    }

    return _entityManager;
}

然后我们创建一个城堡初始化的方法,来进行城堡添加:

- (void)setupCastle {
    //1
    Castle *humanCastle = [[Castle alloc]initWithImageName:@"castle1_atk"];
    SpriteComponent *spriteComponent1 = (SpriteComponent*)[humanCastle componentForClass:[SpriteComponent class]];
    if (spriteComponent1) {
        spriteComponent1.node.position = CGPointMake(spriteComponent1.node.size.width / 2, self.size.height / 2);
    }

    [self.entityManager addEntity:humanCastle];

    //2
    Castle *aiCastle = [[Castle alloc]initWithImageName:@"castle2_atk"];
    SpriteComponent *spriteComponent2 = (SpriteComponent*)[aiCastle componentForClass:[SpriteComponent class]];
    if (spriteComponent2) {
        spriteComponent2.node.position = CGPointMake(self.size.width - spriteComponent2.node.size.width / 2, self.size.height / 2);
    }

    [self.entityManager addEntity:aiCastle];
}

最后,我们将setupCastle方法在didMoveToView:方法中调用。
接下来,看一下代码:
1.创建一个城堡实体来代表我们的人类玩家,在完成实体创建后会进行节点的检查,如果有节点就将它放置到场景左侧,当然,在最后我们还要将它放置到我们的实体管理工具中。
2.跟上面的代码类似,不过这里我们创建的是基于AI的城堡。
完成这些,咱们可以赶紧build运行以下项目:

澳门新浦京娱乐场网站 15

截图5

The Missile Entity

missile 实体和 PlayerNode 略有不同。我们添加一个目标代理,让导弹去追踪。

class Missile: NodeEntity, GKAgentDelegate {

let missileNode = MissileNode()

required init(withTargetAgent targetAgent:GKAgent2D) {
    super.init()

    let renderComponent = RenderComponent(entity: self)
    renderComponent.node.addChild(missileNode)
    addComponent(renderComponent)

    let targetingComponent = TargetingComponent(withTargetAgent: targetAgent)
    targetingComponent.delegate = self
    addComponent(targetingComponent)
}

你可能注意到这个类中没有 GKAgent2D,这是因为我们使用了 TargetingComponent 来控制实体在场景中的移动。稍后,我们会讨论 TargetingComponent. 现在,我们需要知道,我们已经提供了 targetAgent ,我们启动代理的方法。

我们需要生命 agentDidUpdate 和 agentWillUpdate两个代理方法。这和Player类中有什么不同呢?在这个类中,我们还需要为方法提供 Z 轴的数值。

func agentDidUpdate(agent: GKAgent) {
    if let agent2d = agent as? GKAgent2D {
        node.position = CGPoint(x: CGFloat(agent2d.position.x), y: CGFloat(agent2d.position.y))
        node.zRotation = CGFloat(agent2d.rotation)
    }
}

func agentWillUpdate(agent: GKAgent) {
    if let agent2d = agent as? GKAgent2D {
        agent2d.position = float2(Float(node.position.x), Float(node.position.y))
        agent2d.rotation = Float(node.zRotation)
    }
}

Flying Penguin

5. Decision Trees

定义一系列问题和可能的答案,最终采取行动,或根据您提供的数据自动构建预测模型。

  • GKDecisionTree

    • 一个数据结构,模拟一系列具体的问题,他们可能的答案,以及一系列答案中的后续行动。
  • GKDecisionNode

    • 用于手动创建决策树的节点,代表特定的问题和可能的答案,或回答其他问题后的操作。

2,代码实现

实体entity实现:

代码里面,实体部分因为仅仅是个容器,所以比较简单,直接继承了GKEntity。在Maze游戏里面,每个实体其实在迷宫里面移动,都会有位置信息,因此,在公共的AAPLEntity类中,加入gridPosition信息(此信息为迷宫坐标值,并非SKScence里面的sprite位置)。

@import GameplayKit;

@interface AAPLEntity : GKEntity @property vector_int2 gridPosition; @end

组件component实现:

a,组件都是继承的GKComponent对象,该对象仅提供了谁持有该组件的变量entity和系统System对象。

b,实现更新阶段,调用的updateWithDeltaTime方法以及其他Helper方法。

Sprite组件

:继承GKComponent,相关变量和方法负责持有该组件实体的图像表现和计算控制运动表现两部分。Sprite部分的渲染和图像展示方法可查阅我之前写的相关的SpriteKit引擎教程,学习使用。

@interface AAPLSpriteComponent : GKComponent @property AAPLSpriteNode *sprite;... #pragma mark - Appearance// 重生时候心跳动画@property (nonatomic) BOOL pulseEffectEnabled; // 捕猎状态外在- useNormalAppearance;... #pragma mark - Movement // 移动相关方法@property (nonatomic) vector_int2 nextGridPosition;- followPath:(NSArray<GKGridGraphNode *> *)path completion:completionHandler; @end

PlayerControl组件

:实现play的控制逻辑,在更新阶段,通过实体Entity执行updateWithDeltaTime方法来实现对Player实体的控制的计算,这里可以用手势,也可以用键盘来确定移动的方向,PlayerControl组件来完成实际控制操作。

在Player实体的控制移动过程中,实际上也要调用Sprite组件。Sprite组件控制实体在游戏场景中的move渲染,即运动表现部分。

所以这是组件间配合,PlayerControl组件,仅负责处理告诉Player实体该怎么移动,具体移动渲染表现,还是交给Sprite组件来负责。其实运行示意图如下所示:

澳门新浦京娱乐场网站 16PlayerControl组件处理逻辑

Enemies的AI组件:

通过状态机来实现enemies实体状态的迁移。状态机在后面的系列文章里面会具体描述,这里仅理解为控制敌人方块行为的算法即可。状态机初始化代码如下所示:

// 初始化enemies的四种不同状态 AAPLEnemyChaseState *chase = [[AAPLEnemyChaseState alloc] initWithGame:game entity:enemy];AAPLEnemyFleeState *flee = [[AAPLEnemyFleeState alloc] initWithGame:game entity:enemy];AAPLEnemyDefeatedState *defeated = [[AAPLEnemyDefeatedState alloc] initWithGame:game entity:enemy];defeated.respawnPosition = origin;AAPLEnemyRespawnState *respawn = [[AAPLEnemyRespawnState alloc] initWithGame:game entity:enemy]; // 初始化状态机,并进入chase状态_澳门新浦京娱乐场网站:的iOS游戏开发实例,基本概览。stateMachine = [GKStateMachine stateMachineWithStates:@[chase, flee, defeated, respawn]];[_stateMachine enterState:[AAPLEnemyChaseState class]];

enemies的AI组件需要加入到intelligenceSystem的系统进行管理,因为所有的AI都需要在更新阶段进行执行,使用System管理,更加方便。系统的初始化代码如下所示(在游戏初始化过程代码里):

// 初始化intelligenceSystem_intelligenceSystem = [[GKComponentSystem alloc] initWithComponentClass:[AAPLIntelligenceComponent class]];NSMutableArray<AAPLEntity *> *enemies = [NSMutableArray arrayWithCapacity:_level.enemyStartPositions.count];[_level.enemyStartPositions enumerateObjectsUsingBlock:^(GKGridGraphNode *node, NSUInteger index, BOOL *stop) {AAPLEntity *enemy = [[AAPLEntity alloc] init];enemy.gridPosition = node.gridPosition;[enemy addComponent:[[AAPLSpriteComponent alloc] initWithDefaultColor:colors[index]]];[enemy addComponent:[[AAPLIntelligenceComponent alloc] initWithGame:self enemy:enemy startingPosition:node]];// 讲enemy实体加入到intelligenceSystem[_intelligenceSystem addComponentWithEntity:enemy];[enemies addObject:enemy]; }];_enemies = [enemies copy];

其具体的更新运行图见节所示。

更新阶段

SpriteKit引擎的每一帧画面渲染前,SKScene都会调用update:方法(

Maze游戏里是设置了Delegate,执行update:forScene方法

)来执行更新,即前面分析的更新阶段。此阶段,Maze游戏对Player和Enemies两组实体对象进行更新,如代码所示:

// Update components with the new time delta.[self.intelligenceSystem updateWithDeltaTime:dt];[self.player updateWithDeltaTime:dt];

分别执行相关对象的updateWithDeltaTime方法即可。

另外,下图显示了enemies如何在update更新阶段,驱动IntelligenceCompoent运行的:

澳门新浦京娱乐场网站 17Enemies的AI状态处理组件逻辑示意图

现在,我们的Maze游戏的ECS就在Spritekit游戏引擎的驱动下运行着,大家赶快试试这个小游戏啊!

在Maze代码里面,还有很多其它重要功能,如:

1,状态机。

2,寻路功能。

3,一个GKRuleSystem,用来管理判断与Player的距离。

4,随机数的生成。

我会按照写作计划一步一步完成,希望大家都能获得收获。

第二个组件

当你开发一个基于实体组件系统的游戏时,游戏对象所需的所有数据必须存放在某些组件中。
在游戏中所要跟踪的一个数据是对象属于哪个队伍:一队还是二队。由于该信息并不属于你的节点组件,所以你可能希望拥有一个既不是一队也不是二队的实体。我们赶紧为此功能创建一个新组件吧!
右键Components组,在其下创建一个GKComponent子类,名为TeamComponent
打开TeamComponent.h文件,添加以下代码:

//1
typedef NS_ENUM(NSInteger, Team) {
    Team1 = 1,
    Team2 = 2,
};

...

//2
@property (assign, nonatomic) Team team;

- (instancetype)initWithTeam:(Team) team;
- (Team)oppsiteTeam:(Team) team;

打开TeamComponent.m文件,添加以下代码:

- (instancetype)initWithTeam:(Team)team {
    if (self = [super init]) {
        self.team = team;
    }

    return self;
}

- (Team)oppsiteTeam:(Team)team {
    switch (team) {
        case Team1:
            return Team2;
            break;
        case Team2:
            return Team1;
            break;    
        default:
            break;
    }
}

这是个相当简单的文件,我只想指出两点:
1.这是来跟踪判断哪个队伍的枚举:Team1和Team2,下面还有一个来返回对方队伍的方法,以后会用到。
2.这是个非常简单的组件,只是用来跟踪该实体属于哪个队伍。
现在,你有了一个新组件,让我们更新一下城堡实体代码,打开Castle.h文件修改初始化方法,添加一个参数:

- (instancetype)initWithImageName:(NSString*) imageName Team:(Team) team;

打开Castle.m文件修改初始化方法:

- (instancetype)initWithImageName:(NSString *)imageName Team:(Team)team{
    if (self = [super init]) {
        //2
        SpriteComponent *spriteComponent = [[SpriteComponent alloc]initWithTexture:[SKTexture textureWithImageNamed:imageName]];
        [self addComponent:spriteComponent];
        [self addComponent:[[TeamComponent alloc] initWithTeam:team]];
    }

    return self;
}

最后,在GameScene.m中将我们的城堡添加上队伍:

Castle *humanCastle = [[Castle alloc]initWithImageName:@"castle1_atk" Team:Team1];

...

Castle *aiCastle = [[Castle alloc]initWithImageName:@"castle2_atk" Team:Team2];

build运行游戏,你应该看不到任何变化,但其实你已经将一组数据成功与实体绑定,以后将会派上用场:

澳门新浦京娱乐场网站 18

截图6

The Targeting Component

到目前为止,所有的类相对都是轻便的。你可能都忘了还需要在靶子组件中完成逻辑代码
幸运的是, 得益于 GameplayKit,在本例中,我们仅需要写20行代码就可以。

class TargetingComponent: GKAgent2D {

  let target:GKAgent2D

  required init(withTargetAgent targetAgent:GKAgent2D) {

      target = targetAgent

      super.init()

      let seek = GKGoal(toSeekAgent: targetAgent)

      self.behavior = GKBehavior(goals: [seek], andWeights: [1])

      self.maxSpeed = 4000
      self.maxAcceleration = 4000
      self.mass = 0.4
  }
}

这段代码简单的不需解释。你可以看到他继承自 GKAgent2D, 创建了一个GKGoal.然后通过这个goal 创建了CKBehavior对象。如果你有多个 goal,例如去追踪一个目标同时要避开某个目标,你就可以创建多个 GKGoal。 你甚至还可以分别GKGoal 的 weight 属性,这样可以设置避开某个 goal 比追逐某个 goal 的权重更重一些。

我们同时也设置了一些其他的属性:maxSpeed,maxAcceleration 和 mass. 这些属性需要根据你的实际场景进行设置,这里设置成这样对我来说是合适的。刚开始的时候我使用了默认值,然后以为那里出来毛病。后来发现是默认值太低了,导致移动非常慢,完全看不出效果。

PenguinEntity.swift 是用来管理 Component三个组件的

6. Pathfinding

创建图形,模拟您的游戏世界的导航性,允许GameplayKit规划游戏角色的最佳路线。

  • GKGraph

    • 描述游戏世界的可导航性并提供寻路方法来搜索通过该空间的路线的节点的集合。
  • GKObstacleGraph

    • 2D游戏世界的导航图形创建了一个最小的网络,用于在障碍物周围进行精确的路径查找。
  • GKMeshGraph

    • 2D游戏世界的导航图形创建了一个空间填充网络,可以在障碍物周围进行平滑寻路。
  • GKGridGraph

    • 2D游戏世界的导航图,其中运动被限制为整数网格。
  • GKGraphNode

    • 导航图中的单个节点用于寻路。
  • GKGraphNode2D

    • 导航图中的一个节点,与连续2D空间中的一个点相关联。
  • GKGraphNode3D

    • 导航图中的一个节点,与连续三维空间中的一个点相关联。
  • GKGridGraphNode

    • 导航图中的一个节点,与离散二维网格上的位置关联。

第三个组件

你需要跟踪的另一条数据则是每个玩家目前的金币数量。在这个游戏中,由于每边都有一座城堡,你可以把城堡想象成是每位玩家的指挥官,所以城堡将是存储这个信息的最好地方。
右键Components组,创建GKComponent子类,取名为CastleComponent
打开CastleComponent.h,代入以下代码:

//1
@property (assign, nonatomic) NSInteger coins;

打开CastleComponent.m,代入以下代码:

@interface CastleComponent ()

//1
@property (assign, nonatomic) NSTimeInterval lastCoinDrop;

@end

@implementation CastleComponent

- (instancetype)init {
    if (self = [super init]) {
        self.coins = 0;
        self.lastCoinDrop = 0;
    }

    return self;
}

//2
- (void)updateWithDeltaTime:(NSTimeInterval)seconds {
    [super updateWithDeltaTime:seconds];

    //3
    NSTimeInterval coinDropInterval = 0.5;
    NSInteger coinsPerInterval = 10;

    if (CACurrentMediaTime() - self.lastCoinDrop > coinDropInterval) {
        self.lastCoinDrop = CACurrentMediaTime();
        self.coins  = coinsPerInterval;
    }
}

这个组件与之前的有些许不同,所以需要更详细地进行代码解释:
1.这里有两个属性,一个是公开的存储城堡金钱数量的coins属性,另一个是非公开的上一次赚取金钱时间的lastCoinDrop属性。
2.在游戏中每帧进行更新的,SpriteKit调用的updateWithDeltaTime:方法,注意,SpriteKit不会自动调用这个方法,你需要一点设置来实现它,你马上会学到。
3.这里的代码就是定期获得金钱的方法。
打开Castle.m文件,将以下代码添加到initWithImageName:Team:方法中:

[self addComponent:[CastleComponent new]];

接下来所添加的代码即是我之前所提到的需要进行调用updateWithDeltaTime:的方法。为此,我们需要切换到EntityManager.h并添加一个新的属性。

@property (strong, nonatomic) NSMutableArray <GKComponentSystem*>* componentSystems;

EntityManager.m中实现:

- (NSMutableArray<GKComponentSystem *> *)componentSystems {
    if (!_componentSystems) {
        _componentSystems = [NSMutableArray array];
        GKComponentSystem *castleSystem = [[GKComponentSystem alloc]initWithComponentClass:[CastleComponent class]];
        [_componentSystems addObject:castleSystem];
    }

    return _componentSystems;
}

GKComponentSystem是一个存储组件集合的类。在这里,我们创建一个拥有GKComponentSystem的数组,其中就能获取一个专门来跟踪你游戏中所有CastleComponent实例的GKComponentSystem
现在这个数组中可能只会用到CastleComponentGKComponentSystem,但是以后可能会有更多其他的类型。
然后将这个属性运用到addEntity:方法中:

for (GKComponentSystem *componentSystem in self.componentSystems) {
        [componentSystem addComponentWithEntity:entity];
    }

当你添加一个新实体的时候,都会将这个实体的相对应组件添加到componentSystems数组的GKComponentSystem中(现如今这个数组只包含Castle的ComponentSystem的)。不用担心性能问题,因为你的实体不包含CastleComponent,就不会发生任何事情。
EntityManager.h中添加一个新属性:

@property (strong, nonatomic) NSMutableSet <GKEntity*>*toRemove;

EntityManager.m中懒加载toRemove属性,并在removeEntity:中添加此方法:

- (NSMutableSet<GKEntity *> *)toRemove {
    if (!_toRemove) {
        _toRemove = [NSMutableSet set];
    }

    return _toRemove;
}

- (void)removeEntity:(GKEntity *)entity {
...
[self.toRemove addObject:entity];
}

你应该注意到,我们并不会直接从组件系统直接删除实体,而是将实体添加到toRemove中,以便以后删除。这样做的目的就在于我们在componentSystem迭代时更好地删除其中的组件,然后再删除实体,不然我们直接删除实体之后无法进行组件系统中的组件删除。
EntityManager.h中继续添加方法:

- (void)updateWithDeltaTime:(CFTimeInterval) deltaTime;

EntityManager.m中方法:

- (void)updateWithDeltaTime:(CFTimeInterval)deltaTime {
    //1
    for (GKComponentSystem *componentSystem in self.componentSystems) {
        [componentSystem updateWithDeltaTime:deltaTime];
    }

    //2
    for (GKEntity *entity in self.toRemove) {
        for (GKComponentSystem *componentSystem in self.componentSystems) {
            [componentSystem removeComponentWithEntity:entity];
        }
    }

    [self.toRemove removeAllObjects];
}

让我们来看看这一节代码:
1.在这里我们首先遍历了所有组件系统,并调用每个组件系统的updateWithDeltaTime:方法,这样每个组件系统都会依次调用其updateWithDeltaTime:方法。
这实际上就表明了使用GKComponentSystem的好处及目的,这样的设置方法,组件系统更新一次,其下组件就会进行更新,在游戏中,对每个系统(物理引擎,渲染)的处理顺序就能进行精确控制,这很方便。
2.这里我们循环获取将要删除的实体,然后将其在组件系统中的组件删除,最后再将实体删除。
这里还有最后一个方法需要加入到这个类中,打开EntityManager.h文件,添加新方法:

- (GKEntity*)castleForTeam:(Team) team;

EntityManager.m中实现:

- (GKEntity *)castleForTeam:(Team)team {
    for (GKEntity *entity in self.entities) {
        TeamComponent *teamComponent = (TeamComponent*)[entity componentForClass:[TeamComponent class]];
        CastleComponent *castleComponent = (CastleComponent*)[entity componentForClass:[CastleComponent class]];
        if (teamComponent) {
            if (teamComponent.team == team && castleComponent) {
                return entity;
            }
        }
    }

    return nil;
}

这是一个很方便的方法来获取一个指定队伍的城堡。在这里我们遍历所有实体,并检查其team组件,如果其组件相应的队伍和传入的队伍相同,且拥有城堡组件则返回对应的城堡实体。

注意:这样做的另一种方法是在创建城堡实体时,保留对城堡实体的引用。 但是像这样动态地查找东西的优点在于你的游戏更加灵活。 虽然在这种情况下你可能不需要灵活性,但我想向你展示是因为在许多游戏中,这种灵活性非常方便。 实体组件系统架构的主要优点在于灵活性。
现在我们将其挂载到游戏中,打开GameScene.m文件,在update:方法中添加代码:

CFTimeInterval deltaTime = currentTime - _lastUpdateTimeInterval;
    _lastUpdateTimeInterval = currentTime;

    [self.entityManager updateWithDeltaTime:deltaTime];

    GKEntity *human = [self.entityManager castleForTeam:Team1];
    if (human) {
        CastleComponent *humanCastle = (CastleComponent*)[human componentForClass:[CastleComponent class]];
        self.coin1Label.text = [NSString stringWithFormat:@"%ld", humanCastle.coins];
    }

    GKEntity *ai = [self.entityManager castleForTeam:Team2];
    if (ai) {
        CastleComponent *aiCastle = (CastleComponent*)[ai componentForClass:[CastleComponent class]];
        self.coin2Label.text = [NSString stringWithFormat:@"%ld", aiCastle.coins];
    }

在这里你调用了实体管理工具的updateWithDeltaTime:方法,然后你将为每一个队伍找到其城堡(或城堡组件),然后更新每一个队伍上方的金钱显示Label。
build并运行,就能看到队伍双方的金钱不断上涨。

澳门新浦京娱乐场网站 19

截图7

The Missile Node

现在 Missile entity 创建好了,我们需要给它添加一个node,以在场景中显示。这个node 是SKNode的子类,有一个单独的方法。

func setupEmitters(withTargetScene scene:SKScene) {
  let smoke = NSKeyedUnarchiver.unarchiveObjectWithFile(NSBundle.mainBundle().pathForResource("MissileSmoke", ofType:"sks")!) as! SKEmitterNode
  smoke.targetNode = scene
  self.addChild(smoke)

  let fire = NSKeyedUnarchiver.unarchiveObjectWithFile(NSBundle.mainBundle().pathForResource("MissileFire", ofType:"sks")!) as! SKEmitterNode
  fire.targetNode = scene
  self.addChild(fire)
}

你可以看到setupEmitters 方法创建了两个 SKEmitter nodes.把 target node 设置为了场景,如果不设置的话,那么就不会出现跟踪导弹并冒烟的效果。你可以打开 MissileFire.sks 和 MissileSmoke.sks 两个文件,查看具体内容,这里我们不详细解释了。

import SpriteKit
import GameplayKit

class PenguinEntity:GKEntity {

    var spriteComponent:SpriteComponent! // 属性 大小 texture
    var moveComponent:MoveComponent!     // 移动组件功能;
    var animationComponent:AnimationComponent! //拍打翅膀的组件;

    init(imageName:String) {
        super.init()
        let texture = SKTexture(imageNamed: imageName)
        spriteComponent = SpriteComponent(entity: self, texture: texture, size: texture.size())
        addComponent(spriteComponent)
        // 加入上下飞动的组件
        moveComponent = MoveComponent(entity: self)
        addComponent(moveComponent)

        // 加入拍打翅膀的动画
        let textureAltas = SKTextureAtlas(named: "penguin")
        var textures = [SKTexture]()
        for i in 1...textureAltas.textureNames.count {
            let imageName = "penguin0(i)"
            textures.append(SKTexture(imageNamed: imageName))
        }
        animationComponent = AnimationComponent(entity: self, textures: textures)
        addComponent(animationComponent)

    }
     // Add Physics
    func addPhysics(){
        let spriteNode = spriteComponent.node
        spriteNode.physicsBody = SKPhysicsBody(texture: spriteNode.texture!, size: spriteNode.frame.size)
        spriteNode.physicsBody?.categoryBitMask  = PhysicsCategory.Player
        spriteNode.physicsBody?.contactTestBitMask = PhysicsCategory.Coin | PhysicsCategory.Obstacle | PhysicsCategory.Floor
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

7. Agents, Goals, and Behaviors

通过结合高层次的目标,例如移动到目标,遵循路径或避开障碍,将自主移动添加到角色和其他游戏对象。

  • GKAgent

    • 根据一组目标和现实约束来移动游戏实体的组件。
  • GKAgent2D

    • 在二维空间中操作的代理。
  • GKAgent3D

    • 在三维空间中运作的代理
  • GKGoal

    • 激励一个或多个代理人移动的影响。
  • GKBehavior

    • 一套共同影响代理人运动的目标。
  • GKCompositeBehavior

    • 一组行为,每个行为都是一组目标,共同影响一个代理人的行动。
  • GKPath

    • 一个代理可以跟随的多边形路径。
  • GKAgentDelegate

    • 实现这个协议来同步一个代理的状态和它在游戏中的可视化表示。

生产单位

现在,这个游戏已经准备好出现一些单位了。让我们修改游戏以便生成Quirk单位。
右键Entities组,创建GKEntity子类,取名为Quirk
打开Quirk.h文件,添加以下代码:

- (instancetype)initWithTeam:(Team) team;

打开Quirk.m文件,添加以下代码:

- (instancetype)initWithTeam:(Team)team {
    if (self = [super init]) {
        SKTexture *texture = [SKTexture textureWithImageNamed:[NSString stringWithFormat:@"quirk%ld", team]];
        SpriteComponent *spriteComponent = [[SpriteComponent alloc]initWithTexture:texture];
        [self addComponent:spriteComponent];
        [self addComponent:[[TeamComponent alloc] initWithTeam:team]];
    }

    return self;
}

上面的代码与设置城堡实体非常相似,在这里,咱们通过队伍进行纹理设置,并将sprite组件添加到实体中。此外,我们还添加了team组件来完成这个实体的所有需求。
现在是创建Quirk实体的实例的时候了。上一次,你直接在GameScene中创建了城堡实体,但是这一次,我们要将这个产生Quirk单位的代码放到EntityManager中。
我们打开EntityManager.h文件添加方法:

- (void)spawnQuirkWithTeam:(Team) team;

EntityManager.m文件中实现方法:

- (void)spawnQuirkWithTeam:(Team)team {
    //1
    GKEntity *teamEntity = [self castleForTeam:team];
    CastleComponent *teamCastleComponent = (CastleComponent*)[teamEntity componentForClass:[CastleComponent class]];
    SpriteComponent *teamSpriteComponent = (SpriteComponent*)[teamEntity componentForClass:[SpriteComponent class]];

    if (!teamEntity || !teamCastleComponent || !teamSpriteComponent) {
        return;
    }

    //2
    if (teamCastleComponent.coins < costQuirk) {
        return;
    }

    teamCastleComponent.coins -= costQuirk;
    [self.scene runAction:[SingleSoundManager soundSpawn]];

    //3
    Quirk *monster = [[Quirk alloc]initWithTeam:team];
    SpriteComponent *spriteComponent = (SpriteComponent*)[monster componentForClass:[SpriteComponent class]];
    if (spriteComponent) {
        spriteComponent.node.position = CGPointMake(teamSpriteComponent.node.position.x, [MyUtils randomFloatWithMin:self.scene.size.height * 0.25 Max:self.scene.size.height * 0.75]);
        spriteComponent.node.zPosition = 2;
    }

    [self addEntity:monster];
}

解读一下这节代码:
1.哪个城堡的单位就应该在该城堡附近产生,为了做到这一点,我们需要获得城堡的精灵,所以这些代码是动态获取这些信息的。
2.这是用来检查城堡是否拥有足够的金钱购买单位,如果足够,那就减去相应的费用,并播放生产的音效。
3.这是创建一个Quirk实体并将其放置在城堡附近(以随机y值)的代码。
最后,在GameScene.m中的quirkPressed方法中添加生产单位的方法:

- (void)quirkPressed {
    ...

    [self.entityManager spawnQuirkWithTeam:Team1];
}

运行一下,点击Quirk的按钮,只要你拥有足够的金钱,就能生产Quirk单位了!

澳门新浦京娱乐场网站 20

截图8

Combining the Parts

现在我们的nodes, entities 和 components都已经创建好了,我们回到 GameScene.swift文件中,把它们组合起来。 我们需要重载 didMoveToView方法。

override func didMoveToView(view: SKView) {
  super.didMoveToView(view)

我们已经在初始化是创建了 player,所以我们添加player.node到场景中。

 self.addChild(player.node)

对于missile, 我们也必须要在这里创建好。

missile = Missile(withTargetAgent: player.agent)

然后我们为 missile 添加setupEmitters方法,让烟雾可以根据目标移动并扩散,而非只是动一下。

missile!.setupEmitters(withTargetScene: self)
self.addChild(missile!.node)

最后,所有的entities创建好后,我们添加它的components到我们的组件系统中。

for componentSystem in self.componentSystems {
    componentSystem.addComponentWithEntity(player)
    componentSystem.addComponentWithEntity(missile!)
}

现在在update.currentTime方法中,为组件的更新时间数组,添加增量时间。这会使的重新计算时间并进行渲染。

override func update(currentTime: NSTimeInterval) {

  // Calculate the amount of time since `update` was last called.
  let deltaTime = currentTime - lastUpdateTimeInterval

  for componentSystem in componentSystems {
      componentSystem.updateWithDeltaTime(deltaTime)
  }

  lastUpdateTimeInterval = currentTime
}

这就是全部我们做的了。现在运行一下游戏,你会看到一个导弹始终跟随着playe。在这里我们并没有添加碰撞和爆炸效果,如果你感兴趣可以自己做一下。为什么不呢?

SpriteComponent 组件:精灵的皮肤、大小

8. Obstacles

在游戏世界中模拟不可访问区域的类,用于Pathfinding和Agents。

  • GKObstacle

    • 游戏世界中代表不可访问区域的对象的抽象基类。
  • GKCircleObstacle

    • 代理要避免的一个循环不能通行的区域。
  • GKSphereObstacle

    • 代理应避免的球形不可通过的体积。
  • GKPolygonObstacle

    • 2D游戏世界中的多边形不可通行区域。

代理,目标和行为

到目前为止,Quirk单位只是待在那里一动不动,这个游戏需要运动!
幸运的是,GameplayKit有一个叫做”代理,目标和行为“的组合,它们可以让你在游戏中非常复杂的行为变得超级简单,这里使它们的工作方式:

  • GKAgent2DGKComponent的子类,它用来处理游戏中的移动对象,你可以设置它不同属性如最大速度,加速度等等,以及使用GKBehavior
  • GKBehavior类中包含了一组GKGoal,这些用来表示你想要让你的对象如何移动。
  • GKGoal代表了你的一个代理移动的目标-比方说向另一个代理移动。
    所以通常你设置这些对象,然后将GKAgent组件添加到你的类中,GameplayKit会移动一切你想移动的对象。

注意:这里有个说明:GKAgent2D不会直接移动你的精灵,它只是适时地更新自己的位置,所以你需要写一些精灵与GKAgent位置相关联的代码来进行移动处理。

我们首先从行为和目标开始,右键Components组,创建GKBehavior子类,名为MoveBehavior
打开MoveBehavior.h文件,添加以下代码:

- (instancetype)initWithTargetSpeed:(CGFloat) targetSpeed Seek:(GKAgent*) seek Avoid:(NSArray<GKAgent*>*) avoid;

打开MoveBehavior.m文件,实现方法:

- (instancetype)initWithTargetSpeed:(CGFloat)targetSpeed Seek:(GKAgent *)seek Avoid:(NSArray<GKAgent *> *)avoid {
    if (self = [super init]) {
        //1
        if (targetSpeed > 0) {
            //2
            [self setWeight:0.1 forGoal:[GKGoal goalToReachTargetSpeed:targetSpeed]];
            //3
            [self setWeight:0.5 forGoal:[GKGoal goalToSeekAgent:seek]];
            //4
            [self setWeight:1.0 forGoal:[GKGoal goalToAvoidAgents:avoid maxPredictionTime:1.0]];
        }
    }

    return self;
}

这里有很多新东西,让我们一个个看:
1.如果速度小于了0,就不需要添加任何目标来移动。
2.要为行为添加目标,则请使用setWeight:forGoal:方法,它允许你指定一个目标,通过权重来判断重要性-权重越大越优先。在这个实例中,我们设定了一个小的优先级目标来让代理达到目标速度。
3.在这里设置了一个中等优先级的目标,这个目标让代理向另一个代理移动,这样你的单位会向最接近的敌人靠近。
4.在这里我们设置了一个高优先级的目标来让代理避免与其他的一组代理发生冲突。这样可以让你的单位完美地避开盟友,让它们更好地散开来。
现在,我们已经创建了行为和目标,我们就可以设置代理了。右键Components组,创建一个GKAgent2D子类,取名为MoveComponent
打开MoveComponent.h文件,添加协议GKAgentDelegate并添加以下代码:

//1
@property (strong, nonatomic) EntityManager *entityManager;

- (instancetype)initWithMaxSpeed:(CGFloat) maxSpeed MaxAcceleration:(CGFloat) maxAcceleration Radius:(CGFloat) radius EntityManager:(EntityManager*) entityManager;

打开MoveComponent.m文件,添加以下代码:

//2
- (instancetype)initWithMaxSpeed:(CGFloat)maxSpeed MaxAcceleration:(CGFloat)maxAcceleration Radius:(CGFloat)radius EntityManager:(EntityManager *)entityManager {
    if (self = [super init]) {
        self.entityManager = entityManager;

        self.delegate = self;
        self.maxSpeed = maxSpeed;
        self.maxAcceleration = maxAcceleration;
        self.radius = radius;
        self.mass = 0.01;
    }

    return self;
}
//3
- (void)agentWillUpdate:(GKAgent *)agent {
    SpriteComponent *spriteComponent = (SpriteComponent*)[self.entity componentForClass:[SpriteComponent class]];
    if (!spriteComponent) {
        return;
    }

    self.position = [MyUtils initFloat2WithPoint:spriteComponent.node.position];
}
//4
- (void)agentDidUpdate:(GKAgent *)agent {
    SpriteComponent *spriteComponent = (SpriteComponent*)[self.entity componentForClass:[SpriteComponent class]];
    if (!spriteComponent) {
        return;
    }

    spriteComponent.node.position = CGPointMake(self.position.x, self.position.y);
}

这里也有很多新玩意儿,我们逐一介绍:
1.我们需要引用entityManger以便访问其他游戏中的实体。比方说,你需要知道最接近的敌人(所以你能寻找它)和你的盟友列表(所以你要分开它们)。
2.GKAgent2D具有最大速度,最大加速度等等,所以在这里传参来配置初始化。顺便也将自己的代理设置为了自己,并将它的质量设置的很小,这样它的转向就会更方便。
3.在代理更新其位置之前,先将代理的位置设置为sprite组件的位置,这样代理就会被定位在正确的位置。注意这里有一些恶心的转换-GameplayKit使用float2而不是CGPoint,十分恶心。
4.同样的,当代理更新完它的位置调用agentDidUpdate:方法时,你需要将精灵的位置更新以匹配代理的位置。
在这里,你仍然还有许多事情要做,但首先,让我们添加一些辅助方法。打开EntityManager.h文件:

@class MoveComponent;

...

- (NSArray<GKEntity*>*)entitiesForTeam:(Team) team;
- (NSArray<MoveComponent*>*)moveComponentsForTeam:(Team) team;

EntityManager.m文件中:

- (NSArray<GKEntity *> *)entitiesForTeam:(Team)team {
    NSMutableArray *arr = [NSMutableArray array];
    for (GKEntity *entity in self.entityManager.entities) {
        TeamComponent *teamComponent = (TeamComponent*)[entity componentForClass:[TeamComponent class]];
        if (teamComponent && teamComponent.team == team) {
            [arr addObject:entity];
        }
    }

    return arr;
}
- (NSArray<MoveComponent *> *)moveComponentsForTeam:(Team)team {
    NSArray *entitiesToMove = [self entitiesForTeam:team];
    NSMutableArray *moveComponents = [NSMutableArray array];
    for (GKEntity *entity in entitiesToMove) {
        MoveComponent *moveComponent = (MoveComponent*)[entity componentForClass:[MoveComponent class]];
        if (moveComponent) {
            [moveComponents addObject:moveComponent];
        }
    }

    return moveComponents;
}

entitiesForTeam:方法返回了指定队伍的所有实体,moveComponentsForTeam:则返回了指定队伍的所有move组件,你很快就会用到它们。
我们继续在MoveComponent.h文件添加新方法:

- (GKAgent2D*)closestMoveComponentForTeam:(Team) team;

MoveComponent.m文件实现:

- (GKAgent2D *)closestMoveComponentForTeam:(Team)team {
    MoveComponent *closestMoveComponent = nil;
    CGFloat closestDistance = 0;
    NSArray *enemyMoveComponents = [self.entityManager moveComponentsForTeam:team];

    for (MoveComponent *enemyMoveComponent in enemyMoveComponents) {
        CGPoint point1 = CGPointMake(enemyMoveComponent.position.x, enemyMoveComponent.position.y);
        CGPoint point2 = CGPointMake(self.position.x, self.position.y);

        CGFloat distance = [MyUtils lengthWithPoint:[MyUtils minusPointWithPoint1:point1 Point2:point2]];
        if (closestMoveComponent == nil || distance < closestDistance) {
            closestMoveComponent = enemyMoveComponent;
            closestDistance = distance;
        }
    }

    return closestMoveComponent;
}

这些代码用来寻找当前move组件相对于指定队伍中最接近的move组件,你现在可以用这个方法找到最接近的敌人了。
将这个方法添加到该类的最底端:

- (void)updateWithDeltaTime:(NSTimeInterval)seconds {
    [super updateWithDeltaTime:seconds];

    //1
    GKEntity *entity = self.entity;
    TeamComponent *teamComponent = (TeamComponent*)[entity componentForClass:[TeamComponent class]];
    if (!teamComponent) {
        return;
    }

    //2
    GKAgent2D *enemyMoveComponent = [self closestMoveComponentForTeam:[teamComponent oppsiteTeam:teamComponent.team]];
    if (!enemyMoveComponent) {
        return;
    }

    //3
    NSArray *alliedMoveComponents = [self.entityManager moveComponentsForTeam:teamComponent.team];

    //4
    self.behavior = [[MoveBehavior alloc]initWithTargetSpeed:self.maxSpeed Seek:enemyMoveComponent Avoid:alliedMoveComponents];
}

这里有个update方法将所有相关联的联系在一起:
1.寻找到当前实体的team组件。
2.使用辅助方法找到最近的敌人信息。
3.使用辅助方法获取所有盟友的move组件。
4.最后,将最新的行为设置到当前行为上。
几乎要完成了,但还有几个代码需要清理,打开EntityManager.m文件,更新componentSystems属性:

- (NSMutableArray<GKComponentSystem *> *)componentSystems {
    if (!_componentSystems) {
        _componentSystems = [NSMutableArray array];
        GKComponentSystem *castleSystem = [[GKComponentSystem alloc]initWithComponentClass:[CastleComponent class]];
        [_componentSystems addObject:castleSystem];

        GKComponentSystem *moveSystem = [[GKComponentSystem alloc]initWithComponentClass:[MoveComponent class]];
        [_componentSystems addObject:moveSystem];
    }

    return _componentSystems;
}

记住,这是必须的,这样才能使updateWithDeltaTime:方法在MoveComponent中调用。
接下来打开Quirk.h文件,修改它的初始化方法,加入entityManager作为参数:

- (instancetype)initWithTeam:(Team) team EntityManager:(EntityManager*) entityManager;

Quirk.m中:

- (instancetype)initWithTeam:(Team)team EntityManager:(EntityManager *)entityManager{

...

        [self addComponent:[[MoveComponent alloc] initWithMaxSpeed:150 MaxAcceleration:5 Radius:texture.size.width * 0.3 EntityManager:entityManager]];

...
}

这里设置了一些move组件的值,用来工作于Quirk单位。
你还需要一个move组件给城堡-让它成为最大敌人。为了实现这个,打开Castle.h,修改其初始化方法,加入entityManager参数:

- (instancetype)initWithImageName:(NSString*) imageName Team:(Team) team EntityManager:(EntityManager*) entityManager;

Castle.m中修改:

- (instancetype)initWithImageName:(NSString *)imageName Team:(Team)team EntityManager:(EntityManager *)entityManager{

...

        [self addComponent:[[MoveComponent alloc] initWithMaxSpeed:0 MaxAcceleration:0 Radius:spriteComponent.node.size.width / 2 EntityManager:entityManager]];

...

}

最后,移步到EntityManager.m中,修改spawnQuirkWithTeam:方法:

Quirk *monster = [[Quirk alloc]initWithTeam:team EntityManager:self];

同样的,打开GameScene.m文件,修改setupCastle方法:

Castle *humanCastle = [[Castle alloc]initWithImageName:@"castle1_atk" Team:Team1 EntityManager:self.entityManager];

...

Castle *aiCastle = [[Castle alloc]initWithImageName:@"castle2_atk" Team:Team2 EntityManager:self.entityManager];

现在运行起来看看,赶紧欣赏一下你的移动单位吧:

澳门新浦京娱乐场网站 21

截图9

祝贺你,完成到这里,你应该对实体组件系统和代理,目标和行为组合有了一定的了解,愿你在以后的开发道路上越走越远。
澳门新浦京娱乐场网站,这里是OC版本的完整Demo。

延伸阅读

想要了解更多关于 GameplayKit的特性,推荐观看WWDC 2015的session 608, Introducing GameplayKit. 别忘了,可以在Git中找到本文的示例代码。

这是一个系列文章,查看更多请移步目录页

*** 备注:本文译者对 iOS 游戏比较陌生,如有翻译错误,还望大家在评论中指出。***

import SpriteKit
import GameplayKit

class SpriteComponent :GKComponent {
    let node:SKSpriteNode
    init(entity:GKEntity,texture:SKTexture,size:CGSize) {
        node = SKSpriteNode(texture: texture, color: SKColor.clear, size: size)
        node.entity = entity
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

9. Procedural Noise

生成相干随机噪声的场,然后用它们来创建类似云或木纹等自然现象的纹理图像,建立无限大小的程序游戏世界等等。

  • GKNoiseSource

    • 噪声生成器的抽象超类。
  • GKNoise

    • 由噪声源产生的程序性噪声,可以用来处理,变换或合并噪声。
  • GKNoiseMap

    • 程序噪声数据的样本,您可以从中直接读取噪声值或创建噪声纹理。
  • GKCoherentNoiseSource

    • 产生相干噪声的程序噪声发生器的抽象超类。
  • GKBillowNoiseSource

    • 一种程序噪声发生器,其输出是一种具有平滑特征的分形相干噪声。
  • GKPerlinNoiseSource

    • 一种程序性噪声发生器,其输出是一种类似于云和地形等自然现象的分形相干噪声。
  • GKRidgedNoiseSource

    • 程序噪声发生器,其输出是一种具有明确定义的特征的多重分形相干噪声。
  • GKVoronoiNoiseSource

    • 一个程序性噪声发生器,其输出(也称为沃利噪声或蜂窝噪声)将空间划分为围绕随机种子点的离散单元。
  • GKCylindersNoiseSource

    • 程序噪声发生器,其输出是同心圆柱壳的3D场。
  • GKSpheresNoiseSource

    • 程序噪声发生器,其输出是同心球壳的3D场。
  • GKCheckerboardNoiseSource

    • 程序性噪声发生器,其输出是交替方形模式。
  • GKConstantNoiseSource

    • 程序噪声发生器,输出一个单一的恒定值的字段。

MoveComponent 上下飞动

10. Randomization

标准算法的强大而灵活的实现,让您在不影响可测试性的情况下增加游戏玩法的不可预测性。

  • GKRandom·

    • GameplayKit中的所有随机化类(或可用)的通用界面。
  • GKRandomSource

    • GameplayKit中所有基本随机化类的超类。
  • GKARC4RandomSource

    • 实现ARC4算法的基本随机数生成器,适用于大多数游戏机制。
  • GKLinearCongruentialRandomSource

    • 一个基本的随机数生成器实现线性同余生成器算法,比默认的随机源更快但更少随机。
  • GKMersenneTwisterRandomSource

    • 一个实现Mersenne Twister算法的基本随机数生成器,它随机性更强,但比默认随机源慢。
  • GKRandomDistribution

    • 用于随机数的发生器,落在特定的范围内,并在多个采样中显示特定的分布。
  • GKGaussianDistribution

    • 用于随机数的生成器,它跨多个采样,遵循高斯分布(也称为正态分布)。
  • GKShuffledDistribution

    • 一个随机数的生成器,它是在许多采样中均匀分布的,但是类似值的短序列是不可靠的
import Foundation
import GameplayKit
import SpriteKit

class MoveComponent:GKComponent {

// The node on which animations should be run for this animation component.
// 引入SpriteComponent 就不用每次都需要像在SpriteComponent里建立精灵的属性了
// node = SKSpriteNode(texture: texture, color: SKColor.clear, size: size)
    let spriteComponent: SpriteComponent  

    init(entity:GKEntity) {
        self.spriteComponent = entity.component(ofType: SpriteComponent.self)!
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    // 组件 上下飞动;
    func startWobble(){
        let moveUp   = SKAction.moveBy(x: 0, y: 50, duration: 0.5)
        moveUp.timingMode = .easeInEaseOut
        let moveDown = moveUp.reversed()
        let sequence = SKAction.sequence([moveUp,moveDown])
        let repeatWobble = SKAction.repeatForever(sequence)
        spriteComponent.node.run(repeatWobble, withKey: "Wobble")
    }

}

11. Rule Systems

将游戏设计从可执行代码中分离出来,以加速游戏的开发周期,或者实现模糊逻辑推理,为游戏添加真实的行为。

  • GKRule

    • 在规则系统的上下文中使用的规则,要测试谓词并在测试成功时执行操作。
  • GKNSPredicateRule

    • 在使用Foundation NSPredicate对象评估自身的规则系统中使用的规则。
  • GKRuleSystem

    • 规则列表,以及用于评估和解释结果的上下文,用于构建数据驱动的逻辑或模糊逻辑系统。

AnimationComponent 拍打翅膀

12. Xcode and SpriteKit Integration

支持使用Xcode中的SpriteKit场景编辑器轻松创建和编辑GameplayKit功能的类和协议。

  • GKScene

    • 用于将GameplayKit对象与SpriteKit场景相关联的容器。
  • GKSceneRootNodeType

    • 标识支持嵌入的GameplayKit信息的其他框架的场景类。
  • GKSKNodeComponent

    • 管理SpriteKit节点的组件。
import GameplayKit
import SpriteKit

class AnimationComponent:GKComponent {
    let textures:[SKTexture]
    let spriteComponent: SpriteComponent

    init(entity:GKEntity,textures:[SKTexture]) {
        self.spriteComponent = entity.component(ofType: SpriteComponent.self)!
        self.textures = textures
        super.init()
    }
    // 翅膀拍动
    func startAnimation(){
        let flyAction = SKAction.animate(with: textures, timePerFrame: TimeInterval(0.02))
        let repeatAction = SKAction.repeatForever(flyAction)
        spriteComponent.node.run(repeatAction)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

13. Reference

  • GameplayKit Constants

  • GameplayKit Structures

  • GameplayKit Enumerations

在场景GameScene加入管理器Entity对象

14. Classes

  • GKSCNNodeComponent
func setupEntityComponent(){
        let penguin = PenguinEntity(imageName: "penguin01") // 企鹅属于worldNode的子层级;
        let penguinNode = penguin.spriteComponent.node
        penguinNode.position = CGPoint(x: 320, y: 500)
        penguinNode.zPosition = 5
        worldNode.addChild(penguinNode)
       // penguin有移动的功能
        penguin.moveComponent.startWobble()
       // 有拍打翅膀的功能
        penguin.animationComponent.startAnimation()
    }

后记

本篇已结束,后面更精彩~~~

澳门新浦京娱乐场网站 22

以上就是应用GKEntity来管理三个组件GKComponent的运用实例。

源码传送门:
更多游戏教学:http://www.iFIERO.com

补充:英文够好的话,建议上苹果的官网看看 GameplayKit的案例代码:

  • Boxes: GameplayKit Entity-Component Basics

  • Dispenser: GameplayKit State Machine Basics

  • Pathfinder: GameplayKit Pathfinding Basics

  • AgentsCatalog: Using the Agents System in GameplayKit

  • FourInARow: Using the GameplayKit Minmax Strategist for Opponent AI

  • DemoBots: Building a Cross Platform Game with SpriteKit and GameplayKit

本文由澳门新浦京娱乐场网站发布于www.146.net,转载请注明出处:澳门新浦京娱乐场网站:的iOS游戏开发实例,基