unity3D技术分享征服Unity3d程序员

Unity官方教程 2D Roguelike(3):移动逻辑

2018-10-26  本文已影响15人  小巷里有只猫
2D Roguelike 最终效果

前言

Unity官方教程 2D Roguelike(2):生成关卡中,我们已经生成了随机关卡,接下来就是让大胡子可以在关卡里自由自在走动。这一节我们主要完成的内容是:

本节你将学会什么?

一、实现基本移动逻辑——编写父类MovingObject

最终的游戏效果我们可以看到,大胡子和怪物虽然种族样貌均不同,但是它们在移动这块存在很多共同点:

当然,也存在不同点:

根据上述可以得出结论,角色和怪物是使用同样的移动逻辑,差别只是在于遇到其他碰撞体的时候反应不同。那么,创建一个父类MovingObject编写移动逻辑,然后让角色和怪物的脚本都继承它,这样就可以避免同样的代码写两遍!不同的地方在子类里实现就可以了~

٩(๑❛ᴗ❛๑)۶ 子类可以继承父类的成员并且加以扩展,实现代码复用,节省代码时间,并且方便修改。

关于移动逻辑,画了个草草的非常简单的流程图如下:


移动逻辑

MovingObject只管怎么移动,不关心移动的请求来自于哪里,所以第一步“获得移动的请求”是子类各自实现的,比如角色是通过键盘方向键输入,而怪物就要看角色是不是已经移动完毕,毕竟这是个回合游戏嘛!

右键Scripts文件夹,选择Create->C# Script,创建一个新的脚本,命名为MovingObject,双击打开进行编辑。
(多代码预警!!!(;´д`)ゞ)

第1步:自顶向下——AttempMove()

我们创建了一个方法AttempMove(),代码如图:

刚看到的时候我是拒绝的

AttempMove()要实现的其实就是整个移动逻辑:接收方向信息,确定目的地并且判断该点是否存在障碍物(Move方法),否就平滑移动(SmoothMovement方法),是则根据障碍物类型来执行对应操作(OnCantMove方法)。比如移动主体是Player的话,判断如果是Wall则攻击使之破碎消失。

代码简析:

一个函数只有一个输出值,如果想返回多个值的话就需要加out参数修饰符。

举个栗子:移动主体是Player,在前方有障碍的情况下,获取障碍的Wall组件,如果的确是有这个组件证明那就是Wall对象(障碍墙),那么就可以调用OnCantMove()去执行敲墙操作了!其他情况则维持原样,被挡住原地不动。

根据上述解析,我们知道应该要传入一个T参数,代表障碍物身上的某一个指定的组件的参数。这个组件类型不固定,可能是Wall,也可能是Player(假设移动主体是Enemy)。一般函数的参数都是指定了类型的,所以这时候应该怎么样传T才能让子类都适用?在这里我们推荐使用泛型方法。

泛型,其实就是通过把参数类型化来实现同一份代码操作多种数据类型。也就是说,当我们不确定传入的参数是什么类型,并且不同的类型下我们的代码逻辑是一样的时候,就可以使用泛型方法,实现更为灵活的复用。

使用泛型格式如代码所示,方法名后<T>,{之前用where T : 来指定T是属于什么,比如在这里是属于组件Component。关于泛型,感兴趣的可以网上搜索了解更多。

为了代码的可读性和美观,单个函数内的代码不要太多行,过多行的情况下建议拆解成其他方法。

第2步:线性投射检测和移动——Move()

对目的地进行障碍物检测和移动的逻辑我们放在了Move()里。

Move()

SmoothMovement()是协同程序,开启协程需要使用StartCoroutine函数。

因为光线从中心点发射出去的时候会碰到自身的碰撞器,所以需要把自身的碰撞器先关掉,检测完了再开启。

第3步:协同程序平滑移动——SmoothMovement()

物体的移动一般是平滑的过程,不是瞬移。而在Unity里,实现平滑移动比较好的方式就是使用协同程序。

协程是分步骤执行代码的程序,遇到条件(yield return语句)会挂起暂停退出,直到条件满足才会被唤醒继续执行后面的代码。

使用协同程序的方法:声明一个返回值为IEnumrator的方法,然后在方法中使用yield return语法返回,在需要用协程的地方(比如上面Move方法末尾)通过StartCorutine方法去调用。

SmoothMovement()

简单说明下:

由于后面要拿来和最小浮点值float.Epsilon进行比较,在程序里面模长平分的计算成本比数量级要低。

暂停执行的时候程序会把移动的结果展示到屏幕,所以我们就可以看到物体平滑的移动,而不是while循环直接跑完了,我们只看到最终的结果,就是瞬移到终点。

第4步:抽象方法——OnCantMove()

这个方法在父类里特别简单。真的特别简单。

OnCantMove()

因为不需要具体实现!hhhh妈呀前面好多代码啊,看到这个方法好感动o(╥﹏╥)o

emmm,MovingObject类基本编写完毕。为什么说基本呢?切回到Unity编辑器,控制台非常友好地报了一个错误。

抽象方法报错

这是因为有抽象方法的类是抽象类,需要在类名前面用abstract关键词进行修饰。

抽象类

二、创建可被破坏的墙——Wall Script

要想角色遇到Wall的时候能够击打敲碎开辟路线,需要Wall本身挂有一个脚本组件以便认定从而调用OnCantMove()。那么我们就来编写一个Wall脚本吧!(注意这里Wall是中间随机生成的障碍墙,并非周围那一圈OuterWall)

第1步:编写Wall Script

右键Scripts文件夹,选择Create->C# Script,创建一个新的脚本,命名为Wall,双击打开进行编辑。

Wall Script

访问限制为public的类成员,可在Unity编辑器的Inspector窗口设置和更改属性值。

第2步:挂载设置Wall Script

脚本写好之后要挂载在游戏对象上才能生效。回到Unity编辑器,点击Assets内的Prefabs文件夹,同时选择Wall1-Wall8,点击最上方菜单栏的Component-Scripts-Wall,把Wall脚本都添加到Wall预制件。

批量添加脚本到预制件

可以看到现在每个Wall预制件右侧的Inspector窗口都多了个Wall脚本组件。

Wall组件

在上面可以自由设定Wall的生命值hp。现在我们需要点击Dmg Sprite选项右侧的小圆圈,打开Sprite选择页面,为每个Wall预制件选择一个被攻击时候的Sprite!

Wall1的DmgSprite

按顺序选择就好,不过官方只给了7张图,所以咱们Wall5和Wall6都选择了编号52的那张图。

三、让角色先走起来——Player Script

完成MovingObject类只是第一步,还需要Player和Enemy分别继承它并且扩展才能真正的让角色和怪物移动起来。我们首先想要实现的是角色的移动,因此先创建一个Script,命名为Player,双击打开编辑。

第1步:获取输入进行基本移动

在Player脚本里,我们第一时间要做的是获取外部的移动请求,然后才能调用AttempMove方法去进行移动。

获取输入

经历上述一大堆的代码和操作,我们终于可以尝试着去让大胡子移动起来了。切回到Unity编辑器,把Prefabs文件夹的Player预制件拖到Hierarchy窗口生成对象实例,然后把Player Script添加到Player对象上。

Player脚本组件

Blocking Layer选择BlockingLayer,然后运行游戏,再按下键盘的方向键操纵大胡子移动,看看我们辛苦的成果吧!

最初的旅行

啊咧,为何和我们想象中的不大一样?这就是传说中的买家秀吗?!!!∑(゚Д゚ノ)ノ
大家会发现大胡子的确可以动起来了,碰到Wall、Enemy、OuterWall也会被挡住,但是存在好几个问题。

  1. 并不是按一下就走一格,而是比一格还远,而且每次还不一样的距离。
  2. 碰到食物、Wall、Exit、Enemy都没有相应的效果。(还未实现)
  3. 在大胡子走动一次之后,理应是Enemy的回合,但是它们傻傻站在原地不动。(还未实现)

第2和3是因为我们还没编写相关的逻辑代码。而第1点,或许已经有聪明的同学想到是什么原因了。提示一下,和Update()这个方法的特点有关系!仔细想想~~~

思考中

———建—议—思—考—下—再—看—答—案———

前面提到,Update()是在每次渲染新的一帧的时候会调用!在我们的金贵的小手指按下方向键到起来的这短短不到1S的时间内,游戏已经渲染了好几帧,也就是调用了好几次Update(),获取了好几次的移动请求输入!因此虽然我们只按了一次方向键,游戏里的大胡子却移动了好几次,跑的老远。那么,我们要怎么做才可以达到我们想要的效果,就是按一次方向键执行一次Update()走动一格呢?
对这个回合游戏来说,角色的移动是和怪物的移动息息相关的。角色移动两次之后就转变成是怪物回合,每一只怪物都移动完毕了又会转回角色回合,然后一直进行这个循环。
也就是说,现在没有怪物移动逻辑代码,因此没办法切换到怪物回合,而我们暂时也不打算现在就转去编写Enemy的移动代码,所以接下来我们将用一个取巧的办法来解决这个问题,后续做了Enemy的移动之后会把这些再修正。

第2步:临时修正同时获取多次输入

现在的问题是在角色还没移动完毕到位的时候,程序又通过Update()获取了新的输入请求,导致角色在半路又决定走多一格。那我们是否可以人为设置一个开关,在角色开始移动的时候把开关关掉,这期间不能获得新的输入,角色移动完毕再把开关开起来,这时候才能获得新的移动输入请求?让我们试试这个办法。
首先,在GameController脚本里添加起开关作用的变量playerTurn,布尔值,初始值为true。因为要在其他脚本调用所以访问限制为public,但是不希望在Unity编辑器可以进行改动,所以用[HideInInspector]隐藏公有变量。

playerTurn

然后我们在Player脚本的Update()里添加如下代码:

增加获取输入的条件

if语句是判断当playerTurn为false的时候return返回,不执行后续代码获取输入。然后横线处是确定了有实际移动输入请求的时候把playerTurn改成false,这样就不会在移动期间又进入Update()里面获取输入。
移动期间把开关关了,那么移动完毕了要把开关开起来,不然就没法进行下次移动。所以我们在MovingObject脚本的SmoothMovement()和AttempMove()都添加了以下代码:

修改MovingObject

为什么同一句代码需要在两个地方都添加?这是因为每次移动的时候有两种情况,可移动和不可移动。无论是哪种情况,都需要把playerTurn重新改回true,以便获取下一次的移动请求。
好了,这时候我们保存脚本,回到Unity编辑器运行游戏。

正常移动

成功!可以看到,移动一次的距离是刚好一个格子了。
然后我们还有好多没实现,如捡东西吃、开路、被敌人砍、进入下一关等等。我写这些很慢(担心讲不清所以老修改),就让我们在下一篇再见吧!

上一篇下一篇

猜你喜欢

热点阅读