Unity技术分享程序员unity3D技术分享

Unity官方教程 2D Roguelike(2):生成关卡

2018-05-13  本文已影响116人  小巷里有只猫
2D Roguelike 最终效果

前言

Unity官方教程 2D Roguelike(1):动画和预制件中,我们准备了所有生成一个随机关卡必备的素材并且做成了预制件:角色、怪物、Floor、Exit、Food、Soda、OuterWall、Wall。那么这一节我们将主要完成一个内容:

本节你将学会什么?

一、新建脚本BoardManager、GameController

新建一个空白GameObject,重置Transform,命名为GameManager。这是游戏管理者,关卡生成、游戏控制的脚本都需要挂载在它身上。
右键Scripts文件夹,选择Create->C# Script,创建一个新的脚本,命名为BoardManager,所有生成关卡的逻辑代码都会写在里面。
同样的方法再新建一个脚本GameController(官方视频起名是GameManager,和游戏管理者同名,介意混淆所以采取其他名字),相当于游戏的控制中心,包含游戏的主要逻辑代码,比如保存角色生命值、指定生成第几关的关卡等。

BoardManager和GameController

二、铺设地砖Floor和外墙OuterWall

第1步:认识关卡布局

双击BoardManager脚本,它会自动启动MonoDevelop程序并且打开这个脚本文件。默认的空白脚本内容见下图。

默认的BoardManager

在开始写代码之前,我们先清楚地认识我们即将要实现的是怎么样的一个关卡,方形?长方形?资源分布在哪里?这些都清楚了,对于接下来的代码实现就很容易理解。

关卡布局

如图所示:
整个关卡:10x10,正方形。
白色区域:8x8,角色和怪物的活动区域,铺设floor,外面被一圈OuterWall包围着
黄色区域:6x6,所有随机物品(Enemy、Food、Soda、Wall)都会在这里随机生成
角色初始位置:默认为(0,0)
Exit(右上角):(7,7)
白色和黄色之间的道路:留一条道路让角色可以通行。

因此我们很清晰明了地知道,接下来我们是需要在(0,0)-(7,7)的区域内铺设Floor,在横坐标为-18、竖坐标为-18的四条边边铺设OuterWall。哟西!(`・ω・´)开始我们的代码大战吧!

第2步:编辑生成Floor和OuterWall的代码

BoardManager里自带的Start和Update方法在这里用不上,删除他们之后,我们新增了两个方法:SetupScene()BoardSetup()

BoardManager里生成Floor和OuterWall的代码

简单说明下:

为了可读性和美观,单个方法要控制代码行数。代码太多的时候,建议拆分成不同的方法。

类的变量成员和方法前面有public关键字,代表可被外部实例化访问。

OK,生成Floor和OuterWall的代码写好了,我们需要找个地方去调用执行它。双击GameController脚本,我们将在这里实现对BoardManager的调用。

GameController的代码

代码简读:

在对象初始化之后,会立刻调用Awake()方法,然后才会调用Start()方法。一般初始化用Awake()比较安全。

脚本写好了,切回到Unity编辑器,选中BoardManagerGameController脚本,鼠标左键不动拖到Hierarchy窗口下的GameManager游戏对象再松开,右边Inspector窗口会自动加上这两个脚本组件。

GameManager添加了新的脚本

点击Inspector窗口右上角的锁来保持窗口固定。

Inspector固定窗口的锁

打开Prefabs文件夹,选中所有的Floor预制件,拖到BoardManager脚本组件下的Floor Tiles上,这些预制件就会添加到Floor Tiles这个数组里了。Outer Wall Tiles也是同理,把所有OuterWall预制件都拖过去吧!(・ω<)☆

把预制件拖到BoardManager组件内

解除Inspector窗口锁,现在万事俱备只欠东风,验证下辛苦的成果吧。运行游戏看看!

斜眼看关卡

啊咧?!怎么关卡不是在正中央?强迫症看的我好蓝瘦!(▼ヘ▼#)
排除掉坐标错误的可能,我们所看到的所有游戏内容都是摄像机决定的,所以这个位置不是显示在正中央可以通过修改摄像机参数来解决。切到Scene窗口,我们可以看到摄像机的可视区域。

摄像机可视区域

摄像机的中心点是在(0,0),所以未能看到整个关卡。那么我们思考下,把它的中心点和关卡的中间点重叠起来不就可以了?关卡的中心点坐标是(3.5,3.5),选中Main Camera,修改Transfrom的Position的X和Y均为3.5

修改摄像机位置

再运行游戏看,正的不能再正了。

调整摄像机之后关卡显示在正中

第3步:整理Hierarchy窗口的GameObject

在运行游戏的时候,可以看到Hierarchy窗口下面生成了很多GameObject,按照坐标去计算的话应该是生成了10X10=100个游戏对象,如果就这样显示的话会显得很冗长繁琐,我们需要创建一个GameObject容器来收纳floor和outerWall这些游戏对象。

回到BoardManager脚本,我们在里面增加如下代码。

增加Board游戏对象来收纳floor和outerWall

代码简读:

new GameObject()和Object.Instantiate()两种方法都可以在脚本里动态创建GameObject,区别是new方法的结果是一个空白GameObject,只包含transform组件;Instantiate方法可以指定要创建的预制件、位置、旋转。
Quaternion.identity代表不旋转。

切回到Unity编辑器,运行游戏,可以看到Hierarchy窗口下出现了一个游戏对象Board,点击前面小三角可以打开生成的Floor和OuterWall对象们。

Board

这样整个窗口就干净多了。以后在遇到同类的游戏对象太多的时候建议使用这个方法来整理哦!

三、放置Exit

Exit固定放置在活动区域的右上角(7,7),也就是(columns-1,rows-1),生成代码很简单。在BoardManager里加这几行代码。

生成Exit的代码

代码简读:

回到Unity编辑器,把Exit预制件拖到BoardManager脚本组件的ExitTile选项内就好了。

需要配置上Exit预制件

运行游戏。

关卡出现Exit了

四、生成随机物品(Wall、Food、Soda、Enemy)

生成随机物品,除了选的资源是随机的,它生成的位置也是随机的。所以我们不能像之前铺设floor一样的方法去做,而是要先获得黄色区域的坐标集,然后从里面随机取一个坐标,在上面渲染一个随机的预制件。

第1步:初始化随机区域坐标集

我们对关卡的布局了然于胸,所以应该会记得随机物品都是分布在(1,1)——(6,6)这个正方形区域之间。所以我们第一件事就是先添加这个区域的所有坐标。

初始化坐标集的相关代码

代码简读:

第2步:从坐标集获得一个随机坐标

在BoardManager添加一个RandomPosition方法,通过Random.Range()方法获取一个从0到gridPositions数组长度之间的任意数字,并把它作为索引代入到gridPositions获得并返回一个随机坐标。这个坐标的索引需要清除掉,避免重复抽取。

RandomPosition方法

第3步:编写生成随机物品的方法

每个关卡都是随机生成,除了位置不同以外,每种资源的数量也是在一定的范围内变动,并不会固定。根据这个我们创建了LayoutObjectAtRandom()方法用于生成随机物品。

LayoutObjectAtRandom方法

代码简读:

第4步:生成关卡的Wall、Food、Soda、Enemy

生成随机物品的方法写好了,接下来就是调用这个方法去生成关卡内的Wall、Food、Soda、Enemy了。

生成随机物品的代码

代码看起来有点多ヾ(=・ω・=)o,但其实理清了思路也不难。
简单说明下:

  1. 第一个红方框
  1. 第二个红方框
  1. 第三个红方框
    先调用InitialiseList()初始化坐标集,然后分别调用LayoutObjectAtRandom()方法渲染生成Wall、Food、Enemy这些游戏对象。要注意的是,因为怪物的数量是根据关卡的等级来决定的,所以SetupScene()需要增加一个level参数,代表关卡的等级或者天数。

Mathf.log(level,2f) 指的是以2为底,level的对数。比如说level是8的话,结果就是3。(2的3次方是8)

说到这里,不知道大家有没有发现一个异常?

Random.Range方法出错

Random.Range()方法红色,无法使用。回到Unity编辑器,可以看到控制器也报告了这个错误。

控制器报错

仔细研读控制台的报错内容,我们会发现原来存在两个Random。UnityEngine和System下都有Random,然后我们的命名空间都包括了这两者,所以不指明的话,程序不知道我们的Random是用的哪一个。实际上我们只需要UnityEngine.Random,指定清楚就可以了。

指定Random是哪一个

修正了这个错误之后,我们还需要打开GameController脚本,新增变量成员level,在InitGame方法内调用的SetupScene方法加上level参数。

加上level参数

GameController是控制中心,类似关卡等级level、玩家生命等整个游戏生命周期都需要的数据都会保存在这里。
先给level赋值为4,切回到Unity编辑器准备测试效果啦!
选中GameManager,锁定右侧Inspector窗口,打开Prefabs文件夹,分别把Wall、Enemy、Food、Soda预制件们拖到BoardManager组件下的对应选项内。记得Food、Soda预制件是一起拖到Food Tiles选项的!

拖预制件到wall/food/enemy选项

另外可以看看在最上面有显示WallCount和FoodCount,并且他们的minimux和maximum都可以修改。这就是Serializable的作用,序列化一个类,让它的实例显示属性在Inspector窗口并且可以点击收起或显示。

Serializable的效果

运行游戏。

生成关卡

怪物、食物、障碍都有了,而且每种资源的数量也是正确的。很好,做的不错!✧。٩(ˊᗜˋ)و✧*。

五、游戏管理实现单例模式

GameController是游戏管理器,主要是负责对其他脚本发号施令,比如说玩家来到第三关的时候,它就凶巴巴地对BoardManager说:“你,第三关,造出来!”BoardManager就会屁颠屁颠的去把第三关生成。像GameController这样的管理器,游戏从始至终有且只能有一个,否则会造成调用混乱等问题。这个唯一的实例只需要生成一次,并且直到游戏结束才需要销毁。 这就是单例模式。让我们来看看怎么实现。

第1步:GameController实现单例模式

在GameController脚本内添加以下代码:

image.png

代码简读:

第2步:通过摄像机生成GameManager

回到Unity编辑器,把GameManager往下拖到Prefabs做成预制件,然后删除窗口下的GameManager,在Scripts内创建一个新的脚本Loader,在里面编写如下代码。

Loader

代码很简单,声明一个新的成员变量gameManager,是指的GameManager预制件。然后在Awake方法里,判断当前是否存在GameController的实例,如果不存在,则生成gameManager游戏对象。

回到Unity编辑器,把Loader拖到Main Camera,这个组件就添加到了摄像机里了。打开Prefabs文件夹,把GameManager这个预制件拖到Loader组件的Game Manager选项。

Loader组件指定GameObject

运行游戏,可以看到即使之前左边Hierarchy窗口下没有GameManager游戏对象,在运行之后还是会生成关卡所需的GameObject。

单例模式下正常生成关卡

梳理流程:运行游戏的时候,Main Camera对象生成,开始调用Loader脚本的Awake()方法,它判断当前没有GameController的实例,就动态创建了GameManager游戏对象。GameManager游戏对象一生成,马上就执行GameController的Awake()方法,判断出当前静态变量成员instance为null,就把自身这个实例赋值给它,然后获取GameManager的组件BoardManager的一个实例,调用它的SetupScene()方法来生成关卡。

(o゜▽゜)o☆ 总算结束了,里程碑高高升起!

我觉得我写的有点啰嗦ಠ╭╮ಠ

接下来该让我们的大胡子开始动起来了!

移动逻辑真的是很费劲啊哭
上一篇 下一篇

猜你喜欢

热点阅读