如果你已经迫不及待地想下载源码了,可以到GitHub下载游戏完整源码。第二部分教程已经发布在这里
 
除非你最近几年生活在与世隔绝的山洞中,否则你一定玩过一款塔防游戏,比如Plants vs. Zombies(植物大战僵尸),Kingdom Rush(王国保卫战),geoDefense, Jelly Defense(果冻防御)或者其他塔防游戏。你一定花了大量时间在建造防御设施、杀死敌人、和挑战更高等级关卡上面。
 
因为塔防是最常见游戏类型的一种,所以你可以在应用商店那里找到。我决定试试自己的运气,使用unity3D从零开发一款塔防游戏,当前这篇教程就是开始。因为这篇教程比我之前的长一些,所以这个教程拆分为两个部分。在这第一篇教程中,我们将大致了解以下这些知识:游戏描述、关卡编辑器、单独的XML文件创建和解析,为了提升性能将会在游戏中使用对象池(object pool)。通过下面运行在Unity编辑器中的游戏截屏来体验下游戏吧。
实际上,游戏场景和玩法都相当简单。獾子攻击玩家的兔子家园,玩家抛出可爱的小兔子通过射箭来保护自己的家园。为了能有更多的兔子守卫者,玩家需要收集胡萝卜币。如果玩家杀死了本回合预定数目的所有獾子,玩家就赢了,如果预定数目的獾子进入了兔子家园,那么玩家就输了。獾子通过指定路径进入兔子家园,而这条路上不能放置兔子守卫者。
 
下面让我们再深入挖掘一下游戏机制和玩法:
- 兔子家园:初始生命是10,如果10个獾进入家园,游戏结束。
- 路径:獾按照这个路径依次进入房子,指定路径点可以指示出他们的方向。
- 獾:我们的敌人。它有一个速度属性和血条,当它被箭射中时血会减少。它沿着已经被放置在路径种的路径点前行(不可见游戏物体)。
-兔子:我们的守卫。它以一定的频率射箭。它总是寻找附近的敌人攻击。如果找到一个敌人,它就会开始射击。如果敌人死亡或离开它的攻击范围,它就会开始搜索其他敌人。创造兔子需要消耗固定数目的胡萝卜。
- 胡萝卜:从屏幕的顶部的随机落下。玩家需要拍或者点击胡萝卜才能增加金币,来创造更多的兔子。
- 兔子生成器:(哦也,我可以取一个更好的名字!)。它是屏幕左下方的兔子。玩家需要拖出兔子以创造新的小兔子。玩家不能在红色高亮区域放置新的兔子(例如在路径上)。当创造一个新的兔子时,一笔固定数目金币将从玩家的账户上扣除。
-关卡制作器:所有游戏关卡都被存储在一个标准格式的 XML 文件中。Unity开发者可以选择使用Unity编辑器“Custom Editor”菜单“Export Level”功能存储场景编辑数据到 XML 文件。
我们还有几个"角色",将在本教程中提到。
- Unity开发者:使用我们“Custom Editor”的编辑器来为游戏创建新的关卡
- 用户/游戏玩家:最终用户,玩我们的游戏!
 
XML 文件结构
 
正如我们所提到的所有的游戏级别都存储在 XML 文件中。当我们设计游戏关卡时,首先我们需要确定哪些数据是可替换的。看看 Level1.xml文件的内容,文件位于Resources文件夹中(为了能在运行时期间使用)。
从上面的图片,你可以看到我们存储了玩家的初始内存值,两个变量(最小值和最大值)来控制胡萝卜产生时间频率,兔子家园的位置(此后称之为"塔"),PathPieces(路径)的X 和 Y 坐标位置(我们的敌人前行路径),路径点的X 和 Y位置坐标(即我们的敌人将按照这个路径到达兔子家园),最后是每一轮游戏信息(敌人数量)。
 
具体来说,PathPieces 包含关于关卡路径精灵,路径点是空游戏对象的创建信息,用来将敌人"指引"到达塔。产生胡萝卜的时间频率是随机选择的,它的取值范围是我们设置的最小值和最大值之间。在我们深入研究代码之前,提一下我们使用Visual Studio开发游戏代码(免费的社区版在这里)。为了方便调试,可以看下VisualStudio Unity开发集成工具(在这里)。
 
 
XML 解析
 
所有这些信息对应存在于LevelStuffFromXML类。
 
我们使用ReadXMLFile方法在运行时获取 XML 文件中的信息。我们可以使用序列化的类,但我们还是选择了LINQ 到 XML。因为1,rulezzz2,它的语法很好 3,它可以更好地控制如何把对象/属性转换为XML,反之亦然。
因此,我们使用XDocument API来解析Resources文件夹中的Level1.xml 文件,创建的LevelStuffFromXML实例,在我们的游戏中使用。代码用法非常简单也很容易理解,但是,尽管如此,我们使用自顶向下的方法,从XMLRoot元素开始遍历直到最后,在遍历过程中获取指定属性和xml元素。即使你是一个初学者,也很容易理解!
有人也许会问,为什么保存在 XML 文件而不是将每个关卡信息保存到各自关卡场?好吧,那样也是可以的。然而,我们选择使用这种方法,有以下几个原因:
•  XML 是一个可阅读标准,可以无需打开Unity编辑器就可以修改
•  我们的游戏可以通过下载服务器添加的XML文件增加新的关卡
•  用户/游戏玩家可以创建新的关卡(非Unity编辑器),并将其上传到社区网站上,用户可以评分和下载
•  当然,你可以通过教程学习XML的使用(包括此教程)!
 
 
编辑器窗口
 
也许,你可能会问,我们需要自己写XML文件吗?嗯,有人会那样做!但是,我们想要给Unity开发人员提供一种便捷的创建XML文件的方式,所以我们创建一个轻量的Unity编辑器定制窗口。如果你打开Unity项目,你将看到一个新的“Custom editor”菜单,并有一个项标题为“Export level”功能。
点击“Export level”功能,您将看到以下窗口:
通过单击“Add new round”按钮,可以创建一个有特定数目敌人的新回合,可以拖动下面的滑块来修改这些值。试一试吧!经过几次修改,结果如下:
这些就是我们创建的新关卡回合。当然,你可以删除一个回合,如果你觉得做的有不合理的地方。此外,你还可以修改初始金币、 胡萝卜产生时间和导出文件名。如果你试着在一个空的场景导出关卡文件,会出现下面的错误消息。
亲爱的朋友,每个用户输入数据都是恶魔(需要验证)!!!你应该把非正常数据从应用程序和游戏中排除,以保护用户正常输入的数据。在这个空场景,并没有塔的实例对象,所以导出脚本不能正确地创建一个格式良好的游戏xml文件(语法正确),因为我们每个关卡都必定需要一个塔的实例化对象(逻辑上不正确)。我们下面来深入看看代码。
 
 
代码
 
首先,脚本位于Editor文件夹。这是Unity推荐使用的文件夹(通用命名规则公)用来包含夹编辑器相关的代码。
对应的类不是MonoBehavior,和我们之前创建的不一样。它是一个特殊类型,称为EditorWindow,有一个静态方法ShowWindow和一个相应的 C# 属性,通过MenuItem属性创建菜单项。也有一些变量来帮助我们创建编辑器窗口。
OnGUI方法可以定义GUI元素的外观显示。
我们使用ScrollView (通过EditorGUILayout.BeginScrollViewEditorGUILayout.EndScrollView方法) 控件展示Unity开发人员创建的多个Rounds(回合)(因为可能会创建很多回合)。对于每一回合,我们显示一个标签 (用来提示回合的序号),还有这个回合有多少敌人和一个删除回合的按钮。然后下面是一个IntSlider控件,通过拖动滑块来指定敌人的数目,添加一个按钮来增加所需数目敌人的新回合。
 
再创建另外几个IntSlider控件用来设置初始金币和胡萝卜产生时间 (最小值和最大值) ,这样我们就完成了GUI的创建。最后,如果Unity开发者设置正常的话,就可以通过按钮调用“Export”方法保存为新的关卡。
Export方法的开始部分,我们创建新的XDocument对象,并添加Root元素(称为"Elements")。然后开始添加路径XElements对象(每个路径元素都有相应的X和Y属性)。我们通过WaypointsAreValid方法先验证路径点之后再添加“WayPoints”元素,每个路径点元素和路径元素一样都有相应的X和Y属性。
Export方法的第二部分,我们将添加用户创建的所有回合和每个回合敌人的相应数量。我们还要添加塔、 初始金币和胡萝卜产生时间。如果不能在场景中找到一个标签为“塔”的游戏对象,我们将显示一条错误消息然后退出方法。此外,在保存之前,我们有一些验证逻辑(如下图所示),并向用户显示最终确认信息。如果Unity开发人员选择保存,我们将文件保存到Assets文件夹。
当Unity开发人员将路径点放在场景中时,它们的标签必须标记为“WayPoint”,然后添加OrderedWaypointForEditor组件。这些组件都有一个Order字段,敌人将会按照这些路径点的顺序前进。顺序最小的路径点是入口点,最的路径点就是兔子家园的位置。WayPointsAreValid方法检查所有路径点确定他们都有一个OrderedWaypointForEditor组件,并且所有点的顺序字段都是不同的值。如果这两个条件都满足,则该方法返回true。否则通知用户错误并返回false。
InputIsValid方法对各种变量进行无效输入检查,如果变量无效返回false。如果用户忘记添加指定的游戏对象,ShowErrorForNull方法将显示一个错误对话框。
这就是我们的EditorWndow(编辑器窗口)!所以,也许你会问,怎样制作一个新的关卡呢?
 
 
制作塔防游戏的一个新的关卡
 
让我们看看游戏创建新的关卡的步骤!
1.创建一个新的空场景,将相机坐标设置为0,0,-10,投影方式为正交,裁切面取值范围Near0.3,Far为1000。Tag(标签)为"MainCamera"。
2.将塔的预置 (位于Prefabs文件夹) 拖到场景中
3.拖动一些路径到场景,制作路径!使用快捷键Ctrl-D(复制游戏对象),然后使用快捷键V顶点对齐(使游戏对象彼此对齐)。现在场景中应该是这样的:
4.创建一些空的游戏对象来表示路径的点。为了结果更清晰,在路径之外创建一个游戏对象 (下面截图中最下面的),一个在路径的第一部分,一个在转弯处,一个在兔子家园。所有创建的游戏对象增加OrderedWaypointForEditor脚本并分别修改其Order字段。不要忘记修改他们的标签为Waypoints!!!
5.打开“CustomEditor”,然后选择Export Level菜单项。
6.添加一些回合、 编辑你想要修改的项,Filename项Level2.xml作为导出文件名。
7.单击“Export”并选择“Agree”,转到Resources文件夹并右键单击,选择“Refresh”刷新。Level2文件将会显示。将它拖动到Resources文件夹中。
8.转到Utilities.cs文件(位于Scripts文件夹),找到ReadXMLFile方法并将"Level1"更改为"Level2"(是的,我应该支持通过参数加载文件的)。
9.转到新的场景,删除除了摄像机(Camera)之外的所有游戏对象。是的,你相信你自己的选择!
10.将RootGO预设拖到场景中。把它的坐标设置为(0,0,0),如果坐标不是零点的话。
11.准备好了吗?按编辑器的“播放”按钮,将显示你制作的新关卡!恭喜你,你应该感到自己的强大力量!
对象池
 
什么是对象池?
 
另一件事值得一提的事情是在游戏中使用对象池来管理游戏对象。如果你对这方面知识不太熟悉,我们鼓励你可以看看这个Unity视频教程。简而言之,对象池有助于提升性能。换句话说,很多时候你想要在游戏中添加一些生命周期很短的游戏对象。这将会产生以下流程:
•  创建对象(无论是通过“new GameObject”或“Instantiate
•  对象在其生命周期存活
•  当不再需要这个对象时调用Destroy()销毁对象
然而,实例化对象有性能影响,Destroy不会立即从内存删除该对象(这取决于垃圾回收器的生命周期)。而与之相反,对象池的工作流程如下:
•  计算出需要多少个对象(例如我们怀疑游戏在同一时间“存在”的箭的数目不会超过10支)。
•  实例化这些对象并将它们设置为非活动状态
•  每次游戏需要一个对象,从列表中取出第一个非活动状态对象
•  对象设置为活动状态,有自己的生命周期
•  当不再需要该对象时,它被设置为非活动状态
这是对对象池的一个非常基本的解释,现在来看看源代码!
 
 
ObjectPooler 类的源代码
 
开始部分是ObjectPooler类声明的一些变量。父对象Transform和PooledObject对象是可选的,只有在你想为新创建的对象或通过预设实例化对象设置父对象时才会用到父对象的Transform。还有一个PoolLength变量和包含任何我们新创建游戏对象所需组件的数组。Initialize是个过载函数(指可以编写相同名称但不同形式的函数),这样你可以为对象池中的对象添加任何组件类型。PooledObject列表包含所有创建的游戏物体。
 
CreateObjectInPool方法创建一个新的游戏对象(PooledObject预设实例化或调用对象的构造函数),并通过参数判断是否为对象添加组件(通过AddComponent(Type type) 方法)和设置父对象。最重要的是,设置对象状态为非活动,并将其添加到list(列表)中。
GetPooledObject方法是供外部脚本调用。它试图从PooledObjects列表中找到一个处于非活动状态的游戏对象(即准备使用)。如果找到,返回这个对象。否则将列表的容量增加一倍,创建游戏对象并返回第一个创建的对象。
 
ObjectPooler类的分析到此为止,我们现在看一看调用它的类。
 
 
ObjectPoolerManager 
 
这个类的对象会引用ObjectPoolers。出于本教程的目的,我们将使用ObjectPooler来管理小兔子射出的箭和游戏音频对象。
这个类是个单例模式,因为我们希望任何给定时间内都只有这一个实例。对象池管理器有两个引用(箭和音频),当Start()函数被调用时,我们初始化对象池管理器。箭是一个预设,我们在Unity编辑器中设置了它的字段。AudioPooler初始化时需要传入AudioSource类型(用于创建AudioSource组件)而 AudioPooler可以获取箭预设对象。
 
 
 
 
 
原文作者:Dimitris-Ilias Gkanatsios



本文翻译来自蛮牛社区-蛮牛译馆倾情奉献,转载请注明出处。